|
| 1 | +****************** |
| 2 | +Builtin Generators |
| 3 | +****************** |
| 4 | + |
| 5 | +Using a generator, you can convert the data types and routes in your spec into |
| 6 | +objects in your programming language of choice. |
| 7 | + |
| 8 | +Currently, the only generator included with Stone is for `Python |
| 9 | +<#python-guide>`_. Dropbox has written generators for an assortment of |
| 10 | +languages, which we intend to release, including: |
| 11 | + |
| 12 | + * C# |
| 13 | + * Go |
| 14 | + * Java |
| 15 | + * Javascript |
| 16 | + * Swift |
| 17 | + |
| 18 | +If you're looking to make your own generator, see `Writing a Generator |
| 19 | +<generator_ref.rst>`_. We would love to see a contribution of a PHP or Ruby |
| 20 | +generator. |
| 21 | + |
| 22 | +Compile with the CLI |
| 23 | +==================== |
| 24 | + |
| 25 | +Compiling a spec and generating code is done using the ``stone`` |
| 26 | +command-line interface (CLI):: |
| 27 | + |
| 28 | + $ stone -h |
| 29 | + usage: stone [-h] [-v] [--clean-build] [-f FILTER_BY_ROUTE_ATTR] |
| 30 | + [-w WHITELIST_NAMESPACE_ROUTES | -b BLACKLIST_NAMESPACE_ROUTES] |
| 31 | + generator output [spec [spec ...]] |
| 32 | + |
| 33 | + StoneAPI |
| 34 | + |
| 35 | + positional arguments: |
| 36 | + generator Either the name of a built-in generator or the path to |
| 37 | + a generator module. Paths to generator modules must |
| 38 | + end with a .stoneg.py extension. The following |
| 39 | + generators are built-in: js_client, python_types, |
| 40 | + python_client, swift_client |
| 41 | + output The folder to save generated files to. |
| 42 | + spec Path to API specifications. Each must have a .stone |
| 43 | + extension. If omitted or set to "-", the spec is read |
| 44 | + from stdin. Multiple namespaces can be provided over |
| 45 | + stdin by concatenating multiple specs together. |
| 46 | + |
| 47 | + optional arguments: |
| 48 | + -h, --help show this help message and exit |
| 49 | + -v, --verbose Print debugging statements. |
| 50 | + --clean-build The path to the template SDK for the target language. |
| 51 | + -f FILTER_BY_ROUTE_ATTR, --filter-by-route-attr FILTER_BY_ROUTE_ATTR |
| 52 | + Removes routes that do not match the expression. The |
| 53 | + expression must specify a route attribute on the left- |
| 54 | + hand side and a value on the right-hand side. Use |
| 55 | + quotes for strings and bytes. The only supported |
| 56 | + operators are "=" and "!=". For example, if "hide" is |
| 57 | + a route attribute, we can use this filter: |
| 58 | + "hide!=true". You can combine multiple expressions |
| 59 | + with "and"/"or" and use parentheses to enforce |
| 60 | + precedence. |
| 61 | + -w WHITELIST_NAMESPACE_ROUTES, --whitelist-namespace-routes WHITELIST_NAMESPACE_ROUTES |
| 62 | + If set, generators will only see the specified |
| 63 | + namespaces as having routes. |
| 64 | + -b BLACKLIST_NAMESPACE_ROUTES, --blacklist-namespace-routes BLACKLIST_NAMESPACE_ROUTES |
| 65 | + If set, generators will not see any routes for the |
| 66 | + specified namespaces. |
| 67 | + |
| 68 | +We'll generate code based on an ``calc.stone`` spec with the following |
| 69 | +contents:: |
| 70 | + |
| 71 | + namespace calc |
| 72 | + |
| 73 | + route eval(Expression, Result, CalcError) |
| 74 | + |
| 75 | + struct Expression |
| 76 | + "This expression is limited to a binary operation." |
| 77 | + op Operator = add |
| 78 | + left Int64 |
| 79 | + right Int64 |
| 80 | + |
| 81 | + union Operator |
| 82 | + add |
| 83 | + sub |
| 84 | + mult |
| 85 | + div Boolean |
| 86 | + "If value is true, rounds up. Otherwise, rounds down." |
| 87 | + |
| 88 | + struct Result |
| 89 | + answer Int64 |
| 90 | + |
| 91 | + union EvalError |
| 92 | + overflow |
| 93 | + |
| 94 | +Python Guide |
| 95 | +============ |
| 96 | + |
| 97 | +This section explains how to use the pre-packaged Python generators and work |
| 98 | +with the Python classes that have been generated from a spec. |
| 99 | + |
| 100 | +There are two different Python generators: ``python_types`` and |
| 101 | +``python_client``. The former generates Python classes for the data types |
| 102 | +defined in your spec. The latter generates a single Python class with a method |
| 103 | +per route, which is useful for building SDKs. |
| 104 | + |
| 105 | +We'll use the ``python_types`` generator:: |
| 106 | + |
| 107 | + $ stone python_types . calc.stone |
| 108 | + |
| 109 | +This runs the generator on the ``calc.stone`` spec. Its output target is |
| 110 | +``.`` which is the current directory. A Python module is created for |
| 111 | +each declared namespace, so in this case only ``calc.py`` is created. |
| 112 | + |
| 113 | +Three additional modules are copied into the target directory. The first, |
| 114 | +``stone_validators.py``, contains classes for validating Python values against |
| 115 | +their expected Stone types. You will not need to explicitly import this module, |
| 116 | +but the auto-generated Python classes depend on it. The second, |
| 117 | +``stone_serializers.py``, contains a pair of ``json_encode()`` and |
| 118 | +`json_decode()`` functions. You will need to import this module to serialize |
| 119 | +your objects. The last is ``stone_base.py`` which shouldn't be used directly. |
| 120 | + |
| 121 | +In the following sections, we'll interact with the classes generated in |
| 122 | +``calc.py``. For simplicity, we'll assume we've opened a Python interpreter |
| 123 | +with the following shell command:: |
| 124 | + |
| 125 | + $ python -i calc.py |
| 126 | + |
| 127 | +For non-test projects, we recommend that you set the generation target to a |
| 128 | +path within a Python package, and use Python's import facility. |
| 129 | + |
| 130 | +Primitive Types |
| 131 | +--------------- |
| 132 | + |
| 133 | +The following table shows the mapping between a Stone `primitive type |
| 134 | +<lang_ref.rst#primitive-types>`_ and its corresponding type in Python. |
| 135 | + |
| 136 | +========================== ============== ===================================== |
| 137 | +Primitive Python 2.x / 3 Notes |
| 138 | +========================== ============== ===================================== |
| 139 | +Bytes bytes |
| 140 | +Boolean bool |
| 141 | +Float{32,64} float long type within range is converted. |
| 142 | +Int{32,64}, UInt{32,64} long |
| 143 | +List list |
| 144 | +String unicode / str str type is converted to unicode. |
| 145 | +Timestamp datetime |
| 146 | +========================== ============== ===================================== |
| 147 | + |
| 148 | +Struct |
| 149 | +------ |
| 150 | + |
| 151 | +For each struct in your spec, you will see a corresponding Python class of the |
| 152 | +same name. |
| 153 | + |
| 154 | +In our example, ``Expression``, ``Operator``, ``Answer``, ``EvalError``, and |
| 155 | +are Python classes. They have an attribute (getter/setter/deleter property) for |
| 156 | +each field defined in the spec. You can instantiate these classes and specify |
| 157 | +field values either in the constructor or by assigning to an attribute:: |
| 158 | + |
| 159 | + >>> expr = Expression(op=Operator.add, left=1, right=1) |
| 160 | + |
| 161 | +If you assign a value that fails validation, an exception is raised:: |
| 162 | + |
| 163 | + >>> expr.op = '+' |
| 164 | + Traceback (most recent call last) |
| 165 | + ... |
| 166 | + ValidationError: expected type Operator or subtype, got string |
| 167 | + |
| 168 | +Accessing a required field (non-optional with no default) that has not been set |
| 169 | +raises an error:: |
| 170 | + |
| 171 | + >>> res = Result() |
| 172 | + >>> res.answer |
| 173 | + Traceback (most recent call last): |
| 174 | + File "<stdin>", line 1, in <module> |
| 175 | + File "calc.py", line 221, in answer |
| 176 | + raise AttributeError("missing required field 'answer'") |
| 177 | + AttributeError: missing required field 'answer' |
| 178 | + |
| 179 | +Other characteristics: |
| 180 | + |
| 181 | + 1. Inheritance in Stone is represented as inheritance in Python. |
| 182 | + 2. If a field is nullable and was never set, ``None`` is returned. |
| 183 | + 3. If a field has a default but was never set, the default is returned. |
| 184 | + |
| 185 | +Union |
| 186 | +----- |
| 187 | + |
| 188 | +For each union in your spec, you will see a corresponding Python class of the |
| 189 | +same name. |
| 190 | + |
| 191 | +You do not use a union class's constructor directly. To select a tag with a |
| 192 | +void type, use the class attribute of the same name:: |
| 193 | + |
| 194 | + >>> EvalError.overflow |
| 195 | + EvalError('overflow', None) |
| 196 | + |
| 197 | +To select a tag with a value, use the class method of the same name and pass |
| 198 | +in an argument to serve as the value:: |
| 199 | + |
| 200 | + >>> Operator.div(False) |
| 201 | + Operator('div', False) |
| 202 | + |
| 203 | +To write code that handles the union options, use the ``is_[tag]()`` methods. |
| 204 | +We recommend you exhaustively check all tags, or include an else clause to |
| 205 | +ensure that all possibilities are accounted for. For tags that have values, |
| 206 | +use the ``get_[tag]()`` method to access the value:: |
| 207 | + |
| 208 | + >>> # assume that op is an instance of Operator |
| 209 | + >>> if op.is_add(): |
| 210 | + ... # handle addition |
| 211 | + ... elif op.is_sub(): |
| 212 | + ... # handle subtraction |
| 213 | + ... elif op.is_mult(): |
| 214 | + ... # handle multiplication |
| 215 | + ... elif op.is_div(): |
| 216 | + ... round_up = op.get_div() |
| 217 | + ... # handle division |
| 218 | + |
| 219 | +Struct Polymorphism |
| 220 | +------------------- |
| 221 | + |
| 222 | +As with regular structs, structs that enumerate subtypes have corresponding |
| 223 | +Python classes that behave identically to regular structs. |
| 224 | + |
| 225 | +The difference is apparent when a field has a data type that is a struct with |
| 226 | +enumerated subtypes. Expanding on our example from the language reference, |
| 227 | +assume the following spec:: |
| 228 | + |
| 229 | + struct Resource |
| 230 | + union* |
| 231 | + file File |
| 232 | + folder Folder |
| 233 | + |
| 234 | + path String |
| 235 | + |
| 236 | + struct File extends Resource: |
| 237 | + size UInt64 |
| 238 | + |
| 239 | + struct Folder extends Resource: |
| 240 | + "No new fields." |
| 241 | + |
| 242 | + struct Response |
| 243 | + rsrc Resource |
| 244 | + |
| 245 | +If we instantiate ``Response``, the ``rsrc`` field can only be assigned a |
| 246 | +``File`` or ``Folder`` object. It should not be assigned a ``Resource`` object. |
| 247 | + |
| 248 | +An exception to this is on deserialization. Because ``Resource`` is specified |
| 249 | +as a catch-all, it's possible when deserializing a ``Response`` to get a |
| 250 | +``Resource`` object in the ``rsrc`` field. This indicates that the returned |
| 251 | +subtype was unknown because the recipient has an older spec than the sender. |
| 252 | +To handle catch-alls, you should use an else clause:: |
| 253 | + |
| 254 | + >>> print resp.rsrc.path # Guaranteed to work regardless of subtype |
| 255 | + >>> if isinstance(resp, File): |
| 256 | + ... # handle File |
| 257 | + ... elif isinstance(resp, Folder): |
| 258 | + ... # handle Folder |
| 259 | + ... else: |
| 260 | + ... # unknown subtype of Resource |
| 261 | + |
| 262 | +Route |
| 263 | +----- |
| 264 | + |
| 265 | +Routes are represented as instances of a ``Route`` object. The generated Python |
| 266 | +module for the namespace will have a module-level variable for each route:: |
| 267 | + |
| 268 | + >>> eval |
| 269 | + Route('eval', False, ...) |
| 270 | + |
| 271 | +Serialization |
| 272 | +------------- |
| 273 | + |
| 274 | +We can use ``stone_serializers.json_encode()`` to serialize our objects to |
| 275 | +JSON:: |
| 276 | + |
| 277 | + >>> import stone_serializers |
| 278 | + >>> stone_serializers.json_encode(eval.result_type, Result(answer=10)) |
| 279 | + '{"answer": 10}' |
| 280 | + |
| 281 | +To deserialize, we can use ``json_decode``:: |
| 282 | + |
| 283 | + >>> stone_serializers.json_decode(eval.result_type, '{"answer": 10}') |
| 284 | + Result(answer=10) |
| 285 | + |
| 286 | +There's also ``json_compat_obj_encode`` and ``json_compat_obj_decode`` for |
| 287 | +converting to and from Python primitive types rather than JSON strings. |
| 288 | + |
0 commit comments