diff --git a/README.md b/README.md index d817fa4..06d54e7 100644 --- a/README.md +++ b/README.md @@ -59,3 +59,30 @@ def custom_convert(value): convert(value, custom_serializer=custom_convert) ``` + +## Proto File Generation + +[Protocol Buffers](https://developers.google.com/protocol-buffers/docs/proto) are a powerful tool +to describe structured data. In addition to the undocument json serialization it is useful to add +a proto serialization which can be used in many other contexts such as API client generation or +docs generation. + +You can try it out via + +```python +python3 example.py > sample.proto +``` + +> A prerequisite is to install []() and [`protoc`](). This is an example install command for mac: +> ```shell +> brew install protobuf +> go install github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc@latest +> ``` + +And then generate the docs via +```shell +protoc --doc_out=./docs --doc_opt=html,docs.html sample.proto +protoc --doc_out=./docs --doc_opt=markdown,docs.md sample.proto +``` + +Check out the docs in the [`/docs`](/docs) directory. diff --git a/docs/docs.html b/docs/docs.html new file mode 100644 index 0000000..05b079d --- /dev/null +++ b/docs/docs.html @@ -0,0 +1,468 @@ + + + + + Protocol Documentation + + + + + + + + + + +

Protocol Documentation

+ +

Table of Contents

+ +
+ +
+ + + +
+

sample.proto

Top +
+

+ + +

person

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
namestringrequired

the name of the person

ageint32optional

the age of the person

hobbystringoptional

the hobby of the person

+ + + + + +

root

+

+ + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
personpersonrequired

a person object

+ + + + + + + + + + + + + +

Scalar Value Types

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
.proto TypeNotesC++JavaPythonGoC#PHPRuby
doubledoubledoublefloatfloat64doublefloatFloat
floatfloatfloatfloatfloat32floatfloatFloat
int32Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead.int32intintint32intintegerBignum or Fixnum (as required)
int64Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead.int64longint/longint64longinteger/stringBignum
uint32Uses variable-length encoding.uint32intint/longuint32uintintegerBignum or Fixnum (as required)
uint64Uses variable-length encoding.uint64longint/longuint64ulonginteger/stringBignum or Fixnum (as required)
sint32Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s.int32intintint32intintegerBignum or Fixnum (as required)
sint64Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s.int64longint/longint64longinteger/stringBignum
fixed32Always four bytes. More efficient than uint32 if values are often greater than 2^28.uint32intintuint32uintintegerBignum or Fixnum (as required)
fixed64Always eight bytes. More efficient than uint64 if values are often greater than 2^56.uint64longint/longuint64ulonginteger/stringBignum
sfixed32Always four bytes.int32intintint32intintegerBignum or Fixnum (as required)
sfixed64Always eight bytes.int64longint/longint64longinteger/stringBignum
boolboolbooleanbooleanboolboolbooleanTrueClass/FalseClass
stringA string must always contain UTF-8 encoded or 7-bit ASCII text.stringStringstr/unicodestringstringstringString (UTF-8)
bytesMay contain any arbitrary sequence of bytes.stringByteStringstr[]byteByteStringstringString (ASCII-8BIT)
+ + + diff --git a/docs/docs.md b/docs/docs.md new file mode 100644 index 0000000..a355404 --- /dev/null +++ b/docs/docs.md @@ -0,0 +1,81 @@ +# Protocol Documentation + + +## Table of Contents + +- [sample.proto](#sample-proto) + - [person](#sample-person) + - [root](#sample-root) + +- [Scalar Value Types](#scalar-value-types) + + + + +

Top

+ +## sample.proto + + + + + +### person + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| name | [string](#string) | required | the name of the person | +| age | [int32](#int32) | optional | the age of the person | +| hobby | [string](#string) | optional | the hobby of the person | + + + + + + + + +### root + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| person | [person](#sample-person) | required | a person object | + + + + + + + + + + + + + + + +## Scalar Value Types + +| .proto Type | Notes | C++ | Java | Python | Go | C# | PHP | Ruby | +| ----------- | ----- | --- | ---- | ------ | -- | -- | --- | ---- | +| double | | double | double | float | float64 | double | float | Float | +| float | | float | float | float | float32 | float | float | Float | +| int32 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) | +| int64 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | int64 | long | int/long | int64 | long | integer/string | Bignum | +| uint32 | Uses variable-length encoding. | uint32 | int | int/long | uint32 | uint | integer | Bignum or Fixnum (as required) | +| uint64 | Uses variable-length encoding. | uint64 | long | int/long | uint64 | ulong | integer/string | Bignum or Fixnum (as required) | +| sint32 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) | +| sint64 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. | int64 | long | int/long | int64 | long | integer/string | Bignum | +| fixed32 | Always four bytes. More efficient than uint32 if values are often greater than 2^28. | uint32 | int | int | uint32 | uint | integer | Bignum or Fixnum (as required) | +| fixed64 | Always eight bytes. More efficient than uint64 if values are often greater than 2^56. | uint64 | long | int/long | uint64 | ulong | integer/string | Bignum | +| sfixed32 | Always four bytes. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) | +| sfixed64 | Always eight bytes. | int64 | long | int/long | int64 | long | integer/string | Bignum | +| bool | | bool | boolean | boolean | bool | bool | boolean | TrueClass/FalseClass | +| string | A string must always contain UTF-8 encoded or 7-bit ASCII text. | string | String | str/unicode | string | string | string | String (UTF-8) | +| bytes | May contain any arbitrary sequence of bytes. | string | ByteString | str | []byte | ByteString | string | String (ASCII-8BIT) | + diff --git a/example.py b/example.py new file mode 100644 index 0000000..40cfd3a --- /dev/null +++ b/example.py @@ -0,0 +1,13 @@ +from collections import OrderedDict +import voluptuous as vol +import voluptuous_serialize + +s = vol.Schema(vol.Object({ + vol.Required('person', description="a person object"): { + vol.Required('name', description="the name of the person"): vol.All(str, vol.Length(min=5)), + vol.Optional('age', description="the age of the person"): vol.All(vol.Coerce(int), vol.Range(min=18)), + vol.Optional('hobby', description="the hobby of the person"): str + } +})) + +print(voluptuous_serialize.proto(s)) diff --git a/sample.proto b/sample.proto new file mode 100644 index 0000000..4d77437 --- /dev/null +++ b/sample.proto @@ -0,0 +1,14 @@ +syntax = "proto2"; + +package sample; + +message person { + required string name = 2; // the name of the person + optional int32 age = 3; // the age of the person + optional string hobby = 4; // the hobby of the person +} + +message root { + required person person = 1; // a person object +} + diff --git a/tests/test_lib.py b/tests/test_lib.py index 570cf6f..7fc5caf 100644 --- a/tests/test_lib.py +++ b/tests/test_lib.py @@ -204,3 +204,17 @@ class TestEnum(Enum): (2, 2), ], } == convert(vol.Schema(vol.Coerce(TestEnum))) + +def test_dictionary_schema(): + assert [{ + 'type': 'dictionary', + 'dictionary': [{'type': 'string', 'name': 'def'}], + 'name': 'abc' + }] == convert(vol.Schema({"abc": {"def": str}})) + +def test_mapping_schema(): + assert { + 'type': 'mapping', + 'key': {'type': 'integer'}, + 'value': {'type': 'string'} + } == convert(vol.Schema({int: str})) diff --git a/voluptuous_serialize/__init__.py b/voluptuous_serialize/__init__.py index 2fc09b3..3353dfa 100644 --- a/voluptuous_serialize/__init__.py +++ b/voluptuous_serialize/__init__.py @@ -12,6 +12,13 @@ bool: "boolean", } +PROTO_TYPES_MAP = { + "integer": "int32", + "string": "string", + "float": "float", + "boolean": "bool" +} + UNSUPPORTED = object() @@ -37,8 +44,21 @@ def convert(schema, *, custom_serializer=None): else: pkey = key + if not isinstance(pkey, str): + if len(schema) != 1: + raise ValueError( + 'Unable to convert schema: {}'.format(schema) + ) + return { + 'type': 'mapping', + 'key': convert(key), + 'value': convert(value) + } + pval = convert(value, custom_serializer=custom_serializer) - pval["name"] = pkey + if isinstance(pval, list): + pval = {'type': 'dictionary', 'dictionary': pval} + pval['name'] = pkey if description is not None: pval["description"] = description @@ -117,3 +137,56 @@ def convert(schema, *, custom_serializer=None): } raise ValueError("Unable to convert schema: {}".format(schema)) + + +class Message: + def __init__(self, entries, name): + self.entries = entries + self.name = name + + def __repr__(self): + return """message {} {{ + {} +}}""".format( + self.name, + " \n ".join([str(entry) for entry in self.entries]) + ) + + +class Entry: + def __init__(self, entry, i=1): + self.type = entry['type'] + self.name = entry['name'] + self.type = self.name if self.type == 'dictionary' else self.type + self.description = entry.get('description', '') + self.required = entry.get('required', False) + self.i = i + + def __repr__(self): + return "{}{} {} = {};{}".format( + "required " if self.required else "optional ", + PROTO_TYPES_MAP.get(self.type, self.type), + self.name, + self.i, + " // {}".format(self.description) if self.description else "" + ) + + +def proto_convert(_list, name, i=1): + entries = [] + ret = "" + for entry in _list: + entries.append(Entry(entry, i)) + i+=1 + if entry['type'] == 'dictionary': + ret += str(proto_convert(entry['dictionary'], entry['name'], i)) + "\n" + return ret + str(Message(entries, name)) + "\n" + + +def proto(schema, *, custom_serializer=None): + _list = convert(schema, custom_serializer=custom_serializer) + return """syntax = "proto2"; + +package sample; + +""" + proto_convert(_list, "root")