structurize 3.6.0__tar.gz → 3.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {structurize-3.6.0/structurize.egg-info → structurize-3.7.0}/PKG-INFO +1 -1
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/__init__.py +30 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/_version.py +3 -3
- structurize-3.7.0/avrotize/avrotocapnproto.py +183 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/avrotocsharp.py +138 -10
- structurize-3.7.0/avrotize/avrotocue.py +145 -0
- structurize-3.7.0/avrotize/avrotoflatbuffers.py +155 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/avrotojstruct.py +20 -0
- structurize-3.7.0/avrotize/avrotojtd.py +225 -0
- structurize-3.7.0/avrotize/avrotoraml.py +144 -0
- structurize-3.7.0/avrotize/avrotosmithy.py +311 -0
- structurize-3.7.0/avrotize/avrotosurreal.py +138 -0
- structurize-3.7.0/avrotize/avrotothrift.py +140 -0
- structurize-3.7.0/avrotize/capnprototoavro.py +378 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/commands.json +1288 -3
- structurize-3.7.0/avrotize/cuetoavro.py +414 -0
- structurize-3.7.0/avrotize/cuetostructure.py +67 -0
- structurize-3.7.0/avrotize/flatbufferstoavro.py +352 -0
- structurize-3.7.0/avrotize/flatbufferstojstruct.py +76 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/jstructtoavro.py +53 -7
- structurize-3.7.0/avrotize/jstructtoflatbuffers.py +23 -0
- structurize-3.7.0/avrotize/jstructtoraml.py +20 -0
- structurize-3.7.0/avrotize/jstructtosmithy.py +24 -0
- structurize-3.7.0/avrotize/jtdtoavro.py +222 -0
- structurize-3.7.0/avrotize/jtdtostructure.py +24 -0
- structurize-3.7.0/avrotize/ramltoavro.py +240 -0
- structurize-3.7.0/avrotize/ramltojstruct.py +20 -0
- structurize-3.7.0/avrotize/smithytoavro.py +539 -0
- structurize-3.7.0/avrotize/smithytojstruct.py +24 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/structuretocsharp.py +134 -8
- structurize-3.7.0/avrotize/structuretocue.py +26 -0
- structurize-3.7.0/avrotize/structuretojtd.py +24 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/structuretokusto.py +32 -3
- structurize-3.7.0/avrotize/structuretoparquet.py +296 -0
- structurize-3.7.0/avrotize/surrealtoavro.py +290 -0
- structurize-3.7.0/avrotize/thriftstructure.py +48 -0
- structurize-3.7.0/avrotize/thrifttoavro.py +397 -0
- {structurize-3.6.0 → structurize-3.7.0/structurize.egg-info}/PKG-INFO +1 -1
- {structurize-3.6.0 → structurize-3.7.0}/structurize.egg-info/SOURCES.txt +56 -0
- {structurize-3.6.0 → structurize-3.7.0}/.gitignore +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/LICENSE +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/MANIFEST.in +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/README.md +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/__main__.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/asn1toavro.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/avrotize.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/avrotocpp.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/avrotocsv.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/avrotodatapackage.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/avrotodb.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/avrotogo.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/avrotographql.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/avrotoiceberg.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/avrotojava.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/avrotojs.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/avrotojsons.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/avrotokusto.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/avrotomd.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/avrotools.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/avrotoparquet.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/avrotoproto.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/avrotopython.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/avrotorust.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/avrotots.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/avrototsml.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/avrotoxsd.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/avrovalidator.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/cddltostructure.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/choice_inference.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/common.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/constants.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/csvtoavro.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/datapackagetoavro.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/dependencies/cpp/vcpkg/vcpkg.json +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/dependencies/typescript/node22/package.json +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/dependency_resolver.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/dependency_version.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/jsonstoavro.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/jsonstostructure.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/jsontoschema.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/kstructtoavro.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/kustotoavro.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/kustotojstruct.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/mcp_server.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/openapitostructure.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/parquettoavro.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/proto2parser.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/proto3parser.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/prototoavro.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/schema_inference.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/sqltoavro.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/structuretocddl.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/structuretocpp.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/structuretocsv.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/structuretodatapackage.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/structuretodb.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/structuretogo.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/structuretographql.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/structuretoiceberg.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/structuretojava.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/structuretojs.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/structuretojsons.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/structuretomd.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/structuretoproto.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/structuretopython.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/structuretorust.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/structuretots.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/structuretotsml.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/structuretoxsd.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/tmslvalidate.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/validate.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/xmltoschema.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/avrotize/xsdtoavro.py +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/build.ps1 +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/build.sh +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/pyproject.toml +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/setup.cfg +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/structurize.egg-info/dependency_links.txt +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/structurize.egg-info/entry_points.txt +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/structurize.egg-info/requires.txt +0 -0
- {structurize-3.6.0 → structurize-3.7.0}/structurize.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structurize
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.7.0
|
|
4
4
|
Summary: Tools to convert from and to JSON Structure from various other schema languages.
|
|
5
5
|
Author-email: Clemens Vasters <clemensv@microsoft.com>
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -25,6 +25,28 @@ class LazyLoader:
|
|
|
25
25
|
# Define the functions and their corresponding module paths
|
|
26
26
|
_mappings = {
|
|
27
27
|
"convert_proto_to_avro": (f"{mod}.prototoavro", "convert_proto_to_avro"),
|
|
28
|
+
"convert_jtd_to_avro": (f"{mod}.jtdtoavro", "convert_jtd_to_avro"),
|
|
29
|
+
"convert_avro_to_jtd": (f"{mod}.avrotojtd", "convert_avro_to_jtd"),
|
|
30
|
+
"convert_jtd_to_structure": (f"{mod}.jtdtostructure", "convert_jtd_to_structure"),
|
|
31
|
+
"convert_structure_to_jtd": (f"{mod}.structuretojtd", "convert_structure_to_jtd"),
|
|
32
|
+
"convert_surrealql_to_avro": (f"{mod}.surrealtoavro", "convert_surrealql_to_avro"),
|
|
33
|
+
"convert_avro_to_surrealql": (f"{mod}.avrotosurreal", "convert_avro_to_surrealql"),
|
|
34
|
+
"convert_flatbuffers_to_avro": (f"{mod}.flatbufferstoavro", "convert_flatbuffers_to_avro"),
|
|
35
|
+
"convert_avro_to_flatbuffers": (f"{mod}.avrotoflatbuffers", "convert_avro_to_flatbuffers"),
|
|
36
|
+
"convert_flatbuffers_to_json_structure": (f"{mod}.flatbufferstojstruct", "convert_flatbuffers_to_json_structure"),
|
|
37
|
+
"convert_json_structure_to_flatbuffers": (f"{mod}.jstructtoflatbuffers", "convert_json_structure_to_flatbuffers"),
|
|
38
|
+
"convert_thrift_to_avro": (f"{mod}.thrifttoavro", "convert_thrift_to_avro"),
|
|
39
|
+
"convert_avro_to_thrift": (f"{mod}.avrotothrift", "convert_avro_to_thrift"),
|
|
40
|
+
"convert_thrift_to_structure": (f"{mod}.thriftstructure", "convert_thrift_to_structure"),
|
|
41
|
+
"convert_structure_to_thrift": (f"{mod}.thriftstructure", "convert_structure_to_thrift"),
|
|
42
|
+
"convert_smithy_to_avro": (f"{mod}.smithytoavro", "convert_smithy_to_avro"),
|
|
43
|
+
"convert_avro_to_smithy": (f"{mod}.avrotosmithy", "convert_avro_to_smithy"),
|
|
44
|
+
"convert_smithy_to_json_structure": (f"{mod}.smithytojstruct", "convert_smithy_to_json_structure"),
|
|
45
|
+
"convert_json_structure_to_smithy": (f"{mod}.jstructtosmithy", "convert_json_structure_to_smithy"),
|
|
46
|
+
"convert_raml_to_avro": (f"{mod}.ramltoavro", "convert_raml_to_avro"),
|
|
47
|
+
"convert_avro_to_raml": (f"{mod}.avrotoraml", "convert_avro_to_raml"),
|
|
48
|
+
"convert_raml_to_json_structure": (f"{mod}.ramltojstruct", "convert_raml_to_json_structure"),
|
|
49
|
+
"convert_json_structure_to_raml": (f"{mod}.jstructtoraml", "convert_json_structure_to_raml"),
|
|
28
50
|
"convert_jsons_to_avro": (f"{mod}.jsonstoavro", "convert_jsons_to_avro"),
|
|
29
51
|
"convert_kafka_struct_to_avro_schema": (f"{mod}.kstructtoavro", "convert_kafka_struct_to_avro_schema"),
|
|
30
52
|
"convert_kusto_to_avro": (f"{mod}.kustotoavro", "convert_kusto_to_avro"),
|
|
@@ -35,6 +57,14 @@ _mappings = {
|
|
|
35
57
|
"convert_avro_to_kusto_db": (f"{mod}.avrotokusto", "convert_avro_to_kusto_db"),
|
|
36
58
|
"convert_avro_to_parquet": (f"{mod}.avrotoparquet", "convert_avro_to_parquet"),
|
|
37
59
|
"convert_avro_to_proto": (f"{mod}.avrotoproto", "convert_avro_to_proto"),
|
|
60
|
+
"convert_cue_to_avro": (f"{mod}.cuetoavro", "convert_cue_to_avro"),
|
|
61
|
+
"convert_avro_to_cue": (f"{mod}.avrotocue", "convert_avro_to_cue"),
|
|
62
|
+
"convert_cue_to_json_structure": (f"{mod}.cuetostructure", "convert_cue_to_json_structure"),
|
|
63
|
+
"convert_json_structure_to_cue": (f"{mod}.structuretocue", "convert_json_structure_to_cue"),
|
|
64
|
+
"convert_capnproto_to_avro": (f"{mod}.capnprototoavro", "convert_capnproto_to_avro"),
|
|
65
|
+
"convert_avro_to_capnproto": (f"{mod}.avrotocapnproto", "convert_avro_to_capnproto"),
|
|
66
|
+
"convert_capnproto_to_json_structure": (f"{mod}.capnprototoavro", "convert_capnproto_to_json_structure"),
|
|
67
|
+
"convert_json_structure_to_capnproto": (f"{mod}.avrotocapnproto", "convert_json_structure_to_capnproto"),
|
|
38
68
|
"convert_avro_to_sql": (f"{mod}.avrotodb", "convert_avro_to_sql"),
|
|
39
69
|
"convert_avro_to_xsd": (f"{mod}.avrotoxsd", "convert_avro_to_xsd"),
|
|
40
70
|
"convert_avro_to_java": (f"{mod}.avrotojava", "convert_avro_to_java"),
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '3.
|
|
22
|
-
__version_tuple__ = version_tuple = (3,
|
|
21
|
+
__version__ = version = '3.7.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (3, 7, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'gd5dcf3fee'
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Convert Avrotize/Avro schemas to Cap'n Proto .capnp schemas."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import tempfile
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
INDENT = " "
|
|
12
|
+
FILE_ID = "0xd12f8a7e9b0c4a6f"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _safe_ident(name: str) -> str:
|
|
16
|
+
safe = re.sub(r"[^A-Za-z0-9_]", "_", name)
|
|
17
|
+
if not safe or safe[0].isdigit():
|
|
18
|
+
safe = f"_{safe}"
|
|
19
|
+
return safe
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _type_name(schema: dict[str, Any]) -> str:
|
|
23
|
+
namespace = schema.get("namespace")
|
|
24
|
+
name = schema["name"]
|
|
25
|
+
return f"{namespace}.{name}" if namespace else name
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AvroToCapnpConverter:
|
|
29
|
+
primitive_map = {
|
|
30
|
+
"null": "Void",
|
|
31
|
+
"boolean": "Bool",
|
|
32
|
+
"int": "Int32",
|
|
33
|
+
"long": "Int64",
|
|
34
|
+
"float": "Float32",
|
|
35
|
+
"double": "Float64",
|
|
36
|
+
"bytes": "Data",
|
|
37
|
+
"string": "Text",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
def __init__(self, namespace: str | None = None) -> None:
|
|
41
|
+
self.namespace = namespace
|
|
42
|
+
self.named: dict[str, dict[str, Any]] = {}
|
|
43
|
+
self.emitted: set[str] = set()
|
|
44
|
+
|
|
45
|
+
def convert(self, avro_schema: Any) -> str:
|
|
46
|
+
schemas = avro_schema if isinstance(avro_schema, list) else [avro_schema]
|
|
47
|
+
for schema in schemas:
|
|
48
|
+
if isinstance(schema, dict) and schema.get("type") in {"record", "enum"}:
|
|
49
|
+
self.named[schema["name"]] = schema
|
|
50
|
+
self.named[_type_name(schema)] = schema
|
|
51
|
+
lines = [f"@{FILE_ID};", ""]
|
|
52
|
+
if self.namespace:
|
|
53
|
+
lines.append(f"# namespace: {self.namespace}")
|
|
54
|
+
lines.append("# Generated by avrotize. File id is stable for deterministic output; replace if needed.")
|
|
55
|
+
lines.append("")
|
|
56
|
+
for schema in schemas:
|
|
57
|
+
if isinstance(schema, dict) and schema.get("type") in {"enum", "record"}:
|
|
58
|
+
name = _type_name(schema)
|
|
59
|
+
if name not in self.emitted:
|
|
60
|
+
lines.extend(self._render_named(schema, 0))
|
|
61
|
+
lines.append("")
|
|
62
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
63
|
+
|
|
64
|
+
def _render_named(self, schema: dict[str, Any], level: int) -> list[str]:
|
|
65
|
+
if schema.get("type") == "enum":
|
|
66
|
+
return self._render_enum(schema, level)
|
|
67
|
+
if schema.get("type") == "record":
|
|
68
|
+
return self._render_struct(schema, level)
|
|
69
|
+
return []
|
|
70
|
+
|
|
71
|
+
def _render_enum(self, schema: dict[str, Any], level: int) -> list[str]:
|
|
72
|
+
self.emitted.add(_type_name(schema))
|
|
73
|
+
lines = [f"{INDENT * level}enum {_safe_ident(schema['name'])} {{"]
|
|
74
|
+
for index, symbol in enumerate(schema.get("symbols", [])):
|
|
75
|
+
lines.append(f"{INDENT * (level + 1)}{_safe_ident(symbol)} @{index};")
|
|
76
|
+
lines.append(f"{INDENT * level}}}")
|
|
77
|
+
return lines
|
|
78
|
+
|
|
79
|
+
def _render_struct(self, schema: dict[str, Any], level: int) -> list[str]:
|
|
80
|
+
self.emitted.add(_type_name(schema))
|
|
81
|
+
lines = [f"{INDENT * level}struct {_safe_ident(schema['name'])} {{"]
|
|
82
|
+
fields = schema.get("fields", [])
|
|
83
|
+
grouped: dict[str, list[tuple[int, dict[str, Any], Any]]] = {}
|
|
84
|
+
normal: list[tuple[int, dict[str, Any], Any]] = []
|
|
85
|
+
for index, field in enumerate(fields):
|
|
86
|
+
avro_type = field.get("type")
|
|
87
|
+
union_name = field.get("capnpUnion")
|
|
88
|
+
item = (index, field, avro_type)
|
|
89
|
+
if union_name:
|
|
90
|
+
grouped.setdefault(str(union_name), []).append(item)
|
|
91
|
+
else:
|
|
92
|
+
normal.append(item)
|
|
93
|
+
for index, field, avro_type in normal:
|
|
94
|
+
lines.extend(self._render_field(field, avro_type, index, level + 1))
|
|
95
|
+
for _, items in grouped.items():
|
|
96
|
+
lines.append(f"{INDENT * (level + 1)}union {{")
|
|
97
|
+
for index, field, avro_type in items:
|
|
98
|
+
lines.extend(self._render_field(field, avro_type, index, level + 2))
|
|
99
|
+
lines.append(f"{INDENT * (level + 1)}}}")
|
|
100
|
+
lines.append(f"{INDENT * level}}}")
|
|
101
|
+
return lines
|
|
102
|
+
|
|
103
|
+
def _render_field(self, field: dict[str, Any], avro_type: Any, index: int, level: int) -> list[str]:
|
|
104
|
+
capnp_type, nested = self._capnp_type(avro_type, _safe_ident(field.get("name", f"field{index}")))
|
|
105
|
+
ordinal = index
|
|
106
|
+
lines: list[str] = []
|
|
107
|
+
lines.extend(self._render_inline_nested(nested, level))
|
|
108
|
+
lines.append(f"{INDENT * level}{_safe_ident(field.get('name', f'field{index}'))} @{ordinal} :{capnp_type};")
|
|
109
|
+
return lines
|
|
110
|
+
|
|
111
|
+
def _render_inline_nested(self, nested: list[dict[str, Any]], level: int) -> list[str]:
|
|
112
|
+
lines: list[str] = []
|
|
113
|
+
for schema in nested:
|
|
114
|
+
lines.extend(self._render_named(schema, level))
|
|
115
|
+
return lines
|
|
116
|
+
|
|
117
|
+
def _non_null_union(self, avro_type: list[Any]) -> list[Any]:
|
|
118
|
+
return [item for item in avro_type if item != "null" and not (isinstance(item, dict) and item.get("type") == "null")]
|
|
119
|
+
|
|
120
|
+
def _capnp_type(self, avro_type: Any, field_name: str) -> tuple[str, list[dict[str, Any]]]:
|
|
121
|
+
if isinstance(avro_type, list):
|
|
122
|
+
non_null = self._non_null_union(avro_type)
|
|
123
|
+
if len(non_null) == 1:
|
|
124
|
+
return self._capnp_type(non_null[0], field_name)
|
|
125
|
+
union_schema = {
|
|
126
|
+
"type": "record",
|
|
127
|
+
"name": f"{field_name[:1].upper()}{field_name[1:]}Choice",
|
|
128
|
+
"fields": [{"name": f"choice{i}", "type": t, "capnpUnion": "union"} for i, t in enumerate(non_null)],
|
|
129
|
+
}
|
|
130
|
+
return union_schema["name"], [union_schema]
|
|
131
|
+
if isinstance(avro_type, dict):
|
|
132
|
+
avro_kind = avro_type.get("type")
|
|
133
|
+
if isinstance(avro_kind, list):
|
|
134
|
+
return self._capnp_type(avro_kind, field_name)
|
|
135
|
+
if avro_kind == "array":
|
|
136
|
+
item_type, nested = self._capnp_type(avro_type.get("items", "string"), f"{field_name}Item")
|
|
137
|
+
return f"List({item_type})", nested
|
|
138
|
+
if avro_kind == "map":
|
|
139
|
+
entry = {
|
|
140
|
+
"type": "record",
|
|
141
|
+
"name": f"{field_name[:1].upper()}{field_name[1:]}Entry",
|
|
142
|
+
"fields": [
|
|
143
|
+
{"name": "key", "type": "string"},
|
|
144
|
+
{"name": "value", "type": avro_type.get("values", "string")},
|
|
145
|
+
],
|
|
146
|
+
}
|
|
147
|
+
return f"List({entry['name']})", [entry]
|
|
148
|
+
if avro_kind == "record":
|
|
149
|
+
return avro_type["name"], [avro_type]
|
|
150
|
+
if avro_kind == "enum":
|
|
151
|
+
return avro_type["name"], [avro_type]
|
|
152
|
+
if avro_kind == "fixed":
|
|
153
|
+
return "Data", []
|
|
154
|
+
return self._capnp_type(avro_kind, field_name)
|
|
155
|
+
if isinstance(avro_type, str):
|
|
156
|
+
named = self.named.get(avro_type)
|
|
157
|
+
if named:
|
|
158
|
+
return named["name"], []
|
|
159
|
+
return self.primitive_map.get(avro_type, avro_type.split(".")[-1]), []
|
|
160
|
+
return "Text", []
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def convert_avro_to_capnproto(avro_schema_path: str, capnp_file_path: str, namespace: str | None = None) -> None:
|
|
164
|
+
"""Convert an Avrotize/Avro schema file to a Cap'n Proto .capnp schema file."""
|
|
165
|
+
with open(avro_schema_path, "r", encoding="utf-8") as avro_file:
|
|
166
|
+
avro_schema = json.load(avro_file)
|
|
167
|
+
converter = AvroToCapnpConverter(namespace=namespace)
|
|
168
|
+
capnp_schema = converter.convert(avro_schema)
|
|
169
|
+
os.makedirs(os.path.dirname(os.path.abspath(capnp_file_path)) or ".", exist_ok=True)
|
|
170
|
+
with open(capnp_file_path, "w", encoding="utf-8") as capnp_file:
|
|
171
|
+
capnp_file.write(capnp_schema)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def convert_json_structure_to_capnproto(structure_file: str, capnp_file_path: str, namespace: str | None = None) -> None:
|
|
175
|
+
"""Convert JSON Structure to Cap'n Proto by bridging through Avrotize/Avro."""
|
|
176
|
+
from avrotize.jstructtoavro import convert_json_structure_to_avro
|
|
177
|
+
|
|
178
|
+
temp_parent = os.path.dirname(os.path.abspath(capnp_file_path)) or os.getcwd()
|
|
179
|
+
os.makedirs(temp_parent, exist_ok=True)
|
|
180
|
+
with tempfile.TemporaryDirectory(prefix="avrotize-capnp-", dir=temp_parent) as temp_dir:
|
|
181
|
+
avro_file = os.path.join(temp_dir, "schema.avsc")
|
|
182
|
+
convert_json_structure_to_avro(structure_file, avro_file)
|
|
183
|
+
convert_avro_to_capnproto(avro_file, capnp_file_path, namespace=namespace)
|
|
@@ -57,9 +57,11 @@ class AvroToCSharp:
|
|
|
57
57
|
self.cbor_annotation = False
|
|
58
58
|
self.avro_annotation = False
|
|
59
59
|
self.protobuf_net_annotation = False
|
|
60
|
+
self.openapi_generator_compat = False
|
|
60
61
|
self.generated_types: Dict[str,str] = {}
|
|
61
62
|
self.generated_avro_types: Dict[str, Dict[str, Union[str, Dict, List]]] = {}
|
|
62
63
|
self.type_dict: Dict[str, Dict] = {}
|
|
64
|
+
self.emitted_option_namespaces: set[str] = set()
|
|
63
65
|
|
|
64
66
|
def get_qualified_name(self, namespace: str, name: str) -> str:
|
|
65
67
|
""" Concatenates namespace and name with a dot separator """
|
|
@@ -191,14 +193,19 @@ class AvroToCSharp:
|
|
|
191
193
|
if self.msgpack_annotation:
|
|
192
194
|
class_definition += "[MessagePackObject]\n"
|
|
193
195
|
|
|
194
|
-
|
|
196
|
+
fields = avro_schema.get('fields', [])
|
|
197
|
+
fields_str = [self.generate_property(index + 1, field, class_name, avro_namespace) for index, field in enumerate(fields)]
|
|
195
198
|
class_body = "\n".join(fields_str)
|
|
196
199
|
class_definition += f"public partial class {class_name}"
|
|
197
200
|
if self.avro_annotation:
|
|
198
201
|
class_definition += " : global::Avro.Specific.ISpecificRecord"
|
|
202
|
+
if self.openapi_generator_compat:
|
|
203
|
+
class_definition += ", System.ComponentModel.DataAnnotations.IValidatableObject" if self.avro_annotation else " : System.ComponentModel.DataAnnotations.IValidatableObject"
|
|
199
204
|
class_definition += "\n{\n"+class_body
|
|
200
205
|
class_definition += f"\n{INDENT}/// <summary>\n{INDENT}/// Default constructor\n{INDENT}///</summary>\n"
|
|
201
206
|
class_definition += f"{INDENT}public {class_name}()\n{INDENT}{{\n{INDENT}}}"
|
|
207
|
+
if self.openapi_generator_compat and fields:
|
|
208
|
+
class_definition += self.generate_openapi_compat_constructor(class_name, fields, avro_namespace)
|
|
202
209
|
if self.avro_annotation:
|
|
203
210
|
class_definition += f"\n\n{INDENT}/// <summary>\n{INDENT}/// Constructor from Avro GenericRecord\n{INDENT}///</summary>\n"
|
|
204
211
|
class_definition += f"{INDENT}public {class_name}(global::Avro.Generic.GenericRecord obj)\n{INDENT}{{\n"
|
|
@@ -270,7 +277,9 @@ class AvroToCSharp:
|
|
|
270
277
|
system_xml_annotation=self.system_xml_annotation,
|
|
271
278
|
msgpack_annotation=self.msgpack_annotation,
|
|
272
279
|
cbor_annotation=self.cbor_annotation,
|
|
273
|
-
json_match_clauses=self.create_is_json_match_clauses(avro_schema, avro_namespace, class_name)
|
|
280
|
+
json_match_clauses=self.create_is_json_match_clauses(avro_schema, avro_namespace, class_name),
|
|
281
|
+
openapi_generator_compat=self.openapi_generator_compat,
|
|
282
|
+
openapi_fields=self.get_openapi_compat_fields(fields, class_name, avro_namespace)
|
|
274
283
|
)
|
|
275
284
|
|
|
276
285
|
# emit Equals and GetHashCode for value equality
|
|
@@ -394,7 +403,9 @@ class AvroToCSharp:
|
|
|
394
403
|
field_name = field['name']
|
|
395
404
|
if self.is_csharp_reserved_word(field_name):
|
|
396
405
|
field_name = f"@{field_name}"
|
|
397
|
-
if self.
|
|
406
|
+
if self.openapi_generator_compat:
|
|
407
|
+
field_name = self.get_openapi_property_name(field_name, class_name)
|
|
408
|
+
elif self.pascal_properties:
|
|
398
409
|
field_name = pascal(field_name)
|
|
399
410
|
if field_name == class_name:
|
|
400
411
|
field_name += "_"
|
|
@@ -436,7 +447,9 @@ class AvroToCSharp:
|
|
|
436
447
|
field_name = field['name']
|
|
437
448
|
if self.is_csharp_reserved_word(field_name):
|
|
438
449
|
field_name = f"@{field_name}"
|
|
439
|
-
if self.
|
|
450
|
+
if self.openapi_generator_compat:
|
|
451
|
+
field_name = self.get_openapi_property_name(field_name, class_name)
|
|
452
|
+
elif self.pascal_properties:
|
|
440
453
|
field_name = pascal(field_name)
|
|
441
454
|
if field_name == class_name:
|
|
442
455
|
field_name += "_"
|
|
@@ -753,6 +766,8 @@ class AvroToCSharp:
|
|
|
753
766
|
field_name = pascal(field_name)
|
|
754
767
|
if field_name == class_name:
|
|
755
768
|
field_name += "_"
|
|
769
|
+
if self.openapi_generator_compat:
|
|
770
|
+
return self.generate_openapi_compat_property(field, field_type, field_name, annotation_name, class_name)
|
|
756
771
|
prop = ''
|
|
757
772
|
prop += f"{INDENT}/// <summary>\n{INDENT}/// { field.get('doc', field_name) }\n{INDENT}/// </summary>\n"
|
|
758
773
|
|
|
@@ -815,12 +830,116 @@ class AvroToCSharp:
|
|
|
815
830
|
prop += f"{INDENT}public {field_type} {field_name} {{ get; set; }}{initialization}"
|
|
816
831
|
return prop
|
|
817
832
|
|
|
833
|
+
def is_openapi_optional_avro_field(self, field: Dict) -> bool:
|
|
834
|
+
"""Returns true if the Avro field is nullable and should use Option<T>."""
|
|
835
|
+
avro_type = field.get('type')
|
|
836
|
+
return isinstance(avro_type, list) and 'null' in avro_type and len([t for t in avro_type if t != 'null']) == 1
|
|
837
|
+
|
|
838
|
+
def get_openapi_property_name(self, field_name: str, class_name: str) -> str:
|
|
839
|
+
"""Returns the OpenAPI Generator-style C# property name."""
|
|
840
|
+
name = field_name[1:] if field_name.startswith('@') else field_name
|
|
841
|
+
property_name = pascal(name)
|
|
842
|
+
if property_name == class_name:
|
|
843
|
+
property_name += "_"
|
|
844
|
+
return property_name
|
|
845
|
+
|
|
846
|
+
def generate_openapi_compat_property(self, field: Dict, field_type: str, field_name: str, annotation_name: str, class_name: str) -> str:
|
|
847
|
+
"""Generates an OpenAPI Generator-compatible property."""
|
|
848
|
+
property_name = self.get_openapi_property_name(field_name, class_name)
|
|
849
|
+
doc = field.get('doc', property_name)
|
|
850
|
+
prop = f"{INDENT}/// <summary>\n{INDENT}/// {doc}\n{INDENT}/// </summary>\n"
|
|
851
|
+
if self.is_openapi_optional_avro_field(field):
|
|
852
|
+
option_name = f"{property_name}Option"
|
|
853
|
+
prop += f"{INDENT}[System.Text.Json.Serialization.JsonIgnore]\n"
|
|
854
|
+
prop += f"{INDENT}public Option<{field_type}> {option_name} {{ get; private set; }}\n"
|
|
855
|
+
prop += f"{INDENT}[System.Text.Json.Serialization.JsonPropertyName(\"{annotation_name}\")]\n"
|
|
856
|
+
prop += f"{INDENT}public {field_type} {property_name} {{ get => {option_name}; set => {option_name} = new(value); }}"
|
|
857
|
+
else:
|
|
858
|
+
initialization = ""
|
|
859
|
+
if field_type == "string":
|
|
860
|
+
initialization = " = string.Empty;"
|
|
861
|
+
elif field_type.startswith("List<") or field_type.startswith("Dictionary<"):
|
|
862
|
+
initialization = " = new();"
|
|
863
|
+
elif field_type.startswith("global::") and not self.is_csharp_primitive_type(field_type):
|
|
864
|
+
initialization = " = new();"
|
|
865
|
+
prop += f"{INDENT}[System.Text.Json.Serialization.JsonPropertyName(\"{annotation_name}\")]\n"
|
|
866
|
+
prop += f"{INDENT}public {field_type} {property_name} {{ get; set; }}{initialization}"
|
|
867
|
+
return prop
|
|
868
|
+
|
|
869
|
+
def get_openapi_compat_fields(self, fields: List[Dict], class_name: str, parent_namespace: str) -> List[Dict[str, Any]]:
|
|
870
|
+
"""Builds template metadata for OpenAPI Generator-compatible converters."""
|
|
871
|
+
result: List[Dict[str, Any]] = []
|
|
872
|
+
for field in fields:
|
|
873
|
+
raw_name = field['name']
|
|
874
|
+
safe_name = f"@{raw_name}" if self.is_csharp_reserved_word(raw_name) else raw_name
|
|
875
|
+
field_type = self.convert_avro_type_to_csharp(class_name, raw_name, field['type'], parent_namespace)
|
|
876
|
+
property_name = self.get_openapi_property_name(safe_name, class_name)
|
|
877
|
+
result.append({
|
|
878
|
+
"wire_name": raw_name,
|
|
879
|
+
"property_name": property_name,
|
|
880
|
+
"option_name": f"{property_name}Option",
|
|
881
|
+
"type": field_type,
|
|
882
|
+
"is_optional": self.is_openapi_optional_avro_field(field)
|
|
883
|
+
})
|
|
884
|
+
return result
|
|
885
|
+
|
|
886
|
+
def generate_openapi_compat_constructor(self, class_name: str, fields: List[Dict], parent_namespace: str) -> str:
|
|
887
|
+
"""Generates an OpenAPI Generator-compatible all-properties constructor."""
|
|
888
|
+
openapi_fields = self.get_openapi_compat_fields(fields, class_name, parent_namespace)
|
|
889
|
+
parameters = []
|
|
890
|
+
assignments = []
|
|
891
|
+
for field in openapi_fields:
|
|
892
|
+
parameter_name = field["property_name"][0].lower() + field["property_name"][1:]
|
|
893
|
+
if field["is_optional"]:
|
|
894
|
+
parameters.append(f"Option<{field['type']}> {parameter_name}")
|
|
895
|
+
assignments.append(f"{INDENT*2}{field['option_name']} = {parameter_name};")
|
|
896
|
+
else:
|
|
897
|
+
parameters.append(f"{field['type']} {parameter_name}")
|
|
898
|
+
assignments.append(f"{INDENT*2}{field['property_name']} = {parameter_name};")
|
|
899
|
+
return (
|
|
900
|
+
f"\n\n{INDENT}/// <summary>\n{INDENT}/// Initializes a new instance with all properties.\n{INDENT}/// </summary>\n"
|
|
901
|
+
f"{INDENT}public {class_name}({', '.join(parameters)})\n{INDENT}{{\n"
|
|
902
|
+
+ "\n".join(assignments) +
|
|
903
|
+
f"\n{INDENT}}}"
|
|
904
|
+
)
|
|
905
|
+
|
|
906
|
+
def write_openapi_option_file(self, namespace: str) -> None:
|
|
907
|
+
"""Writes the reusable Option<T> type for OpenAPI Generator-compatible output."""
|
|
908
|
+
if namespace in self.emitted_option_namespaces:
|
|
909
|
+
return
|
|
910
|
+
self.emitted_option_namespaces.add(namespace)
|
|
911
|
+
directory_path = os.path.join(self.output_dir, os.path.join('src', namespace.replace('.', os.sep)))
|
|
912
|
+
os.makedirs(directory_path, exist_ok=True)
|
|
913
|
+
file_path = os.path.join(directory_path, "Option.cs")
|
|
914
|
+
file_content = (
|
|
915
|
+
"using System;\n\n"
|
|
916
|
+
f"namespace {namespace}\n{{\n"
|
|
917
|
+
f"{INDENT}/// <summary>\n"
|
|
918
|
+
f"{INDENT}/// Represents an optional value that distinguishes unset from explicit null/default.\n"
|
|
919
|
+
f"{INDENT}/// </summary>\n"
|
|
920
|
+
f"{INDENT}public readonly struct Option<T>\n{INDENT}{{\n"
|
|
921
|
+
f"{INDENT*2}public bool IsSet {{ get; }}\n"
|
|
922
|
+
f"{INDENT*2}public T Value {{ get; }}\n\n"
|
|
923
|
+
f"{INDENT*2}public Option(T value)\n{INDENT*2}{{\n"
|
|
924
|
+
f"{INDENT*3}IsSet = true;\n"
|
|
925
|
+
f"{INDENT*3}Value = value;\n"
|
|
926
|
+
f"{INDENT*2}}}\n\n"
|
|
927
|
+
f"{INDENT*2}public static implicit operator Option<T>(T value) => new(value);\n"
|
|
928
|
+
f"{INDENT*2}public static implicit operator T(Option<T> option) => option.Value;\n"
|
|
929
|
+
f"{INDENT}}}\n"
|
|
930
|
+
"}"
|
|
931
|
+
)
|
|
932
|
+
with open(file_path, 'w', encoding='utf-8') as file:
|
|
933
|
+
file.write(file_content)
|
|
934
|
+
|
|
818
935
|
def write_to_file(self, namespace: str, name: str, definition: str):
|
|
819
936
|
""" Writes the class or enum to a file """
|
|
820
937
|
directory_path = os.path.join(
|
|
821
938
|
self.output_dir, os.path.join('src', namespace.replace('.', os.sep)))
|
|
822
939
|
if not os.path.exists(directory_path):
|
|
823
940
|
os.makedirs(directory_path, exist_ok=True)
|
|
941
|
+
if self.openapi_generator_compat:
|
|
942
|
+
self.write_openapi_option_file(namespace)
|
|
824
943
|
file_path = os.path.join(directory_path, f"{name}.cs")
|
|
825
944
|
|
|
826
945
|
with open(file_path, 'w', encoding='utf-8') as file:
|
|
@@ -829,9 +948,11 @@ class AvroToCSharp:
|
|
|
829
948
|
file_content += "using System.Linq;\n"
|
|
830
949
|
if self.protobuf_net_annotation:
|
|
831
950
|
file_content += "using ProtoBuf;\n"
|
|
832
|
-
if self.system_text_json_annotation:
|
|
951
|
+
if self.system_text_json_annotation or self.openapi_generator_compat:
|
|
833
952
|
file_content += "using System.Text.Json;\n"
|
|
834
953
|
file_content += "using System.Text.Json.Serialization;\n"
|
|
954
|
+
if self.openapi_generator_compat:
|
|
955
|
+
file_content += "using System.ComponentModel.DataAnnotations;\n"
|
|
835
956
|
if self.newtonsoft_json_annotation:
|
|
836
957
|
file_content += "using Newtonsoft.Json;\n"
|
|
837
958
|
if self.system_xml_annotation: # Add XML serialization using directive
|
|
@@ -921,12 +1042,14 @@ class AvroToCSharp:
|
|
|
921
1042
|
if avro_schema and 'fields' in avro_schema:
|
|
922
1043
|
for field in cast(List[Dict[str,JsonNode]],avro_schema['fields']):
|
|
923
1044
|
field_name = str(field['name'])
|
|
924
|
-
if self.
|
|
1045
|
+
if self.is_csharp_reserved_word(field_name):
|
|
1046
|
+
field_name = f"@{field_name}"
|
|
1047
|
+
if self.openapi_generator_compat:
|
|
1048
|
+
field_name = self.get_openapi_property_name(field_name, class_name)
|
|
1049
|
+
elif self.pascal_properties:
|
|
925
1050
|
field_name = pascal(field_name)
|
|
926
1051
|
if field_name == class_name:
|
|
927
1052
|
field_name += "_"
|
|
928
|
-
if self.is_csharp_reserved_word(field_name):
|
|
929
|
-
field_name = f"@{field_name}"
|
|
930
1053
|
field_type = self.convert_avro_type_to_csharp(class_name, field_name, field['type'], str(avro_schema.get('namespace', '')))
|
|
931
1054
|
is_class = field_type in self.generated_types and self.generated_types[field_type] == "class"
|
|
932
1055
|
is_enum = self.is_enum_type(field['type'])
|
|
@@ -1121,7 +1244,8 @@ def convert_avro_to_csharp(
|
|
|
1121
1244
|
msgpack_annotation=False,
|
|
1122
1245
|
cbor_annotation=False,
|
|
1123
1246
|
avro_annotation=False,
|
|
1124
|
-
protobuf_net_annotation=False
|
|
1247
|
+
protobuf_net_annotation=False,
|
|
1248
|
+
openapi_generator_compat=False
|
|
1125
1249
|
):
|
|
1126
1250
|
"""Converts Avro schema to C# classes
|
|
1127
1251
|
|
|
@@ -1138,6 +1262,7 @@ def convert_avro_to_csharp(
|
|
|
1138
1262
|
cbor_annotation (bool, optional): Use Dahomey.Cbor annotations. Defaults to False.
|
|
1139
1263
|
avro_annotation (bool, optional): Use Avro annotations. Defaults to False.
|
|
1140
1264
|
protobuf_net_annotation (bool, optional): Use protobuf-net annotations. Defaults to False.
|
|
1265
|
+
openapi_generator_compat (bool, optional): Generate OpenAPI Generator-compatible C# models. Defaults to False.
|
|
1141
1266
|
"""
|
|
1142
1267
|
|
|
1143
1268
|
if not base_namespace:
|
|
@@ -1152,6 +1277,7 @@ def convert_avro_to_csharp(
|
|
|
1152
1277
|
avrotocs.cbor_annotation = cbor_annotation
|
|
1153
1278
|
avrotocs.avro_annotation = avro_annotation
|
|
1154
1279
|
avrotocs.protobuf_net_annotation = protobuf_net_annotation
|
|
1280
|
+
avrotocs.openapi_generator_compat = openapi_generator_compat
|
|
1155
1281
|
avrotocs.convert(avro_schema_path, cs_file_path)
|
|
1156
1282
|
|
|
1157
1283
|
|
|
@@ -1167,7 +1293,8 @@ def convert_avro_schema_to_csharp(
|
|
|
1167
1293
|
msgpack_annotation: bool = False,
|
|
1168
1294
|
cbor_annotation: bool = False,
|
|
1169
1295
|
avro_annotation: bool = False,
|
|
1170
|
-
protobuf_net_annotation: bool = False
|
|
1296
|
+
protobuf_net_annotation: bool = False,
|
|
1297
|
+
openapi_generator_compat: bool = False
|
|
1171
1298
|
):
|
|
1172
1299
|
"""Converts Avro schema to C# classes
|
|
1173
1300
|
|
|
@@ -1195,4 +1322,5 @@ def convert_avro_schema_to_csharp(
|
|
|
1195
1322
|
avrotocs.cbor_annotation = cbor_annotation
|
|
1196
1323
|
avrotocs.avro_annotation = avro_annotation
|
|
1197
1324
|
avrotocs.protobuf_net_annotation = protobuf_net_annotation
|
|
1325
|
+
avrotocs.openapi_generator_compat = openapi_generator_compat
|
|
1198
1326
|
avrotocs.convert_schema(avro_schema, output_dir)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Avrotize Schema to CUE subset converter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_AVRO_TO_CUE = {
|
|
11
|
+
"string": "string",
|
|
12
|
+
"int": "int",
|
|
13
|
+
"long": "int",
|
|
14
|
+
"float": "float",
|
|
15
|
+
"double": "number",
|
|
16
|
+
"boolean": "bool",
|
|
17
|
+
"bytes": "bytes",
|
|
18
|
+
"null": "null",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AvroToCueConverter:
|
|
23
|
+
"""Emits CUE definitions for the same practical subset parsed by cuetoavro."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, namespace: str | None = None) -> None:
|
|
26
|
+
self.namespace = namespace
|
|
27
|
+
self.emitted: set[str] = set()
|
|
28
|
+
|
|
29
|
+
def convert(self, avro_schema: Any) -> str:
|
|
30
|
+
schemas = avro_schema if isinstance(avro_schema, list) else [avro_schema]
|
|
31
|
+
namespace = self.namespace or self.find_namespace(schemas)
|
|
32
|
+
lines: list[str] = []
|
|
33
|
+
package = self.package_from_namespace(namespace)
|
|
34
|
+
if package:
|
|
35
|
+
lines.extend([f"package {package}", ""])
|
|
36
|
+
for schema in schemas:
|
|
37
|
+
if isinstance(schema, dict) and schema.get("type") == "record":
|
|
38
|
+
lines.extend(self.record_definition(schema))
|
|
39
|
+
lines.append("")
|
|
40
|
+
if not any(isinstance(schema, dict) and schema.get("type") == "record" for schema in schemas):
|
|
41
|
+
lines.extend(["#Document: {}", ""])
|
|
42
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def find_namespace(schemas: list[Any]) -> str | None:
|
|
46
|
+
for schema in schemas:
|
|
47
|
+
if isinstance(schema, dict) and schema.get("namespace"):
|
|
48
|
+
return schema["namespace"]
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def package_from_namespace(namespace: str | None) -> str | None:
|
|
53
|
+
if not namespace:
|
|
54
|
+
return None
|
|
55
|
+
name = namespace.split(".")[-1]
|
|
56
|
+
name = re.sub(r"[^A-Za-z0-9_]", "_", name)
|
|
57
|
+
if not name or name[0].isdigit():
|
|
58
|
+
name = f"_{name}"
|
|
59
|
+
return name
|
|
60
|
+
|
|
61
|
+
def record_definition(self, record: dict[str, Any]) -> list[str]:
|
|
62
|
+
name = record.get("name", "Document").split(".")[-1]
|
|
63
|
+
lines = [f"#{name}: {{"]
|
|
64
|
+
for field in record.get("fields", []):
|
|
65
|
+
lines.append(f" {self.field_to_cue(field)}")
|
|
66
|
+
lines.append("}")
|
|
67
|
+
return lines
|
|
68
|
+
|
|
69
|
+
def field_to_cue(self, field: dict[str, Any]) -> str:
|
|
70
|
+
field_name = field.get("name", "field")
|
|
71
|
+
cue_type, nullable = self.type_to_cue(field.get("type", "string"))
|
|
72
|
+
suffix = "?" if nullable else ""
|
|
73
|
+
default = field.get("default", _NO_DEFAULT)
|
|
74
|
+
if default is not _NO_DEFAULT:
|
|
75
|
+
cue_type = f"{cue_type} | *{self.literal_to_cue(default)}"
|
|
76
|
+
return f"{field_name}{suffix}: {cue_type}"
|
|
77
|
+
|
|
78
|
+
def type_to_cue(self, avro_type: Any) -> tuple[str, bool]:
|
|
79
|
+
if isinstance(avro_type, list):
|
|
80
|
+
non_null = [item for item in avro_type if item != "null"]
|
|
81
|
+
nullable = len(non_null) != len(avro_type)
|
|
82
|
+
if len(non_null) == 1:
|
|
83
|
+
cue_type, _ = self.type_to_cue(non_null[0])
|
|
84
|
+
return cue_type, nullable
|
|
85
|
+
choices = [self.type_to_cue(item)[0] for item in non_null]
|
|
86
|
+
if nullable:
|
|
87
|
+
choices.append("null")
|
|
88
|
+
return " | ".join(choices) if choices else "null", nullable
|
|
89
|
+
if isinstance(avro_type, str):
|
|
90
|
+
if avro_type in _AVRO_TO_CUE:
|
|
91
|
+
return _AVRO_TO_CUE[avro_type], False
|
|
92
|
+
return f"#{avro_type.split('.')[-1]}", False
|
|
93
|
+
if isinstance(avro_type, dict):
|
|
94
|
+
type_name = avro_type.get("type")
|
|
95
|
+
if isinstance(type_name, str) and type_name in _AVRO_TO_CUE:
|
|
96
|
+
return _AVRO_TO_CUE[type_name], False
|
|
97
|
+
if type_name == "array":
|
|
98
|
+
item_type, _ = self.type_to_cue(avro_type.get("items", "string"))
|
|
99
|
+
return f"[...{item_type}]", False
|
|
100
|
+
if type_name == "map":
|
|
101
|
+
value_type, _ = self.type_to_cue(avro_type.get("values", "string"))
|
|
102
|
+
return f"{{ [string]: {value_type} }}", False
|
|
103
|
+
if type_name == "enum":
|
|
104
|
+
values = avro_type.get("x-cue-symbols", {})
|
|
105
|
+
symbols = [values.get(symbol, symbol) for symbol in avro_type.get("symbols", [])]
|
|
106
|
+
return " | ".join(self.literal_to_cue(symbol) for symbol in symbols), False
|
|
107
|
+
if type_name == "record":
|
|
108
|
+
fields = avro_type.get("fields", [])
|
|
109
|
+
if not fields:
|
|
110
|
+
return "{}", False
|
|
111
|
+
inner = ["{"]
|
|
112
|
+
for field in fields:
|
|
113
|
+
inner.append(f" {self.field_to_cue(field)}")
|
|
114
|
+
inner.append(" }")
|
|
115
|
+
return "\n".join(inner), False
|
|
116
|
+
if isinstance(type_name, str):
|
|
117
|
+
return self.type_to_cue(type_name)
|
|
118
|
+
return "string", False
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def literal_to_cue(value: Any) -> str:
|
|
122
|
+
if value is None:
|
|
123
|
+
return "null"
|
|
124
|
+
if isinstance(value, bool):
|
|
125
|
+
return "true" if value else "false"
|
|
126
|
+
if isinstance(value, (int, float)):
|
|
127
|
+
return str(value)
|
|
128
|
+
return json.dumps(str(value))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class _NoDefault:
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
_NO_DEFAULT = _NoDefault()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def convert_avro_to_cue(avro_schema_path: str, cue_file_path: str, namespace: str | None = None) -> None:
|
|
139
|
+
"""Convert an Avrotize/Avro schema file to a CUE subset file."""
|
|
140
|
+
with open(avro_schema_path, "r", encoding="utf-8") as avro_file:
|
|
141
|
+
avro_schema = json.load(avro_file)
|
|
142
|
+
converter = AvroToCueConverter(namespace=namespace)
|
|
143
|
+
cue_text = converter.convert(avro_schema)
|
|
144
|
+
with open(cue_file_path, "w", encoding="utf-8") as cue_file:
|
|
145
|
+
cue_file.write(cue_text)
|