summaryrefslogtreecommitdiff
path: root/rest-api-templates
diff options
context:
space:
mode:
authorDavid M. Lee <dlee@digium.com>2013-07-03 16:32:41 +0000
committerDavid M. Lee <dlee@digium.com>2013-07-03 16:32:41 +0000
commitc9a3d4562ddb1ed5b34f7d5530efd6aa695377c2 (patch)
treedd082285fbb5c7714164e26145acc5c966e663be /rest-api-templates
parentdcf03554a0b38806bf1fe258acc423b070533d6e (diff)
Update events to use Swagger 1.3 subtyping, and related aftermath
This patch started with the simple idea of changing the /events data model to be more sane. The original model would send out events like: { "stasis_start": { "args": [], "channel": { ... } } } The event discriminator was the field name instead of being a value in the object, due to limitations in how Swagger 1.1 could model objects. While technically sufficient in communicating event information, it was really difficult to deal with in terms of client side JSON handling. This patch takes advantage of a proposed extension[1] to Swagger which allows type variance through the use of a discriminator field. This had a domino effect that made this a surprisingly large patch. [1]: https://groups.google.com/d/msg/wordnik-api/EC3rGajE0os/ey_5dBI_jWcJ In changing the models, I also had to change the swagger_model.py processor so it can handle the type discriminator and subtyping. I took that a big step forward, and using that information to generate an ari_model module, which can validate a JSON object against the Swagger model. The REST and WebSocket generators were changed to take advantage of the validators. If compiled with AST_DEVMODE enabled, JSON objects that don't match their corresponding models will not be sent out. For REST API calls, a 500 Internal Server response is sent. For WebSockets, the invalid JSON message is replaced with an error message. Since this took over about half of the job of the existing JSON generators, and the .to_json virtual function on messages took over the other half, I reluctantly removed the generators. The validators turned up all sorts of errors and inconsistencies in our data models, and the code. These were cleaned up, with checks in the code generator avoid some of the consistency problems in the future. * The model for a channel snapshot was trimmed down to match the information sent via AMI. Many of the field being sent were not useful in the general case. * The model for a bridge snapshot was updated to be more consistent with the other ARI models. Another impact of introducing subtyping was that the swagger-codegen documentation generator was insufficient (at least until it catches up with Swagger 1.2). I wanted it to be easier to generate docs for the API anyways, so I ported the wiki pages to use the Asterisk Swagger generator. In the process, I was able to clean up many of the model links, which would occasionally give inconsistent results on the wiki. I also added error responses to the wiki docs, making the wiki documentation more complete. Finally, since Stasis-HTTP will now be named Asterisk REST Interface (ARI), any new functions and files I created carry the ari_ prefix. I changed a few stasis_http references to ari where it was non-intrusive and made sense. (closes issue ASTERISK-21885) Review: https://reviewboard.asterisk.org/r/2639/ git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@393529 65c4cc65-6c06-0410-ace0-fbb531ad65f3
Diffstat (limited to 'rest-api-templates')
-rw-r--r--rest-api-templates/api.wiki.mustache47
-rw-r--r--rest-api-templates/ari_model_validators.c.mustache117
-rw-r--r--rest-api-templates/ari_model_validators.h.mustache159
-rw-r--r--rest-api-templates/asterisk_processor.py87
-rw-r--r--rest-api-templates/event_function_decl.mustache10
-rwxr-xr-xrest-api-templates/make_ari_stubs.py (renamed from rest-api-templates/make_stasis_http_stubs.py)27
-rw-r--r--rest-api-templates/models.wiki.mustache22
-rw-r--r--rest-api-templates/res_stasis_http_resource.c.mustache53
-rw-r--r--rest-api-templates/res_stasis_json_resource.c.mustache151
-rw-r--r--rest-api-templates/res_stasis_json_resource.exports.mustache12
-rw-r--r--rest-api-templates/stasis_json_resource.h.mustache83
-rw-r--r--rest-api-templates/swagger_model.py338
-rw-r--r--rest-api-templates/transform.py15
13 files changed, 750 insertions, 371 deletions
diff --git a/rest-api-templates/api.wiki.mustache b/rest-api-templates/api.wiki.mustache
new file mode 100644
index 000000000..c70e58fc3
--- /dev/null
+++ b/rest-api-templates/api.wiki.mustache
@@ -0,0 +1,47 @@
+{{#api_declaration}}
+h1. {{name_title}}
+
+|| Method || Path || Return Model || Summary ||
+{{#apis}}
+{{#operations}}
+| {{http_method}} | [{{wiki_path}}|#{{nickname}}] | {{#response_class}}{{#is_primitive}}{{name}}{{/is_primitive}}{{^is_primitive}}[{{wiki_name}}|{{wiki_prefix}} REST Data Models#{{singular_name}}]{{/is_primitive}}{{/response_class}} | {{summary}} |
+{{/operations}}
+{{/apis}}
+{{#apis}}
+{{#operations}}
+
+{anchor:{{nickname}}}
+h2. {{http_method}} {{wiki_path}}
+
+{{{summary}}}{{#notes}} {{{notes}}}{{/notes}}
+{{#has_path_parameters}}
+
+h3. Path parameters
+{{#path_parameters}}
+* {{name}}: {{data_type}}{{#default_value}} = {{default_value}}{{/default_value}} - {{description}}
+{{/path_parameters}}
+{{/has_path_parameters}}
+{{#has_query_parameters}}
+
+h3. Query parameters
+{{#query_parameters}}
+* {{name}}: {{data_type}}{{#default_value}} = {{default_value}}{{/default_value}} -{{#required}} *(required)*{{/required}} {{description}}
+{{/query_parameters}}
+{{/has_query_parameters}}
+{{#has_header_parameters}}
+
+h3. Header parameters
+{{#header_parameters}}
+* {{name}}: {{data_type}}{{#default_value}} = {{default_value}}{{/default_value}} -{{#required}} *(required)*{{/required}} {{description}}
+{{/header_parameters}}
+{{/has_header_parameters}}
+{{#has_error_responses}}
+
+h3. Error Responses
+{{#error_responses}}
+* {{code}} - {{{reason}}}
+{{/error_responses}}
+{{/has_error_responses}}
+{{/operations}}
+{{/apis}}
+{{/api_declaration}}
diff --git a/rest-api-templates/ari_model_validators.c.mustache b/rest-api-templates/ari_model_validators.c.mustache
new file mode 100644
index 000000000..0e87f8e24
--- /dev/null
+++ b/rest-api-templates/ari_model_validators.c.mustache
@@ -0,0 +1,117 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2013, Digium, Inc.
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+/*! \file
+ *
+ * \brief Generated file - Build validators for ARI model objects.
+ */
+
+ /*
+{{> do-not-edit}}
+ * This file is generated by a mustache template. Please see the original
+ * template in rest-api-templates/ari_model_validators.h.mustache
+ */
+
+#include "asterisk.h"
+
+ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
+
+#include "asterisk/logger.h"
+#include "asterisk/module.h"
+#include "ari_model_validators.h"
+{{#apis}}
+{{#api_declaration}}
+{{#models}}
+
+int ari_validate_{{c_id}}(struct ast_json *json)
+{
+ int res = 1;
+ struct ast_json_iter *iter;
+{{#properties}}
+{{#required}}
+ int has_{{name}} = 0;
+{{/required}}
+{{/properties}}
+{{#has_subtypes}}
+ const char *discriminator;
+
+ discriminator = ast_json_string_get(ast_json_object_get(json, "{{discriminator.name}}"));
+ if (!discriminator) {
+ ast_log(LOG_ERROR, "ARI {{id}} missing required field {{discriminator.name}}");
+ return 0;
+ }
+
+ if (strcmp("{{id}}", discriminator) == 0) {
+ /* Self type; fall through */
+ } else
+{{#subtypes}}
+ if (strcmp("{{id}}", discriminator) == 0) {
+ return ari_validate_{{c_id}}(json);
+ } else
+{{/subtypes}}
+ {
+ ast_log(LOG_ERROR, "ARI {{id}} has undocumented subtype %s\n",
+ discriminator);
+ res = 0;
+ }
+{{/has_subtypes}}
+
+ for (iter = ast_json_object_iter(json); iter; iter = ast_json_object_iter_next(json, iter)) {
+{{#properties}}
+ if (strcmp("{{name}}", ast_json_object_iter_key(iter)) == 0) {
+ int prop_is_valid;
+{{#required}}
+ has_{{name}} = 1;
+{{/required}}
+{{#type}}
+{{#is_list}}
+ prop_is_valid = ari_validate_list(
+ ast_json_object_iter_value(iter),
+ ari_validate_{{c_singular_name}});
+{{/is_list}}
+{{^is_list}}
+ prop_is_valid = ari_validate_{{c_name}}(
+ ast_json_object_iter_value(iter));
+{{/is_list}}
+{{/type}}
+ if (!prop_is_valid) {
+ ast_log(LOG_ERROR, "ARI {{id}} field {{name}} failed validation\n");
+ res = 0;
+ }
+ } else
+{{/properties}}
+ {
+ ast_log(LOG_ERROR,
+ "ARI {{id}} has undocumented field %s\n",
+ ast_json_object_iter_key(iter));
+ res = 0;
+ }
+ }
+
+{{#properties}}
+{{#required}}
+ if (!has_{{name}}) {
+ ast_log(LOG_ERROR, "ARI {{id}} missing required field {{name}}\n");
+ res = 0;
+ }
+
+{{/required}}
+{{/properties}}
+ return res;
+}
+{{/models}}
+{{/api_declaration}}
+{{/apis}}
diff --git a/rest-api-templates/ari_model_validators.h.mustache b/rest-api-templates/ari_model_validators.h.mustache
new file mode 100644
index 000000000..65efbbd85
--- /dev/null
+++ b/rest-api-templates/ari_model_validators.h.mustache
@@ -0,0 +1,159 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2013, Digium, Inc.
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+/*! \file
+ *
+ * \brief Generated file - Build validators for ARI model objects.
+ */
+
+ /*
+{{> do-not-edit}}
+ * This file is generated by a mustache template. Please see the original
+ * template in rest-api-templates/ari_model_validators.h.mustache
+ */
+
+#ifndef _ASTERISK_ARI_MODEL_H
+#define _ASTERISK_ARI_MODEL_H
+
+#include "asterisk/json.h"
+
+/*! @{ */
+
+/*!
+ * \brief Validator for native Swagger void.
+ *
+ * \param json JSON object to validate.
+ * \returns True (non-zero) if valid.
+ * \returns False (zero) if invalid.
+ */
+int ari_validate_void(struct ast_json *json);
+
+/*!
+ * \brief Validator for native Swagger byte.
+ *
+ * \param json JSON object to validate.
+ * \returns True (non-zero) if valid.
+ * \returns False (zero) if invalid.
+ */
+int ari_validate_byte(struct ast_json *json);
+
+/*!
+ * \brief Validator for native Swagger boolean.
+ *
+ * \param json JSON object to validate.
+ * \returns True (non-zero) if valid.
+ * \returns False (zero) if invalid.
+ */
+int ari_validate_boolean(struct ast_json *json);
+
+/*!
+ * \brief Validator for native Swagger int.
+ *
+ * \param json JSON object to validate.
+ * \returns True (non-zero) if valid.
+ * \returns False (zero) if invalid.
+ */
+int ari_validate_int(struct ast_json *json);
+
+/*!
+ * \brief Validator for native Swagger long.
+ *
+ * \param json JSON object to validate.
+ * \returns True (non-zero) if valid.
+ * \returns False (zero) if invalid.
+ */
+int ari_validate_long(struct ast_json *json);
+
+/*!
+ * \brief Validator for native Swagger float.
+ *
+ * \param json JSON object to validate.
+ * \returns True (non-zero) if valid.
+ * \returns False (zero) if invalid.
+ */
+int ari_validate_float(struct ast_json *json);
+
+/*!
+ * \brief Validator for native Swagger double.
+ *
+ * \param json JSON object to validate.
+ * \returns True (non-zero) if valid.
+ * \returns False (zero) if invalid.
+ */
+int ari_validate_double(struct ast_json *json);
+
+/*!
+ * \brief Validator for native Swagger string.
+ *
+ * \param json JSON object to validate.
+ * \returns True (non-zero) if valid.
+ * \returns False (zero) if invalid.
+ */
+int ari_validate_string(struct ast_json *json);
+
+/*!
+ * \brief Validator for native Swagger date.
+ *
+ * \param json JSON object to validate.
+ * \returns True (non-zero) if valid.
+ * \returns False (zero) if invalid.
+ */
+int ari_validate_date(struct ast_json *json);
+
+/*!
+ * \brief Validator for a Swagger List[]/JSON array.
+ *
+ * \param json JSON object to validate.
+ * \param fn Validator to call on every element in the array.
+ * \returns True (non-zero) if valid.
+ * \returns False (zero) if invalid.
+ */
+int ari_validate_list(struct ast_json *json, int (*fn)(struct ast_json *));
+
+/*! @} */
+{{#apis}}
+{{#api_declaration}}
+{{#models}}
+
+/*!
+ * \brief Validator for {{id}}.
+ *
+ * {{{description_dox}}}
+ *
+ * \param json JSON object to validate.
+ * \returns True (non-zero) if valid.
+ * \returns False (zero) if invalid.
+ */
+int ari_validate_{{c_id}}(struct ast_json *json);
+{{/models}}
+{{/api_declaration}}
+{{/apis}}
+
+/*
+ * JSON models
+ *
+{{#apis}}
+{{#api_declaration}}
+{{#models}}
+ * {{id}}
+{{#properties}}
+ * - {{name}}: {{type.name}}{{#required}} (required){{/required}}
+{{/properties}}
+{{/models}}
+{{/api_declaration}}
+{{/apis}} */
+
+#endif /* _ASTERISK_ARI_MODEL_H */
diff --git a/rest-api-templates/asterisk_processor.py b/rest-api-templates/asterisk_processor.py
index af5f5bdfe..0260b6b55 100644
--- a/rest-api-templates/asterisk_processor.py
+++ b/rest-api-templates/asterisk_processor.py
@@ -24,6 +24,11 @@ import re
from swagger_model import *
+try:
+ from collections import OrderedDict
+except ImportError:
+ from odict import OrderedDict
+
def simple_name(name):
"""Removes the {markers} from a path segement.
@@ -35,6 +40,14 @@ def simple_name(name):
return name
+def wikify(str):
+ """Escapes a string for the wiki.
+
+ @param str: String to escape
+ """
+ return re.sub(r'([{}\[\]])', r'\\\1', str)
+
+
def snakify(name):
"""Helper to take a camelCase or dash-seperated name and make it
snake_case.
@@ -107,6 +120,7 @@ class PathSegment(Stringify):
"""
return len(self.__children)
+
class AsteriskProcessor(SwaggerPostProcessor):
"""A SwaggerPostProcessor which adds fields needed to generate Asterisk
RESTful HTTP binding code.
@@ -131,12 +145,17 @@ class AsteriskProcessor(SwaggerPostProcessor):
'double': 'atof',
}
- def process_api(self, resource_api, context):
+ def __init__(self, wiki_prefix):
+ self.wiki_prefix = wiki_prefix
+
+ def process_resource_api(self, resource_api, context):
+ resource_api.wiki_prefix = self.wiki_prefix
# Derive a resource name from the API declaration's filename
resource_api.name = re.sub('\..*', '',
os.path.basename(resource_api.path))
- # Now in all caps, from include guard
+ # Now in all caps, for include guard
resource_api.name_caps = resource_api.name.upper()
+ resource_api.name_title = resource_api.name.capitalize()
# Construct the PathSegement tree for the API.
if resource_api.api_declaration:
resource_api.root_path = PathSegment('', None)
@@ -145,17 +164,6 @@ class AsteriskProcessor(SwaggerPostProcessor):
for operation in api.operations:
segment.operations.append(operation)
api.full_name = segment.full_name
- resource_api.api_declaration.has_events = False
- for model in resource_api.api_declaration.models:
- if model.id == "Event":
- resource_api.api_declaration.has_events = True
- break
- if resource_api.api_declaration.has_events:
- resource_api.api_declaration.events = \
- [self.process_model(model, context) for model in \
- resource_api.api_declaration.models if model.id != "Event"]
- else:
- resource_api.api_declaration.events = []
# Since every API path should start with /[resource], root should
# have exactly one child.
@@ -169,6 +177,9 @@ class AsteriskProcessor(SwaggerPostProcessor):
"API declaration name should match", context)
resource_api.root_full_name = resource_api.root_path.full_name
+ def process_api(self, api, context):
+ api.wiki_path = wikify(api.path)
+
def process_operation(self, operation, context):
# Nicknames are camelcase, Asterisk coding is snake case
operation.c_nickname = snakify(operation.nickname)
@@ -179,7 +190,7 @@ class AsteriskProcessor(SwaggerPostProcessor):
def process_parameter(self, parameter, context):
if not parameter.data_type in self.type_mapping:
raise SwaggerError(
- "Invalid parameter type %s" % paramter.data_type, context)
+ "Invalid parameter type %s" % parameter.data_type, context)
# Parameter names are camelcase, Asterisk convention is snake case
parameter.c_name = snakify(parameter.name)
parameter.c_data_type = self.type_mapping[parameter.data_type]
@@ -191,41 +202,19 @@ class AsteriskProcessor(SwaggerPostProcessor):
parameter.c_space = ' '
def process_model(self, model, context):
+ model.description_dox = model.description.replace('\n', '\n * ')
+ model.description_dox = re.sub(' *\n', '\n', model.description_dox)
model.c_id = snakify(model.id)
- model.channel = False
- model.channel_desc = ""
- model.bridge = False
- model.bridge_desc = ""
- model.properties = [self.process_property(model, prop, context) for prop in model.properties]
- model.properties = [prop for prop in model.properties if prop]
- model.has_properties = (len(model.properties) != 0)
return model
- def process_property(self, model, prop, context):
- # process channel separately since it will be pulled out
- if prop.name == 'channel' and prop.type == 'Channel':
- model.channel = True
- model.channel_desc = prop.description or ""
- return None
-
- # process bridge separately since it will be pulled out
- if prop.name == 'bridge' and prop.type == 'Bridge':
- model.bridge = True
- model.bridge_desc = prop.description or ""
- return None
-
- prop.c_name = snakify(prop.name)
- if prop.type in self.type_mapping:
- prop.c_type = self.type_mapping[prop.type]
- prop.c_convert = self.convert_mapping[prop.c_type]
- else:
- prop.c_type = "Property type %s not mappable to a C type" % (prop.type)
- prop.c_convert = "Property type %s not mappable to a C conversion" % (prop.type)
- #raise SwaggerError(
- # "Invalid property type %s" % prop.type, context)
- # You shouldn't put a space between 'char *' and the variable
- if prop.c_type.endswith('*'):
- prop.c_space = ''
- else:
- prop.c_space = ' '
- return prop
+ def process_property(self, prop, context):
+ if "-" in prop.name:
+ raise SwaggerError("Property names cannot have dashes", context)
+ if prop.name != prop.name.lower():
+ raise SwaggerError("Property name should be all lowercase",
+ context)
+
+ def process_type(self, swagger_type, context):
+ swagger_type.c_name = snakify(swagger_type.name)
+ swagger_type.c_singular_name = snakify(swagger_type.singular_name)
+ swagger_type.wiki_name = wikify(swagger_type.name)
diff --git a/rest-api-templates/event_function_decl.mustache b/rest-api-templates/event_function_decl.mustache
deleted file mode 100644
index fd2c7eb5b..000000000
--- a/rest-api-templates/event_function_decl.mustache
+++ /dev/null
@@ -1,10 +0,0 @@
-struct ast_json *stasis_json_event_{{c_id}}_create(
-{{#bridge}}
- struct ast_bridge_snapshot *bridge_snapshot{{#channel}},{{/channel}}{{^channel}}{{#has_properties}},{{/has_properties}}{{/channel}}
-{{/bridge}}
-{{#channel}}
- struct ast_channel_snapshot *channel_snapshot{{#has_properties}},{{/has_properties}}
-{{/channel}}
-{{#has_properties}}
- struct ast_json *blob
-{{/has_properties}}
diff --git a/rest-api-templates/make_stasis_http_stubs.py b/rest-api-templates/make_ari_stubs.py
index 1114ea46e..6f59e3813 100755
--- a/rest-api-templates/make_stasis_http_stubs.py
+++ b/rest-api-templates/make_ari_stubs.py
@@ -22,7 +22,6 @@ except ImportError:
print >> sys.stderr, "Pystache required. Please sudo pip install pystache."
import os.path
-import pystache
import sys
from asterisk_processor import AsteriskProcessor
@@ -40,23 +39,27 @@ def rel(file):
"""
return os.path.join(TOPDIR, file)
+WIKI_PREFIX = 'Asterisk 12'
+
API_TRANSFORMS = [
+ Transform(rel('api.wiki.mustache'),
+ 'doc/rest-api/%s {{name_title}} REST API.wiki' % WIKI_PREFIX),
Transform(rel('res_stasis_http_resource.c.mustache'),
- 'res_stasis_http_{{name}}.c'),
+ 'res/res_stasis_http_{{name}}.c'),
Transform(rel('stasis_http_resource.h.mustache'),
- 'stasis_http/resource_{{name}}.h'),
+ 'res/stasis_http/resource_{{name}}.h'),
Transform(rel('stasis_http_resource.c.mustache'),
- 'stasis_http/resource_{{name}}.c', False),
- Transform(rel('res_stasis_json_resource.c.mustache'),
- 'res_stasis_json_{{name}}.c'),
- Transform(rel('res_stasis_json_resource.exports.mustache'),
- 'res_stasis_json_{{name}}.exports.in'),
- Transform(rel('stasis_json_resource.h.mustache'),
- 'stasis_json/resource_{{name}}.h'),
+ 'res/stasis_http/resource_{{name}}.c', overwrite=False),
]
RESOURCES_TRANSFORMS = [
- Transform(rel('stasis_http.make.mustache'), 'stasis_http.make'),
+ Transform(rel('models.wiki.mustache'),
+ 'doc/rest-api/%s REST Data Models.wiki' % WIKI_PREFIX),
+ Transform(rel('stasis_http.make.mustache'), 'res/stasis_http.make'),
+ Transform(rel('ari_model_validators.h.mustache'),
+ 'res/stasis_http/ari_model_validators.h'),
+ Transform(rel('ari_model_validators.c.mustache'),
+ 'res/stasis_http/ari_model_validators.c'),
]
@@ -71,7 +74,7 @@ def main(argv):
source = args[1]
dest_dir = args[2]
renderer = pystache.Renderer(search_dirs=[TOPDIR], missing_tags='strict')
- processor = AsteriskProcessor()
+ processor = AsteriskProcessor(wiki_prefix=WIKI_PREFIX)
# Build the models
base_dir = os.path.dirname(source)
diff --git a/rest-api-templates/models.wiki.mustache b/rest-api-templates/models.wiki.mustache
new file mode 100644
index 000000000..e3d3eb95c
--- /dev/null
+++ b/rest-api-templates/models.wiki.mustache
@@ -0,0 +1,22 @@
+{toc}
+
+{{#apis}}
+{{#api_declaration}}
+{{#models}}
+h1. {{id}}
+{{#extends}}Base type: [{{extends}}|#{{extends}}]{{/extends}}
+{{#has_subtypes}}Subtypes:{{#subtypes}} [{{id}}|#{{id}}]{{/subtypes}}{{/has_subtypes}}
+{{#description}}
+
+{{{description}}}
+{{/description}}
+{code:language=javascript|collapse=true}
+{{{model_json}}}
+{code}
+{{#properties}}
+* {{name}}: {{#type}}{{#is_primitive}}{{wiki_name}}{{/is_primitive}}{{^is_primitive}}[{{wiki_name}}|#{{singular_name}}]{{/is_primitive}}{{/type}}{{^required}} _(optional)_{{/required}}{{#description}} - {{{description}}}{{/description}}
+{{/properties}}
+
+{{/models}}
+{{/api_declaration}}
+{{/apis}}
diff --git a/rest-api-templates/res_stasis_http_resource.c.mustache b/rest-api-templates/res_stasis_http_resource.c.mustache
index 0bdc1d014..0f0535bcf 100644
--- a/rest-api-templates/res_stasis_http_resource.c.mustache
+++ b/rest-api-templates/res_stasis_http_resource.c.mustache
@@ -49,6 +49,9 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
#include "asterisk/module.h"
#include "asterisk/stasis_app.h"
#include "stasis_http/resource_{{name}}.h"
+#if defined(AST_DEVMODE)
+#include "stasis_http/ari_model_validators.h"
+#endif
{{#apis}}
{{#operations}}
@@ -61,11 +64,50 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
* \param[out] response Response to the HTTP request.
*/
static void stasis_http_{{c_nickname}}_cb(
- struct ast_variable *get_params, struct ast_variable *path_vars,
- struct ast_variable *headers, struct stasis_http_response *response)
+ struct ast_variable *get_params, struct ast_variable *path_vars,
+ struct ast_variable *headers, struct stasis_http_response *response)
{
+#if defined(AST_DEVMODE)
+ int is_valid;
+ int code;
+#endif /* AST_DEVMODE */
+
{{> param_parsing}}
stasis_http_{{c_nickname}}(headers, &args, response);
+#if defined(AST_DEVMODE)
+ code = response->response_code;
+
+ switch (code) {
+ case 500: /* Internal server error */
+{{#error_responses}}
+ case {{code}}: /* {{{reason}}} */
+{{/error_responses}}
+ is_valid = 1;
+ break;
+ default:
+ if (200 <= code && code <= 299) {
+{{#response_class}}
+{{#is_list}}
+ is_valid = ari_validate_list(response->message,
+ ari_validate_{{c_singular_name}});
+{{/is_list}}
+{{^is_list}}
+ is_valid = ari_validate_{{c_name}}(
+ response->message);
+{{/is_list}}
+{{/response_class}}
+ } else {
+ ast_log(LOG_ERROR, "Invalid error response %d for {{path}}\n", code);
+ is_valid = 0;
+ }
+ }
+
+ if (!is_valid) {
+ ast_log(LOG_ERROR, "Response validation failed for {{path}}\n");
+ stasis_http_response_error(response, 500,
+ "Internal Server Error", "Response validation failed");
+ }
+#endif /* AST_DEVMODE */
}
{{/is_req}}
{{#is_websocket}}
@@ -81,7 +123,12 @@ static void stasis_http_{{c_nickname}}_ws_cb(struct ast_websocket *ws_session,
struct ast_variable *path_vars = NULL;
{{/has_path_parameters}}
{{> param_parsing}}
- session = ari_websocket_session_create(ws_session);
+#if defined(AST_DEVMODE)
+ session = ari_websocket_session_create(ws_session,
+ ari_validate_{{response_class.c_name}});
+#else
+ session = ari_websocket_session_create(ws_session, NULL);
+#endif
if (!session) {
ast_log(LOG_ERROR, "Failed to create ARI session\n");
return;
diff --git a/rest-api-templates/res_stasis_json_resource.c.mustache b/rest-api-templates/res_stasis_json_resource.c.mustache
deleted file mode 100644
index a25bdc228..000000000
--- a/rest-api-templates/res_stasis_json_resource.c.mustache
+++ /dev/null
@@ -1,151 +0,0 @@
-{{#api_declaration}}
-/*
- * Asterisk -- An open source telephony toolkit.
- *
- * {{{copyright}}}
- *
- * {{{author}}}
-{{! Template Copyright
- * Copyright (C) 2013, Digium, Inc.
- *
- * Kinsey Moore <kmoore@digium.com>
-}}
- *
- * See http://www.asterisk.org for more information about
- * the Asterisk project. Please do not directly contact
- * any of the maintainers of this project for assistance;
- * the project provides a web site, mailing lists and IRC
- * channels for your use.
- *
- * This program is free software, distributed under the terms of
- * the GNU General Public License Version 2. See the LICENSE file
- * at the top of the source tree.
- */
-
-{{! Template for rendering the res_ module for an HTTP resource. }}
-/*
-{{> do-not-edit}}
- * This file is generated by a mustache template. Please see the original
- * template in rest-api-templates/res_stasis_http_resource.c.mustache
- */
-
-/*! \file
- *
- * \brief {{{description}}}
- *
- * \author {{{author}}}
- */
-
-/*** MODULEINFO
- <support_level>core</support_level>
- ***/
-
-#include "asterisk.h"
-
-ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
-
-#include "asterisk/module.h"
-#include "asterisk/json.h"
-#include "stasis_json/resource_{{name}}.h"
-{{#has_events}}
-#include "asterisk/stasis_channels.h"
-#include "asterisk/stasis_bridging.h"
-
-{{#events}}
-{{> event_function_decl}}
- )
-{
- RAII_VAR(struct ast_json *, message, NULL, ast_json_unref);
- RAII_VAR(struct ast_json *, event, NULL, ast_json_unref);
-{{#has_properties}}
- struct ast_json *validator;
-{{/has_properties}}
-{{#channel}}
- int ret;
-{{/channel}}
-{{#bridge}}
-{{^channel}}
- int ret;
-{{/channel}}
-{{/bridge}}
-
-{{#channel}}
- ast_assert(channel_snapshot != NULL);
-{{/channel}}
-{{#bridge}}
- ast_assert(bridge_snapshot != NULL);
-{{/bridge}}
-{{#has_properties}}
- ast_assert(blob != NULL);
-{{#channel}}
- ast_assert(ast_json_object_get(blob, "channel") == NULL);
-{{/channel}}
-{{#bridge}}
- ast_assert(ast_json_object_get(blob, "bridge") == NULL);
-{{/bridge}}
- ast_assert(ast_json_object_get(blob, "type") == NULL);
-{{#properties}}
-
- validator = ast_json_object_get(blob, "{{name}}");
- if (validator) {
- /* do validation? XXX */
-{{#required}}
- } else {
- /* fail message generation if the required parameter doesn't exist */
- return NULL;
-{{/required}}
- }
-{{/properties}}
-
- event = ast_json_deep_copy(blob);
-{{/has_properties}}
-{{^has_properties}}
-
- event = ast_json_object_create();
-{{/has_properties}}
- if (!event) {
- return NULL;
- }
-
-{{#channel}}
- ret = ast_json_object_set(event,
- "channel", ast_channel_snapshot_to_json(channel_snapshot));
- if (ret) {
- return NULL;
- }
-
-{{/channel}}
-{{#bridge}}
- ret = ast_json_object_set(event,
- "bridge", ast_bridge_snapshot_to_json(bridge_snapshot));
- if (ret) {
- return NULL;
- }
-
-{{/bridge}}
- message = ast_json_pack("{s: o}", "{{c_id}}", ast_json_ref(event));
- if (!message) {
- return NULL;
- }
-
- return ast_json_ref(message);
-}
-
-{{/events}}
-{{/has_events}}
-static int load_module(void)
-{
- return 0;
-}
-
-static int unload_module(void)
-{
- return 0;
-}
-
-AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS | AST_MODFLAG_LOAD_ORDER, "Stasis JSON Generators and Validators - {{{description}}}",
- .load = load_module,
- .unload = unload_module,
- .load_pri = AST_MODPRI_DEFAULT,
- );
-{{/api_declaration}}
diff --git a/rest-api-templates/res_stasis_json_resource.exports.mustache b/rest-api-templates/res_stasis_json_resource.exports.mustache
deleted file mode 100644
index 0f958fa04..000000000
--- a/rest-api-templates/res_stasis_json_resource.exports.mustache
+++ /dev/null
@@ -1,12 +0,0 @@
-{
-{{#api_declaration}}
-{{#has_events}}
- global:
-{{#events}}
- LINKER_SYMBOL_PREFIXstasis_json_event_{{c_id}}_create;
-{{/events}}
-{{/has_events}}
-{{/api_declaration}}
- local:
- *;
-};
diff --git a/rest-api-templates/stasis_json_resource.h.mustache b/rest-api-templates/stasis_json_resource.h.mustache
deleted file mode 100644
index 8cfd2c1f7..000000000
--- a/rest-api-templates/stasis_json_resource.h.mustache
+++ /dev/null
@@ -1,83 +0,0 @@
-{{#api_declaration}}
-/*
- * Asterisk -- An open source telephony toolkit.
- *
- * {{{copyright}}}
- *
- * {{{author}}}
- *
- * See http://www.asterisk.org for more information about
- * the Asterisk project. Please do not directly contact
- * any of the maintainers of this project for assistance;
- * the project provides a web site, mailing lists and IRC
- * channels for your use.
- *
- * This program is free software, distributed under the terms of
- * the GNU General Public License Version 2. See the LICENSE file
- * at the top of the source tree.
- */
-
-/*! \file
- *
- * \brief Generated file - declares stubs to be implemented in
- * res/stasis_json/resource_{{name}}.c
- *
- * {{{description}}}
- *
- * \author {{{author}}}
- */
-
-/*
-{{> do-not-edit}}
- * This file is generated by a mustache template. Please see the original
- * template in rest-api-templates/stasis_http_resource.h.mustache
- */
-
-#ifndef _ASTERISK_RESOURCE_{{name_caps}}_H
-#define _ASTERISK_RESOURCE_{{name_caps}}_H
-
-{{#has_events}}
-struct ast_channel_snapshot;
-struct ast_bridge_snapshot;
-
-{{#events}}
-/*!
- * \brief {{description}}
-{{#notes}}
- *
- * {{{notes}}}
-{{/notes}}
- *
-{{#channel}}
- * \param channel {{#channel_desc}}{{channel_desc}}{{/channel_desc}}{{^channel_desc}}The channel to be used to generate this event{{/channel_desc}}
-{{/channel}}
-{{#bridge}}
- * \param bridge {{#bridge_desc}}{{bridge_desc}}{{/bridge_desc}}{{^bridge_desc}}The bridge to be used to generate this event{{/bridge_desc}}
-{{/bridge}}
-{{#has_properties}}
- * \param blob JSON blob containing the following parameters:
-{{/has_properties}}
-{{#properties}}
- * - {{name}}: {{type}} {{#description}}- {{description}}{{/description}}{{#required}} (required){{/required}}
-{{/properties}}
- *
- * \retval NULL on error
- * \retval JSON (ast_json) describing the event
- */
-{{> event_function_decl}}
- );
-
-{{/events}}
-{{/has_events}}
-/*
- * JSON models
- *
-{{#models}}
- * {{id}}
-{{#properties}}
- * - {{name}}: {{type}}{{#required}} (required){{/required}}
-{{/properties}}
-{{/models}} */
-
-#endif /* _ASTERISK_RESOURCE_{{name_caps}}_H */
-{{/api_declaration}}
diff --git a/rest-api-templates/swagger_model.py b/rest-api-templates/swagger_model.py
index 47461b406..2907688c5 100644
--- a/rest-api-templates/swagger_model.py
+++ b/rest-api-templates/swagger_model.py
@@ -29,16 +29,101 @@ See https://github.com/wordnik/swagger-core/wiki/API-Declaration for the spec.
import json
import os.path
import pprint
+import re
import sys
import traceback
-try:
- from collections import OrderedDict
-except ImportError:
- from odict import OrderedDict
+# I'm not quite sure what was in Swagger 1.2, but apparently I missed it
+SWAGGER_VERSIONS = ["1.1", "1.3"]
+SWAGGER_PRIMITIVES = [
+ 'void',
+ 'string',
+ 'boolean',
+ 'number',
+ 'int',
+ 'long',
+ 'double',
+ 'float',
+ 'Date',
+]
-SWAGGER_VERSION = "1.1"
+
+class Stringify(object):
+ """Simple mix-in to make the repr of the model classes more meaningful.
+ """
+ def __repr__(self):
+ return "%s(%s)" % (self.__class__, pprint.saferepr(self.__dict__))
+
+
+def compare_versions(lhs, rhs):
+ '''Performs a lexicographical comparison between two version numbers.
+
+ This properly handles simple major.minor.whatever.sure.why.not version
+ numbers, but fails miserably if there's any letters in there.
+
+ For reference:
+ 1.0 == 1.0
+ 1.0 < 1.0.1
+ 1.2 < 1.10
+
+ @param lhs Left hand side of the comparison
+ @param rhs Right hand side of the comparison
+ @return < 0 if lhs < rhs
+ @return == 0 if lhs == rhs
+ @return > 0 if lhs > rhs
+ '''
+ lhs = [int(v) for v in lhs.split('.')]
+ rhs = [int(v) for v in rhs.split('.')]
+ return cmp(lhs, rhs)
+
+
+class ParsingContext(object):
+ """Context information for parsing.
+
+ This object is immutable. To change contexts (like adding an item to the
+ stack), use the next() and next_stack() functions to build a new one.
+ """
+
+ def __init__(self, swagger_version, stack):
+ self.__swagger_version = swagger_version
+ self.__stack = stack
+
+ def __repr__(self):
+ return "ParsingContext(swagger_version=%s, stack=%s)" % (
+ self.swagger_version, self.stack)
+
+ def get_swagger_version(self):
+ return self.__swagger_version
+
+ def get_stack(self):
+ return self.__stack
+
+ swagger_version = property(get_swagger_version)
+
+ stack = property(get_stack)
+
+ def version_less_than(self, ver):
+ return compare_versions(self.swagger_version, ver) < 0
+
+ def next_stack(self, json, id_field):
+ """Returns a new item pushed to the stack.
+
+ @param json: Current JSON object.
+ @param id_field: Field identifying this object.
+ @return New context with additional item in the stack.
+ """
+ if not id_field in json:
+ raise SwaggerError("Missing id_field: %s" % id_field, self)
+ new_stack = self.stack + ['%s=%s' % (id_field, str(json[id_field]))]
+ return ParsingContext(self.swagger_version, new_stack)
+
+ def next(self, version=None, stack=None):
+ if version is None:
+ version = self.version
+ if stack is None:
+ stack = self.stack
+ return ParsingContext(version, stack)
class SwaggerError(Exception):
@@ -50,7 +135,7 @@ class SwaggerError(Exception):
"""Ctor.
@param msg: String message for the error.
- @param context: Array of strings for current context in the API.
+ @param context: ParsingContext object
@param cause: Optional exception that caused this one.
"""
super(Exception, self).__init__(msg, context, cause)
@@ -61,7 +146,7 @@ class SwaggerPostProcessor(object):
fields to model objects for additional information to use in the
templates.
"""
- def process_api(self, resource_api, context):
+ def process_resource_api(self, resource_api, context):
"""Post process a ResourceApi object.
@param resource_api: ResourceApi object.
@@ -69,6 +154,14 @@ class SwaggerPostProcessor(object):
"""
pass
+ def process_api(self, api, context):
+ """Post process an Api object.
+
+ @param api: Api object.
+ @param context: Current context in the API.
+ """
+ pass
+
def process_operation(self, operation, context):
"""Post process a Operation object.
@@ -85,12 +178,37 @@ class SwaggerPostProcessor(object):
"""
pass
+ def process_model(self, model, context):
+ """Post process a Model object.
-class Stringify(object):
- """Simple mix-in to make the repr of the model classes more meaningful.
- """
- def __repr__(self):
- return "%s(%s)" % (self.__class__, pprint.saferepr(self.__dict__))
+ @param model: Model object.
+ @param context: Current context in the API.
+ """
+ pass
+
+ def process_property(self, property, context):
+ """Post process a Property object.
+
+ @param property: Property object.
+ @param context: Current context in the API.
+ """
+ pass
+
+ def process_type(self, swagger_type, context):
+ """Post process a SwaggerType object.
+
+ @param swagger_type: ResourceListing object.
+ @param context: Current context in the API.
+ """
+ pass
+
+ def process_resource_listing(self, resource_listing, context):
+ """Post process the overall ResourceListing object.
+
+ @param resource_listing: ResourceListing object.
+ @param context: Current context in the API.
+ """
+ pass
class AllowableRange(Stringify):
@@ -158,17 +276,22 @@ class Parameter(Stringify):
self.allow_multiple = None
def load(self, parameter_json, processor, context):
- context = add_context(context, parameter_json, 'name')
+ context = context.next_stack(parameter_json, 'name')
validate_required_fields(parameter_json, self.required_fields, context)
self.name = parameter_json.get('name')
self.param_type = parameter_json.get('paramType')
self.description = parameter_json.get('description') or ''
self.data_type = parameter_json.get('dataType')
self.required = parameter_json.get('required') or False
+ self.default_value = parameter_json.get('defaultValue')
self.allowable_values = load_allowable_values(
parameter_json.get('allowableValues'), context)
self.allow_multiple = parameter_json.get('allowMultiple') or False
processor.process_parameter(self, context)
+ if parameter_json.get('allowedValues'):
+ raise SwaggerError(
+ "Field 'allowedValues' invalid; use 'allowableValues'",
+ context)
return self
def is_type(self, other_type):
@@ -188,13 +311,41 @@ class ErrorResponse(Stringify):
self.reason = None
def load(self, err_json, processor, context):
- context = add_context(context, err_json, 'code')
+ context = context.next_stack(err_json, 'code')
validate_required_fields(err_json, self.required_fields, context)
self.code = err_json.get('code')
self.reason = err_json.get('reason')
return self
+class SwaggerType(Stringify):
+ """Model of a data type.
+ """
+
+ def __init__(self):
+ self.name = None
+ self.is_discriminator = None
+ self.is_list = None
+ self.singular_name = None
+ self.is_primitive = None
+
+ def load(self, type_name, processor, context):
+ # Some common errors
+ if type_name == 'integer':
+ raise SwaggerError("The type for integer should be 'int'", context)
+
+ self.name = type_name
+ type_param = get_list_parameter_type(self.name)
+ self.is_list = type_param is not None
+ if self.is_list:
+ self.singular_name = type_param
+ else:
+ self.singular_name = self.name
+ self.is_primitive = self.singular_name in SWAGGER_PRIMITIVES
+ processor.process_type(self, context)
+ return self
+
+
class Operation(Stringify):
"""Model of an operation on an API
@@ -213,11 +364,14 @@ class Operation(Stringify):
self.error_responses = []
def load(self, op_json, processor, context):
- context = add_context(context, op_json, 'nickname')
+ context = context.next_stack(op_json, 'nickname')
validate_required_fields(op_json, self.required_fields, context)
self.http_method = op_json.get('httpMethod')
self.nickname = op_json.get('nickname')
- self.response_class = op_json.get('responseClass')
+ response_class = op_json.get('responseClass')
+ self.response_class = response_class and SwaggerType().load(
+ response_class, processor, context)
+
# Specifying WebSocket URL's is our own extension
self.is_websocket = op_json.get('upgrade') == 'websocket'
self.is_req = not self.is_websocket
@@ -247,6 +401,7 @@ class Operation(Stringify):
err_json = op_json.get('errorResponses') or []
self.error_responses = [
ErrorResponse().load(j, processor, context) for j in err_json]
+ self.has_error_responses = self.error_responses != []
processor.process_operation(self, context)
return self
@@ -265,7 +420,7 @@ class Api(Stringify):
self.operations = []
def load(self, api_json, processor, context):
- context = add_context(context, api_json, 'path')
+ context = context.next_stack(api_json, 'path')
validate_required_fields(api_json, self.required_fields, context)
self.path = api_json.get('path')
self.description = api_json.get('description')
@@ -274,9 +429,20 @@ class Api(Stringify):
Operation().load(j, processor, context) for j in op_json]
self.has_websocket = \
filter(lambda op: op.is_websocket, self.operations) != []
+ processor.process_api(self, context)
return self
+def get_list_parameter_type(type_string):
+ """Returns the type parameter if the given type_string is List[].
+
+ @param type_string: Type string to parse
+ @returns Type parameter of the list, or None if not a List.
+ """
+ list_match = re.match('^List\[(.*)\]$', type_string)
+ return list_match and list_match.group(1)
+
+
class Property(Stringify):
"""Model of a Swagger property.
@@ -293,9 +459,15 @@ class Property(Stringify):
def load(self, property_json, processor, context):
validate_required_fields(property_json, self.required_fields, context)
- self.type = property_json.get('type')
+ # Bit of a hack, but properties do not self-identify
+ context = context.next_stack({'name': self.name}, 'name')
self.description = property_json.get('description') or ''
self.required = property_json.get('required') or False
+
+ type = property_json.get('type')
+ self.type = type and SwaggerType().load(type, processor, context)
+
+ processor.process_property(self, context)
return self
@@ -305,24 +477,95 @@ class Model(Stringify):
See https://github.com/wordnik/swagger-core/wiki/datatypes
"""
+ required_fields = ['description', 'properties']
+
def __init__(self):
self.id = None
+ self.extends = None
+ self.extends_type = None
self.notes = None
self.description = None
- self.properties = None
+ self.__properties = None
+ self.__discriminator = None
+ self.__subtypes = []
def load(self, id, model_json, processor, context):
- context = add_context(context, model_json, 'id')
- # This arrangement is required by the Swagger API spec
+ context = context.next_stack(model_json, 'id')
+ validate_required_fields(model_json, self.required_fields, context)
+ # The duplication of the model's id is required by the Swagger spec.
self.id = model_json.get('id')
if id != self.id:
- raise SwaggerError("Model id doesn't match name", c)
+ raise SwaggerError("Model id doesn't match name", context)
+ self.extends = model_json.get('extends')
+ if self.extends and context.version_less_than("1.3"):
+ raise SwaggerError("Type extension support added in Swagger 1.3",
+ context)
self.description = model_json.get('description')
props = model_json.get('properties').items() or []
- self.properties = [
+ self.__properties = [
Property(k).load(j, processor, context) for (k, j) in props]
+ self.__properties = sorted(self.__properties, key=lambda p: p.name)
+
+ discriminator = model_json.get('discriminator')
+
+ if discriminator:
+ if context.version_less_than("1.3"):
+ raise SwaggerError("Discriminator support added in Swagger 1.3",
+ context)
+
+ discr_props = [p for p in self.__properties if p.name == discriminator]
+ if not discr_props:
+ raise SwaggerError(
+ "Discriminator '%s' does not name a property of '%s'" % (
+ discriminator, self.id),
+ context)
+
+ self.__discriminator = discr_props[0]
+
+ self.model_json = json.dumps(model_json,
+ indent=2, separators=(',', ': '))
+
+ processor.process_model(self, context)
return self
+ def add_subtype(self, subtype):
+ """Add subtype to this model.
+
+ @param subtype: Model instance for the subtype.
+ """
+ self.__subtypes.append(subtype)
+
+ def set_extends_type(self, extends_type):
+ self.extends_type = extends_type
+
+ def discriminator(self):
+ """Returns the discriminator, digging through base types if needed.
+ """
+ return self.__discriminator or \
+ self.extends_type and self.extends_type.discriminator()
+
+ def properties(self):
+ base_props = []
+ if self.extends_type:
+ base_props = self.extends_type.properties()
+ return base_props + self.__properties
+
+ def has_properties(self):
+ return len(self.properties()) > 0
+
+ def subtypes(self):
+ """Returns the full list of all subtypes.
+ """
+ res = self.__subtypes + \
+ [subsubtypes for subtype in self.__subtypes
+ for subsubtypes in subtype.subtypes()]
+ return sorted(res, key=lambda m: m.id)
+
+ def has_subtypes(self):
+ """Returns True if type has any subtypes.
+ """
+ return len(self.subtypes()) > 0
+
class ApiDeclaration(Stringify):
"""Model class for an API Declaration.
@@ -345,8 +588,8 @@ class ApiDeclaration(Stringify):
self.apis = []
self.models = []
- def load_file(self, api_declaration_file, processor, context=[]):
- context = context + [api_declaration_file]
+ def load_file(self, api_declaration_file, processor):
+ context = ParsingContext(None, [api_declaration_file])
try:
return self.__load_file(api_declaration_file, processor, context)
except SwaggerError:
@@ -376,9 +619,10 @@ class ApiDeclaration(Stringify):
"""
# If the version doesn't match, all bets are off.
self.swagger_version = api_decl_json.get('swaggerVersion')
- if self.swagger_version != SWAGGER_VERSION:
+ context = context.next(version=self.swagger_version)
+ if not self.swagger_version in SWAGGER_VERSIONS:
raise SwaggerError(
- "Unsupported Swagger version %s" % swagger_version, context)
+ "Unsupported Swagger version %s" % self.swagger_version, context)
validate_required_fields(api_decl_json, self.required_fields, context)
@@ -391,9 +635,19 @@ class ApiDeclaration(Stringify):
self.apis = [
Api().load(j, processor, context) for j in api_json]
models = api_decl_json.get('models').items() or []
- self.models = [
- Model().load(k, j, processor, context) for (k, j) in models]
-
+ self.models = [Model().load(id, json, processor, context)
+ for (id, json) in models]
+ self.models = sorted(self.models, key=lambda m: m.id)
+ # Now link all base/extended types
+ model_dict = dict((m.id, m) for m in self.models)
+ for m in self.models:
+ if m.extends:
+ extends_type = model_dict.get(m.extends)
+ if not extends_type:
+ raise SwaggerError("%s extends non-existing model %s",
+ m.id, m.extends)
+ extends_type.add_subtype(m)
+ m.set_extends_type(extends_type)
return self
@@ -409,20 +663,20 @@ class ResourceApi(Stringify):
self.api_declaration = None
def load(self, api_json, processor, context):
- context = add_context(context, api_json, 'path')
+ context = context.next_stack(api_json, 'path')
validate_required_fields(api_json, self.required_fields, context)
self.path = api_json['path']
self.description = api_json['description']
if not self.path or self.path[0] != '/':
raise SwaggerError("Path must start with /", context)
- processor.process_api(self, context)
+ processor.process_resource_api(self, context)
return self
def load_api_declaration(self, base_dir, processor):
self.file = (base_dir + self.path).replace('{format}', 'json')
self.api_declaration = ApiDeclaration().load_file(self.file, processor)
- processor.process_api(self, [self.file])
+ processor.process_resource_api(self, [self.file])
class ResourceListing(Stringify):
@@ -438,7 +692,7 @@ class ResourceListing(Stringify):
self.apis = None
def load_file(self, resource_file, processor):
- context = [resource_file]
+ context = ParsingContext(None, [resource_file])
try:
return self.__load_file(resource_file, processor, context)
except SwaggerError:
@@ -455,7 +709,7 @@ class ResourceListing(Stringify):
def load(self, resources_json, processor, context):
# If the version doesn't match, all bets are off.
self.swagger_version = resources_json.get('swaggerVersion')
- if self.swagger_version != SWAGGER_VERSION:
+ if not self.swagger_version in SWAGGER_VERSIONS:
raise SwaggerError(
"Unsupported Swagger version %s" % swagger_version, context)
@@ -465,6 +719,7 @@ class ResourceListing(Stringify):
apis_json = resources_json['apis']
self.apis = [
ResourceApi().load(j, processor, context) for j in apis_json]
+ processor.process_resource_listing(self, context)
return self
@@ -482,16 +737,3 @@ def validate_required_fields(json, required_fields, context):
if missing_fields:
raise SwaggerError(
"Missing fields: %s" % ', '.join(missing_fields), context)
-
-
-def add_context(context, json, id_field):
- """Returns a new context with a new item added to it.
-
- @param context: Old context.
- @param json: Current JSON object.
- @param id_field: Field identifying this object.
- @return New context with additional item.
- """
- if not id_field in json:
- raise SwaggerError("Missing id_field: %s" % id_field, context)
- return context + ['%s=%s' % (id_field, str(json[id_field]))]
diff --git a/rest-api-templates/transform.py b/rest-api-templates/transform.py
index d0ef3c4a1..fc12efe85 100644
--- a/rest-api-templates/transform.py
+++ b/rest-api-templates/transform.py
@@ -16,8 +16,11 @@
# at the top of the source tree.
#
+import filecmp
import os.path
import pystache
+import shutil
+import tempfile
class Transform(object):
@@ -46,8 +49,14 @@ class Transform(object):
"""
dest_file = pystache.render(self.dest_file_template, model)
dest_file = os.path.join(dest_dir, dest_file)
- if os.path.exists(dest_file) and not self.overwrite:
+ dest_exists = os.path.exists(dest_file)
+ if dest_exists and not self.overwrite:
return
- print "Rendering %s" % dest_file
- with open(dest_file, "w") as out:
+ tmp_file = tempfile.mkstemp()
+ with tempfile.NamedTemporaryFile() as out:
out.write(renderer.render(self.template, model))
+ out.flush()
+
+ if not dest_exists or not filecmp.cmp(out.name, dest_file):
+ print "Writing %s" % dest_file
+ shutil.copyfile(out.name, dest_file)