├── Dockerfile
├── src
├── ExitCode.php
├── FilePosition
│ ├── PathState.php
│ └── PositionResolver.php
├── JsonSchema
│ └── ResolverMux.php
├── Rearrange.php
├── SchemaResolver.php
├── PrettyPrint.php
├── Minify.php
├── Resolve.php
├── Json
│ └── LoadFile.php
├── Diff.php
├── BaseDiff.php
├── GenPhp
│ └── BuilderOptions.php
├── DiffInfo.php
├── GenJSDoc.php
├── ValidateSchema.php
├── Replace.php
├── GenMarkdown.php
├── App.php
├── Apply.php
├── GenJson.php
├── ResolvePos.php
├── GenPhp.php
├── GenGo.php
├── GenGo
│ └── BuilderOptions.php
├── Base.php
└── BuildSchema.php
├── phpstan.neon
├── bin
└── json-cli
├── .github
└── workflows
│ ├── test-unit.yml
│ ├── test-unit-cov.yml
│ └── cloc.yml
├── LICENSE
├── composer.json
├── knife.svg
├── CHANGELOG.md
├── README.md
└── composer.lock
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM php:cli-alpine
2 |
3 | COPY ./json-cli /bin/json-cli
4 |
5 | WORKDIR /code
--------------------------------------------------------------------------------
/src/ExitCode.php:
--------------------------------------------------------------------------------
1 | resolvers as $resolver) {
15 | $data = $resolver->getSchemaData($url);
16 | if (false !== $data) {
17 | return $data;
18 | }
19 | }
20 |
21 | return false;
22 | }
23 | }
--------------------------------------------------------------------------------
/bin/json-cli:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | run();
19 | } catch (ExitCode $exception) {
20 | die($exception->getCode());
21 | }
--------------------------------------------------------------------------------
/src/Rearrange.php:
--------------------------------------------------------------------------------
1 | description = 'Rearrange json document in the order of another (original) json document';
13 | }
14 |
15 |
16 | public function performAction()
17 | {
18 | $this->prePerform();
19 | if (null === $this->diff) {
20 | return;
21 | }
22 |
23 | $this->out = $this->diff->getRearranged();
24 |
25 | $this->postPerform();
26 | }
27 |
28 | }
--------------------------------------------------------------------------------
/src/SchemaResolver.php:
--------------------------------------------------------------------------------
1 | schemaData = Schema::object()->setAdditionalProperties(Schema::object())
22 | ->setDescription('Map of schema url to schema data.');
23 | $properties->schemaFiles = Schema::object()->setAdditionalProperties(Schema::string())
24 | ->setDescription('Map of schema url to file path containing schema data.');
25 | }
26 | }
--------------------------------------------------------------------------------
/src/PrettyPrint.php:
--------------------------------------------------------------------------------
1 | path = Command\Option::create()->setIsUnnamed()->setIsRequired()
20 | ->setDescription('Path to .json/.yaml/.yml/.serialized file');
21 | parent::setUpDefinition($definition, $options);
22 | $definition->description = 'Pretty print/convert document';
23 | unset($options->pretty);
24 | }
25 |
26 | public function performAction()
27 | {
28 | $this->out = $this->readData($this->path);
29 | $this->postPerform();
30 | }
31 |
32 | }
--------------------------------------------------------------------------------
/.github/workflows/test-unit.yml:
--------------------------------------------------------------------------------
1 | name: test-unit
2 | on:
3 | push:
4 | branches:
5 | - master
6 | - main
7 | pull_request:
8 | jobs:
9 | run:
10 | runs-on: ${{ matrix.operating-system }}
11 | strategy:
12 | matrix:
13 | operating-system: [ 'ubuntu-latest' ]
14 | php-versions: [ '5.6', '7.0', '7.1', '7.2', '7.3', '8.0' ]
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v2
18 |
19 | - name: Cache vendor
20 | uses: actions/cache@v2
21 | with:
22 | path: |
23 | vendor
24 | key: vendor-${{ hashFiles('composer.lock') }}
25 |
26 | - name: Setup PHP
27 | uses: shivammathur/setup-php@v2
28 | with:
29 | php-version: ${{ matrix.php-versions }}
30 | ini-values: post_max_size=256M, max_execution_time=180
31 | tools: composer
32 |
33 | - name: Populate vendor
34 | run: '[ -e vendor ] || composer install'
35 |
36 | - name: Run Tests
37 | run: make test
--------------------------------------------------------------------------------
/src/Minify.php:
--------------------------------------------------------------------------------
1 | path = Command\Option::create()->setIsUnnamed()->setIsRequired()
20 | ->setDescription('Path to JSON/YAML file');
21 | parent::setUpDefinition($definition, $options);
22 | unset($options->pretty);
23 | unset($options->toYaml);
24 | $options->eol = Command\Option::create()->setDescription('Add line break to the output');
25 | $definition->description = 'Minify JSON document';
26 | }
27 |
28 | public function performAction()
29 | {
30 | $this->out = $this->readData($this->path);
31 | $this->postPerform();
32 | if ($this->eol) {
33 | echo "\n";
34 | }
35 | }
36 |
37 | }
--------------------------------------------------------------------------------
/.github/workflows/test-unit-cov.yml:
--------------------------------------------------------------------------------
1 | name: test-unit-cov
2 | on:
3 | push:
4 | branches:
5 | - master
6 | - main
7 | pull_request:
8 | jobs:
9 | run:
10 | runs-on: ${{ matrix.operating-system }}
11 | strategy:
12 | matrix:
13 | operating-system: [ 'ubuntu-latest' ]
14 | php-versions: [ '7.4' ]
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v2
18 |
19 | - name: Cache vendor
20 | uses: actions/cache@v2
21 | with:
22 | path: |
23 | vendor
24 | key: vendor-${{ hashFiles('composer.lock') }}
25 |
26 | - name: Setup PHP
27 | uses: shivammathur/setup-php@v2
28 | with:
29 | php-version: ${{ matrix.php-versions }}
30 | ini-values: post_max_size=256M, max_execution_time=180
31 | coverage: xdebug
32 | tools: composer
33 |
34 | - name: Populate vendor
35 | run: '[ -e vendor ] || composer install'
36 |
37 | - name: Run Tests With Coverage
38 | run: make test-coverage && bash <(curl -s https://codecov.io/bash)
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 swaggest
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "swaggest/json-cli",
3 | "description": "JSON CLI tool (diff, rearrange, pretty print, minify, yaml convert, etc...)",
4 | "type": "tool",
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Viacheslav Poturaev",
9 | "email": "vearutop@gmail.com"
10 | }
11 | ],
12 | "require": {
13 | "ext-json": "*",
14 | "swaggest/json-diff": "^3.8.3",
15 | "php-yaoi/php-yaoi": "^1",
16 | "symfony/yaml": "^3",
17 | "salsify/json-streaming-parser": "^7.0",
18 | "swaggest/json-schema": "^0.12.41",
19 | "swaggest/go-code-builder": "0.4.51",
20 | "swaggest/php-code-builder": "^0.2.41",
21 | "swaggest/code-builder": "^0.3.5",
22 | "swaggest/json-schema-maker": "^0.3.7"
23 | },
24 | "require-dev": {
25 | "phperf/phpunit": "4.8.38"
26 | },
27 | "autoload": {
28 | "psr-4": {
29 | "Swaggest\\JsonCli\\": "src/"
30 | }
31 | },
32 | "autoload-dev": {
33 | "psr-4": {
34 | "Swaggest\\JsonCli\\Tests\\": "tests/src"
35 | }
36 | },
37 | "config": {
38 | "platform": {
39 | "php": "5.6.0"
40 | }
41 | },
42 | "bin": [
43 | "bin/json-cli"
44 | ]
45 | }
46 |
--------------------------------------------------------------------------------
/.github/workflows/cloc.yml:
--------------------------------------------------------------------------------
1 | name: cloc
2 | on:
3 | pull_request:
4 | jobs:
5 | cloc:
6 | runs-on: ubuntu-latest
7 | steps:
8 | - name: Checkout code
9 | uses: actions/checkout@v2
10 | with:
11 | path: pr
12 | - name: Checkout base code
13 | uses: actions/checkout@v2
14 | with:
15 | ref: ${{ github.event.pull_request.base.sha }}
16 | path: base
17 | - name: Count Lines Of Code
18 | id: loc
19 | run: |
20 | curl -OL https://github.com/vearutop/sccdiff/releases/download/v1.0.1/linux_amd64.tar.gz && tar xf linux_amd64.tar.gz
21 | OUTPUT=$(cd pr && ../sccdiff -basedir ../base)
22 | OUTPUT="${OUTPUT//'%'/'%25'}"
23 | OUTPUT="${OUTPUT//$'\n'/'%0A'}"
24 | OUTPUT="${OUTPUT//$'\r'/'%0D'}"
25 | echo "::set-output name=diff::$OUTPUT"
26 |
27 | - name: Comment Code Lines
28 | uses: marocchino/sticky-pull-request-comment@v2
29 | with:
30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
31 | header: LOC
32 | message: |
33 | ### Lines Of Code
34 |
35 | ${{ steps.loc.outputs.diff }}
36 |
--------------------------------------------------------------------------------
/src/Resolve.php:
--------------------------------------------------------------------------------
1 | path = Option::create()->setType()->setIsUnnamed()
23 | ->setDescription('Path to JSON/YAML file');
24 | $options->pointer = Option::create()->setType()->setIsUnnamed()
25 | ->setDescription('JSON Pointer, example /key4/1/a');
26 | }
27 |
28 | /**
29 | * @throws ExitCode
30 | */
31 | public function performAction()
32 | {
33 | $jsonData = $this->readData($this->path);
34 | try {
35 | $this->out = JsonPointer::getByPointer($jsonData, $this->pointer);
36 | } catch (Exception $e) {
37 | $this->response->error($e->getMessage());
38 | throw new ExitCode('', 1);
39 | }
40 | $this->postPerform();
41 | }
42 |
43 | }
--------------------------------------------------------------------------------
/src/Json/LoadFile.php:
--------------------------------------------------------------------------------
1 | patches = Option::create()->setType()->setIsVariadic()
23 | ->setDescription('JSON patches to apply to schema file before processing, merge patches are also supported');
24 |
25 | }
26 |
27 | protected function loadFile()
28 | {
29 | $dataValue = Base::readJsonOrYaml($this->schema, $this->response);
30 | if (!$dataValue) {
31 | throw new ExitCode('Unable to find schema in ' . $this->schema, 1);
32 | }
33 |
34 | if (!empty($this->patches)) {
35 | foreach ($this->patches as $patchPath) {
36 | $patch = Base::readJsonOrYaml($patchPath, $this->response);
37 | if (is_array($patch)) {
38 | $jp = JsonPatch::import($patch);
39 | try {
40 | $jp->apply($dataValue);
41 | } catch (Exception $e) {
42 | throw new ExitCode($e->getMessage(), 1);
43 | }
44 | } else {
45 | JsonMergePatch::apply($dataValue, $patch);
46 | }
47 | }
48 | }
49 |
50 | return $dataValue;
51 | }
52 | }
--------------------------------------------------------------------------------
/src/Diff.php:
--------------------------------------------------------------------------------
1 | description = 'Make patch from two json documents, output to STDOUT';
21 | $options->prettyShort = Command\Option::create()
22 | ->setDescription('Pretty short format');
23 | $options->merge = Command\Option::create()
24 | ->setDescription('Use merge patch (RFC 7386)');
25 | }
26 |
27 |
28 | public function performAction()
29 | {
30 | $this->prePerform();
31 | if (null === $this->diff) {
32 | return;
33 | }
34 |
35 | if ($this->merge) {
36 | $this->out = $this->diff->getMergePatch();
37 | } else {
38 | $this->out = $this->diff->getPatch();
39 | $outJson = $this->out->jsonSerialize();
40 | if ($this->prettyShort && !empty($outJson)) {
41 | $out = '[';
42 | foreach ($outJson as $item) {
43 | $out .= "\n " . json_encode($item, JSON_UNESCAPED_SLASHES) . ',';
44 | }
45 | $out = substr($out, 0, -1);
46 | $out .= "\n]";
47 | $this->response->addContent($out);
48 | return;
49 | }
50 | }
51 |
52 | $this->postPerform();
53 | }
54 | }
--------------------------------------------------------------------------------
/src/BaseDiff.php:
--------------------------------------------------------------------------------
1 | originalPath = Command\Option::create()->setIsUnnamed()->setIsRequired()
22 | ->setDescription('Path to old (original) json file');
23 |
24 | $options->newPath = Command\Option::create()->setIsUnnamed()->setIsRequired()
25 | ->setDescription('Path to new json file');
26 |
27 | $options->rearrangeArrays = Command\Option::create()
28 | ->setDescription('Rearrange arrays to match original');
29 |
30 | parent::setUpDefinition($definition, $options);
31 | }
32 |
33 | /** @var JsonDiff */
34 | protected $diff;
35 |
36 | protected function prePerform()
37 | {
38 | $original = Base::readJsonOrYaml($this->originalPath, $this->response);
39 | $new = Base::readJsonOrYaml($this->newPath, $this->response);
40 |
41 | $options = 0;
42 | if ($this->rearrangeArrays) {
43 | $options += JsonDiff::REARRANGE_ARRAYS;
44 | }
45 | try {
46 | $this->diff = new JsonDiff($original, $new, $options);
47 | } catch (Exception $e) {
48 | $this->response->error($e->getMessage());
49 | return;
50 | }
51 |
52 | $this->out = '';
53 | }
54 |
55 | }
--------------------------------------------------------------------------------
/src/GenPhp/BuilderOptions.php:
--------------------------------------------------------------------------------
1 | setters = Option::create()->setDescription('Build setters');
29 | $options->getters = Option::create()->setDescription('Build getters');
30 | $options->noEnumConst = Option::create()
31 | ->setDescription('Do not create constants for enum/const values');
32 |
33 | $options->declarePropertyDefaults = Option::create()
34 | ->setDescription('Use default values to initialize properties');
35 |
36 | $options->buildAdditionalPropertiesAccessors = Option::create()
37 | ->setDescription('Build accessors for additionalProperties');
38 | }
39 |
40 | protected function setupBuilder(PhpBuilder $builder)
41 | {
42 | $builder->buildSetters = $this->setters;
43 | $builder->buildGetters = $this->getters;
44 |
45 | $builder->makeEnumConstants = !$this->noEnumConst;
46 | $builder->declarePropertyDefaults = $this->declarePropertyDefaults;
47 | $builder->buildAdditionalPropertyMethodsOnTrue = $this->buildAdditionalPropertiesAccessors;
48 | }
49 |
50 | }
--------------------------------------------------------------------------------
/src/DiffInfo.php:
--------------------------------------------------------------------------------
1 | withContents = Command\Option::create()->setDescription('Add content to output');
20 | $options->withPaths = Command\Option::create()->setDescription('Add paths to output');
21 | $definition->description = 'Show diff info for two JSON documents';
22 | }
23 |
24 |
25 | public function performAction()
26 | {
27 | $this->prePerform();
28 |
29 | $this->out = array(
30 | 'addedCnt' => $this->diff->getAddedCnt(),
31 | 'modifiedCnt' => $this->diff->getAddedCnt(),
32 | 'removedCnt' => $this->diff->getRemovedCnt(),
33 | );
34 | if ($this->withPaths) {
35 | $this->out = array_merge($this->out, array(
36 | 'addedPaths' => $this->diff->getAddedPaths(),
37 | 'modifiedPaths' => $this->diff->getModifiedPaths(),
38 | 'removedPaths' => $this->diff->getRemovedPaths(),
39 | ));
40 | }
41 | if ($this->withContents) {
42 | $this->out = array_merge($this->out, array(
43 | 'added' => $this->diff->getAdded(),
44 | 'modifiedNew' => $this->diff->getModifiedNew(),
45 | 'modifiedOriginal' => $this->diff->getModifiedOriginal(),
46 | 'removed' => $this->diff->getRemoved(),
47 | ));
48 | }
49 | $this->postPerform();
50 | }
51 |
52 |
53 | }
--------------------------------------------------------------------------------
/src/GenJSDoc.php:
--------------------------------------------------------------------------------
1 | description = 'Generate JSDoc code from JSON schema';
21 | Base::setupGenOptions($definition, $options);
22 | }
23 |
24 |
25 | public function performAction()
26 | {
27 | try {
28 | $skipRoot = false;
29 | $baseName = null;
30 | $schema = $this->loadSchema($skipRoot, $baseName);
31 |
32 | $jb = new TypeBuilder();
33 | $jb->trimNamePrefix = $this->defPtr;
34 |
35 | if (!$schema instanceof Schema) {
36 | $this->response->error('failed to assert Schema type, ' . get_class($schema) . ' received');
37 | throw new ExitCode('', 1);
38 | }
39 |
40 | $jb->getTypeString($schema);
41 |
42 | if ($this->output) {
43 | if (!file_exists(dirname($this->output))) {
44 | $this->response->error('Destination directory does not exist, please create: ' . dirname($this->output));
45 | throw new ExitCode('', 1);
46 | }
47 | file_put_contents($this->output, $jb->file);
48 |
49 | } else {
50 | echo $jb->file;
51 | }
52 | } catch (\Exception $e) {
53 | $this->response->error($e->getMessage());
54 | throw new ExitCode('', 1);
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/src/ValidateSchema.php:
--------------------------------------------------------------------------------
1 | data = Command\Option::create()->setIsUnnamed()->setIsRequired()
21 | ->setDescription('Path to data (JSON/YAML)');
22 | $options->schema = Command\Option::create()->setIsUnnamed()
23 | ->setDescription('Path to schema, default JSON Schema');
24 | }
25 |
26 |
27 | /**
28 | * @throws ExitCode
29 | * @throws \Swaggest\JsonSchema\Exception
30 | */
31 | public function performAction()
32 | {
33 | if ($this->schema) {
34 | $schemaData = $this->readData($this->schema);
35 | try {
36 | $schema = Schema::import($schemaData);
37 | } catch (InvalidValue $e) {
38 | $this->response->error('Invalid schema');
39 | $this->response->addContent($e->getMessage());
40 | throw new ExitCode('', 1);
41 | } catch (\Exception $e) {
42 | $this->response->error('Failed to import schema:' . $e->getMessage());
43 | throw new ExitCode('', 1);
44 | }
45 | } else {
46 | $schema = Schema::schema();
47 | }
48 |
49 | $data = $this->readData($this->data);
50 |
51 | try {
52 | $schema->in($data);
53 | $this->response->success('Data is valid');
54 | } catch (InvalidValue $exception) {
55 | $this->response->error('Data is invalid');
56 | $this->response->addContent($exception->getMessage());
57 | throw new ExitCode('', 1);
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/src/Replace.php:
--------------------------------------------------------------------------------
1 | path = Command\Option::create()->setIsUnnamed()->setIsRequired()
24 | ->setDescription('Path to JSON/YAML file');
25 | $options->search = Command\Option::create()->setIsUnnamed()->setIsRequired()
26 | ->setDescription('Search JSON value');
27 | $options->replace = Command\Option::create()->setIsUnnamed()->setIsRequired()
28 | ->setDescription('Replace JSON value');
29 | $options->pathFilter = Command\Option::create()->setType()
30 | ->setDescription('JSON path filter regex, example "/definitions/.*/properties/deletedAt"');
31 | $definition->description = 'Minify JSON document';
32 | }
33 |
34 | public function performAction()
35 | {
36 | $jsonData = $this->readData($this->path);
37 |
38 | $search = json_decode($this->search);
39 | if (json_last_error()) {
40 | $this->response->error('Invalid JSON: ' . $this->search);
41 | return;
42 | }
43 | $replace = json_decode($this->replace);
44 | if (json_last_error()) {
45 | $this->response->error('Invalid JSON: ' . $this->replace);
46 | return;
47 | }
48 |
49 | $pathFilter = null;
50 | if ($this->pathFilter) {
51 | $pathFilter = '|' . $this->pathFilter . '|';
52 | }
53 |
54 | $replacer = new JsonValueReplace($search, $replace, $pathFilter);
55 | $this->out = $replacer->process($jsonData);
56 | $this->postPerform();
57 | }
58 |
59 | }
--------------------------------------------------------------------------------
/src/GenMarkdown.php:
--------------------------------------------------------------------------------
1 | description = 'Generate Markdown document from JSON schema';
21 | Base::setupGenOptions($definition, $options);
22 | }
23 |
24 |
25 | public function performAction()
26 | {
27 | try {
28 | $skipRoot = false;
29 | $baseName = null;
30 | $schema = $this->loadSchema($skipRoot, $baseName);
31 |
32 | $jb = new TypeBuilder();
33 | $jb->trimNamePrefix = $this->defPtr;
34 |
35 | if (!$schema instanceof Schema) {
36 | $this->response->error('failed to assert Schema type, ' . get_class($schema) . ' received');
37 | throw new ExitCode('', 1);
38 | }
39 |
40 | $jb->getTypeString($schema);
41 |
42 | $jb->sortTypes();
43 | $out = $jb->tableOfContents();
44 | foreach ($jb->types as $typeName => $doc) {
45 | $out .= $doc;
46 | }
47 |
48 | if ($this->output) {
49 | if (!file_exists(dirname($this->output))) {
50 | $this->response->error('Destination directory does not exist, please create: ' . dirname($this->output));
51 | throw new ExitCode('', 1);
52 | }
53 | file_put_contents($this->output, $out);
54 |
55 | } else {
56 | echo $out;
57 | }
58 | } catch (\Exception $e) {
59 | $this->response->error($e->getMessage());
60 | throw new ExitCode('', 1);
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/src/App.php:
--------------------------------------------------------------------------------
1 | name = 'json-cli';
36 | $definition->version = self::$ver;
37 | $definition->description = 'JSON CLI tool, https://github.com/swaggest/json-cli';
38 |
39 | $commandDefinitions->diff = Diff::definition();
40 | $commandDefinitions->apply = Apply::definition();
41 | $commandDefinitions->rearrange = Rearrange::definition();
42 | $commandDefinitions->diffInfo = DiffInfo::definition();
43 | $commandDefinitions->prettyPrint = PrettyPrint::definition();
44 | $commandDefinitions->minify = Minify::definition();
45 | $commandDefinitions->replace = Replace::definition();
46 | $commandDefinitions->resolve = Resolve::definition();
47 | $commandDefinitions->resolvePos = ResolvePos::definition();
48 | $commandDefinitions->validateSchema = ValidateSchema::definition();
49 | $commandDefinitions->genGo = GenGo::definition();
50 | $commandDefinitions->genPhp = GenPhp::definition();
51 | $commandDefinitions->genJSDoc = GenJSDoc::definition();
52 | $commandDefinitions->genJson = GenJson::definition();
53 | $commandDefinitions->genMarkdown = GenMarkdown::definition();
54 | $commandDefinitions->buildSchema = BuildSchema::definition();
55 | }
56 | }
--------------------------------------------------------------------------------
/src/Apply.php:
--------------------------------------------------------------------------------
1 | patchPath = Command\Option::create()->setType()->setIsUnnamed()
29 | ->setDescription('Path to JSON patch file');
30 | $options->basePath = Command\Option::create()->setType()->setIsUnnamed()
31 | ->setDescription('Path to JSON base file');
32 |
33 | parent::setUpDefinition($definition, $options);
34 |
35 | $definition->description = 'Apply patch to base json document, output to STDOUT';
36 | $options->tolerateErrors = Command\Option::create()
37 | ->setDescription('Continue on error');
38 | $options->merge = Command\Option::create()
39 | ->setDescription('Use merge patch (RFC 7386)');
40 | }
41 |
42 | /**
43 | * @throws ExitCode
44 | */
45 | public function performAction()
46 | {
47 | $patchJson = $this->readData($this->patchPath);
48 | $base = $this->readData($this->basePath);
49 |
50 | try {
51 | if ($this->merge) {
52 | JsonMergePatch::apply($base, $patchJson);
53 | $this->out = $base;
54 | } else {
55 | $patch = JsonPatch::import($patchJson);
56 | $errors = $patch->apply($base, !$this->tolerateErrors);
57 | foreach ($errors as $error) {
58 | $this->response->error($error->getMessage());
59 | }
60 | $this->out = $base;
61 | }
62 | } catch (Exception $e) {
63 | $this->response->error($e->getMessage());
64 | throw new ExitCode('', 1);
65 | }
66 |
67 | $this->postPerform();
68 | }
69 |
70 | }
--------------------------------------------------------------------------------
/src/GenJson.php:
--------------------------------------------------------------------------------
1 | description = 'Generate JSON sample from JSON schema';
26 | Base::setupGenOptions($definition, $options);
27 | $options->maxNesting = Command\Option::create()->setType()
28 | ->setDescription('Max nesting level, default 10');
29 | $options->defaultAdditionalProperties = Command\Option::create()
30 | ->setDescription('Treat non-existent `additionalProperties` as `additionalProperties: true`');
31 | $options->randSeed = Command\Option::create()->setType()
32 | ->setDescription('Integer random seed for deterministic output');
33 | Base::setUpDefinition($definition, $options);
34 | Base::setupLoadFileOptions($options);
35 | }
36 |
37 | public function performAction()
38 | {
39 | if ($this->randSeed !== null) {
40 | mt_srand((int)$this->randSeed);
41 | }
42 |
43 | try {
44 | $skipRoot = false;
45 | $baseName = null;
46 | $schema = $this->loadSchema($skipRoot, $baseName);
47 |
48 | if (!$schema instanceof Schema) {
49 | $this->response->error('failed to assert Schema type, ' . get_class($schema) . ' received');
50 | throw new ExitCode('', 1);
51 | }
52 |
53 | $options = new Options;
54 | $options->maxNesting = $this->maxNesting;
55 | $options->defaultAdditionalProperties = $this->defaultAdditionalProperties;
56 | $instanceFaker = new InstanceFaker($schema, $options);
57 |
58 | $this->out = $instanceFaker->makeValue();
59 |
60 | $this->postPerform();
61 | } catch (\Exception $e) {
62 | $this->response->error($e->getMessage());
63 | throw new ExitCode('', 1);
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/src/ResolvePos.php:
--------------------------------------------------------------------------------
1 | path = Command\Option::create()->setType()->setIsUnnamed()
26 | ->setDescription('Path to JSON file');
27 | $options->pointer = Command\Option::create()->setType()->setIsUnnamed()
28 | ->setDescription('JSON Pointer, example /key4/1/a');
29 | $options->dumpAll = Command\Option::create()->setDescription('Dump all pointer positions from JSON');
30 | }
31 |
32 | /**
33 | * @throws ExitCode
34 | */
35 | public function performAction()
36 | {
37 | $listener = new FilePosition\PositionResolver();
38 | $stream = fopen($this->path, 'r');
39 | if ($stream === false) {
40 | $this->response->error('Failed to open ' . $this->path);
41 | die(1);
42 | }
43 | try {
44 | if ($stream !== false) {
45 | $parser = new Parser($stream, $listener);
46 | $parser->parse();
47 | fclose($stream);
48 | }
49 | } catch (\Exception $e) {
50 | fclose($stream);
51 | $this->response->error($e->getMessage());
52 | throw new ExitCode('', 1);
53 | }
54 |
55 | if ($this->dumpAll) {
56 | $rows = array();
57 | foreach ($listener->resolved as $pointer => $pos) {
58 | $rows[] = array(
59 | 'Pos' => $pos,
60 | 'Ptr' => $pointer,
61 | );
62 | }
63 | $this->response->addContent(new Rows(new \ArrayIterator($rows)));
64 | } else {
65 | try {
66 | // Convert to non-URI pointer
67 | $pointer = JsonPointer::buildPath(JsonPointer::splitPath($this->pointer));
68 | } catch (Exception $e) {
69 | $this->response->error($e->getMessage());
70 | throw new ExitCode('', 1);
71 | }
72 |
73 | if (isset($listener->resolved[$pointer])) {
74 | $this->response->addContent($listener->resolved[$pointer]);
75 | } else {
76 | $this->response->error('Pointer not found');
77 | throw new ExitCode('', 1);
78 | }
79 | }
80 |
81 |
82 | }
83 |
84 |
85 | }
--------------------------------------------------------------------------------
/knife.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
60 |
--------------------------------------------------------------------------------
/src/FilePosition/PositionResolver.php:
--------------------------------------------------------------------------------
1 | pathState = new PathState();
18 | $this->pathState->path = '';
19 | }
20 |
21 | public function startDocument()
22 | {
23 | }
24 |
25 | public function endDocument()
26 | {
27 | }
28 |
29 | public function startObject()
30 | {
31 | $path = $this->pathState->path;
32 | if ($this->pathState->isArray) {
33 | $index = $this->pathState->arrayIndex++;
34 | $path = $this->pathState->path . '/' . $index;
35 | $this->resolved[$path] = $this->currentLine . ':' . $this->currentChar;
36 | }
37 |
38 | $this->stack[] = $this->pathState;
39 | $pathState = new PathState();
40 | $pathState->path = $path;
41 | $this->pathState = $pathState;
42 | }
43 |
44 | public function endObject()
45 | {
46 | $this->pathState = array_pop($this->stack);
47 | if ($this->pathState->isKey) {
48 | $this->pathState = array_pop($this->stack);
49 | }
50 | }
51 |
52 |
53 | public function startArray()
54 | {
55 | $path = $this->pathState->path;
56 | if ($this->pathState->isArray) {
57 | $index = $this->pathState->arrayIndex++;
58 | $path = $this->pathState->path . '/' . $index;
59 | $this->resolved[$path] = $this->currentLine . ':' . $this->currentChar;
60 | }
61 |
62 | $this->stack[] = $this->pathState;
63 | $pathState = new PathState();
64 | $pathState->path = $path;
65 | $pathState->isArray = true;
66 | $this->pathState = $pathState;
67 | }
68 |
69 | public function endArray()
70 | {
71 | $this->pathState = array_pop($this->stack);
72 | if ($this->pathState->isKey) {
73 | $this->pathState = array_pop($this->stack);
74 | }
75 | }
76 |
77 | public function key($key)
78 | {
79 | $path = $this->pathState->path . '/' . JsonPointer::escapeSegment($key);
80 |
81 | $this->stack[] = $this->pathState;
82 | $pathState = new PathState();
83 | $pathState->path = $path;
84 | $pathState->isKey = true;
85 | $this->pathState = $pathState;
86 |
87 |
88 | $this->resolved[$path] = $this->currentLine . ':' . $this->currentChar;
89 | }
90 |
91 | public function value($value)
92 | {
93 | if ($this->pathState->isArray) {
94 | $index = $this->pathState->arrayIndex++;
95 | $itemPath = $this->pathState->path . '/' . $index;
96 | $this->resolved[$itemPath] = $this->currentLine . ':' . $this->currentChar;
97 | } elseif ($this->pathState->isKey) {
98 | $this->pathState = array_pop($this->stack);
99 | }
100 | }
101 |
102 | public function whitespace($whitespace)
103 | {
104 | }
105 |
106 | private $currentLine;
107 | private $currentChar;
108 |
109 | /**
110 | * @param int $line
111 | * @param int $char
112 | */
113 | public function filePosition($line, $char)
114 | {
115 | $this->currentLine = $line;
116 | $this->currentChar = $char;
117 | }
118 |
119 |
120 | }
--------------------------------------------------------------------------------
/src/GenPhp.php:
--------------------------------------------------------------------------------
1 | description = 'Generate PHP code from JSON schema';
32 | Base::setupGenOptions($definition, $options);
33 |
34 | $options->ns = Command\Option::create()
35 | ->setDescription('Namespace to use for generated classes, example \MyClasses')->setType()->setIsRequired();
36 |
37 | $options->nsPath = Command\Option::create()
38 | ->setDescription('Path to store generated classes, example ./src/MyClasses')
39 | ->setType()
40 | ->setIsRequired();
41 |
42 | $options->rootName = Command\Option::create()->setType()
43 | ->setDescription('Root class name, default "Structure", only used for # pointer');
44 |
45 | static::setupBuilderOptions($options);
46 | Base::setupGenOptions($definition, $options);
47 | }
48 |
49 |
50 | public function performAction()
51 | {
52 | try {
53 | $skipRoot = false;
54 | $baseName = null;
55 | $schema = $this->loadSchema($skipRoot, $baseName);
56 |
57 | $appPath = realpath($this->nsPath);
58 | if (!$appPath) {
59 | $this->response->error('Could not find directory ' . $this->nsPath);
60 | throw new ExitCode('', 1);
61 | }
62 | $appNs = $this->ns;
63 |
64 | $app = new PhpApp();
65 | $app->setNamespaceRoot($appNs, '.');
66 |
67 | $builder = new PhpBuilder();
68 | $this->setupBuilder($builder);
69 |
70 | $builder->classCreatedHook = new ClassHookCallback(function (PhpClass $class, $path, $schema)
71 | use ($app, $appNs, $skipRoot, $baseName) {
72 | if ($skipRoot && '#' === $path) {
73 | return;
74 | }
75 |
76 | $desc = '';
77 | if ($schema->title) {
78 | $desc = $schema->title;
79 | }
80 | if ($schema->description) {
81 | $desc .= "\n" . $schema->description;
82 | }
83 | if ($fromRefs = $schema->getFromRefs()) {
84 | $desc .= "\nBuilt from " . implode("\n" . ' <- ', $fromRefs);
85 | }
86 | $class->setDescription(trim($desc));
87 |
88 | $class->setNamespace($appNs);
89 | if ('#' === $path) {
90 | $class->setName($this->rootName);
91 | } else {
92 | if (!empty($fromRefs)) {
93 | $path = $fromRefs[0];
94 | }
95 | foreach ($this->defPtr as $defPtr) {
96 | if (isset($baseName)) {
97 | $baseNameDefPtr = $baseName . $defPtr;
98 | if ($baseNameDefPtr === substr($path, 0, strlen($baseNameDefPtr))) {
99 | $path = substr($path, strlen($baseNameDefPtr));
100 | $className = PhpCode::makePhpClassName($path);
101 | $class->setName($className);
102 | }
103 | }
104 |
105 | if ($defPtr === substr($path, 0, strlen($defPtr))) {
106 | $className = PhpCode::makePhpClassName(substr($path, strlen($defPtr)));
107 | $class->setName($className);
108 | }
109 | }
110 | }
111 | $app->addClass($class);
112 | });
113 |
114 | if (!$schema instanceof Schema) {
115 | $this->response->error('failed to assert Schema type, ' . get_class($schema) . ' received');
116 | throw new ExitCode('', 1);
117 | }
118 | $builder->getType($schema);
119 | $app->store($appPath);
120 | $this->response->success("Classes are generated in " . $appPath);
121 | } catch (\Exception $e) {
122 | $this->response->error($e->getMessage());
123 | throw new ExitCode('', 1);
124 | }
125 | }
126 | }
--------------------------------------------------------------------------------
/src/GenGo.php:
--------------------------------------------------------------------------------
1 | description = 'Generate Go code from JSON schema';
41 | Base::setupGenOptions($definition, $options);
42 |
43 | $options->output = Command\Option::create()
44 | ->setDescription('Path to output .go file, STDOUT is used by default')->setType();
45 |
46 | $options->packageName = Command\Option::create()->setType()
47 | ->setDescription('Go package name, default "entities"');
48 |
49 | $options->rootName = Command\Option::create()->setType()
50 | ->setDescription('Go root struct name, default "Structure", only used for # pointer');
51 |
52 | $options->withTests = Command\Option::create()
53 | ->setDescription('Generate (un)marshaling tests for entities (experimental feature)');
54 |
55 | static::setUpBuilderOptions($options);
56 | }
57 |
58 |
59 | public function performAction()
60 | {
61 | try {
62 | $skipRoot = false;
63 | $baseName = null;
64 | $schema = $this->loadSchema($skipRoot, $baseName);
65 |
66 | $builderOptions = $this->makeGoBuilderOptions();
67 | $builder = new GoBuilder();
68 | $builder->options = $builderOptions;
69 |
70 | $pathToNameHook = new StripPrefixPathToNameHook();
71 |
72 | if (!empty($this->defPtr)) {
73 | $pathToNameHook->prefixes = [];
74 | foreach ($this->defPtr as $defPtr) {
75 | if (isset($baseName)) {
76 | $pathToNameHook->prefixes[] = $baseName . $defPtr;
77 | }
78 | $pathToNameHook->prefixes[] = $defPtr;
79 | }
80 | }
81 |
82 | if (isset($baseName)) {
83 | $pathToNameHook->prefixes[] = $baseName;
84 | }
85 |
86 | $builder->pathToNameHook = $pathToNameHook;
87 |
88 | $builder->structCreatedHook = new StructHookCallback(function (StructDef $structDef, $path, $schema) {
89 | if ('#' === $path) {
90 | $structDef->setName($this->rootName);
91 | }
92 | });
93 | if ($schema instanceof Schema) {
94 | $builder->getType($schema);
95 | }
96 |
97 | $goFile = new GoFile($this->packageName);
98 | $goFile->fileComment = 'Code generated by github.com/swaggest/json-cli ' . App::$ver . ', DO NOT EDIT.';
99 | $goFile->setComment('Package ' . $this->packageName . ' contains JSON mapping structures.');
100 |
101 | $goTestFile = new GoFile($this->packageName . '_test');
102 | $goTestFile->setPackage($this->packageName);
103 | $goTestFile->fileComment = 'Code generated by github.com/swaggest/json-cli ' . App::$ver . ', DO NOT EDIT.';
104 |
105 | mt_srand(1);
106 |
107 | foreach ($builder->getGeneratedStructs() as $generatedStruct) {
108 | if ($skipRoot && $generatedStruct->path === '#') {
109 | continue;
110 | }
111 | $goFile->getCode()->addSnippet($generatedStruct->structDef);
112 | if ($this->withTests) {
113 | $goTestFile->getCode()->addSnippet(MarshalingTestFunc::make($generatedStruct, $builder->options));
114 | }
115 | }
116 | $goFile->getCode()->addSnippet($builder->getCode());
117 |
118 | if ($this->output) {
119 | if (!file_exists(dirname($this->output))) {
120 | $this->response->error('Destination directory does not exist, please create: ' . dirname($this->output));
121 | throw new ExitCode('', 1);
122 | }
123 | file_put_contents($this->output, $goFile->render());
124 |
125 | if ($this->withTests) {
126 | file_put_contents(str_replace('.go', '_test.go', $this->output), $goTestFile->render());
127 | }
128 | } else {
129 | echo $goFile->render();
130 | }
131 | } catch (\Exception $e) {
132 | $this->response->error($e->getMessage());
133 | throw new ExitCode('', 1);
134 | }
135 | }
136 | }
--------------------------------------------------------------------------------
/src/GenGo/BuilderOptions.php:
--------------------------------------------------------------------------------
1 | */
32 | public $renames = [];
33 |
34 | /** @var bool */
35 | public $requireXGenerate = false;
36 |
37 | /** @var bool */
38 | public $validateRequired = false;
39 |
40 | /** @var string[] */
41 | public $nameTags = [];
42 |
43 | /** @var string */
44 | public $config;
45 |
46 | /**
47 | * @param \stdClass|static $options
48 | */
49 | public static function setUpBuilderOptions($options)
50 | {
51 | $options->showConstProperties = Option::create()
52 | ->setDescription('Show properties with constant values, hidden by default');
53 |
54 | $options->keepParentInPropertyNames = Option::create()
55 | ->setDescription('Keep parent prefix in property name, removed by default');
56 |
57 | $options->ignoreNullable = Option::create()
58 | ->setDescription('Add `omitempty` to nullable properties, removed by default');
59 |
60 | $options->ignoreXGoType = Option::create()
61 | ->setName('ignore-x-go-type')
62 | ->setDescription('Ignore `x-go-type` in schema to skip generation');
63 |
64 | $options->withZeroValues = Option::create()
65 | ->setDescription('Use pointer types to avoid zero value ambiguity');
66 |
67 | $options->enableXNullable = Option::create()
68 | ->setDescription('Add `null` to types if `x-nullable` or `nullable` is available');
69 |
70 | $options->enableDefaultAdditionalProperties = Option::create()
71 | ->setDescription('Add field property for undefined `additionalProperties`');
72 |
73 | $options->fluentSetters = Option::create()
74 | ->setDescription('Add fluent setters to struct fields');
75 |
76 | $options->ignoreRequired = Option::create()
77 | ->setDescription('Ignore if property is required when deciding on pointer type or omitempty');
78 |
79 | $options->renames = Option::create()->setIsVariadic()->setType()
80 | ->setDescription('Map of exported symbol renames, example From:To');
81 |
82 | $options->requireXGenerate = Option::create()
83 | ->setDescription('Generate properties with `x-generate: true` only');
84 |
85 | $options->validateRequired = Option::create()
86 | ->setDescription('Generate validation code to check required properties during unmarshal');
87 |
88 | $options->nameTags = Option::create()->setIsVariadic()->setType()
89 | ->setDescription('Set additional field tags with property name, example "msgp bson"');
90 |
91 | $options->config = Option::create()->setType()
92 | ->setDescription('Path to config JSON file to load options from. Schema:');
93 |
94 | $tb = new TypeBuilder();
95 | $tb->getTypeString(Options::schema()->exportSchema());
96 | $options->config->description .= "\n" . trim(substr($tb->file, 97)); // Stripping header.
97 | }
98 |
99 | /**
100 | * @return Options
101 | * @throws ExitCode
102 | */
103 | protected function makeGoBuilderOptions()
104 | {
105 | $options = new Options();
106 | if (!empty($this->config)) {
107 | $data = file_get_contents($this->config);
108 | if (empty($data)) {
109 | throw new ExitCode("empty or missing config file", 1);
110 | }
111 | $json = json_decode($data);
112 | if (empty($json)) {
113 | throw new ExitCode("invalid json in config file", 1);
114 | }
115 |
116 | $options = Options::import($json);
117 | }
118 |
119 | $options = $options->jsonSerialize();
120 |
121 | $options->hideConstProperties = !$this->showConstProperties;
122 | $options->trimParentFromPropertyNames = !$this->keepParentInPropertyNames;
123 | $options->ignoreNullable = $this->ignoreNullable;
124 | $options->ignoreXGoType = $this->ignoreXGoType;
125 | $options->withZeroValues = $this->withZeroValues;
126 | $options->enableXNullable = $this->enableXNullable;
127 | $options->defaultAdditionalProperties = $this->enableDefaultAdditionalProperties;
128 | $options->fluentSetters = $this->fluentSetters;
129 | $options->ignoreRequired = $this->ignoreRequired;
130 | $options->requireXGenerate = $this->requireXGenerate;
131 | $options->validateRequired = $this->validateRequired;
132 | $options->nameTags = $this->nameTags;
133 | if (!empty($this->renames)) {
134 | foreach ($this->renames as $rename) {
135 | $rename = explode(':', $rename, 2);
136 | $options->renames[$rename[0]] = $rename[1];
137 | }
138 | }
139 |
140 | return $options;
141 | }
142 |
143 | }
--------------------------------------------------------------------------------
/src/Base.php:
--------------------------------------------------------------------------------
1 | pretty = Command\Option::create()
34 | ->setDescription('Pretty-print result JSON');
35 | $options->output = Command\Option::create()->setType()
36 | ->setDescription('Path to output result, default STDOUT');
37 | $options->schemaResolver = Command\Option::create()->setType()
38 | ->setDescription('Path to schema resolver JSON file. Schema:');
39 | $tb = new TypeBuilder();
40 | $tb->getTypeString(SchemaResolver::schema()->exportSchema());
41 | $options->schemaResolver->description .= "\n" . trim(substr($tb->file, 77)); // Stripping header.
42 |
43 |
44 | $options->toYaml = Command\Option::create()->setDescription('Output in YAML format');
45 | $options->toSerialized = Command\Option::create()->setDescription('Output in PHP serialized format');
46 | }
47 |
48 |
49 | protected $out;
50 |
51 | /**
52 | * @param string $path
53 | * @return mixed
54 | * @throws ExitCode
55 | */
56 | protected function readData($path)
57 | {
58 | return self::readJsonOrYaml($path, $this->response);
59 | }
60 |
61 | /**
62 | * @param string $path
63 | * @param Response $response
64 | * @return mixed
65 | * @throws ExitCode
66 | */
67 | public static function readJsonOrYaml($path, $response)
68 | {
69 | $fileData = file_get_contents($path);
70 | if (!$fileData) {
71 | $response->error('Unable to read ' . $path);
72 | throw new ExitCode('', 1);
73 | }
74 | if (substr($path, -5) === '.yaml' || substr($path, -4) === '.yml') {
75 | $jsonData = Yaml::parse($fileData, Yaml::PARSE_OBJECT + Yaml::PARSE_OBJECT_FOR_MAP);
76 | } elseif (substr($path, -11) === '.serialized') {
77 | $jsonData = unserialize($fileData);
78 | } else {
79 | $jsonData = json_decode($fileData);
80 | }
81 |
82 | return $jsonData;
83 | }
84 |
85 |
86 | protected function postPerform()
87 | {
88 | $options = JSON_UNESCAPED_SLASHES + JSON_UNESCAPED_UNICODE;
89 | if ($this->pretty) {
90 | $options += JSON_PRETTY_PRINT;
91 | }
92 |
93 | if ($this->toYaml) {
94 | $result = Yaml::dump($this->out, 2, 2, Yaml::DUMP_OBJECT_AS_MAP);
95 | } elseif ($this->toSerialized) {
96 | $result = serialize($this->out);
97 | } else {
98 | $result = json_encode($this->out, $options);
99 | }
100 |
101 | if ($this->output) {
102 | file_put_contents($this->output, $result);
103 | } else {
104 | echo $result, "\n";
105 | }
106 | }
107 |
108 |
109 | /** @var string */
110 | public $schema;
111 | /** @var string[] */
112 | public $defPtr = ['#/definitions'];
113 | /** @var string[] */
114 | public $ptrInSchema;
115 |
116 | /**
117 | * @param Command\Definition $definition
118 | * @param static|mixed $options
119 | */
120 | protected static function setupGenOptions(Command\Definition $definition, $options)
121 | {
122 | $options->schema = Command\Option::create()
123 | ->setDescription('Path to JSON schema file, use `-` for STDIN')->setIsUnnamed()->setIsRequired();
124 |
125 | $options->ptrInSchema = Command\Option::create()->setType()->setIsVariadic()
126 | ->setDescription('JSON pointers to structure in root schema, default #');
127 |
128 | $options->defPtr = Command\Option::create()->setType()->setIsVariadic()
129 | ->setDescription('Definitions pointers to strip from symbol names, default #/definitions');
130 |
131 | $options->schemaResolver = Command\Option::create()->setType()
132 | ->setDescription('Path to schema resolver JSON file. Schema:');
133 | $tb = new TypeBuilder();
134 | $tb->getTypeString(SchemaResolver::schema()->exportSchema());
135 | $options->schemaResolver->description .= "\n" . trim(substr($tb->file, 77)); // Stripping header.
136 |
137 | static::setupLoadFileOptions($options);
138 | }
139 |
140 | /**
141 | * @param bool $skipRoot
142 | * @param string $baseName
143 | * @return \Swaggest\JsonSchema\SchemaContract
144 | * @throws Exception
145 | * @throws ExitCode
146 | * @throws \Swaggest\JsonSchema\Exception
147 | * @throws \Swaggest\JsonSchema\InvalidValue
148 | */
149 | protected function loadSchema(&$skipRoot, &$baseName)
150 | {
151 | if ($this->schema === '-') {
152 | $this->schema = 'php://stdin';
153 | }
154 |
155 | $resolver = new ResolverMux();
156 | $preloaded = new Preloaded();
157 |
158 | if ($this->schemaResolver !== null) {
159 | $data = file_get_contents($this->schemaResolver);
160 | if (empty($data)) {
161 | throw new ExitCode("empty or missing schema resolver file", 1);
162 | }
163 | $json = json_decode($data);
164 | if (empty($json)) {
165 | throw new ExitCode("invalid json in schema resolver file", 1);
166 | }
167 |
168 | $schemaResolver = SchemaResolver::import($json);
169 |
170 | foreach ($schemaResolver->schemaData as $url => $data) {
171 | $preloaded->setSchemaData($url, $data);
172 | }
173 |
174 | foreach ($schemaResolver->schemaFiles as $url => $file) {
175 | $preloaded->setSchemaFile($url, $file);
176 | }
177 | }
178 |
179 | $resolver->resolvers[] = $preloaded;
180 |
181 | $dataValue = $this->loadFile();
182 | $data = $dataValue;
183 |
184 | if (!empty($this->ptrInSchema)) {
185 | $baseName = basename($this->schema);
186 | $skipRoot = true;
187 | $preloaded->setSchemaData($baseName, $dataValue);
188 | $data = new \stdClass();
189 |
190 | foreach ($this->defPtr as $defPtr) {
191 | $this->defPtr[] = $baseName . $defPtr;
192 | }
193 | $this->defPtr[] = $baseName;
194 |
195 | foreach ($this->ptrInSchema as $i => $ptr) {
196 | $data->oneOf[$i] = (object)[Schema::PROP_REF => $baseName . $ptr];
197 | }
198 | }
199 |
200 | $resolver->resolvers[] = new BasicFetcher();
201 | return Schema::import($data, new Context($resolver));
202 | }
203 | }
--------------------------------------------------------------------------------
/src/BuildSchema.php:
--------------------------------------------------------------------------------
1 | data = Command\Option::create()->setIsUnnamed()->setIsRequired()
49 | ->setDescription('Path to data (JSON/YAML)');
50 | $options->schema = Command\Option::create()->setIsUnnamed()
51 | ->setDescription('Path to parent schema');
52 | $options->ptrInSchema = Command\Option::create()->setType()
53 | ->setDescription('JSON pointer to structure in root schema, default #');
54 | $options->ptrInData = Command\Option::create()->setType()
55 | ->setDescription('JSON pointer to structure in data, default #');
56 | $options->jsonl = Command\Option::create()->setDescription('Data is a stream of JSON Lines');
57 |
58 | $options->useNullable = Command\Option::create()
59 | ->setDescription('Use `nullable: true` instead of `type: null`, OAS 3.0 compatibility');
60 |
61 | $options->useXNullable = Command\Option::create()
62 | ->setDescription('Use `x-nullable: true` instead of `type: null`, Swagger 2.0 compatibility');
63 |
64 | $options->defsPtr = Command\Option::create()->setType()
65 | ->setDescription('Location to put new definitions. default: "#/definitions/"');
66 |
67 | $options->collectExamples = Command\Option::create()
68 | ->setDescription('Collect scalar values example');
69 |
70 | $options->heuristicRequired = Command\Option::create()
71 | ->setDescription('Mark properties that are available in all samples as `required`.');
72 |
73 | $options->additionalData = Command\Option::create()->setType()->setIsVariadic()
74 | ->setDescription('Additional paths to data');
75 |
76 | parent::setUpDefinition($definition, $options);
77 | }
78 |
79 |
80 | /**
81 | * @throws ExitCode
82 | * @throws \Swaggest\JsonSchema\Exception
83 | */
84 | public function performAction()
85 | {
86 | $schema = new Schema();
87 |
88 | if ($this->schema) {
89 | $schemaDataOrig = $this->readData($this->schema);
90 | $schemaData = $schemaDataOrig;
91 |
92 | $resolver = new ResolverMux();
93 |
94 | if (!empty($this->ptrInSchema)) {
95 | $baseName = basename($this->schema);
96 | $preloaded = new Preloaded();
97 | $preloaded->setSchemaData($baseName, $schemaData);
98 | $resolver->resolvers[] = $preloaded;
99 | $schemaData = (object)[Schema::PROP_REF => $baseName . $this->ptrInSchema];
100 | }
101 |
102 | $resolver->resolvers[] = new BasicFetcher();
103 |
104 | try {
105 | $schemaContract = Schema::import($schemaData, new Context($resolver));
106 | if ($schemaContract instanceof Schema) {
107 | $schema = $schemaContract;
108 | }
109 | } catch (InvalidValue $e) {
110 | $this->response->error('Invalid schema');
111 | $this->response->addContent($e->getMessage());
112 | throw new ExitCode('', 1);
113 | } catch (\Exception $e) {
114 | $this->response->error('Failed to import schema:' . $e->getMessage());
115 | throw new ExitCode('', 1);
116 | }
117 | }
118 |
119 | $maker = new SchemaMaker($schema);
120 | $maker->options->useXNullable = $this->useXNullable;
121 | $maker->options->useNullable = $this->useNullable;
122 | $maker->options->defsPtr = $this->defsPtr;
123 | $maker->options->collectExamples = $this->collectExamples;
124 | $maker->options->heuristicRequired = $this->heuristicRequired;
125 |
126 | if ($this->jsonl) {
127 | $pathInData = [];
128 | if ($this->ptrInData) {
129 | $pathInData = JsonPointer::splitPath($this->ptrInData);
130 | }
131 |
132 | $handle = fopen($this->data, "r");
133 | if ($handle) {
134 | while (($buffer = fgets($handle)) !== false) {
135 | $item = json_decode($buffer);
136 | // Tolerate and skip malformed JSON line.
137 | if (null === $item) {
138 | continue;
139 | }
140 | if ($this->ptrInData) {
141 | $item = JsonPointer::get($item, $pathInData);
142 | }
143 | $maker->addInstanceValue($item);
144 | }
145 | if (!feof($handle)) {
146 | echo "Error: unexpected fgets() fail\n";
147 | }
148 | fclose($handle);
149 | }
150 | } else {
151 | $data = $this->readData($this->data);
152 | $maker->addInstanceValue($data);
153 |
154 | if (!empty($this->additionalData)) {
155 | foreach ($this->additionalData as $path) {
156 | $data = $this->readData($path);
157 | $maker->addInstanceValue($data);
158 | }
159 | }
160 | }
161 |
162 |
163 | $s = Schema::export($schema);
164 | $this->out = $s;
165 |
166 | if ($this->ptrInSchema && isset($schemaDataOrig)) {
167 | $tmp = json_encode($schemaDataOrig);
168 | if ($tmp === false) {
169 | throw new ExitCode('Failed to encode JSON', 1);
170 | }
171 | $schemaDataResult = json_decode($tmp);
172 |
173 | $defs = JsonPointer::get($s, JsonPointer::splitPath(rtrim($this->defsPtr, '/')));
174 | foreach ($defs as $name => $def) {
175 | JsonPointer::add($schemaDataResult, JsonPointer::splitPath($this->defsPtr . $name), $def);
176 | }
177 | JsonPointer::remove($s, JsonPointer::splitPath(rtrim($this->defsPtr, '/')));
178 | JsonPointer::add($schemaDataResult, JsonPointer::splitPath($this->ptrInSchema), $s);
179 |
180 | $tmp = json_encode($schemaDataResult);
181 | if ($tmp === false) {
182 | throw new ExitCode('Failed to encode JSON', 1);
183 | }
184 | $schemaDataResult = json_decode($tmp);
185 | $diff = new JsonDiff($schemaDataOrig, $schemaDataResult, JsonDiff::REARRANGE_ARRAYS);
186 | $this->out = $diff->getRearranged();
187 | }
188 |
189 | $this->postPerform();
190 | }
191 | }
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | All notable changes to this project will be documented in this file.
3 |
4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6 |
7 | ## [1.11.2] - 2023-12-19
8 |
9 | ### Added
10 | - Dependencies updated.
11 |
12 | ## [1.11.1] - 2022-11-14
13 |
14 | ### Fixed
15 | - Handling of `*Of` in PHP generation.
16 |
17 | ## [1.11.0] - 2022-09-18
18 |
19 | ### Added
20 | - Configurable schema url resolver
21 |
22 | ## [1.10.0] - 2022-09-16
23 |
24 | ### Added
25 | - Dependencies updated.
26 | - Generate Markdown from JSON Schema.
27 | - Options for Go generator with JSON file.
28 |
29 | ## [1.9.1] - 2022-04-21
30 |
31 | ### Added
32 | - Dependencies updated.
33 |
34 | ### Fixed
35 | - Compatibility with PHP 8.1.
36 |
37 | ## [1.9.0] - 2021-10-28
38 |
39 | ### Added
40 | - Dependencies updated.
41 | - Command `gen-json` to generate sample JSON value from JSON Schema.
42 | - Support for STDIN via `-` file path.
43 |
44 | ### Changed
45 | - Terminal output now has trailing line break.
46 |
47 | ### Fixed
48 | - Disabled preloaded standard schemas.
49 |
50 | ## [1.8.8] - 2021-09-26
51 |
52 | ### Added
53 | - Dependencies updated.
54 |
55 | ## [1.8.7] - 2021-04-20
56 |
57 | ### Fixed
58 | - Out of memory error with infinite recursion in some JSON Schema references.
59 |
60 | ## [1.8.6] - 2021-04-20
61 |
62 | ### Added
63 | - Dependencies updated.
64 |
65 | ## [1.8.5] - 2021-04-20
66 |
67 | ### Added
68 | - Dependencies updated.
69 |
70 | ## [1.8.4] - 2021-04-07
71 |
72 | ### Added
73 | - Generation of JSDoc type definitions from JSON Schema with `gen-jsdoc`.
74 |
75 | ## [1.8.3] - 2020-12-14
76 |
77 | ### Fixed
78 | - Stale app version
79 |
80 | ## [1.8.2] - 2020-12-13
81 |
82 | ### Changed
83 | - Internal refactoring of CLI options.
84 |
85 | ## [1.8.1] - 2020-12-13
86 |
87 | ### Added
88 | - Dependencies updated.
89 |
90 | ## [1.8.0] - 2020-09-30
91 |
92 | ### Added
93 | - Dependencies updated.
94 | - Added schema patches to `gen-go` and `gen-php`.
95 | - Added property name fields controls in `gen-go`.
96 |
97 | ## [1.7.13] - 2020-09-26
98 |
99 | ### Added
100 | - Dependency `swaggest/json-diff` updated.
101 |
102 | ## [1.7.12] - 2020-09-25
103 |
104 | ### Added
105 | - Dependency `swaggest/json-diff` updated.
106 |
107 | ## [1.7.11] - 2020-09-22
108 |
109 | ### Added
110 | - Dependencies updated.
111 |
112 | ### Fixed
113 | - Removing empty destination directory when generating PHP classes, [#19](https://github.com/swaggest/json-cli/issues/19).
114 |
115 | ## [1.7.10] - 2020-05-20
116 |
117 | ### Added
118 | - Dependencies updated.
119 |
120 | ## [1.7.9] - 2020-04-29
121 |
122 | ### Added
123 | - Dependencies updated.
124 |
125 | ## [1.7.8] - 2020-04-28
126 |
127 | ### Changed
128 | - Hardcoded time limit for 60 seconds removed.
129 |
130 | ### Added
131 | - Option `--validate-required` to validate required properties during unmarshal in `gen-go`.
132 | - Dependencies updated.
133 |
134 | ## [1.7.7] - 2020-04-21
135 |
136 | ### Added
137 | - Dependencies updated.
138 |
139 | ## [1.7.6] - 2020-04-04
140 |
141 | ### Added
142 | - Option `--require-xgenerate` to generate properties with `x-generate: true` only in `gen-go`.
143 |
144 | ### Fixed
145 | - Handling of malformed JSONL in `build-schema`, invalid lines are skipped.
146 |
147 | ## [1.7.5] - 2020-03-30
148 |
149 | ### Added
150 | - Option to add multiple data samples in `build-schema`.
151 | - Option to add new line in `minify`.
152 |
153 | ### Fixed
154 | - Generated tests do not honor `--enable-default-additional-properties`.
155 |
156 | ## [1.7.4] - 2020-03-17
157 |
158 | ### Added
159 | - Option to rename generated symbols in `gen-go`.
160 |
161 | ## [1.7.3] - 2020-03-10
162 |
163 | ### Added
164 | - Dependencies updated.
165 | - Tests generator in `gen-go`.
166 | - Example collector in `build-schema`.
167 |
168 | ## [1.7.2] - 2020-02-25
169 |
170 | ### Added
171 | - Dependencies updated.
172 | - Backwards compatibility option `--ignore-required` in `gen-go` to ignore if property is required when deciding on pointer type or omitempty.
173 |
174 | ## [1.7.1] - 2020-02-02
175 |
176 | ### Added
177 | - Dependencies updated.
178 |
179 | ## [1.7.0] - 2020-01-26
180 |
181 | ### Added
182 | - Command to build JSON Schema from instance value(s).
183 | - Dependencies updated.
184 |
185 | ## [1.6.8] - 2020-01-24
186 |
187 | ### Added
188 | - Option to build accessors for additional properties in generated `PHP` classes.
189 | - Option to declare default values for properties in generated `PHP` classes.
190 | - Option to create and apply JSON Merge Patches (RFC 7386).
191 |
192 | ## [1.6.7] - 2020-01-04
193 |
194 | ### Added
195 | - Option to build fluent setters in generated `Go` structures.
196 |
197 | ## [1.6.6] - 2019-12-03
198 |
199 | ### Added
200 | - Dependencies updated to fix issues in `swaggest/php-code-builder` and `swaggest/php-json-schema`.
201 |
202 | ## [1.6.5] - 2019-11-18
203 |
204 | ### Added
205 | - Updated `swaggest/go-code-builder` to improve memory efficiency of generated `Go` structures.
206 |
207 | ## [1.6.4] - 2019-10-27
208 |
209 | ### Added
210 | - Dependencies updated.
211 |
212 | ## [1.6.3] - 2019-10-15
213 |
214 | ### Added
215 | - Option to disable null `additionalProperties` (`--enable-default-additional-properties`) rendering in `gen-go`.
216 | - Option to ignore [`x-go-type`](https://github.com/swaggest/go-code-builder#x-go-type) (`--ignore-xgo-type`) in `gen-go`.
217 | - Option to add `omitempty` on nullable types (`--ignore-nullable`) in `gen-go`.
218 | - Option to use pointer types to distinguish zero from unset (`--with-zero-values`) in `gen-go`.
219 | - Option to inherit nullability from [`x-nullable`/`nullable`](https://github.com/swaggest/go-code-builder#x-nullable-nullable) vendor extensions (`--enable-xnullable`) in `gen-go`.
220 | - Version of `json-cli` to head comment of `gen-go` output.
221 |
222 | ## [1.6.2] - 2019-09-22
223 |
224 | ### Added
225 | - Docker image.
226 | - Dependencies updated.
227 |
228 | ### Fixed
229 | - Local file resolver in references.
230 |
231 | [1.11.2]: https://github.com/swaggest/json-cli/compare/v1.11.1...v1.11.2
232 | [1.11.1]: https://github.com/swaggest/json-cli/compare/v1.10.1...v1.11.1
233 | [1.11.0]: https://github.com/swaggest/json-cli/compare/v1.10.0...v1.11.0
234 | [1.10.0]: https://github.com/swaggest/json-cli/compare/v1.9.1...v1.10.0
235 | [1.9.1]: https://github.com/swaggest/json-cli/compare/v1.9.0...v1.9.1
236 | [1.9.0]: https://github.com/swaggest/json-cli/compare/v1.8.8...v1.9.0
237 | [1.8.8]: https://github.com/swaggest/json-cli/compare/v1.8.7...v1.8.8
238 | [1.8.7]: https://github.com/swaggest/json-cli/compare/v1.8.6...v1.8.7
239 | [1.8.6]: https://github.com/swaggest/json-cli/compare/v1.8.5...v1.8.6
240 | [1.8.5]: https://github.com/swaggest/json-cli/compare/v1.8.4...v1.8.5
241 | [1.8.4]: https://github.com/swaggest/json-cli/compare/v1.8.3...v1.8.4
242 | [1.8.3]: https://github.com/swaggest/json-cli/compare/v1.8.2...v1.8.3
243 | [1.8.2]: https://github.com/swaggest/json-cli/compare/v1.8.1...v1.8.2
244 | [1.8.1]: https://github.com/swaggest/json-cli/compare/v1.8.0...v1.8.1
245 | [1.8.0]: https://github.com/swaggest/json-cli/compare/v1.7.13...v1.8.0
246 | [1.7.13]: https://github.com/swaggest/json-cli/compare/v1.7.12...v1.7.13
247 | [1.7.12]: https://github.com/swaggest/json-cli/compare/v1.7.11...v1.7.12
248 | [1.7.11]: https://github.com/swaggest/json-cli/compare/v1.7.10...v1.7.11
249 | [1.7.10]: https://github.com/swaggest/json-cli/compare/v1.7.9...v1.7.10
250 | [1.7.9]: https://github.com/swaggest/json-cli/compare/v1.7.8...v1.7.9
251 | [1.7.8]: https://github.com/swaggest/json-cli/compare/v1.7.7...v1.7.8
252 | [1.7.7]: https://github.com/swaggest/json-cli/compare/v1.7.6...v1.7.7
253 | [1.7.6]: https://github.com/swaggest/json-cli/compare/v1.7.5...v1.7.6
254 | [1.7.5]: https://github.com/swaggest/json-cli/compare/v1.7.4...v1.7.5
255 | [1.7.4]: https://github.com/swaggest/json-cli/compare/v1.7.3...v1.7.4
256 | [1.7.3]: https://github.com/swaggest/json-cli/compare/v1.7.2...v1.7.3
257 | [1.7.2]: https://github.com/swaggest/json-cli/compare/v1.7.1...v1.7.2
258 | [1.7.1]: https://github.com/swaggest/json-cli/compare/v1.7.0...v1.7.1
259 | [1.7.0]: https://github.com/swaggest/json-cli/compare/v1.6.8...v1.7.0
260 | [1.6.8]: https://github.com/swaggest/json-cli/compare/v1.6.7...v1.6.8
261 | [1.6.7]: https://github.com/swaggest/json-cli/compare/v1.6.6...v1.6.7
262 | [1.6.6]: https://github.com/swaggest/json-cli/compare/v1.6.5...v1.6.6
263 | [1.6.5]: https://github.com/swaggest/json-cli/compare/v1.6.4...v1.6.5
264 | [1.6.4]: https://github.com/swaggest/json-cli/compare/v1.6.3...v1.6.4
265 | [1.6.3]: https://github.com/swaggest/json-cli/compare/v1.6.2...v1.6.3
266 | [1.6.2]: https://github.com/swaggest/json-cli/compare/v1.6.1...v1.6.2
267 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # JSON CLI multitool
2 |
3 |
4 |
5 | A CLI app to find unordered diff between two `JSON` documents (based
6 | on [`swaggest/json-diff`](https://github.com/swaggest/json-diff)), generate JSON Schema and Go/PHP code, pretty print,
7 | minify, yaml convert, etc....
8 |
9 | [](https://travis-ci.org/swaggest/json-cli)
10 | [](https://scrutinizer-ci.com/g/swaggest/json-cli/?branch=master)
11 | [](https://codeclimate.com/github/swaggest/json-cli)
12 | [](https://scrutinizer-ci.com/g/swaggest/json-cli/code-structure/master/code-coverage)
13 | [](https://microbadger.com/images/swaggest/json-cli)
14 | 
15 | 
16 |
17 | ## Purpose
18 |
19 | * To simplify changes review between two `JSON` files you can use a standard `diff` tool on rearranged
20 | pretty-printed `JSON`.
21 | * To detect breaking changes by analyzing removals and changes from original `JSON`.
22 | * To keep original order of object sets (for
23 | example `swagger.json` [parameters](https://swagger.io/docs/specification/describing-parameters/) list).
24 | * To make and apply JSON Patches, specified in [RFC 6902](http://tools.ietf.org/html/rfc6902) from the IETF.
25 | * To convert between YAML/JSON/PHP serialization.
26 | * To resolve `JSON Pointer` to data.
27 | * To resolve `JSON Pointer` to file position.
28 | * To validate JSON data against [`JSON Schema`](http://json-schema.org/).
29 | * To [generate or update](#buildschema) JSON Schema with instance value(s).
30 | * To [generate](#genjson) sample JSON value from JSON Schema.
31 | * To [render](#gengo) `JSON Schema` as [`Go`](http://golang.org/) structure.
32 | * To [render](#genphp) `JSON Schema` as `PHP` classes.
33 | * To [render](#genjsdoc) `JSON Schema` as `JSDoc` type definitions.
34 | * To [render](#genmarkdown) `JSON Schema` as `Markdown` documentation.
35 |
36 | ## Installation
37 |
38 | ### Docker
39 |
40 | ```
41 | docker run swaggest/json-cli json-cli --help
42 | v1.6.1 json-cli
43 | JSON CLI tool, https://github.com/swaggest/json-cli
44 | ...
45 | ```
46 |
47 | `json-cli` can load schema from stdin (using `-` as a file path) which can be handy with docker, for example:
48 |
49 | ```
50 | cat ./tests/assets/swagger-schema.json | docker run -i --rm swaggest/json-cli json-cli gen-jsdoc -
51 | ```
52 |
53 | ### Composer
54 |
55 | [Install PHP Composer](https://getcomposer.org/doc/00-intro.md)
56 |
57 | ```bash
58 | composer require swaggest/json-cli
59 | ```
60 |
61 | ## CLI tool
62 |
63 | ### Usage
64 |
65 | ```
66 | v1.9.0 json-cli
67 | JSON CLI tool, https://github.com/swaggest/json-cli
68 | Usage:
69 | json-cli
70 | action Action name
71 | Allowed values: diff, apply, rearrange, diff-info, pretty-print, minify, replace, resolve,
72 | resolve-pos, validate-schema, gen-go, gen-php, gen-jsdoc, gen-json, build-schema
73 | ```
74 |
75 | Input paths can be .json/.yaml/.yml/.serialized files, file format is detected by file extension:
76 |
77 | * `.json` JSON
78 | * `.yaml`, `.yml` YAML
79 | * `.serialized` PHP serialization format
80 |
81 | #### Diff, make `JSON Patch` from two documents
82 |
83 | ```
84 | v1.3.0 json-cli diff
85 | JSON CLI tool, https://github.com/swaggest/json-cli
86 | Make patch from two json documents, output to STDOUT
87 | Usage:
88 | json-cli diff
89 | originalPath Path to old (original) json file
90 | newPath Path to new json file
91 |
92 | Options:
93 | --rearrange-arrays Rearrange arrays to match original
94 | --pretty Pretty-print result JSON
95 | --output