structurize 3.6.1__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.
Files changed (121) hide show
  1. {structurize-3.6.1/structurize.egg-info → structurize-3.7.0}/PKG-INFO +1 -1
  2. {structurize-3.6.1 → structurize-3.7.0}/avrotize/__init__.py +30 -0
  3. {structurize-3.6.1 → structurize-3.7.0}/avrotize/_version.py +3 -3
  4. structurize-3.7.0/avrotize/avrotocapnproto.py +183 -0
  5. {structurize-3.6.1 → structurize-3.7.0}/avrotize/avrotocsharp.py +138 -10
  6. structurize-3.7.0/avrotize/avrotocue.py +145 -0
  7. structurize-3.7.0/avrotize/avrotoflatbuffers.py +155 -0
  8. {structurize-3.6.1 → structurize-3.7.0}/avrotize/avrotojstruct.py +20 -0
  9. structurize-3.7.0/avrotize/avrotojtd.py +225 -0
  10. structurize-3.7.0/avrotize/avrotoraml.py +144 -0
  11. structurize-3.7.0/avrotize/avrotosmithy.py +311 -0
  12. structurize-3.7.0/avrotize/avrotosurreal.py +138 -0
  13. structurize-3.7.0/avrotize/avrotothrift.py +140 -0
  14. structurize-3.7.0/avrotize/capnprototoavro.py +378 -0
  15. {structurize-3.6.1 → structurize-3.7.0}/avrotize/commands.json +1288 -3
  16. structurize-3.7.0/avrotize/cuetoavro.py +414 -0
  17. structurize-3.7.0/avrotize/cuetostructure.py +67 -0
  18. structurize-3.7.0/avrotize/flatbufferstoavro.py +352 -0
  19. structurize-3.7.0/avrotize/flatbufferstojstruct.py +76 -0
  20. structurize-3.7.0/avrotize/jstructtoflatbuffers.py +23 -0
  21. structurize-3.7.0/avrotize/jstructtoraml.py +20 -0
  22. structurize-3.7.0/avrotize/jstructtosmithy.py +24 -0
  23. structurize-3.7.0/avrotize/jtdtoavro.py +222 -0
  24. structurize-3.7.0/avrotize/jtdtostructure.py +24 -0
  25. structurize-3.7.0/avrotize/ramltoavro.py +240 -0
  26. structurize-3.7.0/avrotize/ramltojstruct.py +20 -0
  27. structurize-3.7.0/avrotize/smithytoavro.py +539 -0
  28. structurize-3.7.0/avrotize/smithytojstruct.py +24 -0
  29. {structurize-3.6.1 → structurize-3.7.0}/avrotize/structuretocsharp.py +134 -8
  30. structurize-3.7.0/avrotize/structuretocue.py +26 -0
  31. structurize-3.7.0/avrotize/structuretojtd.py +24 -0
  32. structurize-3.7.0/avrotize/structuretoparquet.py +296 -0
  33. structurize-3.7.0/avrotize/surrealtoavro.py +290 -0
  34. structurize-3.7.0/avrotize/thriftstructure.py +48 -0
  35. structurize-3.7.0/avrotize/thrifttoavro.py +397 -0
  36. {structurize-3.6.1 → structurize-3.7.0/structurize.egg-info}/PKG-INFO +1 -1
  37. {structurize-3.6.1 → structurize-3.7.0}/structurize.egg-info/SOURCES.txt +56 -0
  38. {structurize-3.6.1 → structurize-3.7.0}/.gitignore +0 -0
  39. {structurize-3.6.1 → structurize-3.7.0}/LICENSE +0 -0
  40. {structurize-3.6.1 → structurize-3.7.0}/MANIFEST.in +0 -0
  41. {structurize-3.6.1 → structurize-3.7.0}/README.md +0 -0
  42. {structurize-3.6.1 → structurize-3.7.0}/avrotize/__main__.py +0 -0
  43. {structurize-3.6.1 → structurize-3.7.0}/avrotize/asn1toavro.py +0 -0
  44. {structurize-3.6.1 → structurize-3.7.0}/avrotize/avrotize.py +0 -0
  45. {structurize-3.6.1 → structurize-3.7.0}/avrotize/avrotocpp.py +0 -0
  46. {structurize-3.6.1 → structurize-3.7.0}/avrotize/avrotocsv.py +0 -0
  47. {structurize-3.6.1 → structurize-3.7.0}/avrotize/avrotodatapackage.py +0 -0
  48. {structurize-3.6.1 → structurize-3.7.0}/avrotize/avrotodb.py +0 -0
  49. {structurize-3.6.1 → structurize-3.7.0}/avrotize/avrotogo.py +0 -0
  50. {structurize-3.6.1 → structurize-3.7.0}/avrotize/avrotographql.py +0 -0
  51. {structurize-3.6.1 → structurize-3.7.0}/avrotize/avrotoiceberg.py +0 -0
  52. {structurize-3.6.1 → structurize-3.7.0}/avrotize/avrotojava.py +0 -0
  53. {structurize-3.6.1 → structurize-3.7.0}/avrotize/avrotojs.py +0 -0
  54. {structurize-3.6.1 → structurize-3.7.0}/avrotize/avrotojsons.py +0 -0
  55. {structurize-3.6.1 → structurize-3.7.0}/avrotize/avrotokusto.py +0 -0
  56. {structurize-3.6.1 → structurize-3.7.0}/avrotize/avrotomd.py +0 -0
  57. {structurize-3.6.1 → structurize-3.7.0}/avrotize/avrotools.py +0 -0
  58. {structurize-3.6.1 → structurize-3.7.0}/avrotize/avrotoparquet.py +0 -0
  59. {structurize-3.6.1 → structurize-3.7.0}/avrotize/avrotoproto.py +0 -0
  60. {structurize-3.6.1 → structurize-3.7.0}/avrotize/avrotopython.py +0 -0
  61. {structurize-3.6.1 → structurize-3.7.0}/avrotize/avrotorust.py +0 -0
  62. {structurize-3.6.1 → structurize-3.7.0}/avrotize/avrotots.py +0 -0
  63. {structurize-3.6.1 → structurize-3.7.0}/avrotize/avrototsml.py +0 -0
  64. {structurize-3.6.1 → structurize-3.7.0}/avrotize/avrotoxsd.py +0 -0
  65. {structurize-3.6.1 → structurize-3.7.0}/avrotize/avrovalidator.py +0 -0
  66. {structurize-3.6.1 → structurize-3.7.0}/avrotize/cddltostructure.py +0 -0
  67. {structurize-3.6.1 → structurize-3.7.0}/avrotize/choice_inference.py +0 -0
  68. {structurize-3.6.1 → structurize-3.7.0}/avrotize/common.py +0 -0
  69. {structurize-3.6.1 → structurize-3.7.0}/avrotize/constants.py +0 -0
  70. {structurize-3.6.1 → structurize-3.7.0}/avrotize/csvtoavro.py +0 -0
  71. {structurize-3.6.1 → structurize-3.7.0}/avrotize/datapackagetoavro.py +0 -0
  72. {structurize-3.6.1 → structurize-3.7.0}/avrotize/dependencies/cpp/vcpkg/vcpkg.json +0 -0
  73. {structurize-3.6.1 → structurize-3.7.0}/avrotize/dependencies/typescript/node22/package.json +0 -0
  74. {structurize-3.6.1 → structurize-3.7.0}/avrotize/dependency_resolver.py +0 -0
  75. {structurize-3.6.1 → structurize-3.7.0}/avrotize/dependency_version.py +0 -0
  76. {structurize-3.6.1 → structurize-3.7.0}/avrotize/jsonstoavro.py +0 -0
  77. {structurize-3.6.1 → structurize-3.7.0}/avrotize/jsonstostructure.py +0 -0
  78. {structurize-3.6.1 → structurize-3.7.0}/avrotize/jsontoschema.py +0 -0
  79. {structurize-3.6.1 → structurize-3.7.0}/avrotize/jstructtoavro.py +0 -0
  80. {structurize-3.6.1 → structurize-3.7.0}/avrotize/kstructtoavro.py +0 -0
  81. {structurize-3.6.1 → structurize-3.7.0}/avrotize/kustotoavro.py +0 -0
  82. {structurize-3.6.1 → structurize-3.7.0}/avrotize/kustotojstruct.py +0 -0
  83. {structurize-3.6.1 → structurize-3.7.0}/avrotize/mcp_server.py +0 -0
  84. {structurize-3.6.1 → structurize-3.7.0}/avrotize/openapitostructure.py +0 -0
  85. {structurize-3.6.1 → structurize-3.7.0}/avrotize/parquettoavro.py +0 -0
  86. {structurize-3.6.1 → structurize-3.7.0}/avrotize/proto2parser.py +0 -0
  87. {structurize-3.6.1 → structurize-3.7.0}/avrotize/proto3parser.py +0 -0
  88. {structurize-3.6.1 → structurize-3.7.0}/avrotize/prototoavro.py +0 -0
  89. {structurize-3.6.1 → structurize-3.7.0}/avrotize/schema_inference.py +0 -0
  90. {structurize-3.6.1 → structurize-3.7.0}/avrotize/sqltoavro.py +0 -0
  91. {structurize-3.6.1 → structurize-3.7.0}/avrotize/structuretocddl.py +0 -0
  92. {structurize-3.6.1 → structurize-3.7.0}/avrotize/structuretocpp.py +0 -0
  93. {structurize-3.6.1 → structurize-3.7.0}/avrotize/structuretocsv.py +0 -0
  94. {structurize-3.6.1 → structurize-3.7.0}/avrotize/structuretodatapackage.py +0 -0
  95. {structurize-3.6.1 → structurize-3.7.0}/avrotize/structuretodb.py +0 -0
  96. {structurize-3.6.1 → structurize-3.7.0}/avrotize/structuretogo.py +0 -0
  97. {structurize-3.6.1 → structurize-3.7.0}/avrotize/structuretographql.py +0 -0
  98. {structurize-3.6.1 → structurize-3.7.0}/avrotize/structuretoiceberg.py +0 -0
  99. {structurize-3.6.1 → structurize-3.7.0}/avrotize/structuretojava.py +0 -0
  100. {structurize-3.6.1 → structurize-3.7.0}/avrotize/structuretojs.py +0 -0
  101. {structurize-3.6.1 → structurize-3.7.0}/avrotize/structuretojsons.py +0 -0
  102. {structurize-3.6.1 → structurize-3.7.0}/avrotize/structuretokusto.py +0 -0
  103. {structurize-3.6.1 → structurize-3.7.0}/avrotize/structuretomd.py +0 -0
  104. {structurize-3.6.1 → structurize-3.7.0}/avrotize/structuretoproto.py +0 -0
  105. {structurize-3.6.1 → structurize-3.7.0}/avrotize/structuretopython.py +0 -0
  106. {structurize-3.6.1 → structurize-3.7.0}/avrotize/structuretorust.py +0 -0
  107. {structurize-3.6.1 → structurize-3.7.0}/avrotize/structuretots.py +0 -0
  108. {structurize-3.6.1 → structurize-3.7.0}/avrotize/structuretotsml.py +0 -0
  109. {structurize-3.6.1 → structurize-3.7.0}/avrotize/structuretoxsd.py +0 -0
  110. {structurize-3.6.1 → structurize-3.7.0}/avrotize/tmslvalidate.py +0 -0
  111. {structurize-3.6.1 → structurize-3.7.0}/avrotize/validate.py +0 -0
  112. {structurize-3.6.1 → structurize-3.7.0}/avrotize/xmltoschema.py +0 -0
  113. {structurize-3.6.1 → structurize-3.7.0}/avrotize/xsdtoavro.py +0 -0
  114. {structurize-3.6.1 → structurize-3.7.0}/build.ps1 +0 -0
  115. {structurize-3.6.1 → structurize-3.7.0}/build.sh +0 -0
  116. {structurize-3.6.1 → structurize-3.7.0}/pyproject.toml +0 -0
  117. {structurize-3.6.1 → structurize-3.7.0}/setup.cfg +0 -0
  118. {structurize-3.6.1 → structurize-3.7.0}/structurize.egg-info/dependency_links.txt +0 -0
  119. {structurize-3.6.1 → structurize-3.7.0}/structurize.egg-info/entry_points.txt +0 -0
  120. {structurize-3.6.1 → structurize-3.7.0}/structurize.egg-info/requires.txt +0 -0
  121. {structurize-3.6.1 → 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.6.1
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.6.1'
22
- __version_tuple__ = version_tuple = (3, 6, 1)
21
+ __version__ = version = '3.7.0'
22
+ __version_tuple__ = version_tuple = (3, 7, 0)
23
23
 
24
- __commit_id__ = commit_id = 'ga0404973f'
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
- fields_str = [self.generate_property(index + 1, field, class_name, avro_namespace) for index, field in enumerate(avro_schema.get('fields', []))]
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.pascal_properties:
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.pascal_properties:
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.pascal_properties:
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)