structurize 3.7.0__tar.gz → 3.7.1__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.7.0/structurize.egg-info → structurize-3.7.1}/PKG-INFO +1 -1
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/_version.py +3 -3
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotocapnproto.py +57 -33
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotocsharp.py +110 -76
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotoflatbuffers.py +33 -9
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotojtd.py +5 -2
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/capnprototoavro.py +51 -22
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/common.py +18 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/cuetoavro.py +49 -13
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/dependency_resolver.py +60 -1
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/flatbufferstoavro.py +97 -16
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/jtdtoavro.py +19 -8
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/ramltoavro.py +21 -10
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/smithytoavro.py +10 -7
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/structuretocsharp.py +44 -40
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/surrealtoavro.py +56 -28
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/thrifttoavro.py +16 -1
- {structurize-3.7.0 → structurize-3.7.1/structurize.egg-info}/PKG-INFO +1 -1
- {structurize-3.7.0 → structurize-3.7.1}/.gitignore +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/LICENSE +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/MANIFEST.in +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/README.md +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/__init__.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/__main__.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/asn1toavro.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotize.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotocpp.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotocsv.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotocue.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotodatapackage.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotodb.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotogo.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotographql.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotoiceberg.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotojava.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotojs.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotojsons.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotojstruct.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotokusto.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotomd.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotools.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotoparquet.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotoproto.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotopython.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotoraml.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotorust.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotosmithy.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotosurreal.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotothrift.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotots.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrototsml.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrotoxsd.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/avrovalidator.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/cddltostructure.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/choice_inference.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/commands.json +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/constants.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/csvtoavro.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/cuetostructure.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/datapackagetoavro.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/dependencies/cpp/vcpkg/vcpkg.json +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/dependencies/typescript/node22/package.json +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/dependency_version.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/flatbufferstojstruct.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/jsonstoavro.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/jsonstostructure.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/jsontoschema.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/jstructtoavro.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/jstructtoflatbuffers.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/jstructtoraml.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/jstructtosmithy.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/jtdtostructure.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/kstructtoavro.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/kustotoavro.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/kustotojstruct.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/mcp_server.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/openapitostructure.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/parquettoavro.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/proto2parser.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/proto3parser.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/prototoavro.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/ramltojstruct.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/schema_inference.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/smithytojstruct.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/sqltoavro.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/structuretocddl.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/structuretocpp.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/structuretocsv.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/structuretocue.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/structuretodatapackage.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/structuretodb.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/structuretogo.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/structuretographql.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/structuretoiceberg.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/structuretojava.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/structuretojs.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/structuretojsons.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/structuretojtd.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/structuretokusto.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/structuretomd.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/structuretoparquet.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/structuretoproto.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/structuretopython.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/structuretorust.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/structuretots.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/structuretotsml.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/structuretoxsd.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/thriftstructure.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/tmslvalidate.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/validate.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/xmltoschema.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/avrotize/xsdtoavro.py +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/build.ps1 +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/build.sh +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/pyproject.toml +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/setup.cfg +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/structurize.egg-info/SOURCES.txt +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/structurize.egg-info/dependency_links.txt +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/structurize.egg-info/entry_points.txt +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/structurize.egg-info/requires.txt +0 -0
- {structurize-3.7.0 → structurize-3.7.1}/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.7.
|
|
3
|
+
Version: 3.7.1
|
|
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
|
|
@@ -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.7.
|
|
22
|
-
__version_tuple__ = version_tuple = (3, 7,
|
|
21
|
+
__version__ = version = '3.7.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (3, 7, 1)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g2f9460efb'
|
|
@@ -8,6 +8,8 @@ import re
|
|
|
8
8
|
import tempfile
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
|
+
from avrotize.common import unique_name
|
|
12
|
+
|
|
11
13
|
INDENT = " "
|
|
12
14
|
FILE_ID = "0xd12f8a7e9b0c4a6f"
|
|
13
15
|
|
|
@@ -41,6 +43,8 @@ class AvroToCapnpConverter:
|
|
|
41
43
|
self.namespace = namespace
|
|
42
44
|
self.named: dict[str, dict[str, Any]] = {}
|
|
43
45
|
self.emitted: set[str] = set()
|
|
46
|
+
self.type_names: dict[int, str] = {}
|
|
47
|
+
self.top_level_type_names: set[str] = set()
|
|
44
48
|
|
|
45
49
|
def convert(self, avro_schema: Any) -> str:
|
|
46
50
|
schemas = avro_schema if isinstance(avro_schema, list) else [avro_schema]
|
|
@@ -48,6 +52,7 @@ class AvroToCapnpConverter:
|
|
|
48
52
|
if isinstance(schema, dict) and schema.get("type") in {"record", "enum"}:
|
|
49
53
|
self.named[schema["name"]] = schema
|
|
50
54
|
self.named[_type_name(schema)] = schema
|
|
55
|
+
self._assign_type_name(schema, self.top_level_type_names)
|
|
51
56
|
lines = [f"@{FILE_ID};", ""]
|
|
52
57
|
if self.namespace:
|
|
53
58
|
lines.append(f"# namespace: {self.namespace}")
|
|
@@ -57,83 +62,102 @@ class AvroToCapnpConverter:
|
|
|
57
62
|
if isinstance(schema, dict) and schema.get("type") in {"enum", "record"}:
|
|
58
63
|
name = _type_name(schema)
|
|
59
64
|
if name not in self.emitted:
|
|
60
|
-
lines.extend(self._render_named(schema, 0))
|
|
65
|
+
lines.extend(self._render_named(schema, 0, self.top_level_type_names))
|
|
61
66
|
lines.append("")
|
|
62
67
|
return "\n".join(lines).rstrip() + "\n"
|
|
63
68
|
|
|
64
|
-
def
|
|
69
|
+
def _assign_type_name(self, schema: dict[str, Any], used: set[str] | None = None) -> str:
|
|
70
|
+
key = id(schema)
|
|
71
|
+
if key not in self.type_names:
|
|
72
|
+
self.type_names[key] = unique_name(_safe_ident(schema["name"]), used if used is not None else self.top_level_type_names)
|
|
73
|
+
return self.type_names[key]
|
|
74
|
+
|
|
75
|
+
def _render_named(self, schema: dict[str, Any], level: int, used_type_names: set[str] | None = None) -> list[str]:
|
|
65
76
|
if schema.get("type") == "enum":
|
|
66
|
-
return self._render_enum(schema, level)
|
|
77
|
+
return self._render_enum(schema, level, used_type_names)
|
|
67
78
|
if schema.get("type") == "record":
|
|
68
|
-
return self._render_struct(schema, level)
|
|
79
|
+
return self._render_struct(schema, level, used_type_names)
|
|
69
80
|
return []
|
|
70
81
|
|
|
71
|
-
def _render_enum(self, schema: dict[str, Any], level: int) -> list[str]:
|
|
82
|
+
def _render_enum(self, schema: dict[str, Any], level: int, used_type_names: set[str] | None = None) -> list[str]:
|
|
72
83
|
self.emitted.add(_type_name(schema))
|
|
73
|
-
lines = [f"{INDENT * level}enum {
|
|
84
|
+
lines = [f"{INDENT * level}enum {self._assign_type_name(schema, used_type_names)} {{"]
|
|
85
|
+
used_symbols: set[str] = set()
|
|
74
86
|
for index, symbol in enumerate(schema.get("symbols", [])):
|
|
75
|
-
lines.append(f"{INDENT * (level + 1)}{_safe_ident(symbol)} @{index};")
|
|
87
|
+
lines.append(f"{INDENT * (level + 1)}{unique_name(_safe_ident(symbol), used_symbols)} @{index};")
|
|
76
88
|
lines.append(f"{INDENT * level}}}")
|
|
77
89
|
return lines
|
|
78
90
|
|
|
79
|
-
def _render_struct(self, schema: dict[str, Any], level: int) -> list[str]:
|
|
91
|
+
def _render_struct(self, schema: dict[str, Any], level: int, used_type_names: set[str] | None = None) -> list[str]:
|
|
80
92
|
self.emitted.add(_type_name(schema))
|
|
81
|
-
lines = [f"{INDENT * level}struct {
|
|
93
|
+
lines = [f"{INDENT * level}struct {self._assign_type_name(schema, used_type_names)} {{"]
|
|
82
94
|
fields = schema.get("fields", [])
|
|
83
|
-
grouped: dict[str, list[tuple[int, dict[str, Any], Any]]] = {}
|
|
84
|
-
normal: list[tuple[int, dict[str, Any], Any]] = []
|
|
95
|
+
grouped: dict[str, list[tuple[int, dict[str, Any], Any, str]]] = {}
|
|
96
|
+
normal: list[tuple[int, dict[str, Any], Any, str]] = []
|
|
97
|
+
used_fields: set[str] = set()
|
|
85
98
|
for index, field in enumerate(fields):
|
|
86
99
|
avro_type = field.get("type")
|
|
87
100
|
union_name = field.get("capnpUnion")
|
|
88
|
-
|
|
101
|
+
safe_field_name = unique_name(_safe_ident(field.get("name", f"field{index}")), used_fields)
|
|
102
|
+
item = (index, field, avro_type, safe_field_name)
|
|
89
103
|
if union_name:
|
|
90
104
|
grouped.setdefault(str(union_name), []).append(item)
|
|
91
105
|
else:
|
|
92
106
|
normal.append(item)
|
|
93
|
-
|
|
94
|
-
|
|
107
|
+
nested_type_names: set[str] = set()
|
|
108
|
+
for index, field, avro_type, safe_field_name in normal:
|
|
109
|
+
lines.extend(self._render_field(field, avro_type, index, level + 1, safe_field_name, nested_type_names))
|
|
95
110
|
for _, items in grouped.items():
|
|
96
111
|
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))
|
|
112
|
+
for index, field, avro_type, safe_field_name in items:
|
|
113
|
+
lines.extend(self._render_field(field, avro_type, index, level + 2, safe_field_name, nested_type_names))
|
|
99
114
|
lines.append(f"{INDENT * (level + 1)}}}")
|
|
100
115
|
lines.append(f"{INDENT * level}}}")
|
|
101
116
|
return lines
|
|
102
117
|
|
|
103
|
-
def _render_field(
|
|
104
|
-
|
|
118
|
+
def _render_field(
|
|
119
|
+
self,
|
|
120
|
+
field: dict[str, Any],
|
|
121
|
+
avro_type: Any,
|
|
122
|
+
index: int,
|
|
123
|
+
level: int,
|
|
124
|
+
safe_field_name: str,
|
|
125
|
+
used_type_names: set[str],
|
|
126
|
+
) -> list[str]:
|
|
127
|
+
capnp_type, nested = self._capnp_type(avro_type, safe_field_name, used_type_names)
|
|
105
128
|
ordinal = index
|
|
106
129
|
lines: list[str] = []
|
|
107
|
-
lines.extend(self._render_inline_nested(nested, level))
|
|
108
|
-
lines.append(f"{INDENT * level}{
|
|
130
|
+
lines.extend(self._render_inline_nested(nested, level, used_type_names))
|
|
131
|
+
lines.append(f"{INDENT * level}{safe_field_name} @{ordinal} :{capnp_type};")
|
|
109
132
|
return lines
|
|
110
133
|
|
|
111
|
-
def _render_inline_nested(self, nested: list[dict[str, Any]], level: int) -> list[str]:
|
|
134
|
+
def _render_inline_nested(self, nested: list[dict[str, Any]], level: int, used_type_names: set[str]) -> list[str]:
|
|
112
135
|
lines: list[str] = []
|
|
113
136
|
for schema in nested:
|
|
114
|
-
lines.extend(self._render_named(schema, level))
|
|
137
|
+
lines.extend(self._render_named(schema, level, used_type_names))
|
|
115
138
|
return lines
|
|
116
139
|
|
|
117
140
|
def _non_null_union(self, avro_type: list[Any]) -> list[Any]:
|
|
118
141
|
return [item for item in avro_type if item != "null" and not (isinstance(item, dict) and item.get("type") == "null")]
|
|
119
142
|
|
|
120
|
-
def _capnp_type(self, avro_type: Any, field_name: str) -> tuple[str, list[dict[str, Any]]]:
|
|
143
|
+
def _capnp_type(self, avro_type: Any, field_name: str, used_type_names: set[str] | None = None) -> tuple[str, list[dict[str, Any]]]:
|
|
144
|
+
used_type_names = used_type_names if used_type_names is not None else self.top_level_type_names
|
|
121
145
|
if isinstance(avro_type, list):
|
|
122
146
|
non_null = self._non_null_union(avro_type)
|
|
123
147
|
if len(non_null) == 1:
|
|
124
|
-
return self._capnp_type(non_null[0], field_name)
|
|
148
|
+
return self._capnp_type(non_null[0], field_name, used_type_names)
|
|
125
149
|
union_schema = {
|
|
126
150
|
"type": "record",
|
|
127
151
|
"name": f"{field_name[:1].upper()}{field_name[1:]}Choice",
|
|
128
152
|
"fields": [{"name": f"choice{i}", "type": t, "capnpUnion": "union"} for i, t in enumerate(non_null)],
|
|
129
153
|
}
|
|
130
|
-
return union_schema
|
|
154
|
+
return self._assign_type_name(union_schema, used_type_names), [union_schema]
|
|
131
155
|
if isinstance(avro_type, dict):
|
|
132
156
|
avro_kind = avro_type.get("type")
|
|
133
157
|
if isinstance(avro_kind, list):
|
|
134
|
-
return self._capnp_type(avro_kind, field_name)
|
|
158
|
+
return self._capnp_type(avro_kind, field_name, used_type_names)
|
|
135
159
|
if avro_kind == "array":
|
|
136
|
-
item_type, nested = self._capnp_type(avro_type.get("items", "string"), f"{field_name}Item")
|
|
160
|
+
item_type, nested = self._capnp_type(avro_type.get("items", "string"), f"{field_name}Item", used_type_names)
|
|
137
161
|
return f"List({item_type})", nested
|
|
138
162
|
if avro_kind == "map":
|
|
139
163
|
entry = {
|
|
@@ -144,19 +168,19 @@ class AvroToCapnpConverter:
|
|
|
144
168
|
{"name": "value", "type": avro_type.get("values", "string")},
|
|
145
169
|
],
|
|
146
170
|
}
|
|
147
|
-
return f"List({entry
|
|
171
|
+
return f"List({self._assign_type_name(entry, used_type_names)})", [entry]
|
|
148
172
|
if avro_kind == "record":
|
|
149
|
-
return avro_type
|
|
173
|
+
return self._assign_type_name(avro_type, used_type_names), [avro_type]
|
|
150
174
|
if avro_kind == "enum":
|
|
151
|
-
return avro_type
|
|
175
|
+
return self._assign_type_name(avro_type, used_type_names), [avro_type]
|
|
152
176
|
if avro_kind == "fixed":
|
|
153
177
|
return "Data", []
|
|
154
|
-
return self._capnp_type(avro_kind, field_name)
|
|
178
|
+
return self._capnp_type(avro_kind, field_name, used_type_names)
|
|
155
179
|
if isinstance(avro_type, str):
|
|
156
180
|
named = self.named.get(avro_type)
|
|
157
181
|
if named:
|
|
158
|
-
return named
|
|
159
|
-
return self.primitive_map.get(avro_type, avro_type.split(".")[-1]), []
|
|
182
|
+
return self._assign_type_name(named), []
|
|
183
|
+
return self.primitive_map.get(avro_type, _safe_ident(avro_type.split(".")[-1])), []
|
|
160
184
|
return "Text", []
|
|
161
185
|
|
|
162
186
|
|
|
@@ -8,7 +8,7 @@ import re
|
|
|
8
8
|
from typing import Any, Dict, List, Tuple, Union, cast
|
|
9
9
|
import uuid
|
|
10
10
|
|
|
11
|
-
from avrotize.common import build_flat_type_dict, inline_avro_references, is_generic_avro_type, pascal, process_template
|
|
11
|
+
from avrotize.common import build_flat_type_dict, inline_avro_references, is_generic_avro_type, pascal, process_template, unique_name
|
|
12
12
|
from avrotize.constants import (
|
|
13
13
|
CSHARP_AVRO_VERSION,
|
|
14
14
|
NEWTONSOFT_JSON_VERSION,
|
|
@@ -76,7 +76,7 @@ class AvroToCSharp:
|
|
|
76
76
|
else:
|
|
77
77
|
return name
|
|
78
78
|
|
|
79
|
-
def map_primitive_to_csharp(self, avro_type: str) -> str:
|
|
79
|
+
def map_primitive_to_csharp(self, avro_type: str, parent_namespace: str = '') -> str:
|
|
80
80
|
""" Maps Avro primitive types to C# types """
|
|
81
81
|
mapping = {
|
|
82
82
|
'null': 'void', # Placeholder, actual handling for nullable types is in the union logic
|
|
@@ -92,9 +92,43 @@ class AvroToCSharp:
|
|
|
92
92
|
if qualified_class_name in self.generated_avro_types:
|
|
93
93
|
result = qualified_class_name
|
|
94
94
|
else:
|
|
95
|
-
result = mapping.get(avro_type, '
|
|
95
|
+
result = mapping.get(avro_type, '')
|
|
96
|
+
if not result:
|
|
97
|
+
avro_type_name = avro_type if '.' in avro_type or not parent_namespace else f"{parent_namespace}.{avro_type}"
|
|
98
|
+
if avro_type_name in self.type_dict:
|
|
99
|
+
namespace = ".".join(avro_type_name.split('.')[:-1])
|
|
100
|
+
name = avro_type_name.split('.')[-1]
|
|
101
|
+
result = 'global::'+self.get_qualified_name(pascal(self.concat_namespace(self.base_namespace, namespace)), pascal(name))
|
|
102
|
+
else:
|
|
103
|
+
result = 'object'
|
|
96
104
|
return result
|
|
97
105
|
|
|
106
|
+
def safe_identifier(self, name: str, class_name: str = '', fallback_prefix: str = 'field', allow_unicode: bool = False) -> str:
|
|
107
|
+
"""Converts a source name to a safe C# identifier."""
|
|
108
|
+
safe = re.sub(r'[^\w]', '_', str(name), flags=re.UNICODE) if allow_unicode else re.sub(r'[^a-zA-Z0-9_]', '_', str(name))
|
|
109
|
+
safe = safe.strip('_') if safe != name else safe
|
|
110
|
+
starts_ok = safe and (safe[0] == '_' or safe[0] == '@' or (safe[0].isalpha() if allow_unicode else re.match(r'^[a-zA-Z_@]', safe)))
|
|
111
|
+
if not starts_ok:
|
|
112
|
+
safe = '_' + safe if safe and safe[0].isdigit() else f"{fallback_prefix}_unnamed"
|
|
113
|
+
if self.is_csharp_reserved_word(safe):
|
|
114
|
+
safe = f"@{safe}"
|
|
115
|
+
if class_name and safe == class_name:
|
|
116
|
+
safe += "_"
|
|
117
|
+
return safe
|
|
118
|
+
|
|
119
|
+
def safe_enum_member_name(self, symbol: str, used: set) -> str:
|
|
120
|
+
"""Converts an Avro enum symbol to a unique C# enum member name."""
|
|
121
|
+
parts = [part for part in re.split(r'[^\w]+', str(symbol), flags=re.UNICODE) if part]
|
|
122
|
+
if not parts:
|
|
123
|
+
candidate = "Value"
|
|
124
|
+
else:
|
|
125
|
+
candidate = ''.join(part if part.isupper() else part[:1].upper() + part[1:] for part in parts)
|
|
126
|
+
if not candidate or not (candidate[0] == '_' or candidate[0].isalpha()):
|
|
127
|
+
candidate = f"Value{candidate}"
|
|
128
|
+
if self.is_csharp_reserved_word(candidate):
|
|
129
|
+
candidate = f"@{candidate}"
|
|
130
|
+
return unique_name(candidate, used)
|
|
131
|
+
|
|
98
132
|
def is_csharp_reserved_word(self, word: str) -> bool:
|
|
99
133
|
""" Checks if a word is a reserved C# keyword """
|
|
100
134
|
reserved_words = [
|
|
@@ -133,7 +167,7 @@ class AvroToCSharp:
|
|
|
133
167
|
def convert_avro_type_to_csharp(self, class_name: str, field_name: str, avro_type: JsonNode, parent_namespace: str) -> str:
|
|
134
168
|
""" Converts Avro type to C# type """
|
|
135
169
|
if isinstance(avro_type, str):
|
|
136
|
-
return self.map_primitive_to_csharp(avro_type)
|
|
170
|
+
return self.map_primitive_to_csharp(avro_type, parent_namespace)
|
|
137
171
|
elif isinstance(avro_type, list):
|
|
138
172
|
# Handle nullable types and unions
|
|
139
173
|
if is_generic_avro_type(avro_type):
|
|
@@ -194,7 +228,8 @@ class AvroToCSharp:
|
|
|
194
228
|
class_definition += "[MessagePackObject]\n"
|
|
195
229
|
|
|
196
230
|
fields = avro_schema.get('fields', [])
|
|
197
|
-
|
|
231
|
+
property_names = self.get_csharp_property_names(fields, class_name)
|
|
232
|
+
fields_str = [self.generate_property(index + 1, field, class_name, avro_namespace, property_names[index]) for index, field in enumerate(fields)]
|
|
198
233
|
class_body = "\n".join(fields_str)
|
|
199
234
|
class_definition += f"public partial class {class_name}"
|
|
200
235
|
if self.avro_annotation:
|
|
@@ -229,14 +264,8 @@ class AvroToCSharp:
|
|
|
229
264
|
put_method = f"{INDENT}void global::Avro.Specific.ISpecificRecord.Put(int fieldPos, object fieldValue)\n" + \
|
|
230
265
|
INDENT+"{"+f"\n{INDENT*2}switch (fieldPos)\n{INDENT*2}"+"{"
|
|
231
266
|
for pos, field in enumerate(avro_schema.get('fields', [])):
|
|
232
|
-
field_name =
|
|
233
|
-
|
|
234
|
-
field_name = f"@{field_name}"
|
|
235
|
-
field_type = self.convert_avro_type_to_csharp(class_name, field_name, field['type'], avro_namespace)
|
|
236
|
-
if self.pascal_properties:
|
|
237
|
-
field_name = pascal(field_name)
|
|
238
|
-
if field_name == class_name:
|
|
239
|
-
field_name += "_"
|
|
267
|
+
field_name = property_names[pos]
|
|
268
|
+
field_type = self.convert_avro_type_to_csharp(class_name, field['name'], field['type'], avro_namespace)
|
|
240
269
|
if field_type in self.generated_types:
|
|
241
270
|
if self.generated_types[field_type] == "union":
|
|
242
271
|
get_method += f"\n{INDENT*3}case {pos}: return this.{field_name}?.ToObject();"
|
|
@@ -279,7 +308,7 @@ class AvroToCSharp:
|
|
|
279
308
|
cbor_annotation=self.cbor_annotation,
|
|
280
309
|
json_match_clauses=self.create_is_json_match_clauses(avro_schema, avro_namespace, class_name),
|
|
281
310
|
openapi_generator_compat=self.openapi_generator_compat,
|
|
282
|
-
openapi_fields=self.get_openapi_compat_fields(fields, class_name, avro_namespace)
|
|
311
|
+
openapi_fields=self.get_openapi_compat_fields(fields, class_name, avro_namespace, property_names)
|
|
283
312
|
)
|
|
284
313
|
|
|
285
314
|
# emit Equals and GetHashCode for value equality
|
|
@@ -294,26 +323,45 @@ class AvroToCSharp:
|
|
|
294
323
|
self.generated_avro_types[ref] = avro_schema
|
|
295
324
|
return ref
|
|
296
325
|
|
|
326
|
+
def get_csharp_property_names(self, fields: List[Dict], class_name: str) -> List[str]:
|
|
327
|
+
"""Returns unique C# property names for the fields in a class."""
|
|
328
|
+
used: set[str] = set()
|
|
329
|
+
property_names: List[str] = []
|
|
330
|
+
for field in fields:
|
|
331
|
+
raw_name = str(field['name'])
|
|
332
|
+
if self.openapi_generator_compat:
|
|
333
|
+
safe_name = self.safe_identifier(raw_name, class_name)
|
|
334
|
+
candidate = self.get_openapi_property_name(safe_name, class_name)
|
|
335
|
+
property_names.append(re.sub(r'_(\d+)$', r'\1', unique_name(candidate, used)))
|
|
336
|
+
else:
|
|
337
|
+
field_name = self.safe_identifier(raw_name, class_name, allow_unicode=True)
|
|
338
|
+
if self.pascal_properties:
|
|
339
|
+
field_name = pascal(field_name.lstrip('@'))
|
|
340
|
+
if self.is_csharp_reserved_word(field_name):
|
|
341
|
+
field_name = f"@{field_name}"
|
|
342
|
+
if field_name == class_name:
|
|
343
|
+
field_name += "_"
|
|
344
|
+
property_names.append(unique_name(field_name, used))
|
|
345
|
+
return property_names
|
|
346
|
+
|
|
297
347
|
def create_is_json_match_clauses(self, avro_schema, parent_namespace, class_name) -> List[str]:
|
|
298
348
|
""" Generates the IsJsonMatch method for System.Text.Json """
|
|
299
349
|
clauses: List[str] = []
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
if field_name == class_name:
|
|
305
|
-
field_name += "_"
|
|
350
|
+
fields = avro_schema.get('fields', [])
|
|
351
|
+
used_names: set[str] = set()
|
|
352
|
+
for field in fields:
|
|
353
|
+
field_name = unique_name(self.safe_identifier(field['name'], class_name, allow_unicode=True), used_names)
|
|
306
354
|
field_type = self.convert_avro_type_to_csharp(
|
|
307
|
-
class_name,
|
|
308
|
-
clauses.append(self.get_is_json_match_clause(class_name, field_name, field_type))
|
|
355
|
+
class_name, field['name'], field['type'], parent_namespace)
|
|
356
|
+
clauses.append(self.get_is_json_match_clause(class_name, field_name, field_type, str(field['name'])))
|
|
309
357
|
if len(clauses) == 0:
|
|
310
358
|
clauses.append("true")
|
|
311
359
|
return clauses
|
|
312
360
|
|
|
313
|
-
def get_is_json_match_clause(self, class_name, field_name, field_type) -> str:
|
|
361
|
+
def get_is_json_match_clause(self, class_name, field_name, field_type, wire_name: str | None = None) -> str:
|
|
314
362
|
""" Generates the IsJsonMatch clause for a field """
|
|
315
363
|
class_definition = ''
|
|
316
|
-
field_name_js = field_name[1:] if field_name[0] == '@' else field_name
|
|
364
|
+
field_name_js = wire_name if wire_name is not None else (field_name[1:] if field_name[0] == '@' else field_name)
|
|
317
365
|
is_optional = field_type[-1] == '?'
|
|
318
366
|
field_type = field_type[:-1] if is_optional else field_type
|
|
319
367
|
if field_type == 'byte[]':
|
|
@@ -399,18 +447,11 @@ class AvroToCSharp:
|
|
|
399
447
|
|
|
400
448
|
# Build equality comparisons for each field
|
|
401
449
|
equality_checks = []
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
field_name = f"@{field_name}"
|
|
406
|
-
if self.openapi_generator_compat:
|
|
407
|
-
field_name = self.get_openapi_property_name(field_name, class_name)
|
|
408
|
-
elif self.pascal_properties:
|
|
409
|
-
field_name = pascal(field_name)
|
|
410
|
-
if field_name == class_name:
|
|
411
|
-
field_name += "_"
|
|
450
|
+
property_names = self.get_csharp_property_names(fields, class_name)
|
|
451
|
+
for index, field in enumerate(fields):
|
|
452
|
+
field_name = property_names[index]
|
|
412
453
|
|
|
413
|
-
field_type = self.convert_avro_type_to_csharp(class_name,
|
|
454
|
+
field_type = self.convert_avro_type_to_csharp(class_name, field['name'], field['type'], parent_namespace)
|
|
414
455
|
|
|
415
456
|
# Handle different types of comparisons
|
|
416
457
|
if field_type == 'byte[]' or field_type == 'byte[]?':
|
|
@@ -443,18 +484,10 @@ class AvroToCSharp:
|
|
|
443
484
|
|
|
444
485
|
# Collect field names for HashCode.Combine
|
|
445
486
|
hash_fields = []
|
|
446
|
-
for field in fields:
|
|
447
|
-
field_name =
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
if self.openapi_generator_compat:
|
|
451
|
-
field_name = self.get_openapi_property_name(field_name, class_name)
|
|
452
|
-
elif self.pascal_properties:
|
|
453
|
-
field_name = pascal(field_name)
|
|
454
|
-
if field_name == class_name:
|
|
455
|
-
field_name += "_"
|
|
456
|
-
|
|
457
|
-
field_type = self.convert_avro_type_to_csharp(class_name, field_name, field['type'], parent_namespace)
|
|
487
|
+
for index, field in enumerate(fields):
|
|
488
|
+
field_name = property_names[index]
|
|
489
|
+
|
|
490
|
+
field_type = self.convert_avro_type_to_csharp(class_name, field['name'], field['type'], parent_namespace)
|
|
458
491
|
|
|
459
492
|
# Handle special types that need custom hash code computation
|
|
460
493
|
if field_type == 'byte[]' or field_type == 'byte[]?':
|
|
@@ -502,10 +535,14 @@ class AvroToCSharp:
|
|
|
502
535
|
else:
|
|
503
536
|
enum_definition += f"[XmlType(\"{enum_name}\")]\n"
|
|
504
537
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
538
|
+
used_symbols: set[str] = set()
|
|
539
|
+
symbols_str = []
|
|
540
|
+
for symbol in avro_schema['symbols']:
|
|
541
|
+
member_name = self.safe_enum_member_name(str(symbol), used_symbols)
|
|
542
|
+
if self.system_xml_annotation:
|
|
543
|
+
symbols_str.append(f"{INDENT}/// <summary>\n{INDENT}/// {member_name}\n{INDENT}/// </summary>\n{INDENT}[XmlEnum(Name=\"{symbol}\")]\n{INDENT}{member_name}")
|
|
544
|
+
else:
|
|
545
|
+
symbols_str.append(f"{INDENT}/// <summary>\n{INDENT}/// {member_name}\n{INDENT}/// </summary>\n{INDENT}{member_name}")
|
|
509
546
|
enum_body = ",\n".join(symbols_str)
|
|
510
547
|
enum_definition += f"public enum {enum_name}\n{{\n{enum_body}\n}}"
|
|
511
548
|
|
|
@@ -753,19 +790,14 @@ class AvroToCSharp:
|
|
|
753
790
|
return avro_type.get('type') == 'enum'
|
|
754
791
|
return False
|
|
755
792
|
|
|
756
|
-
def generate_property(self, field_index: int, field: Dict, class_name: str, parent_namespace: str) -> str:
|
|
793
|
+
def generate_property(self, field_index: int, field: Dict, class_name: str, parent_namespace: str, property_name: str | None = None) -> str:
|
|
757
794
|
""" Generates a property """
|
|
758
795
|
is_enum_type = self.is_enum_type(field['type'])
|
|
759
796
|
field_type = self.convert_avro_type_to_csharp(
|
|
760
797
|
class_name, field['name'], field['type'], parent_namespace)
|
|
761
798
|
field_default = field.get('const', field.get('default', None))
|
|
762
|
-
annotation_name =
|
|
763
|
-
if self.
|
|
764
|
-
field_name = f"@{field_name}"
|
|
765
|
-
if self.pascal_properties:
|
|
766
|
-
field_name = pascal(field_name)
|
|
767
|
-
if field_name == class_name:
|
|
768
|
-
field_name += "_"
|
|
799
|
+
annotation_name = field['name']
|
|
800
|
+
field_name = property_name if property_name is not None else self.get_csharp_property_names([field], class_name)[0]
|
|
769
801
|
if self.openapi_generator_compat:
|
|
770
802
|
return self.generate_openapi_compat_property(field, field_type, field_name, annotation_name, class_name)
|
|
771
803
|
prop = ''
|
|
@@ -809,7 +841,8 @@ class AvroToCSharp:
|
|
|
809
841
|
# For enum types, use qualified enum value (e.g., Type.Circle)
|
|
810
842
|
# Get the base enum type name (strip nullable ? suffix if present)
|
|
811
843
|
enum_type = field_type.rstrip('?')
|
|
812
|
-
|
|
844
|
+
enum_default = self.safe_enum_member_name(str(field_default), set())
|
|
845
|
+
initialization = f" = {enum_type}.{enum_default};"
|
|
813
846
|
elif isinstance(field_default, str):
|
|
814
847
|
initialization = f" = \"{field_default}\";"
|
|
815
848
|
else:
|
|
@@ -845,7 +878,7 @@ class AvroToCSharp:
|
|
|
845
878
|
|
|
846
879
|
def generate_openapi_compat_property(self, field: Dict, field_type: str, field_name: str, annotation_name: str, class_name: str) -> str:
|
|
847
880
|
"""Generates an OpenAPI Generator-compatible property."""
|
|
848
|
-
property_name =
|
|
881
|
+
property_name = field_name
|
|
849
882
|
doc = field.get('doc', property_name)
|
|
850
883
|
prop = f"{INDENT}/// <summary>\n{INDENT}/// {doc}\n{INDENT}/// </summary>\n"
|
|
851
884
|
if self.is_openapi_optional_avro_field(field):
|
|
@@ -866,14 +899,15 @@ class AvroToCSharp:
|
|
|
866
899
|
prop += f"{INDENT}public {field_type} {property_name} {{ get; set; }}{initialization}"
|
|
867
900
|
return prop
|
|
868
901
|
|
|
869
|
-
def get_openapi_compat_fields(self, fields: List[Dict], class_name: str, parent_namespace: str) -> List[Dict[str, Any]]:
|
|
902
|
+
def get_openapi_compat_fields(self, fields: List[Dict], class_name: str, parent_namespace: str, property_names: List[str] | None = None) -> List[Dict[str, Any]]:
|
|
870
903
|
"""Builds template metadata for OpenAPI Generator-compatible converters."""
|
|
904
|
+
if property_names is None:
|
|
905
|
+
property_names = self.get_csharp_property_names(fields, class_name)
|
|
871
906
|
result: List[Dict[str, Any]] = []
|
|
872
|
-
for field in fields:
|
|
907
|
+
for index, field in enumerate(fields):
|
|
873
908
|
raw_name = field['name']
|
|
874
|
-
safe_name = f"@{raw_name}" if self.is_csharp_reserved_word(raw_name) else raw_name
|
|
875
909
|
field_type = self.convert_avro_type_to_csharp(class_name, raw_name, field['type'], parent_namespace)
|
|
876
|
-
property_name =
|
|
910
|
+
property_name = property_names[index]
|
|
877
911
|
result.append({
|
|
878
912
|
"wire_name": raw_name,
|
|
879
913
|
"property_name": property_name,
|
|
@@ -1040,17 +1074,11 @@ class AvroToCSharp:
|
|
|
1040
1074
|
|
|
1041
1075
|
fields: List[Field] = []
|
|
1042
1076
|
if avro_schema and 'fields' in avro_schema:
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
field_name = self.get_openapi_property_name(field_name, class_name)
|
|
1049
|
-
elif self.pascal_properties:
|
|
1050
|
-
field_name = pascal(field_name)
|
|
1051
|
-
if field_name == class_name:
|
|
1052
|
-
field_name += "_"
|
|
1053
|
-
field_type = self.convert_avro_type_to_csharp(class_name, field_name, field['type'], str(avro_schema.get('namespace', '')))
|
|
1077
|
+
avro_fields = cast(List[Dict[str,JsonNode]], avro_schema['fields'])
|
|
1078
|
+
property_names = self.get_csharp_property_names(avro_fields, class_name)
|
|
1079
|
+
for index, field in enumerate(avro_fields):
|
|
1080
|
+
field_name = property_names[index]
|
|
1081
|
+
field_type = self.convert_avro_type_to_csharp(class_name, str(field['name']), field['type'], str(avro_schema.get('namespace', '')))
|
|
1054
1082
|
is_class = field_type in self.generated_types and self.generated_types[field_type] == "class"
|
|
1055
1083
|
is_enum = self.is_enum_type(field['type'])
|
|
1056
1084
|
is_union = field_type in self.generated_types and self.generated_types[field_type] == "union"
|
|
@@ -1128,6 +1156,12 @@ class AvroToCSharp:
|
|
|
1128
1156
|
|
|
1129
1157
|
self.schema_doc = schema
|
|
1130
1158
|
self.type_dict = build_flat_type_dict(self.schema_doc)
|
|
1159
|
+
if isinstance(self.schema_doc, list):
|
|
1160
|
+
for type_schema in self.type_dict.values():
|
|
1161
|
+
if isinstance(type_schema, dict) and type_schema.get('type') in ['record', 'enum']:
|
|
1162
|
+
full_name = self.concat_namespace(str(type_schema.get('namespace', '')), str(type_schema.get('name', '')))
|
|
1163
|
+
if full_name not in [self.concat_namespace(str(item.get('namespace', '')), str(item.get('name', ''))) for item in self.schema_doc if isinstance(item, dict)]:
|
|
1164
|
+
self.schema_doc.append(type_schema)
|
|
1131
1165
|
if not os.path.exists(output_dir):
|
|
1132
1166
|
os.makedirs(output_dir, exist_ok=True)
|
|
1133
1167
|
if not glob.glob(os.path.join(output_dir, "src", "*.sln")):
|
|
@@ -7,6 +7,8 @@ import os
|
|
|
7
7
|
import re
|
|
8
8
|
from typing import Any
|
|
9
9
|
|
|
10
|
+
from avrotize.common import unique_name
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
def _sanitize(name: str) -> str:
|
|
12
14
|
cleaned = re.sub(r"\W", "_", name)
|
|
@@ -25,16 +27,21 @@ class AvroToFlatBuffersConverter:
|
|
|
25
27
|
self.namespace_override = namespace
|
|
26
28
|
self.named: dict[str, dict[str, Any]] = {}
|
|
27
29
|
self.union_defs: dict[str, list[str]] = {}
|
|
30
|
+
self.type_names: dict[str, str] = {}
|
|
31
|
+
self.used_top_names: set[str] = set()
|
|
28
32
|
|
|
29
33
|
def convert_file(self, avro_schema_path: str) -> str:
|
|
30
34
|
with open(avro_schema_path, "r", encoding="utf-8") as f:
|
|
31
35
|
schema = json.load(f)
|
|
32
36
|
schemas = schema if isinstance(schema, list) else [schema]
|
|
37
|
+
self._prepare_names(schemas)
|
|
33
38
|
for item in schemas:
|
|
34
39
|
if isinstance(item, dict) and item.get("type") in {"record", "enum"}:
|
|
35
40
|
qn = self.qualified_name(item)
|
|
36
41
|
self.named[qn] = item
|
|
37
42
|
self.named[item["name"]] = item
|
|
43
|
+
self.named[self.fbs_type_name(qn)] = item
|
|
44
|
+
self.named[self.fbs_type_name(item["name"])] = item
|
|
38
45
|
namespace = self.namespace_override or self.first_namespace(schemas)
|
|
39
46
|
lines: list[str] = []
|
|
40
47
|
if namespace:
|
|
@@ -53,9 +60,22 @@ class AvroToFlatBuffersConverter:
|
|
|
53
60
|
lines.extend(record_lines)
|
|
54
61
|
root = self.find_root(schemas)
|
|
55
62
|
if root:
|
|
56
|
-
lines.append(f"root_type {root};")
|
|
63
|
+
lines.append(f"root_type {self.fbs_type_name(root)};")
|
|
57
64
|
return "\n".join(lines).rstrip() + "\n"
|
|
58
65
|
|
|
66
|
+
def _prepare_names(self, schemas: list[Any]) -> None:
|
|
67
|
+
self.type_names = {}
|
|
68
|
+
self.used_top_names = set()
|
|
69
|
+
for item in schemas:
|
|
70
|
+
if isinstance(item, dict) and item.get("type") in {"record", "enum"}:
|
|
71
|
+
candidate = _sanitize(item["name"])
|
|
72
|
+
converted = unique_name(candidate, self.used_top_names)
|
|
73
|
+
self.type_names[item["name"]] = converted
|
|
74
|
+
self.type_names[self.qualified_name(item)] = converted
|
|
75
|
+
|
|
76
|
+
def fbs_type_name(self, name: str) -> str:
|
|
77
|
+
return self.type_names.get(name, self.type_names.get(name.split(".")[-1], _sanitize(name.split(".")[-1])))
|
|
78
|
+
|
|
59
79
|
@staticmethod
|
|
60
80
|
def qualified_name(schema: dict[str, Any]) -> str:
|
|
61
81
|
return f"{schema.get('namespace')}.{schema['name']}" if schema.get("namespace") else schema["name"]
|
|
@@ -80,15 +100,19 @@ class AvroToFlatBuffersConverter:
|
|
|
80
100
|
def render_enum(self, schema: dict[str, Any]) -> list[str]:
|
|
81
101
|
ordinals = schema.get("ordinals", {})
|
|
82
102
|
parts = []
|
|
103
|
+
used_symbols: set[str] = set()
|
|
83
104
|
for i, symbol in enumerate(schema.get("symbols", [])):
|
|
84
105
|
value = ordinals.get(symbol, i)
|
|
85
|
-
parts.append(f"{_sanitize(symbol)} = {value}")
|
|
86
|
-
return [f"enum {schema
|
|
106
|
+
parts.append(f"{unique_name(_sanitize(symbol), used_symbols)} = {value}")
|
|
107
|
+
return [f"enum {self.fbs_type_name(self.qualified_name(schema))} : int {{ {', '.join(parts)} }}"]
|
|
87
108
|
|
|
88
109
|
def render_record(self, schema: dict[str, Any]) -> list[str]:
|
|
89
|
-
|
|
110
|
+
record_name = self.fbs_type_name(self.qualified_name(schema))
|
|
111
|
+
lines = [f"table {record_name} {{"]
|
|
112
|
+
used_fields: set[str] = set()
|
|
90
113
|
for field in schema.get("fields", []):
|
|
91
|
-
|
|
114
|
+
field_name = unique_name(_sanitize(field["name"]), used_fields)
|
|
115
|
+
fbs_type, nullable = self.avro_type_to_fbs(field["type"], record_name, field_name)
|
|
92
116
|
default = ""
|
|
93
117
|
avro_default = field.get("fbsDefault", field.get("default"))
|
|
94
118
|
if avro_default is not None and not isinstance(field["type"], list):
|
|
@@ -96,7 +120,7 @@ class AvroToFlatBuffersConverter:
|
|
|
96
120
|
attr = ""
|
|
97
121
|
if not nullable and self.can_be_required(fbs_type):
|
|
98
122
|
attr = " (required)"
|
|
99
|
-
lines.append(f" {
|
|
123
|
+
lines.append(f" {field_name}: {fbs_type}{default}{attr};")
|
|
100
124
|
lines.append("}")
|
|
101
125
|
return lines
|
|
102
126
|
|
|
@@ -125,11 +149,11 @@ class AvroToFlatBuffersConverter:
|
|
|
125
149
|
fbs, _ = self.avro_type_to_fbs(non_null[0], record_name, field_name)
|
|
126
150
|
return fbs, nullable
|
|
127
151
|
members = [self.avro_type_to_fbs(t, record_name, field_name)[0] for t in non_null]
|
|
128
|
-
union_name = f"{record_name}_{field_name}_Union"
|
|
152
|
+
union_name = unique_name(_sanitize(f"{record_name}_{field_name}_Union"), self.used_top_names)
|
|
129
153
|
self.union_defs[union_name] = [m for m in members if not m.startswith("[")]
|
|
130
154
|
return union_name, nullable
|
|
131
155
|
if isinstance(avro_type, str):
|
|
132
|
-
return self.primitive_map.get(avro_type,
|
|
156
|
+
return self.primitive_map.get(avro_type, self.fbs_type_name(avro_type)), nullable
|
|
133
157
|
if isinstance(avro_type, dict):
|
|
134
158
|
typ = avro_type.get("type")
|
|
135
159
|
if typ == "array":
|
|
@@ -139,7 +163,7 @@ class AvroToFlatBuffersConverter:
|
|
|
139
163
|
return "string", nullable
|
|
140
164
|
if typ == "enum" or typ == "record":
|
|
141
165
|
self.named[avro_type["name"]] = avro_type
|
|
142
|
-
return avro_type["name"], nullable
|
|
166
|
+
return self.fbs_type_name(avro_type["name"]), nullable
|
|
143
167
|
if typ == "bytes":
|
|
144
168
|
return "[ubyte]", nullable
|
|
145
169
|
return self.primitive_map.get(typ, typ), nullable
|