1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
|
****************
Builtin Backends
****************
Using a backend, you can convert the data types and routes in your spec into
objects in your programming language of choice.
Stone includes backends for an assortment of languages, including:
* `Python <#python-guide>`_
* Python `Type Stubs <https://www.python.org/dev/peps/pep-0484/#id42>`_
* Javascript
* Objective-C
* Swift
* Typescript
If you're looking to write your own backend, see `Backend Reference
<backend_ref.rst>`_. We would love to see a contribution of a PHP or Ruby
backend.
Compile with the CLI
====================
Compiling a spec and generating code is done using the ``stone``
command-line interface (CLI)::
$ stone -h
usage: stone [-h] [-v] [--clean-build] [-f FILTER_BY_ROUTE_ATTR]
[-w WHITELIST_NAMESPACE_ROUTES | -b BLACKLIST_NAMESPACE_ROUTES]
backend output [spec [spec ...]]
StoneAPI
positional arguments:
backend Either the name of a built-in backend or the path to
a backend module. Paths to backend modules must
end with a .stoneg.py extension. The following
backends are built-in: js_client, js_types,
tsd_client, tsd_types, python_types, python_client,
swift_client
output The folder to save generated files to.
spec Path to API specifications. Each must have a .stone
extension. If omitted or set to "-", the spec is read
from stdin. Multiple namespaces can be provided over
stdin by concatenating multiple specs together.
optional arguments:
-h, --help show this help message and exit
-v, --verbose Print debugging statements.
--clean-build The path to the template SDK for the target language.
-f FILTER_BY_ROUTE_ATTR, --filter-by-route-attr FILTER_BY_ROUTE_ATTR
Removes routes that do not match the expression. The
expression must specify a route attribute on the left-
hand side and a value on the right-hand side. Use
quotes for strings and bytes. The only supported
operators are "=" and "!=". For example, if "hide" is
a route attribute, we can use this filter:
"hide!=true". You can combine multiple expressions
with "and"/"or" and use parentheses to enforce
precedence.
-w WHITELIST_NAMESPACE_ROUTES, --whitelist-namespace-routes WHITELIST_NAMESPACE_ROUTES
If set, backends will only see the specified
namespaces as having routes.
-b BLACKLIST_NAMESPACE_ROUTES, --blacklist-namespace-routes BLACKLIST_NAMESPACE_ROUTES
If set, backends will not see any routes for the
specified namespaces.
We'll generate code based on an ``calc.stone`` spec with the following
contents::
namespace calc
route eval(Expression, Result, EvalError)
struct Expression
"This expression is limited to a binary operation."
op Operator = add
left Int64
right Int64
union Operator
add
sub
mult
div Boolean
"If value is true, rounds up. Otherwise, rounds down."
struct Result
answer Int64
union EvalError
overflow
Python Guide
============
This section explains how to use the pre-packaged Python backends and work
with the Python classes that have been generated from a spec.
There are two different Python backends: ``python_types`` and
``python_client``. The former generates Python classes for the data types
defined in your spec. The latter generates a single Python class with a method
per route, which is useful for building SDKs.
We'll use the ``python_types`` backend::
$ stone python_types . calc.stone
This runs the backend on the ``calc.stone`` spec. Its output target is
``.`` which is the current directory. A Python module is created for
each declared namespace, so in this case only ``calc.py`` is created.
Three additional modules are copied into the target directory. The first,
``stone_validators.py``, contains classes for validating Python values against
their expected Stone types. You will not need to explicitly import this module,
but the auto-generated Python classes depend on it. The second,
``stone_serializers.py``, contains a pair of ``json_encode()`` and
``json_decode()`` functions. You will need to import this module to serialize
your objects. The last is ``stone_base.py`` which shouldn't be used directly.
In the following sections, we'll interact with the classes generated in
``calc.py``. For simplicity, we'll assume we've opened a Python interpreter
with the following shell command::
$ python -i calc.py
For non-test projects, we recommend that you set the generation target to a
path within a Python package, and use Python's import facility.
Primitive Types
---------------
The following table shows the mapping between a Stone `primitive type
<lang_ref.rst#primitive-types>`_ and its corresponding type in Python.
========================== ============== =====================================
Primitive Python 2.x / 3 Notes
========================== ============== =====================================
Bytes bytes
Boolean bool
Float{32,64} float long type within range is converted.
Int{32,64}, UInt{32,64} long
List list
String unicode / str str type is converted to unicode.
Timestamp datetime
========================== ============== =====================================
Struct
------
For each struct in your spec, you will see a corresponding Python class of the
same name.
In our example, ``Expression``, ``Operator``, ``Answer``, ``EvalError``, and
are Python classes. They have an attribute (getter/setter/deleter property) for
each field defined in the spec. You can instantiate these classes and specify
field values either in the constructor or by assigning to an attribute::
>>> expr = Expression(op=Operator.add, left=1, right=1)
If you assign a value that fails validation, an exception is raised::
>>> expr.op = '+'
Traceback (most recent call last)
...
ValidationError: expected type Operator or subtype, got string
Accessing a required field (non-optional with no default) that has not been set
raises an error::
>>> res = Result()
>>> res.answer
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "calc.py", line 221, in answer
raise AttributeError("missing required field 'answer'")
AttributeError: missing required field 'answer'
Other characteristics:
1. Inheritance in Stone is represented as inheritance in Python.
2. If a field is nullable and was never set, ``None`` is returned.
3. If a field has a default but was never set, the default is returned.
Union
-----
For each union in your spec, you will see a corresponding Python class of the
same name.
You do not use a union class's constructor directly. To select a tag with a
void type, use the class attribute of the same name::
>>> EvalError.overflow
EvalError('overflow', None)
To select a tag with a value, use the class method of the same name and pass
in an argument to serve as the value::
>>> Operator.div(False)
Operator('div', False)
To write code that handles the union options, use the ``is_[tag]()`` methods.
We recommend you exhaustively check all tags, or include an else clause to
ensure that all possibilities are accounted for. For tags that have values,
use the ``get_[tag]()`` method to access the value::
>>> # assume that op is an instance of Operator
>>> if op.is_add():
... # handle addition
... elif op.is_sub():
... # handle subtraction
... elif op.is_mult():
... # handle multiplication
... elif op.is_div():
... round_up = op.get_div()
... # handle division
Struct Polymorphism
-------------------
As with regular structs, structs that enumerate subtypes have corresponding
Python classes that behave identically to regular structs.
The difference is apparent when a field has a data type that is a struct with
enumerated subtypes. Expanding on our example from the language reference,
assume the following spec::
struct Resource
union
file File
folder Folder
path String
struct File extends Resource:
size UInt64
struct Folder extends Resource:
"No new fields."
struct Response
rsrc Resource
If we instantiate ``Response``, the ``rsrc`` field can only be assigned a
``File`` or ``Folder`` object. It should not be assigned a ``Resource`` object.
An exception to this is on deserialization. Because ``Resource`` is specified
as a catch-all, it's possible when deserializing a ``Response`` to get a
``Resource`` object in the ``rsrc`` field. This indicates that the returned
subtype was unknown because the recipient has an older spec than the sender.
To handle catch-alls, you should use an else clause::
>>> print resp.rsrc.path # Guaranteed to work regardless of subtype
>>> if isinstance(resp, File):
... # handle File
... elif isinstance(resp, Folder):
... # handle Folder
... else:
... # unknown subtype of Resource
Route
-----
Routes are represented as instances of a ``Route`` object. The generated Python
module for the namespace will have a module-level variable for each route::
>>> eval
Route('eval', 1, False, ...)
Route attributes specified in the spec are available as a dict in the ``attrs``
member variable. Route deprecation is stored in the ``deprecated`` member
variable. The name and version of a route are stored in the ``name`` and ``version`` member
variables, respectively.
Serialization
-------------
We can use ``stone_serializers.json_encode()`` to serialize our objects to
JSON::
>>> import stone_serializers
>>> stone_serializers.json_encode(eval.result_type, Result(answer=10))
'{"answer": 10}'
To deserialize, we can use ``json_decode``::
>>> stone_serializers.json_decode(eval.result_type, '{"answer": 10}')
Result(answer=10)
There's also ``json_compat_obj_encode`` and ``json_compat_obj_decode`` for
converting to and from Python primitive types rather than JSON strings.
Route Functions
---------------
To generate functions that represent routes, use the ``python_client``
generator::
$ stone python_client . calc.stone -- -m client -c Client -t myservice
``-m`` specifies the name of the Python module to generate, in this case
``client.py``. The important contents of the file look as follows::
class Client(object):
__metaclass__ = ABCMeta
@abstractmethod
def request(self, route, namespace, arg, arg_binary=None):
pass
# ------------------------------------------
# Routes in calc namespace
def calc_eval(self,
left,
right,
op=calc.Operator.add):
"""
:type op: :class:`myservice.calc.Operator`
:type left: long
:type right: long
:rtype: :class:`myservice.calc.Result`
:raises: :class:`.exceptions.ApiError`
If this raises, ApiError will contain:
:class:`myservice.calc.EvalError`
"""
arg = calc.Expression(left,
right,
op)
r = self.request(
calc.eval,
'calc',
arg,
None,
)
return r
``-c`` specified the name of the abstract class to generate. Using this class,
you'll likely want to inherit the class and implement the request function. For
example, an API that goes over HTTP might have the following client::
import requests # use the popular HTTP library
from .stone_serializers import json_decode, json_encode
from .exceptions import ApiError # You must implement this
class MyServiceClient(Client):
def request(self, route, namespace, arg, arg_binary=None):
url = 'https://api.myservice.xyz/{}/{}'.format(
namespace, route.name)
r = requests.get(
url,
headers={'Content-Type': 'application/json'},
data=json_encode(route.arg_type, arg))
if r.status_code != 200:
raise ApiError(...)
return json_decode(route.result_type, r.content)
Note that care is taken to ensure that that the return type and exception type
match those that were specified in the automatically generated documentation.
Routes with Version Numbers
---------------------------
There can be multiple versions of routes sharing the same name. For each route with a version
numbers other than 1, the generated module-level route variable and route function have a version
suffix appended in the form of ``{name}_v{version}``.
For example, suppose we add a new version of route ``eval`` in ``calc.stone`` as follows::
...
route eval:2(Expression, ResultV2, EvalError)
struct ResultV2
answer String
...
The module-level variable for the route will be::
>>> eval_v2
Route('eval', 2, False, ...)
And the corresponding route function in ``client.py`` will be ``calc_eval_v2``.
|