summaryrefslogtreecommitdiff
path: root/README.extensions.md
blob: 0a22d4191366a9f9f4a912495cd6123dd7e18208 (plain)
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
# Protocol extensions

The `amqp_codegen.py` AMQP specification compiler has recently been
enhanced to take more than a single specification file, which allows
AMQP library authors to include extensions to the core protocol
without needing to modify the core AMQP specification file as
distributed.

The compiler is invoked with the path to a single "main" specification
document and zero or more paths to "extension" documents.

The order of the extensions matters: any later class property
definitions, for instance, are added to the list of definitions in
order of appearance. In general, composition of extensions with a core
specification document is therefore non-commutative.

## The main document

Written in the style of a
[json-shapes](https://github.com/tonyg/json-shapes) schema:

    DomainDefinition = _and(array_of(string()), array_length_equals(2));

    ConstantDefinition = {
        "name": string(),
        "value": number(),
        "class": optional(_or("soft-error", "hard-error"))
    };

    FieldDefinition = {
        "name": string(),
        "type": string(),
        "default-value": optional(anything())
    };

    MethodDefinition = {
        "name": string(),
        "id": number(),
        "arguments": array_of(FieldDefinition),
        "synchronous": optional(boolean()),
        "content": optional(boolean())
    };

    ClassDefinition = {
        "name": string(),
        "id": number(),
        "methods": array_of(MethodDefinition),
        "properties": optional(array_of(FieldDefinition))
    };

    MainDocument = {
        "major-version": number(),
        "minor-version": number(),
        "revision": optional(number()),
        "port": number(),
        "domains": array_of(DomainDefinition),
        "constants": array_of(ConstantDefinition),
        "classes": array_of(ClassDefinition),
    }

Within a `FieldDefinition`, the keyword `domain` can be used instead
of `type`, but `type` is preferred and `domain` is deprecated.

Type names can either be a defined `domain` name or a built-in name
from the following list:

 - octet
 - shortstr
 - longstr
 - short
 - long
 - longlong
 - bit
 - table
 - timestamp

Method and class IDs must be integers between 0 and 65535,
inclusive. Note that there is no specific subset of the space reserved
for experimental or site-local extensions, so be careful not to
conflict with IDs used by the AMQP core specification.

If the `synchronous` field of a `MethodDefinition` is missing, it is
assumed to be `false`; the same applies to the `content` field.

A `ConstantDefinition` with a `class` attribute is considered to be an
error-code definition; otherwise, it is considered to be a
straightforward numeric constant.

## Extensions

Written in the style of a
[json-shapes](https://github.com/tonyg/json-shapes) schema, and
referencing some of the type definitions given above:

    ExtensionDocument = {
        "extension": anything(),
        "domains": array_of(DomainDefinition),
        "constants": array_of(ConstantDefinition),
        "classes": array_of(ClassDefinition)
    };

The `extension` keyword is used to describe the extension informally
for human readers. Typically it will be a dictionary, with members
such as:

    {
        "name": "The name of the extension",
        "version": "1.0",
        "copyright": "Copyright (C) 1234 Yoyodyne, Inc."
    }

## Merge behaviour

In the case of conflicts between values specified in the main document
and in any extension documents, type-specific merge operators are
invoked.

 - Any doubly-defined domain names are regarded as true
   conflicts. Otherwise, all the domain definitions from all the main
   and extension documents supplied to the compiler are merged into a
   single dictionary.

 - Constant definitions are treated as per domain names above,
   *mutatis mutandis*.

 - Classes and their methods are a little trickier: if an extension
   defines a class with the same name as one previously defined, then
   only the `methods` and `properties` fields of the extension's class
   definition are attended to.

    - Any doubly-defined method names or property names within a class
      are treated as true conflicts.

    - Properties defined in an extension are added to the end of the
      extant property list for the class.

   (Extensions are of course permitted to define brand new classes as
   well as to extend existing ones.)

 - Any other kind of conflict leads to a raised
   `AmqpSpecFileMergeConflict` exception.

## Invoking the spec compiler

Your code generation code should invoke `amqp_codegen.do_main_dict`
with a dictionary of functions as the sole argument.  Each will be
used for generationg a separate file.  The `do_main_dict` function
will parse the command-line arguments supplied when python was
invoked.

The command-line will be parsed as:

    python your_codegen.py <action> <mainspec> [<extspec> ...] <outfile>

where `<action>` is a key into the dictionary supplied to
`do_main_dict` and is used to select which generation function is
called. The `<mainspec>` and `<extspec>` arguments are file names of
specification documents containing expressions in the syntax given
above. The *final* argument on the command line, `<outfile>`, is the
name of the source-code file to generate.

Here's a tiny example of the layout of a code generation module that
uses `amqp_codegen`:

    import amqp_codegen

    def generateHeader(specPath):
        spec = amqp_codegen.AmqpSpec(specPath)
        ...

    def generateImpl(specPath):
        spec = amqp_codegen.AmqpSpec(specPath)
        ...

    if __name__ == "__main__":
        amqp_codegen.do_main_dict({"header": generateHeader,
                                   "body": generateImpl})

The reasons for allowing more than one action, are that

 - many languages have separate "header"-type files (C and Erlang, to
   name two)
 - `Makefile`s often require separate rules for generating the two
   kinds of file, but it's convenient to keep the generation code
   together in a single python module

The main reason things are laid out this way, however, is simply that
it's an accident of the history of the code. We may change the API to
`amqp_codegen` in future to clean things up a little.