Skip to content

Instantly share code, notes, and snippets.

@ossobuffo
Last active June 25, 2019 15:25
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ossobuffo/aad5c65f7c9a23888b18156b324ff210 to your computer and use it in GitHub Desktop.
Save ossobuffo/aad5c65f7c9a23888b18156b324ff210 to your computer and use it in GitHub Desktop.
Script to convert SmartDocs export JSON to OpenAPI JSON
<?php
/**
* SmartDocs2OpenAPI
*
* @author danielejohnson@google.com
*
* 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