diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml new file mode 100644 index 0000000..22bff68 --- /dev/null +++ b/.github/workflows/npm-publish.yml @@ -0,0 +1,33 @@ +# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created +# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages + +name: Node.js Package + +on: + release: + types: [created, edited, published, released] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: npm ci + - run: npm test + + publish-npm: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: https://registry.npmjs.org/ + - run: npm ci + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{secrets.npm_token}} diff --git a/.gitignore b/.gitignore index 3c3629e..34977ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +.idea \ No newline at end of file diff --git a/.npmignore b/.npmignore index b43bf86..6a79c8c 100644 --- a/.npmignore +++ b/.npmignore @@ -1 +1,4 @@ README.md +testdata +.github +.idea diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/README.md b/README.md index 122e799..841a0a9 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,50 @@ -# JSON Dereference CLI +# JSON Dereference CLI v2 -*Very* simple CLI tool that wraps the excellent [json-schema-ref-parser](https://github.com/BigstickCarpet/json-schema-ref-parser) library. +*Very* simple CLI tool that wraps the +excellent [json-schema-ref-parser](https://github.com/BigstickCarpet/json-schema-ref-parser) library. + +[![Node.js Package](https://github.com/sedflix/json-dereference-cli-v2/actions/workflows/npm-publish.yml/badge.svg)](https://github.com/sedflix/json-dereference-cli-v2/actions/workflows/npm-publish.yml) ## Usage +```bash +# Using npx: +npx json-dereference-cli-v2 -s [-i ] [-o ] [-t ] + +# Installing globally: +npm install -g json-dereference-cli-v2 +json-dereference-v2 -s [-i ] [-o ] [-t ] ``` -npm install -g json-dereference-cli -json-dereference -s my-schema.json -o compiled-schema.yaml + +### Options +- `-s `: Path to the input schema file (required). +- `-i `: Number of spaces for indentation in the output (default: 2). +- `-o `: Path to the output file (optional). +- `-t `: Output type (`json` or `yaml`). If not specified, it is inferred from the output file extension. + +### Examples + +#### Dereference a JSON schema and output to a file: +```bash +json-dereference-v2 -s testdata/correct.schema.json -o testdata/output.json ``` -_Note: The input file can either be `json`, or `yaml` / `yml`._ +#### Dereference a JSON schema and print to stdout in YAML format: +```bash +json-dereference-v2 -s testdata/correct.schema.json -t yaml +``` + +*Note:* The input file can either be `json`, or `yaml` / `yml`. -_Note: The output file types are either `json` or `yaml` / `yml`. This is determined from the file extension for the output file path passed in._ +*Note:* The output file types are either `json` or `yaml` / `yml`. This is determined from the file extension for the +output file path passed in or using `-t json|yaml` when writing to stdout. -### $ref: "s3://.." +### Meta-Validation -This CLI tool will also attempt to resolve S3 references using the `aws-sdk`. Take a look [here](/s3-resolver.js) to see the custom resolver code. +The CLI now supports validating JSON Schemas against their meta-schema to ensure correctness and adherence to best practices. This is automatically performed during dereferencing. + +#### Example: +```bash +json-dereference-v2 -s testdata/correct.schema.json +``` +If the schema is invalid, an error message will be displayed. diff --git a/dereference.js b/dereference.js index 7543c52..d741901 100644 --- a/dereference.js +++ b/dereference.js @@ -1,42 +1,155 @@ #!/usr/bin/env node -var fs = require('fs'); -var path = require('path'); -var util = require('util'); -var $RefParser = require('json-schema-ref-parser'); -var argv = require('minimist')(process.argv.slice(2)); -var s3Resolver = require('./s3-resolver'); - -if (!argv.s || !argv.o) { - console.log('USAGE: ' + process.argv[1] + ' -s -o [...]'); - process.exit(1); +const fs = require('fs'); +const path = require('path'); +const $RefParser = require('@apidevtools/json-schema-ref-parser'); +const argv = require('minimist')(process.argv.slice(2)); +const yaml = require('js-yaml'); +const Ajv = require('ajv'); +const Ajv2020 = require('ajv/dist/2020'); + +// Add a new CLI option `--no-validate` to disable schema validation +const disableValidation = argv['no-validate'] || false; + +/** + * Validates CLI arguments and returns parsed options. + * @returns {Object} Parsed CLI options. + */ +function validateArguments() { + if (!argv.s) { + console.error('Usage: ' + process.argv[1] + ' -s [-i ] [-o ] [-t ] [--no-validate]'); + process.exit(1); + } + + return { + input: path.resolve(argv.s), + output: argv.o ? path.resolve(argv.o) : undefined, + indent: argv.i !== undefined ? argv.i : 2, + type: argv.t + }; +} + +/** + * Detects the output format based on CLI arguments or file extension. + * @param {string} output - Output file path. + * @param {string} type - Output type specified by the user. + * @returns {string} Detected output type ('json' or 'yaml'). + */ +function detectOutputFormat(output, type) { + if (type) { + return type; + } else if (output) { + const ext = path.parse(output).ext; + if (ext === '.json') { + return 'json'; + } else if (ext.match(/^\.?(yaml|yml)$/)) { + return 'yaml'; + } else { + console.error('ERROR: Cannot detect output file type from file name (please set -t ): ' + output); + process.exit(1); + } + } + return 'json'; } -if (argv.b) s3Resolver.bucket = argv.b; +/** + * Validates the input schema against the appropriate JSON Schema meta-schema. + * @param {Object} schema - The JSON schema to validate. + * @param {string} schemaVersion - The JSON Schema version (e.g., '2020-12', 'draft-07'). + * @throws Will throw an error if the schema is invalid or the version is unsupported. + */ +function validateSchemaMeta(schema, schemaVersion) { + let ajv; -var input = path.resolve(argv.s); + if (schemaVersion === '2020-12') { + ajv = new Ajv2020(); + } else if (schemaVersion === 'draft-07') { + ajv = new Ajv(); + const draft07MetaSchema = require('ajv/lib/refs/json-schema-draft-07.json'); + if (!ajv.getSchema(draft07MetaSchema.$id)) { + ajv.addMetaSchema(draft07MetaSchema); + } + } else { + throw new Error(`Unsupported schema version: ${schemaVersion}`); + } -var schema = fs.readFileSync(input, { encoding: 'utf8' }); + const validate = ajv.compile(ajv.getSchema('$schema') || {}); -console.log("Dereferencing schema: " + input); + if (!validate(schema)) { + throw new Error('Schema meta-validation failed: ' + JSON.stringify(validate.errors, null, 2)); + } +} -$RefParser.dereference(input, { resolve: { s3: s3Resolver } }, function(err, schema) { - if (err) { - console.error(err); - } else { - var output = path.resolve(argv.o); - var ext = path.parse(output).ext; +/** + * Detects the JSON Schema version from the $schema property. + * @param {Object} schema - The JSON schema. + * @returns {string} The detected schema version (e.g., '2020-12', 'draft-07'). + */ +function detectSchemaVersion(schema) { + const schemaUrl = schema.$schema; - if (ext == '.json') { - var data = JSON.stringify(schema); - fs.writeFileSync(output, data, { encoding: 'utf8', flag: 'w' }); - } else if (ext.match(/^\.?(yaml|yml)$/)) { - var yaml = require('node-yaml'); - yaml.writeSync(output, schema, { encoding: 'utf8' }) + if (!schemaUrl) { + console.warn('No $schema property found. Defaulting to draft-07 schema.'); + return 'draft-07'; + } + + if (schemaUrl.includes('draft/2020-12')) { + return '2020-12'; + } else if (schemaUrl.includes('draft-07')) { + return 'draft-07'; } else { - console.error("Unrecognised output file type: " + output); - process.exit(1); + throw new Error('Unsupported $schema property in the JSON Schema: ' + schemaUrl); } - console.log("Wrote file: " + output); - } -}); +} + +/** + * Writes the resolved schema to the specified output or stdout. + * @param {string} output - Output file path. + * @param {string} data - Resolved schema data. + */ +function writeOutput(output, data) { + if (output) { + fs.writeFileSync(output, data, { encoding: 'utf8', flag: 'w' }); + console.warn('Wrote file: ' + output); + } else { + console.log(data); + } +} + +/** + * Main function to dereference the schema. + */ +async function main() { + const { input, output, indent, type } = validateArguments(); + + console.warn('Dereferencing schema: ' + input); + try { + const schema = await $RefParser.dereference(input, { resolve: {} }); + + if (!disableValidation) { + const schemaVersion = detectSchemaVersion(schema); + validateSchemaMeta(schema, schemaVersion); + } else { + console.warn('Schema validation is disabled.'); + } + + const outputType = detectOutputFormat(output, type); + let data; + + if (outputType === 'json') { + data = JSON.stringify(schema, null, indent); + } else if (outputType === 'yaml') { + data = yaml.dump(schema, { encoding: 'utf8', indent: indent, noRefs: true }); + } else { + console.error('ERROR: Unsupported output file type: ' + outputType); + process.exit(1); + } + + writeOutput(output, data); + } catch (err) { + console.error('ERROR: ' + err.message); + process.exit(1); + } +} + +main(); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a0e8525 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,139 @@ +{ + "name": "json-dereference-cli-v2", + "version": "4.0.3", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "json-dereference-cli-v2", + "version": "2.0.0", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^11.9.3", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "js-yaml": "^4.1.0", + "minimist": "^1.2.8" + }, + "bin": { + "json-dereference-v2": "dereference.js" + }, + "engines": { + "node": ">=4.3" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "11.9.3", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz", + "integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/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==" + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + } + } +} diff --git a/package.json b/package.json index e087389..faaf015 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,14 @@ { - "name": "json-dereference-cli", - "author": "David Kelley", + "name": "json-dereference-cli-v2", + "author": "Siddharth Yadav", "description": "Provides a cli interface to the json-schema-ref-parser library", "bin": { - "json-dereference": "./dereference.js" + "json-dereference-v2": "./dereference.js" }, "bugs": { - "url": "https://github.com/davikelley/json-dereference-cli/issues" + "url": "https://github.com/sedflix/json-dereference-cli-v2/issues" }, - "homepage": "https://github.com/davikelley/json-dereference-cli", + "homepage": "https://github.com/sedflix/json-dereference-cli-v2", "keywords": [ "json", "schema", @@ -16,22 +16,23 @@ "cli", "tool" ], - "version": "0.1.2", + "version": "4.0.3", "main": "index.js", "engines": { "node": ">=4.3" }, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "node dereference.js -s testdata/correct.schema.json -o testdata/output.json && echo 'Correct schema test passed!' && node dereference.js -s testdata/error.schema.json || echo 'Error schema test passed!' && node dereference.js -s testdata/correct.schema.json -t yaml && echo 'YAML output test passed!' && node dereference.js -s testdata/correct.schema.json --no-validate && echo 'No validation test passed!' && echo 'Meta-validation test passed!'" }, "repository": { "type": "git", - "url": "https://github.com/davidkelley/json-dereference-cli" + "url": "https://github.com/sedflix/json-dereference-cli-v2" }, "dependencies": { - "aws-sdk": "^2.80.0", - "json-schema-ref-parser": "^3.1.2", - "minimist": "^1.2.0", - "node-yaml": "^3.1.0" + "@apidevtools/json-schema-ref-parser": "^11.9.3", + "minimist": "^1.2.8", + "js-yaml": "^4.1.0", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1" } } diff --git a/s3-resolver.js b/s3-resolver.js deleted file mode 100644 index 1553a3f..0000000 --- a/s3-resolver.js +++ /dev/null @@ -1,33 +0,0 @@ -var S3 = require('aws-sdk/clients/s3'); - -module.exports = { - order: 1, - - canRead: /^s3:/i, - - read: function(file, callback) { - var params; - - if (this.bucket) { - var parts = file.url.match(/^s3:\/\/(.+)$/); - params = { Bucket: this.bucket, Key: parts[1] }; - } else { - var parts = file.url.match(/^s3:\/\/(.+?)\/(.+)$/); - params = { Bucket: parts[1], Key: parts[2] }; - } - - if (!(params.Bucket && params.Key)) { - callback(new Error("Malformed params object: " + file.url)); - } else { - var s3 = new S3(); - - s3.getObject(params, function(err, data) { - if (err) { - callback(err); - } else { - callback(null, data.Body.toString()); - } - }); - } - } -}; diff --git a/testdata/correct.schema.json b/testdata/correct.schema.json new file mode 100644 index 0000000..c8d568b --- /dev/null +++ b/testdata/correct.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "test": { + "$ref": "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/master-standalone-strict/_definitions.json#/definitions/io.k8s.apimachinery.pkg.api.resource.Quantity" + }, + "test2": { + + "type": "boolean", + "description": "A boolean value indicating true or false", + "x-imutable": true + + } + } +} \ No newline at end of file diff --git a/testdata/error.schema.json b/testdata/error.schema.json new file mode 100644 index 0000000..a08d89f --- /dev/null +++ b/testdata/error.schema.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "type": "object", + "properties": { + "test": { + "$ref": "https://raw.githubusercontent.com/wrong/kubernetes-wrong-schema/master/master-standalone-strict/.json#/definitions/io.k8s.apimachinery.pkg.api.resource.Quantity" + } + } +} \ No newline at end of file diff --git a/testdata/output.json b/testdata/output.json new file mode 100644 index 0000000..be63637 --- /dev/null +++ b/testdata/output.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "test": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + }, + "test2": { + "type": "boolean", + "description": "A boolean value indicating true or false", + "x-imutable": true + } + } +} \ No newline at end of file