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
This commit is contained in:
David M. Lee
2013-07-03 16:32:41 +00:00
parent dcf03554a0
commit c9a3d4562d
73 changed files with 6422 additions and 2908 deletions

View File

@@ -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}}

View File

@@ -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}}

View File

@@ -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 */

View File

@@ -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
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)
# 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_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)

View File

@@ -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}}

View File

@@ -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)

View File

@@ -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}}

View File

@@ -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;

View File

@@ -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}}

View File

@@ -1,12 +0,0 @@
{
{{#api_declaration}}
{{#has_events}}
global:
{{#events}}
LINKER_SYMBOL_PREFIXstasis_json_event_{{c_id}}_create;
{{/events}}
{{/has_events}}
{{/api_declaration}}
local:
*;
};

View File

@@ -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}}

View File

@@ -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]))]

View File

@@ -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)