Last active
June 25, 2019 15:25
-
-
Save ossobuffo/aad5c65f7c9a23888b18156b324ff210 to your computer and use it in GitHub Desktop.
Script to convert SmartDocs export JSON to OpenAPI JSON
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* SmartDocs2OpenAPI | |
* | |
* @author [email protected] | |
* | |
* IMPORTANT: Apigee and Google do not offer support for this script. It is | |
* only provided to customers as a courtesy. | |
* | |
* This script takes the output of a SmartDocs model export, and attempts to | |
* create an OpenAPI JSON document out of it, with varying levels of success. | |
* Since SmartDocs saves much OpenAPI metadata in JSON string blobs in | |
* customAttributes, we are able to much more cleanly re-create a complete-ish | |
* OpenAPI document. Models which were created by hand, or which were | |
* originally imported as WADL, will have many fields with a value of FIXME. | |
* | |
* If you compare an original OpenAPI doc with the output of this script, | |
* you'll notice a number of things that are likely to be different: | |
* | |
* - Model-level tags are missing. SmartDocs ignores these on import, so they | |
* are not available in the export JSON. | |
* - Resources and methods do not occur in a deterministic order. | |
* - "200 OK" responses are only shown if the response body is declared with a | |
* schema. | |
* | |
* The output of this script should be regarded as a starting point. You should | |
* always carefully verify that the resulting OpenAPI document describes your | |
* API accurately. In particular, you should search the output for the string | |
* ‘FIXME’ — this indicates required information that could not be properly | |
* determined from the SmartDocs output JSON. | |
* | |
* An important caveat: If a security scheme has multiple scopes, and a method | |
* refers to that security scheme, we make a best-effort to set the scopes | |
* properly as they apply to the method, but there is room for error here. If | |
* the exact scopes of a security scheme can't be accurately determined, by | |
* default we apply *ALL* of the scopes in that scheme to the method. You | |
* should therefore be careful to check the security settings of your methods | |
* before doing anything with the resulting document. | |
* | |
* === LICENSE === | |
* | |
* Copyright (c) 2017 Google Corporation. | |
* | |
* Permission is hereby granted, free of charge, to any person obtaining a copy | |
* of this software and associated documentation files (the "Software"), to | |
* deal in the Software without restriction, including without limitation the | |
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or | |
* sell copies of the Software, and to permit persons to whom the Software is | |
* furnished to do so, subject to the following conditions: | |
* | |
* The above copyright notice and this permission notice shall be included in | |
* all copies or substantial portions of the Software. | |
* | |
* This 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 or copyright holders 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. | |
*/ | |
if ($argc < 2) { | |
print "Usage: " . $argv[0] . " <filename.json>\n"; | |
exit(0); | |
} | |
// Ensure that the file we were passed is valid. | |
if (!is_file($argv[1])) { | |
file_put_contents(STDERR, 'File ‘' . $argv[1] . "’ not found.\n"); | |
exit(1); | |
} | |
if (!is_readable($argv[1])) { | |
file_put_contents(STDERR, 'File ‘' . $argv[1] . "’ not readable.\n"); | |
exit(1); | |
} | |
$raw = file_get_contents($argv[1]); | |
$json = json_decode($raw, TRUE); | |
if (!is_array($json) || !isset($json['baseUrl']) || !isset($json['resources'])) { | |
file_put_contents(STDERR, 'File ‘' . $argv[1] . "’ does not appear to be valid SmartDocs JSON.\n"); | |
exit(1); | |
} | |
// Start creating the OpenAPI document. | |
$output = ['swagger' => '2.0']; | |
// Default info block, may be overwritten if we can ascertain required values. | |
$output['info'] = [ | |
'description' => 'FIXME', | |
'version' => (isset($json['releaseVersion']) ? $json['releaseVersion'] : 'FIXME'), | |
'title' => 'FIXME', | |
]; | |
// If the original file was OpenAPI, we may be able to read more metadata. | |
if (isset($json['customAttributes']['SWAGGER_INFO'])) { | |
$raw_info = json_decode($json['customAttributes']['SWAGGER_INFO'], TRUE); | |
if (is_array($raw_info)) { | |
$output['info'] = $raw_info + $output['info']; | |
} | |
} | |
$host = parse_url($json['baseUrl'], PHP_URL_HOST); | |
$output['host'] = empty($host) ? 'FIXME' : $host; | |
$basepath = parse_url($json['baseUrl'], PHP_URL_PATH); | |
if (!empty($basepath)) { | |
$output['basePath'] = $basepath; | |
} | |
$scheme = parse_url($json['baseUrl'], PHP_URL_SCHEME); | |
$output['schemes'] = [$scheme]; | |
$output['paths'] = []; | |
$securityDefs = NULL; | |
// If present, security definitions will be written to the end of the | |
// OpenAPI document, but we need to read them here. | |
if (!empty($json['customAttributes']['SWAGGER_AUTH'])) { | |
$securityDefs = json_decode($json['customAttributes']['SWAGGER_AUTH'], TRUE); | |
} | |
$defs = []; | |
// Cycle through all resources in the original document. | |
foreach ($json['resources'] as $resource) { | |
$path = $resource['path']; | |
if (!empty($resource['customAttributes']['SWAGGER_PROTOCOLS'])) { | |
$protos = json_decode($resource['customAttributes']['SWAGGER_PROTOCOLS']); | |
if (is_array($protos)) { | |
foreach ($protos as $proto) { | |
if (!in_array($proto, $output['schemes'])) { | |
$output['schemes'][] = $proto; | |
} | |
} | |
} | |
} | |
// Handle parameters on the resource level, if any are present. | |
if (!empty($resource['parameters'])) { | |
foreach ($resource['parameters'] as $r_param) { | |
$output['paths'][$path]['parameters'][] = parse_parameter($r_param); | |
} | |
} | |
// Parse each method in the resource. | |
foreach ($resource['methods'] as $method_in) { | |
$verb = strtolower($method_in['verb']); | |
$method_out = ['operationId' => $method_in['name']]; | |
if (isset($method_in['customAttributes']['SWAGGER_METHOD_SUMMARY'])) { | |
$method_out['summary'] = $method_in['customAttributes']['SWAGGER_METHOD_SUMMARY']; | |
} | |
else { | |
// This is a fallback. | |
$method_out['summary'] = $method_in['name']; | |
} | |
$method_out['description'] = $method_in['description']; | |
if (!empty($method_in['tags']) && is_array($method_in['tags'])) { | |
// Note: method-level tags are supported; model-level tags aren't. | |
$method_out['tags'] = $method_in['tags']; | |
} | |
// MIME types produced by the method. | |
$produces = NULL; | |
if (isset($method_in['customAttributes']['SWAGGER_PRODUCES'])) { | |
$produces_in = json_decode($method_in['customAttributes']['SWAGGER_PRODUCES']); | |
if (is_array($produces_in) && array_keys($produces_in)[0] == 0) { | |
$produces = $produces_in; | |
} | |
} | |
elseif (isset($method_in['body']['accept'])) { | |
$produces = explode(',', $method_in['body']['accept']); | |
} | |
// If we can't determine the content-type, throw a FIXME. | |
$method_out['produces'] = (empty($produces) ? ['FIXME'] : $produces); | |
// If we specify values for the Accept header, parse them. | |
if (isset($method_in['body']['contentType'])) { | |
$method_out['consumes'] = explode(',', $method_in['body']['contentType']); | |
} | |
// Start parsing response codes. 200 is a special case. | |
if (!empty($method_in['response']['schema']['dataType'])) { | |
$type = json_decode($method_in['response']['schema']['dataType'], TRUE); | |
if (is_array($type)) { | |
$method_out['responses']['200'] = ['description' => 'Success', 'schema' => $type]; | |
} | |
} | |
// Parse 4xx and 5xx response codes. | |
if (!empty($method_in['response']['errors'])) { | |
foreach ($method_in['response']['errors'] as $error) { | |
$method_out['responses'][(string)$error['code']]['description'] = $error['description']; | |
} | |
} | |
if (empty($method_out['responses'])) { | |
$method_out['responses']['default'] = ['description' => 'Success']; | |
} | |
// Check for auth on this method. | |
$security = NULL; | |
if (isset($method_in['customAttributes']['SWAGGER_METHOD_AUTH'])) { | |
$security = json_decode($method_in['customAttributes']['SWAGGER_METHOD_AUTH'], TRUE); | |
} | |
elseif (!empty($method_in['security'])) { | |
$security = []; | |
foreach ($method_in['security'] as $sec_scheme) { | |
if (!is_array($securityDefs) || !isset($securityDefs[$sec_scheme]['scopes'])) { | |
$security[] = [$sec_scheme => []]; | |
} | |
else { | |
// Assume all scopes here. Possibly erroneous; caveat emptor. | |
$security[] = [$sec_scheme => $securityDefs[$sec_scheme]['scopes']]; | |
} | |
} | |
} | |
if (!empty($security)) { | |
$method_out['security'] = $security; | |
} | |
// Treat method parameters and body parameters the same. | |
$local_parameters = []; | |
if (!empty($method_in['parameters'])) { | |
$local_parameters = array_merge($local_parameters, $method_in['parameters']); | |
} | |
if (!empty($method_in['body']['parameters'])) { | |
$local_parameters = array_merge($local_parameters, $method_in['body']['parameters']); | |
} | |
$method_out['parameters'] = []; | |
if (!empty($local_parameters)) { | |
foreach ($local_parameters as $parameter_in) { | |
$method_out['parameters'][] = parse_parameter($parameter_in, $method_in); | |
} | |
} | |
// If an OpenAPI schema definition is embedded, make sure all referenced | |
// definitions make it into the top-level definitions collection. | |
if (isset($method_in['apiSchema']['expandedSchema'])) { | |
$schema = json_decode($method_in['apiSchema']['expandedSchema'], TRUE); | |
if (is_array($schema)) { | |
foreach ($schema as $key => $detail) { | |
if (is_string($key) && !array_key_exists($key, $defs)) { | |
$defs[$key] = $detail; | |
} | |
} | |
} | |
} | |
// Add all our compiled info for this method to our master output. | |
$output['paths'][$path][$verb] = $method_out; | |
} | |
} | |
if (!empty($securityDefs)) { | |
$output['securityDefinitions'] = $securityDefs; | |
} | |
if (!empty($defs)) { | |
$output['definitions'] = $defs; | |
} | |
echo json_encode($output, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n"; | |
exit; | |
/** | |
* Parses a SmartDocs parameter and generates an OpenAPI parameter from it. | |
* | |
* @param array $param_in | |
* Descriptor of the parameter as SmartDocs has stored it. | |
* @param array $method_in | |
* Descriptor of the method owning the parameter. | |
* | |
* @return array | |
* An OpenAPI-formatted descriptor of the parameter. | |
*/ | |
function parse_parameter(array $param_in, array $method_in = NULL) { | |
if ($param_in['type'] == 'template') { | |
$param_in['type'] = 'path'; | |
} | |
$parameter_out = [ | |
'name' => $param_in['name'], | |
'in' => strtolower($param_in['type']), | |
'description' => isset($param_in['description']) ? $param_in['description'] : '', | |
'required' => (bool)$param_in['required'], | |
'type' => $param_in['dataType'], | |
]; | |
if (empty($parameter_out['description']) && ($param_in['type']) == 'body') { | |
if (!empty($method_in['body']['doc'])) { | |
$parameter_out['description'] = $method_in['body']['doc']; | |
} | |
} | |
if (isset($param_in['schema'])) { | |
$schema = json_decode($param_in['schema'], TRUE); | |
if (is_array($schema)) { | |
unset($parameter_out['type']); | |
$parameter_out['schema'] = $schema; | |
} | |
} | |
elseif ($param_in['type'] == 'body' && isset($method_in['response']['schema']['dataType'])) { | |
$schema = json_decode($method_in['response']['schema']['dataType']); | |
if (is_array($schema)) { | |
unset($parameter_out['type']); | |
$parameter_out['schema'] = $schema; | |
} | |
} | |
if (isset($method_in['body']['contentType'])) { | |
$content_types = explode(',', $method_in['body']['contentType']); | |
} | |
else { | |
$content_types = []; | |
} | |
if ( | |
$param_in['type'] == 'body' | |
&& (in_array('application/x-www-form-urlencoded', $content_types) || in_array('multipart/form-data', $content_types)) | |
&& $parameter_out['in'] == 'body' | |
) { | |
$parameter_out['in'] = 'formData'; | |
} | |
if (isset($param_in['defaultValue'])) { | |
$parameter_out['default'] = $param_in['defaultValue']; | |
} | |
if (isset($param_in['items'])) { | |
$items = json_decode($param_in['items'], TRUE); | |
if (is_array($items)) { | |
$parameter_out['items'] = $items; | |
} | |
} | |
if (isset($param_in['allowMultiple']) && $param_in['allowMultiple']) { | |
$parameter_out['collectionFormat'] = 'multi'; | |
} | |
if (isset($param_in['options'])) { | |
$parameter_out['enum'] = $param_in['options']; | |
} | |
return $parameter_out; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment