Skip to content

Commit dbdd965

Browse files
committed
implement cli tool for validating against OpenAPI schema v3
1 parent ddbd551 commit dbdd965

12 files changed

+3094
-1
lines changed

Makefile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ install:
1515
test:
1616
vendor/bin/phpunit
1717

18+
# copy openapi3 json schema
19+
schemas/openapi-v3.0.json: vendor/oai/openapi-specification/schemas/v3.0/schema.json
20+
cp $< $@
21+
22+
schemas/openapi-v3.0.yaml: vendor/oai/openapi-specification/schemas/v3.0/schema.yaml
23+
cp $< $@
24+
25+
1826
# find spec classes that are not mentioned in tests with @covers yet
1927
coverage: .php-openapi-covA .php-openapi-covB
2028
diff $^

bin/php-openapi

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
/**
5+
* PHP OpenAPI validation tool
6+
*
7+
* @copyright Copyright (c) 2018 Carsten Brandt <[email protected]> and contributors
8+
* @license https://github.com/cebe/php-openapi/blob/master/LICENSE
9+
*/
10+
11+
$composerAutoload = [
12+
__DIR__ . '/../vendor/autoload.php', // standalone with "composer install" run
13+
__DIR__ . '/../../../autoload.php', // script is installed as a composer binary
14+
];
15+
foreach ($composerAutoload as $autoload) {
16+
if (file_exists($autoload)) {
17+
require($autoload);
18+
break;
19+
}
20+
}
21+
22+
// Send all errors to stderr
23+
ini_set('display_errors', 'stderr');
24+
// open streams if not in CLI sapi
25+
defined('STDOUT') or define('STDOUT', fopen('php://stdout', 'w'));
26+
defined('STDERR') or define('STDERR', fopen('php://stderr', 'w'));
27+
28+
$command = null;
29+
$inputFile = null;
30+
$inputFormat = null;
31+
$outputFile = null;
32+
$outputFormat = null;
33+
foreach($argv as $k => $arg) {
34+
if ($k == 0) {
35+
continue;
36+
}
37+
if ($arg[0] == '-' || $arg === 'help') {
38+
$arg = explode('=', $arg);
39+
switch($arg[0]) {
40+
case '--read-yaml':
41+
if ($inputFormat === null) {
42+
$inputFormat = 'yaml';
43+
} else {
44+
error("Conflicting arguments: only one of --read-json or --read-yaml is allowed!", "usage");
45+
}
46+
break;
47+
case '--read-json':
48+
if ($inputFormat === null) {
49+
$inputFormat = 'json';
50+
} else {
51+
error("Conflicting arguments: only one of --read-json or --read-yaml is allowed!", "usage");
52+
}
53+
break;
54+
case '--write-yaml':
55+
if ($outputFormat === null) {
56+
$outputFormat = 'yaml';
57+
} else {
58+
error("Conflicting arguments: only one of --write-json or --write-yaml is allowed!", "usage");
59+
}
60+
break;
61+
case '--write-json':
62+
if ($outputFormat === null) {
63+
$outputFormat = 'json';
64+
} else {
65+
error("Conflicting arguments: only one of --write-json or --write-yaml is allowed!", "usage");
66+
}
67+
break;
68+
case '-h':
69+
case '--help':
70+
case 'help':
71+
print_formatted(
72+
"\BPHP OpenAPI 3 tool\C\n"
73+
. "\B------------------\C\n"
74+
. "by Carsten Brandt <[email protected]>\n\n",
75+
STDERR
76+
);
77+
usage();
78+
break;
79+
default:
80+
error("Unknown argument " . $arg[0], "usage");
81+
}
82+
} else {
83+
if ($command === null) {
84+
$command = $arg;
85+
} elseif ($inputFile === null) {
86+
$inputFile = $arg;
87+
} elseif ($outputFile === null) {
88+
if ($command !== 'convert') {
89+
error("Too many arguments: " . $arg, "usage");
90+
}
91+
$outputFile = $arg;
92+
} else {
93+
error("Too many arguments: " . $arg, "usage");
94+
}
95+
}
96+
}
97+
switch ($command) {
98+
case 'validate':
99+
100+
$openApi = read_input($inputFile, $inputFormat);
101+
102+
// Validate
103+
104+
$openApi->validate();
105+
$errors = $openApi->getErrors();
106+
107+
$validator = new JsonSchema\Validator;
108+
$validator->validate($openApi->getSerializableData(), (object)['$ref' => 'file://' . dirname(__DIR__) . '/schemas/openapi-v3.0.json']);
109+
110+
if ($validator->isValid() && empty($errors)) {
111+
print_formatted("The supplied API Description \B\Gvalidates\C against the OpenAPI v3.0 schema.\n", STDERR);
112+
exit(0);
113+
}
114+
115+
if (!empty($errors)) {
116+
print_formatted("\BErrors found while reading the API Description:\C\n", STDERR);
117+
foreach ($errors as $error) {
118+
fwrite(STDERR, "- $error\n");
119+
}
120+
}
121+
if (!$validator->isValid()) {
122+
print_formatted("\BOpenAPI v3.0 schema violations:\C\n", STDERR);
123+
foreach ($validator->getErrors() as $error) {
124+
print_formatted(sprintf("- [\Y%s\C] %s\n", $error['property'], $error['message']), STDERR);
125+
}
126+
}
127+
exit(2);
128+
129+
break;
130+
case 'convert':
131+
132+
$openApi = read_input($inputFile, $inputFormat);
133+
134+
if ($outputFile === null) {
135+
if ($outputFormat === null) {
136+
error("No output fromat specified, please specify --write-json or --write-yaml.", "usage");
137+
} elseif ($outputFormat === 'json') {
138+
fwrite(STDOUT, \cebe\openapi\Writer::writeToJson($openApi));
139+
} else {
140+
fwrite(STDOUT, \cebe\openapi\Writer::writeToYaml($openApi));
141+
}
142+
fclose(STDOUT);
143+
exit(0);
144+
}
145+
146+
if ($outputFormat === null) {
147+
if (strtolower(substr($outputFile, -5, 5)) === '.json') {
148+
$outputFormat = 'json';
149+
} elseif (strtolower(substr($outputFile, -5, 5)) === '.yaml') {
150+
$outputFormat = 'yaml';
151+
} elseif (strtolower(substr($outputFile, -4, 4)) === '.yml') {
152+
$outputFormat = 'yaml';
153+
} else {
154+
error("Failed to detect output format from file name, please specify --write-json or --write-yaml.", "usage");
155+
}
156+
}
157+
if ($outputFormat === 'json') {
158+
\cebe\openapi\Writer::writeToJsonFile($openApi, $outputFile);
159+
} else {
160+
\cebe\openapi\Writer::writeToYamlFile($openApi, $outputFile);
161+
}
162+
exit(0);
163+
164+
break;
165+
case null:
166+
error("No command specified.", "usage");
167+
break;
168+
default:
169+
error("Unknown command " . $command, "usage");
170+
}
171+
172+
173+
174+
// functions
175+
176+
function read_input($inputFile, $inputFormat)
177+
{
178+
try {
179+
if ($inputFile === null) {
180+
$fileContent = file_get_contents("php://stdin");
181+
if ($inputFormat === null) {
182+
$inputFormat = (ltrim($fileContent) === '{' && rtrim($fileContent) === '}') ? 'json' : 'yaml';
183+
}
184+
if ($inputFormat === 'json') {
185+
$openApi = \cebe\openapi\Reader::readFromJson($fileContent);
186+
} else {
187+
$openApi = \cebe\openapi\Reader::readFromYaml($fileContent);
188+
}
189+
} else {
190+
if (!file_exists($inputFile)) {
191+
error("File does not exist: " . $inputFile);
192+
}
193+
if ($inputFormat === null) {
194+
if (strtolower(substr($inputFile, -5, 5)) === '.json') {
195+
$inputFormat = 'json';
196+
} elseif (strtolower(substr($inputFile, -5, 5)) === '.yaml') {
197+
$inputFormat = 'yaml';
198+
} elseif (strtolower(substr($inputFile, -4, 4)) === '.yml') {
199+
$inputFormat = 'yaml';
200+
} else {
201+
error("Failed to detect input format from file name, please specify --read-json or --read-yaml.", "usage");
202+
}
203+
}
204+
if ($inputFormat === 'json') {
205+
$openApi = \cebe\openapi\Reader::readFromJsonFile(realpath($inputFile));
206+
} else {
207+
$openApi = \cebe\openapi\Reader::readFromYamlFile(realpath($inputFile));
208+
}
209+
}
210+
} catch (Symfony\Component\Yaml\Exception\ParseException $e) {
211+
error($e->getMessage());
212+
exit(1);
213+
}
214+
return $openApi;
215+
}
216+
217+
/**
218+
* Display usage information
219+
*/
220+
function usage() {
221+
global $argv;
222+
$cmd = basename($argv[0]);
223+
print_formatted(<<<EOF
224+
Usage:
225+
$cmd \B<command>\C [\Y<options>\C] [\Ginput.yml\C|\Ginput.json\C] [\Goutput.yml\C|\Goutput.json\C]
226+
227+
The following commands are available:
228+
229+
\Bvalidate\C Validate the API description in the specified \Ginput file\C against the OpenAPI v3.0 schema.
230+
Note: the validation is performed in two steps. The results is composed of
231+
(1) structural errors found while reading the API description file, and
232+
(2) violations of the OpenAPI v3.0 schema.
233+
234+
If no input file is specified input will be read from STDIN.
235+
The tool will try to auto-detect the content type of the input, but may fail
236+
to do so, you may specify \Y--read-yaml\C or \Y--read-json\C to force the file type.
237+
238+
Exits with code 2 on validation errors, 1 on other errors and 0 on success.
239+
240+
\Bconvert\C Convert a JSON or YAML input file to JSON or YAML output file.
241+
References are being resolved so the output will be a single specification file.
242+
243+
If no input file is specified input will be read from STDIN.
244+
If no output file is specified output will be written to STDOUT.
245+
The tool will try to auto-detect the content type of the input and output file, but may fail
246+
to do so, you may specify \Y--read-yaml\C or \Y--read-json\C to force the input file type.
247+
and \Y--write-yaml\C or \Y--write-json\C to force the output file type.
248+
249+
\Bhelp\C Shows this usage information.
250+
251+
Options:
252+
253+
\Y--read-json\C force reading input as JSON. Auto-detect if not specified.
254+
\Y--read-yaml\C force reading input as YAML. Auto-detect if not specified.
255+
\Y--write-json\C force writing output as JSON. Auto-detect if not specified.
256+
\Y--write-yaml\C force writing output as YAML. Auto-detect if not specified.
257+
258+
259+
EOF
260+
, STDERR
261+
);
262+
exit(1);
263+
}
264+
265+
/**
266+
* Send custom error message to stderr
267+
* @param $message string
268+
* @param $callback mixed called before script exit
269+
* @return void
270+
*/
271+
function error($message, $callback = null) {
272+
print_formatted("\B\RError\C: " . $message . "\n", STDERR);
273+
if (is_callable($callback)) {
274+
call_user_func($callback);
275+
}
276+
exit(1);
277+
}
278+
279+
function print_formatted($string, $stream) {
280+
fwrite($stream, strtr($string, [
281+
'\\Y' => "\033[33m", // yellow
282+
'\\G' => "\033[32m", // green
283+
'\\R' => "\033[31m", // green
284+
'\\B' => "\033[1m", // bold
285+
'\\C' => "\033[0m", // clear
286+
]));
287+
}

composer.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"require": {
2121
"php": ">=7.1.0",
2222
"ext-json": "*",
23-
"symfony/yaml": "^4.0"
23+
"symfony/yaml": "^4.0",
24+
"justinrainbow/json-schema": "^5.0"
2425
},
2526
"require-dev": {
2627
"cebe/indent": "*",
@@ -43,6 +44,9 @@
4344
"dev-master": "1.0.x-dev"
4445
}
4546
},
47+
"bin": [
48+
"bin/php-openapi"
49+
],
4650
"repositories": [
4751
{
4852
"type": "package",

0 commit comments

Comments
 (0)