Skip to content

Commit

Permalink
Merge pull request #143 from freddrake/fd-data-type-examples
Browse files Browse the repository at this point in the history
data type examples
  • Loading branch information
econchick authored May 24, 2017
2 parents 1cc8e26 + a10fec3 commit 890396e
Show file tree
Hide file tree
Showing 19 changed files with 858 additions and 26 deletions.
4 changes: 4 additions & 0 deletions ramlfications/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ class BaseRAMLParserError(BaseRAMLError):
pass


class InvalidRAMLStructureError(BaseRAMLParserError):
"""Disallowed structure was found in YAML."""


class InvalidRootNodeError(BaseRAMLParserError):
pass

Expand Down
20 changes: 17 additions & 3 deletions ramlfications/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,11 @@ class BaseNamedParameter(object):
in RAML file, defaults to ``name``.
:param list enum: Array of valid parameter values, or ``None``. \
Only applies when primative ``type`` is ``string``.
:param example: Example value for property, or ``None``. Type of \
``example`` will match that of ``type``.
:param example: Example value for property, or ``None``. \
For RAML 0.8, the type of ``example`` will match that of ``type``. \
For RAML 1.0, ``example`` will be an ``Example`` object with a \
``value`` attribute whose type matches that of ``type``.
:param examples: List of ``Example`` objects (RAML 1.0 only).
:param int max_length: Parameter value's maximum number of \
characters, or ``None``. Only applies when primative ``type`` \
is ``string``.
Expand Down Expand Up @@ -144,7 +147,6 @@ class BaseNamedParameter(object):
validator=string_type_parameter)
pattern = attr.ib(repr=False, default=None,
validator=string_type_parameter)
repeat = attr.ib(repr=False, default=False)


@attr.s
Expand Down Expand Up @@ -183,3 +185,15 @@ def description(self):
if self.desc:
return BaseContent(self.desc)
return None


@attr.s
class BaseParameterRaml08(object):
"""TODO: writeme"""
repeat = attr.ib(repr=False)


@attr.s
class BaseParameterRaml10(object):
"""TODO: writeme"""
examples = attr.ib(repr=False)
24 changes: 24 additions & 0 deletions ramlfications/models/examples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-

from __future__ import absolute_import, division, print_function

import attr


@attr.s
class Example(object):
"""
Single example.
Used starting with RAML 1.0.
"""
value = attr.ib(repr=False)
name = attr.ib(default=None)
description = attr.ib(default=None, repr=False)
display_name = attr.ib(default=None, repr=False)

# Not sure when validation of examples should get done; leave for now.
strict = attr.ib(default=True, repr=False)

# TODO: this will need to support annotations.
68 changes: 63 additions & 5 deletions ramlfications/parser/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@

from __future__ import absolute_import, division, print_function

import attr
from six import iteritems, itervalues, string_types

from ramlfications.config import MEDIA_TYPES
from ramlfications.models.base import (
BaseParameterRaml08, BaseParameterRaml10
)
from ramlfications.models.parameters import (
Body, Header, Response, URIParameter
)
Expand All @@ -15,11 +19,21 @@
map_object, resolve_scalar_data, add_missing_uri_data
)
from ramlfications.utils.parser import get_data_type_obj_by_name
from ramlfications.utils.examples import parse_examples


class BaseParameterParser(object):

# We keep a cache of concrete classes created when we mix in the
# RAML-version-specific specialization below so general equality
# support provided by the attr package remains useful.
#
# Creating a new class every time through breaks that.
#
_classes = {}

def create_base_param_obj(self, attribute_data, param_obj,
config, errors, **kw):
config, errors, root, **kw):
"""
Helper function to create a child of a
:py:class:`.parameters.BaseParameter` object
Expand All @@ -31,7 +45,6 @@ def create_base_param_obj(self, attribute_data, param_obj,
required = _get(value, "required", default=True)
else:
required = _get(value, "required", default=False)
root = kw.get('root')
data_type_name = _get(value, "type")
data_type = get_data_type_obj_by_name(data_type_name, root)
kwargs = dict(
Expand All @@ -47,7 +60,6 @@ def create_base_param_obj(self, attribute_data, param_obj,
enum=_get(value, "enum"),
example=_get(value, "example"),
required=required,
repeat=_get(value, "repeat", False),
pattern=_get(value, "pattern"),
type=_get(value, "type", "string"),
config=config,
Expand All @@ -58,7 +70,52 @@ def create_base_param_obj(self, attribute_data, param_obj,
if param_obj is not Body:
kwargs["desc"] = _get(value, "description")

item = param_obj(**kwargs)
# Getting the RAML version we're working with is a little
# tricky since we may have a root node, which always allows
# us to get it, but sometimes we don't (while processing
# parameters directly associated with the RAML root).
#
if root is None:
raml_version = self.kwargs['data']._raml_version
else:
raml_version = root.raml_version

if raml_version == "0.8":
kwargs["repeat"] = _get(value, "repeat", False)

if raml_version == "0.8" and isinstance(value, list):
# This is a sneaky union; need to handle this differently.
# Applies only to RAML 0.8; see:
#
# https://github.com/raml-org/raml-spec/blob/master/versions/
# raml-08/raml-08.md#named-parameters-with-multiple-types
#
# TODO: Complete once union types are implemented.
pass
else:
kwargs.update(parse_examples(raml_version, value))

# build object class based off of raml version
ParamObj = param_obj
mixin = BaseParameterRaml10
suffix = "10"
if raml_version == "0.8":
mixin = BaseParameterRaml08
suffix = "08"

key = param_obj, mixin

if key in self._classes:
ParamObj = self._classes[key]
else:
@attr.s
class ParamObj(param_obj, mixin):
pass

ParamObj.__name__ = param_obj.__name__ + suffix
self._classes[key] = ParamObj

item = ParamObj(**kwargs)
objects.append(item)

return objects or None
Expand Down Expand Up @@ -220,7 +277,8 @@ def parse_response_headers(self):
header_objects = self.create_base_param_obj(headers, Header,
self.root.config,
self.root.errors,
method=self.method)
method=self.method,
root=self.root)
return header_objects or None

def parse_response_body(self):
Expand Down
64 changes: 64 additions & 0 deletions ramlfications/utils/examples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-

from __future__ import absolute_import, division, print_function

from six import iteritems

from ramlfications.errors import InvalidRAMLStructureError
from ramlfications.models.base import BaseContent
from ramlfications.models.examples import Example
from ramlfications.utils.nodelist import NodeList


def parse_examples(raml_version, data):
#
# This is the only place that knows when to supply examples and when
# not, and that checks when both are supplied.
#
# This is also responsible for generating structured Example objects
# when appropriate (RAML >= 1.0).
#
data = data or {}
kw = {}
example = data.get("example")

if raml_version == "0.8":
if "examples" in data:
del data["examples"]
# TODO: Emit a lint warning if there's an examples node and
# we're processing RAML 0.8: authors may be expecting it to
# be honored.
kw["example"] = example

if raml_version != "0.8":
if "example" in data:
kw["example"] = parse_example(None, example)

kw["examples"] = None
if "examples" in data:
if "example" in data:
# TODO: Emit a lint warning during validation.
raise InvalidRAMLStructureError(
"example and examples cannot co-exist")
examples = data["examples"]
# Must be a map:
if not isinstance(examples, dict):
# Need to decide what exception to make this.
raise InvalidRAMLStructureError("examples must be a map node")
kw["examples"] = NodeList([parse_example(nm, val)
for nm, val in iteritems(examples)])

return kw


def parse_example(name, node):
data = dict(name=name, value=node)
if name:
data["display_name"] = name
if isinstance(node, dict) and "value" in node:
data["value"] = node["value"]
data["description"] = BaseContent(node.get("description", ""))
data["display_name"] = node.get("displayName", name)
data["strict"] = node.get("strict", True)

return Example(**data)
4 changes: 4 additions & 0 deletions ramlfications/utils/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from ramlfications.errors import UnknownDataTypeError
from ramlfications.models import RAML_DATA_TYPES, STANDARD_RAML_TYPES
from .common import merge_dicts
from .examples import parse_examples
from .parser import convert_camel_case


Expand Down Expand Up @@ -63,4 +64,7 @@ def parse_type(name, raw, root):
data.pop("properties")
except KeyError:
pass

data.update(parse_examples(root.raml_version, data))

return data_type_cls(**data)
77 changes: 77 additions & 0 deletions tests/data/raml_08/examples.raml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#%RAML 0.8

title: Example
baseUri: https://example.test/{param}/

baseUriParameters:
param:
type: string
description: account name
example: splat

mediaType: application/json
protocols: [ HTTPS ]

/with_example_and_examples:
post:
queryParameters:
cached:
type: boolean
example: false
description: Allow cached data?
body:
application/json:
formParameters:
key:
type: string
example: This is the example.

# examples is ignored; may produce an lint warning later.
examples:
simple: "abc"
fancy: "two words"
excessive:
displayName: "Serious Overkill"
description: |
There's way too much text here.
value: "This is a long example."
strict: false

# This really isn't supported yet.
multityped:
- type: string
description: Long explanation.
example: Pile o text.
- type: file
description: File upload.

/with_uri_param/{id}:
uriParameters:
id:
example: s234gs9

/with_example_structured:
post:
body:
application/json:
formParameters:
key:
type: object
example:
value: This whole map is a value.

/with_example_unstructured:
post:
body:
application/json:
formParameters:
key:
type: string
example: This is a value.

/with_header:
displayName: Silly, silly example.
headers:
x-extra-fluff:
type: boolean
example: true
18 changes: 18 additions & 0 deletions tests/data/raml_10/broken-example-with-examples.raml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#%RAML 1.0

title: Example
baseUri: https://example.test/{param}/

baseUriParameters:
param:
type: string
description: account name
example: splat
examples:
first: some text
second:
value: more text
strict: false

mediaType: application/json
protocols: [ HTTPS ]
16 changes: 16 additions & 0 deletions tests/data/raml_10/broken-non-map-examples.raml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#%RAML 1.0

title: Example
baseUri: https://example.test/{param}/

baseUriParameters:
param:
type: string
description: account name
examples:
- some text
- value: more text
strict: false

mediaType: application/json
protocols: [ HTTPS ]
13 changes: 13 additions & 0 deletions tests/data/raml_10/embedded-json-schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{"type": "object",
"properties": {
"name": {
"type": "string"
},
"age": {
"description": "Age in years",
"type": "integer",
"minimum": 0
}
},
"required": ["firstName", "lastName"]
}
Loading

0 comments on commit 890396e

Please sign in to comment.