diff --git a/.gitignore b/.gitignore index 66fd13c..6db605c 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,8 @@ # Dependency directories (remove the comment below to include it) # vendor/ +node_modules + +*.yaml +openapi-jsonschema* + diff --git a/.golangci.yml b/.golangci.yml deleted file mode 100644 index 5efdc65..0000000 --- a/.golangci.yml +++ /dev/null @@ -1,150 +0,0 @@ -linters-settings: - depguard: - list-type: blacklist - packages: - # logging is allowed only by logutils.Log, logrus - # is allowed to use only in logutils package - - github.com/sirupsen/logrus - packages-with-error-message: - - github.com/sirupsen/logrus: "logging is allowed only by logutils.Log" - dupl: - threshold: 100 - exhaustive: - default-signifies-exhaustive: false - funlen: - lines: 100 - statements: 50 - gci: - local-prefixes: github.com/golangci/golangci-lint - goconst: - min-len: 2 - min-occurrences: 2 - gocritic: - enabled-tags: - - diagnostic - - experimental - - opinionated - - performance - - style - disabled-checks: - - dupImport # https://github.com/go-critic/go-critic/issues/845 - - ifElseChain - - octalLiteral - - whyNoLint - - wrapperFunc - gocyclo: - min-complexity: 15 - goimports: - local-prefixes: github.com/golangci/golangci-lint - golint: - min-confidence: 0 - gomnd: - settings: - mnd: - # don't include the "operation" and "assign" - checks: argument,case,condition,return - gosec: - settings: - exclude: -G204 - govet: - check-shadowing: false - settings: - printf: - funcs: - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf - lll: - line-length: 950 - maligned: - suggest-new: true - misspell: - locale: US - nolintlint: - allow-leading-space: true # don't require machine-readable nolint directives (i.e. with no leading space) - allow-unused: false # report any unused nolint directives - require-explanation: false # don't require an explanation for nolint directives - require-specific: false # don't require nolint directives to be specific about which linter is being skipped - -linters: - # please, do not use `enable-all`: it's deprecated and will be removed soon. - # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint - disable-all: true - enable: - # todo[kushthedude]: commenting most of the checks as our code can't persist all of the changes, however we can plan further on code-quality after v1.0 release. - # - bodyclose - # - deadcode - - dogsled - - errcheck - # - exhaustive - # - funlen - # - goconst - # - gocritic - # - gocyclo - - gofmt - - goimports - - golint - # todo[kusthedude]: restore gosec check, once this issue is resolved https://github.com/golangci/golangci-lint/issues/177 - # - gosec - # - gomnd - # - goprintffuncname - # - gosimple - - govet - # - ineffassign - # - interfacer - - lll - - misspell - # - nakedret - # - nolintlint - # - rowserrcheck - # - scopelint - - staticcheck - # - structcheck - - stylecheck - - typecheck - # - unconvert - # - unparam - # - unused - # - varcheck - - whitespace - - # don't enable: - # - asciicheck - # - gochecknoglobals - # - gocognit - # - godot - # - godox - # - goerr113 - # - maligned - # - nestif - # - prealloc - # - testpackage - # - wsl - -issues: - # Excluding configuration per-path, per-linter, per-text and per-source - exclude-rules: - - path: _test\.go - linters: - - gomnd - - # https://github.com/go-critic/go-critic/issues/926 - - linters: - - gocritic - text: "unnecessaryDefer:" - -run: - skip-dirs: - - test/testdata_etc - - internal/cache - - internal/renameio - - internal/robustio - timeout: 5m - -# golangci.com configuration -# https://github.com/golangci/golangci/wiki/Configuration -service: - golangci-lint-version: 1.23.x # use the fixed version to not introduce new linters unexpectedly - prepare: - - echo "here I can run custom commands, but no preparation needed for this repo" diff --git a/Makefile b/Makefile index e624969..ddf4ab5 100644 --- a/Makefile +++ b/Makefile @@ -1,24 +1,10 @@ -check: - golangci-lint run +make: linux darwin windows -check-clean-cache: - golangci-lint cache clean +darwin: + nexe index.js -t darwin-x64 -o kubeopenapi-jsonschema-darwin -protoc-setup: - wget -P meshes https://raw.githubusercontent.com/layer5io/meshery/master/meshes/meshops.proto +linux: + nexe index.js -t linux-x64 -o kubeopenapi-jsonschema -proto: - protoc -I meshes/ meshes/meshops.proto --go_out=plugins=grpc:./meshes/ - - - - - -site: - $(jekyll) serve --drafts --livereload - -build: - $(jekyll) build --drafts - -docker: - docker run --name site -d --rm -p 4000:4000 -v `pwd`:"/srv/jekyll" jekyll/jekyll:4.0.0 bash -c "bundle install; jekyll serve --drafts --livereload" +windows: + nexe index.js -t windows-x64 -o kubeopenapi-jsonschema diff --git a/README.md b/README.md index 4367189..7427f36 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,29 @@ -# layer5-repo-template -This repository is used as the boilerplate for consistency across all Layer5 repos. +# KubeOpenAPI - JSON Schema + +This is a very basic node based CLI for converting OpenAPI schema to JSON Schema Draft 4 + + +``` +Usage: openapi-jsonschema [options] + +Options: + -t, --type [type] set type of input, can be either yaml or json (default: "yaml") + -l, --location location of the schema + -f, --filter [query] give a query if a OpenAPISchema is nested + --kubernetes enable kubernetes specific filters (default: false) + -o [output-format] output format (default: "json") + --o-filter [output-filter] output filter query + --silent skip output (default: false) + -h, --help display help for command +``` + +## Example + +Download the binaries from the github releases. Only linux-x64, darwin-x64 and windows-x64 binaries are released + +```bash +openapi-jsonschema --location ./istio.yaml -t yaml --filter '$[?(@.kind=="CustomResourceDefinition" && @.spec.names.kind=="EnvoyFilter")]..validation.openAPIV3Schema.properties.spec' -o yaml --o-filter '$[0]' +```
 
diff --git a/helper/createQuery.js b/helper/createQuery.js new file mode 100644 index 0000000..7bfe23a --- /dev/null +++ b/helper/createQuery.js @@ -0,0 +1,14 @@ +/** + * CreateQuery generates a jsonpath based query + * @param {string} query jsonpath query + * @param {boolean} isKubernetes is the query to be generated for K8s CRD + * @returns {string} generated query + */ +function CreateQuery(query = "", isKubernetes = true) { + if (isKubernetes || !query) + return `$[?(@.kind=="CustomResourceDefinition")]..validation.openAPIV3Schema`; + + return query; +} + +module.exports = CreateQuery; diff --git a/helper/output.js b/helper/output.js new file mode 100644 index 0000000..b883af8 --- /dev/null +++ b/helper/output.js @@ -0,0 +1,18 @@ +const { dump } = require("js-yaml"); +const jp = require("jsonpath"); + +/** + * Output takes in the data that needs to be printed and + * an output format + * @param {*} data + * @param {"json" | "yaml"} format output format + */ +function Output(data, format = "json", filter = "", silent = false) { + if (silent) return; + + data = jp.query(data, filter); + if (format === "yaml") return console.log(dump(data)); + if (format === "json") return console.log(JSON.stringify(data, null, 2)); +} + +module.exports = Output; diff --git a/helper/toJSONSchema.js b/helper/toJSONSchema.js new file mode 100644 index 0000000..4ba347b --- /dev/null +++ b/helper/toJSONSchema.js @@ -0,0 +1,87 @@ +// @ts-check +const toJSONSchema = require("@openapi-contrib/openapi-schema-to-json-schema"); +const yaml = require("js-yaml"); +const { readFileSync, writeFileSync } = require("fs"); +const { tmpdir } = require("os"); +const path = require("path"); +const jp = require("jsonpath"); + +/** + * convertAllSchemasToJSONSchema takes in the OpenAPIV3 Schemas in an array + * and return an array of an equivalent JSON Schema Draft 4 schemas + * @param {any[]} schemas array of schemas in JSON format + * @returns {any[]} JSON Schema draft 4 formatted schemas + */ +function convertAllSchemasToJSONSchema(schemas) { + if (Array.isArray(schemas)) + return schemas.map((schema) => toJSONSchema(schema)); + + return []; +} + +/** + * readSchema will read schema file from the given location, it expects + * the schema to be in JSON format + * + * readSchema will also apply the given jsonpath filter to the read schema + * and will return only the filtered JSONs + * @param {string} location + * @param {string} query jsonpath based query + * @returns {any[]} + */ +function readSchema(location, query) { + const data = readFileSync(location, "utf-8"); + const parsed = JSON.parse(data); + + return jp.query(parsed, query); +} + +/** + * setupFiles takes the location of the files and convert them into json + * and return the new location + * @param {string} location + * @param {"yaml" | "json"} type + * @returns {string} location of the schema files + */ +function setupFiles(location, type) { + if (type === "json") return location; + + if (type === "yaml") { + try { + // Create a file name + const filename = `ucnv-${Math.random().toString(36).substr(2, 5)}.json`; + + // Create destination path + const dest = path.join(tmpdir(), filename); + + // Read file into memory and convert into json + const doc = yaml.loadAll(readFileSync(location, "utf-8")); + + // Write the converted file to the disk + writeFileSync(dest, JSON.stringify(doc)); + + return dest; + } catch (error) { + return ""; + } + } +} + +/** + * ToJSONSchema will convert he OpenAPIV3 based schema to JSONSchema Draft 4 schemas + * @param {string} location location of the schemas in open api v3 format + * @param {"yaml" | "json"} type encoding in which the openapi schema is present + * @param {string} query jsonpath query to filter the read schemas + */ +function ToJSONSchema(location, type = "yaml", query = "") { + if (type !== "yaml" && type !== "json") + throw Error('invalid type received: can be either "yaml" or "json"'); + + const source = setupFiles(location, type); + + const schemas = readSchema(source, query); + + return convertAllSchemasToJSONSchema(schemas); +} + +module.exports = ToJSONSchema; diff --git a/index.js b/index.js new file mode 100644 index 0000000..3006fcd --- /dev/null +++ b/index.js @@ -0,0 +1,33 @@ +// @ts-check +const { program } = require("commander"); +const CreateQuery = require("./helper/createQuery"); +const Output = require("./helper/output"); +const ToJSONSchema = require("./helper/toJSONSchema"); + +program + .option( + "-t, --type [type]", + "set type of input, can be either yaml or json", + "yaml" + ) + .option("-l, --location ", "location of the schema") + .option("-f, --filter [query]", "give a query if a OpenAPISchema is nested") + .option("--kubernetes", "enable kubernetes specific filters", false) + .option("-o [output-format]", "output format", "json") + .option("--o-filter [output-filter]", "output filter query") + .option("--silent", "skip output", false); + +program.parse(process.argv); + +const options = program.opts(); + +Output( + ToJSONSchema( + options.location, + options.type, + CreateQuery(options.filter, options.kubernetes) + ), + options.o, + options.oFilter, + options.silent +); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4afb38b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,152 @@ +{ + "name": "openapi-jsonschema", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@openapi-contrib/openapi-schema-to-json-schema": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@openapi-contrib/openapi-schema-to-json-schema/-/openapi-schema-to-json-schema-3.1.1.tgz", + "integrity": "sha512-FMvdhv9Jr9tULjJAQaQzhCmNYYj2vQFVnl7CGlLAImZvJal71oedXMGszpPaZTLftAk5TCHqjnirig+P6LZxug==", + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + }, + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + } + } + }, + "esprima": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", + "integrity": "sha1-dqD9Zvz+FU/SkmZ9wmQBl1CxZXs=" + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + } + }, + "jsonpath": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", + "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==", + "requires": { + "esprima": "1.2.2", + "static-eval": "2.0.2", + "underscore": "1.12.1" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + }, + "static-eval": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", + "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", + "requires": { + "escodegen": "^1.8.1" + } + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "underscore": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..fcbc375 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "openapi-jsonschema", + "version": "0.0.1", + "description": "Convert OpenAPI v3 spec to JSONSchema draft 4", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Utkarsh Srivastava", + "license": "MIT", + "dependencies": { + "@openapi-contrib/openapi-schema-to-json-schema": "^3.1.1", + "commander": "^7.2.0", + "js-yaml": "^4.1.0", + "jsonpath": "^1.1.1" + } +}