structurize 2.16.5__py3-none-any.whl → 2.17.0__py3-none-any.whl
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.
- avrotize/__init__.py +1 -0
- avrotize/_version.py +3 -3
- avrotize/avrotocsharp.py +74 -10
- avrotize/avrotojava.py +1130 -51
- avrotize/avrotopython.py +4 -2
- avrotize/commands.json +671 -53
- avrotize/common.py +6 -1
- avrotize/jsonstoavro.py +518 -49
- avrotize/structuretocpp.py +697 -0
- avrotize/structuretocsv.py +365 -0
- avrotize/structuretodatapackage.py +659 -0
- avrotize/structuretodb.py +1125 -0
- avrotize/structuretogo.py +720 -0
- avrotize/structuretographql.py +502 -0
- avrotize/structuretoiceberg.py +355 -0
- avrotize/structuretojava.py +853 -0
- avrotize/structuretokusto.py +639 -0
- avrotize/structuretomd.py +322 -0
- avrotize/structuretoproto.py +764 -0
- avrotize/structuretorust.py +714 -0
- avrotize/structuretoxsd.py +679 -0
- structurize-2.17.0.dist-info/METADATA +107 -0
- {structurize-2.16.5.dist-info → structurize-2.17.0.dist-info}/RECORD +27 -14
- structurize-2.16.5.dist-info/METADATA +0 -848
- {structurize-2.16.5.dist-info → structurize-2.17.0.dist-info}/WHEEL +0 -0
- {structurize-2.16.5.dist-info → structurize-2.17.0.dist-info}/entry_points.txt +0 -0
- {structurize-2.16.5.dist-info → structurize-2.17.0.dist-info}/licenses/LICENSE +0 -0
- {structurize-2.16.5.dist-info → structurize-2.17.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
# pylint: disable=line-too-long
|
|
2
|
+
|
|
3
|
+
""" StructureToGo class for converting JSON Structure schema to Go structs """
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from typing import Any, Dict, List, Set, Union, Optional, cast
|
|
8
|
+
|
|
9
|
+
from avrotize.common import pascal, render_template
|
|
10
|
+
|
|
11
|
+
JsonNode = Dict[str, 'JsonNode'] | List['JsonNode'] | str | None
|
|
12
|
+
|
|
13
|
+
INDENT = ' '
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class StructureToGo:
|
|
17
|
+
""" Converts JSON Structure schema to Go structs """
|
|
18
|
+
|
|
19
|
+
def __init__(self, base_package: str = '') -> None:
|
|
20
|
+
self.base_package = base_package
|
|
21
|
+
self.output_dir = os.getcwd()
|
|
22
|
+
self.json_annotation = False
|
|
23
|
+
self.avro_annotation = False
|
|
24
|
+
self.package_site = 'github.com'
|
|
25
|
+
self.package_username = 'username'
|
|
26
|
+
self.schema_doc: JsonNode = None
|
|
27
|
+
self.generated_types: Dict[str, str] = {}
|
|
28
|
+
self.generated_structure_types: Dict[str, Dict[str, Union[str, Dict, List]]] = {}
|
|
29
|
+
self.definitions: Dict[str, Any] = {}
|
|
30
|
+
self.schema_registry: Dict[str, Dict] = {}
|
|
31
|
+
self.structs: List[Dict] = []
|
|
32
|
+
self.enums: List[Dict] = []
|
|
33
|
+
|
|
34
|
+
def safe_identifier(self, name: str) -> str:
|
|
35
|
+
"""Converts a name to a safe Go identifier"""
|
|
36
|
+
reserved_words = [
|
|
37
|
+
'break', 'default', 'func', 'interface', 'select', 'case', 'defer', 'go', 'map', 'struct', 'chan',
|
|
38
|
+
'else', 'goto', 'package', 'switch', 'const', 'fallthrough', 'if', 'range', 'type', 'continue', 'for',
|
|
39
|
+
'import', 'return', 'var',
|
|
40
|
+
]
|
|
41
|
+
if name in reserved_words:
|
|
42
|
+
return f"{name}_"
|
|
43
|
+
return name
|
|
44
|
+
|
|
45
|
+
def go_type_name(self, name: str, namespace: str = '') -> str:
|
|
46
|
+
"""Returns a qualified name for a Go struct or enum"""
|
|
47
|
+
if namespace:
|
|
48
|
+
namespace = ''.join([pascal(part) for part in namespace.split('.')])
|
|
49
|
+
return f"{namespace}{pascal(name)}"
|
|
50
|
+
return pascal(name)
|
|
51
|
+
|
|
52
|
+
def map_primitive_to_go(self, structure_type: str, is_optional: bool) -> str:
|
|
53
|
+
"""Maps JSON Structure primitive types to Go types"""
|
|
54
|
+
optional_mapping = {
|
|
55
|
+
'null': 'interface{}',
|
|
56
|
+
'boolean': '*bool',
|
|
57
|
+
'string': '*string',
|
|
58
|
+
'integer': '*int',
|
|
59
|
+
'number': '*float64',
|
|
60
|
+
'int8': '*int8',
|
|
61
|
+
'uint8': '*uint8',
|
|
62
|
+
'int16': '*int16',
|
|
63
|
+
'uint16': '*uint16',
|
|
64
|
+
'int32': '*int32',
|
|
65
|
+
'uint32': '*uint32',
|
|
66
|
+
'int64': '*int64',
|
|
67
|
+
'uint64': '*uint64',
|
|
68
|
+
'int128': '*big.Int',
|
|
69
|
+
'uint128': '*big.Int',
|
|
70
|
+
'float8': '*float32',
|
|
71
|
+
'float': '*float32',
|
|
72
|
+
'double': '*float64',
|
|
73
|
+
'binary32': '*float32',
|
|
74
|
+
'binary64': '*float64',
|
|
75
|
+
'decimal': '*float64',
|
|
76
|
+
'binary': '[]byte',
|
|
77
|
+
'date': '*time.Time',
|
|
78
|
+
'time': '*time.Time',
|
|
79
|
+
'datetime': '*time.Time',
|
|
80
|
+
'timestamp': '*time.Time',
|
|
81
|
+
'duration': '*time.Duration',
|
|
82
|
+
'uuid': '*string',
|
|
83
|
+
'uri': '*string',
|
|
84
|
+
'jsonpointer': '*string',
|
|
85
|
+
'any': 'interface{}',
|
|
86
|
+
}
|
|
87
|
+
required_mapping = {
|
|
88
|
+
'null': 'interface{}',
|
|
89
|
+
'boolean': 'bool',
|
|
90
|
+
'string': 'string',
|
|
91
|
+
'integer': 'int',
|
|
92
|
+
'number': 'float64',
|
|
93
|
+
'int8': 'int8',
|
|
94
|
+
'uint8': 'uint8',
|
|
95
|
+
'int16': 'int16',
|
|
96
|
+
'uint16': 'uint16',
|
|
97
|
+
'int32': 'int32',
|
|
98
|
+
'uint32': 'uint32',
|
|
99
|
+
'int64': 'int64',
|
|
100
|
+
'uint64': 'uint64',
|
|
101
|
+
'int128': 'big.Int',
|
|
102
|
+
'uint128': 'big.Int',
|
|
103
|
+
'float8': 'float32',
|
|
104
|
+
'float': 'float32',
|
|
105
|
+
'double': 'float64',
|
|
106
|
+
'binary32': 'float32',
|
|
107
|
+
'binary64': 'float64',
|
|
108
|
+
'decimal': 'float64',
|
|
109
|
+
'binary': '[]byte',
|
|
110
|
+
'date': 'time.Time',
|
|
111
|
+
'time': 'time.Time',
|
|
112
|
+
'datetime': 'time.Time',
|
|
113
|
+
'timestamp': 'time.Time',
|
|
114
|
+
'duration': 'time.Duration',
|
|
115
|
+
'uuid': 'string',
|
|
116
|
+
'uri': 'string',
|
|
117
|
+
'jsonpointer': 'string',
|
|
118
|
+
'any': 'interface{}',
|
|
119
|
+
}
|
|
120
|
+
if structure_type in self.generated_types:
|
|
121
|
+
return structure_type
|
|
122
|
+
else:
|
|
123
|
+
return required_mapping.get(structure_type, 'interface{}') if not is_optional else optional_mapping.get(structure_type, 'interface{}')
|
|
124
|
+
|
|
125
|
+
def resolve_ref(self, ref: str, context_schema: Optional[Dict] = None) -> Optional[Dict]:
|
|
126
|
+
""" Resolves a $ref to the actual schema definition """
|
|
127
|
+
# Check if it's an absolute URI reference
|
|
128
|
+
if not ref.startswith('#/'):
|
|
129
|
+
if ref in self.schema_registry:
|
|
130
|
+
return self.schema_registry[ref]
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
# Handle fragment-only references
|
|
134
|
+
path = ref[2:].split('/')
|
|
135
|
+
schema = context_schema if context_schema else self.schema_doc
|
|
136
|
+
|
|
137
|
+
for part in path:
|
|
138
|
+
if not isinstance(schema, dict) or part not in schema:
|
|
139
|
+
return None
|
|
140
|
+
schema = schema[part]
|
|
141
|
+
|
|
142
|
+
return schema
|
|
143
|
+
|
|
144
|
+
def register_schema_ids(self, schema: Dict, base_uri: str = '') -> None:
|
|
145
|
+
""" Recursively registers schemas with $id keywords """
|
|
146
|
+
if not isinstance(schema, dict):
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
if '$id' in schema:
|
|
150
|
+
schema_id = schema['$id']
|
|
151
|
+
if base_uri and not schema_id.startswith(('http://', 'https://', 'urn:')):
|
|
152
|
+
from urllib.parse import urljoin
|
|
153
|
+
schema_id = urljoin(base_uri, schema_id)
|
|
154
|
+
self.schema_registry[schema_id] = schema
|
|
155
|
+
base_uri = schema_id
|
|
156
|
+
|
|
157
|
+
if 'definitions' in schema:
|
|
158
|
+
for def_name, def_schema in schema['definitions'].items():
|
|
159
|
+
if isinstance(def_schema, dict):
|
|
160
|
+
self.register_schema_ids(def_schema, base_uri)
|
|
161
|
+
|
|
162
|
+
if 'properties' in schema:
|
|
163
|
+
for prop_name, prop_schema in schema['properties'].items():
|
|
164
|
+
if isinstance(prop_schema, dict):
|
|
165
|
+
self.register_schema_ids(prop_schema, base_uri)
|
|
166
|
+
|
|
167
|
+
for key in ['items', 'values', 'additionalProperties']:
|
|
168
|
+
if key in schema and isinstance(schema[key], dict):
|
|
169
|
+
self.register_schema_ids(schema[key], base_uri)
|
|
170
|
+
|
|
171
|
+
def convert_structure_type_to_go(self, class_name: str, field_name: str,
|
|
172
|
+
structure_type: JsonNode, parent_namespace: str,
|
|
173
|
+
nullable: bool = False) -> str:
|
|
174
|
+
""" Converts JSON Structure type to Go type """
|
|
175
|
+
if isinstance(structure_type, str):
|
|
176
|
+
return self.map_primitive_to_go(structure_type, nullable)
|
|
177
|
+
elif isinstance(structure_type, list):
|
|
178
|
+
# Handle type unions
|
|
179
|
+
non_null_types = [t for t in structure_type if t != 'null']
|
|
180
|
+
has_null = 'null' in structure_type
|
|
181
|
+
if len(non_null_types) == 1:
|
|
182
|
+
inner_type = self.convert_structure_type_to_go(
|
|
183
|
+
class_name, field_name, non_null_types[0], parent_namespace, has_null)
|
|
184
|
+
return inner_type
|
|
185
|
+
else:
|
|
186
|
+
# Generate union type
|
|
187
|
+
return self.generate_union_class(field_name, structure_type, parent_namespace)
|
|
188
|
+
elif isinstance(structure_type, dict):
|
|
189
|
+
# Handle $ref
|
|
190
|
+
if '$ref' in structure_type:
|
|
191
|
+
ref_schema = self.resolve_ref(structure_type['$ref'], self.schema_doc)
|
|
192
|
+
if ref_schema:
|
|
193
|
+
ref_path = structure_type['$ref'].split('/')
|
|
194
|
+
type_name = ref_path[-1]
|
|
195
|
+
ref_namespace = '.'.join(ref_path[2:-1]) if len(ref_path) > 3 else parent_namespace
|
|
196
|
+
return self.generate_class_or_choice(ref_schema, ref_namespace, write_file=True, explicit_name=type_name)
|
|
197
|
+
return 'interface{}'
|
|
198
|
+
|
|
199
|
+
# Handle enum keyword
|
|
200
|
+
if 'enum' in structure_type:
|
|
201
|
+
return self.generate_enum(structure_type, field_name, parent_namespace, write_file=True)
|
|
202
|
+
|
|
203
|
+
# Handle type keyword
|
|
204
|
+
if 'type' not in structure_type:
|
|
205
|
+
return 'interface{}'
|
|
206
|
+
|
|
207
|
+
struct_type = structure_type['type']
|
|
208
|
+
|
|
209
|
+
# Handle complex types
|
|
210
|
+
if struct_type == 'object':
|
|
211
|
+
return self.generate_class(structure_type, parent_namespace, write_file=True)
|
|
212
|
+
elif struct_type == 'array':
|
|
213
|
+
items_type = self.convert_structure_type_to_go(
|
|
214
|
+
class_name, field_name+'List', structure_type.get('items', {'type': 'any'}),
|
|
215
|
+
parent_namespace, nullable=True)
|
|
216
|
+
if items_type.startswith('*'):
|
|
217
|
+
return f"[]{items_type[1:]}"
|
|
218
|
+
return f"[]{items_type}"
|
|
219
|
+
elif struct_type == 'set':
|
|
220
|
+
# Go doesn't have a built-in set, use map[T]bool
|
|
221
|
+
items_type = self.convert_structure_type_to_go(
|
|
222
|
+
class_name, field_name+'Set', structure_type.get('items', {'type': 'any'}),
|
|
223
|
+
parent_namespace, nullable=True)
|
|
224
|
+
if items_type.startswith('*'):
|
|
225
|
+
items_type = items_type[1:]
|
|
226
|
+
return f"map[{items_type}]bool"
|
|
227
|
+
elif struct_type == 'map':
|
|
228
|
+
values_type = self.convert_structure_type_to_go(
|
|
229
|
+
class_name, field_name+'Map', structure_type.get('values', {'type': 'any'}),
|
|
230
|
+
parent_namespace, nullable=True)
|
|
231
|
+
return f"map[string]{values_type}"
|
|
232
|
+
elif struct_type == 'choice':
|
|
233
|
+
return self.generate_choice(structure_type, parent_namespace, write_file=True)
|
|
234
|
+
elif struct_type == 'tuple':
|
|
235
|
+
return self.generate_tuple(structure_type, parent_namespace, write_file=True)
|
|
236
|
+
else:
|
|
237
|
+
return self.convert_structure_type_to_go(class_name, field_name, struct_type, parent_namespace, nullable)
|
|
238
|
+
return 'interface{}'
|
|
239
|
+
|
|
240
|
+
def generate_class_or_choice(self, structure_schema: Dict, parent_namespace: str,
|
|
241
|
+
write_file: bool = True, explicit_name: str = '') -> str:
|
|
242
|
+
""" Generates a Class or Choice """
|
|
243
|
+
struct_type = structure_schema.get('type', 'object')
|
|
244
|
+
if struct_type == 'object':
|
|
245
|
+
return self.generate_class(structure_schema, parent_namespace, write_file, explicit_name=explicit_name)
|
|
246
|
+
elif struct_type == 'choice':
|
|
247
|
+
return self.generate_choice(structure_schema, parent_namespace, write_file, explicit_name=explicit_name)
|
|
248
|
+
elif struct_type == 'tuple':
|
|
249
|
+
return self.generate_tuple(structure_schema, parent_namespace, write_file, explicit_name=explicit_name)
|
|
250
|
+
return 'interface{}'
|
|
251
|
+
|
|
252
|
+
def generate_class(self, structure_schema: Dict, parent_namespace: str,
|
|
253
|
+
write_file: bool, explicit_name: str = '') -> str:
|
|
254
|
+
""" Generates a Go struct from JSON Structure object type """
|
|
255
|
+
class_name = pascal(explicit_name if explicit_name else structure_schema.get('name', 'UnnamedClass'))
|
|
256
|
+
schema_namespace = structure_schema.get('namespace', parent_namespace)
|
|
257
|
+
go_struct_name = self.go_type_name(class_name, schema_namespace)
|
|
258
|
+
|
|
259
|
+
if go_struct_name in self.generated_types:
|
|
260
|
+
return go_struct_name
|
|
261
|
+
|
|
262
|
+
# Check if this is an abstract type
|
|
263
|
+
is_abstract = structure_schema.get('abstract', False)
|
|
264
|
+
|
|
265
|
+
# If abstract, generate interface instead
|
|
266
|
+
if is_abstract:
|
|
267
|
+
return self.generate_interface(structure_schema, parent_namespace, write_file, explicit_name)
|
|
268
|
+
|
|
269
|
+
self.generated_types[go_struct_name] = "struct"
|
|
270
|
+
self.generated_structure_types[go_struct_name] = structure_schema
|
|
271
|
+
|
|
272
|
+
# Handle inheritance ($extends)
|
|
273
|
+
base_interface = None
|
|
274
|
+
base_properties = {}
|
|
275
|
+
base_required = []
|
|
276
|
+
if '$extends' in structure_schema:
|
|
277
|
+
base_ref = structure_schema['$extends']
|
|
278
|
+
base_schema = self.resolve_ref(base_ref, self.schema_doc)
|
|
279
|
+
if base_schema:
|
|
280
|
+
ref_path = base_ref.split('/')
|
|
281
|
+
base_name = ref_path[-1]
|
|
282
|
+
ref_namespace = '.'.join(ref_path[2:-1]) if len(ref_path) > 3 else parent_namespace
|
|
283
|
+
base_interface = self.generate_class(base_schema, ref_namespace, write_file=True, explicit_name=base_name)
|
|
284
|
+
# Collect base properties to include in the concrete type
|
|
285
|
+
base_properties = base_schema.get('properties', {})
|
|
286
|
+
base_required = base_schema.get('required', [])
|
|
287
|
+
|
|
288
|
+
# Generate properties - merge base properties with current properties
|
|
289
|
+
properties = {**base_properties, **structure_schema.get('properties', {})}
|
|
290
|
+
required_props = base_required + structure_schema.get('required', [])
|
|
291
|
+
|
|
292
|
+
fields = []
|
|
293
|
+
for prop_name, prop_schema in properties.items():
|
|
294
|
+
is_required = prop_name in required_props if not isinstance(required_props, list) or \
|
|
295
|
+
len(required_props) == 0 or not isinstance(required_props[0], list) else \
|
|
296
|
+
any(prop_name in req_set for req_set in required_props)
|
|
297
|
+
|
|
298
|
+
field_type = self.convert_structure_type_to_go(
|
|
299
|
+
class_name, prop_name, prop_schema, schema_namespace, nullable=not is_required)
|
|
300
|
+
|
|
301
|
+
# Add nullable marker if not required and not already nullable
|
|
302
|
+
if not is_required and not field_type.startswith('*') and not field_type.startswith('[') and not field_type.startswith('map[') and field_type != 'interface{}':
|
|
303
|
+
field_type = f'*{field_type}'
|
|
304
|
+
|
|
305
|
+
fields.append({
|
|
306
|
+
'name': pascal(prop_name),
|
|
307
|
+
'type': field_type,
|
|
308
|
+
'original_name': prop_name
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
# Get imports needed
|
|
312
|
+
imports = self.get_imports_for_fields([f['type'] for f in fields])
|
|
313
|
+
|
|
314
|
+
# Generate Avro schema if avro_annotation is enabled
|
|
315
|
+
avro_schema_str = None
|
|
316
|
+
if self.avro_annotation:
|
|
317
|
+
try:
|
|
318
|
+
from avrotize.jstructtoavro import JsonStructureToAvro
|
|
319
|
+
converter = JsonStructureToAvro()
|
|
320
|
+
avro_schema = converter.convert(structure_schema)
|
|
321
|
+
avro_schema_str = json.dumps(avro_schema)
|
|
322
|
+
except Exception as e:
|
|
323
|
+
# If conversion fails, log but continue without Avro schema
|
|
324
|
+
print(f"Warning: Failed to generate Avro schema for {go_struct_name}: {e}")
|
|
325
|
+
|
|
326
|
+
context = {
|
|
327
|
+
'doc': structure_schema.get('description', structure_schema.get('doc', class_name)),
|
|
328
|
+
'struct_name': go_struct_name,
|
|
329
|
+
'fields': fields,
|
|
330
|
+
'imports': imports,
|
|
331
|
+
'json_annotation': self.json_annotation,
|
|
332
|
+
'avro_annotation': self.avro_annotation,
|
|
333
|
+
'avro_schema': avro_schema_str,
|
|
334
|
+
'base_package': self.base_package,
|
|
335
|
+
'base_interface': base_interface,
|
|
336
|
+
'referenced_packages': set(),
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
pkg_dir = os.path.join(self.output_dir, 'pkg', self.base_package)
|
|
340
|
+
if not os.path.exists(pkg_dir):
|
|
341
|
+
os.makedirs(pkg_dir, exist_ok=True)
|
|
342
|
+
file_name = os.path.join(pkg_dir, f"{go_struct_name}.go")
|
|
343
|
+
render_template('structuretogo/go_struct.jinja', file_name, **context)
|
|
344
|
+
|
|
345
|
+
self.structs.append({
|
|
346
|
+
'name': go_struct_name,
|
|
347
|
+
'fields': fields,
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
self.generate_unit_test('struct', go_struct_name, fields)
|
|
351
|
+
|
|
352
|
+
return go_struct_name
|
|
353
|
+
|
|
354
|
+
def generate_enum(self, structure_schema: Dict, field_name: str, parent_namespace: str,
|
|
355
|
+
write_file: bool) -> str:
|
|
356
|
+
""" Generates a Go enum from JSON Structure enum """
|
|
357
|
+
enum_name = pascal(structure_schema.get('name', field_name + 'Enum'))
|
|
358
|
+
schema_namespace = structure_schema.get('namespace', parent_namespace)
|
|
359
|
+
go_enum_name = self.go_type_name(enum_name, schema_namespace)
|
|
360
|
+
|
|
361
|
+
if go_enum_name in self.generated_types:
|
|
362
|
+
return go_enum_name
|
|
363
|
+
|
|
364
|
+
self.generated_types[go_enum_name] = "enum"
|
|
365
|
+
self.generated_structure_types[go_enum_name] = structure_schema
|
|
366
|
+
|
|
367
|
+
symbols = structure_schema.get('enum', [])
|
|
368
|
+
|
|
369
|
+
# Determine base type
|
|
370
|
+
base_type = structure_schema.get('type', 'string')
|
|
371
|
+
go_base_type = self.map_primitive_to_go(base_type, False)
|
|
372
|
+
|
|
373
|
+
context = {
|
|
374
|
+
'doc': structure_schema.get('description', structure_schema.get('doc', enum_name)),
|
|
375
|
+
'enum_name': go_enum_name,
|
|
376
|
+
'symbols': symbols,
|
|
377
|
+
'base_type': go_base_type,
|
|
378
|
+
'base_package': self.base_package,
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
pkg_dir = os.path.join(self.output_dir, 'pkg', self.base_package)
|
|
382
|
+
if not os.path.exists(pkg_dir):
|
|
383
|
+
os.makedirs(pkg_dir, exist_ok=True)
|
|
384
|
+
file_name = os.path.join(pkg_dir, f"{go_enum_name}.go")
|
|
385
|
+
render_template('structuretogo/go_enum.jinja', file_name, **context)
|
|
386
|
+
|
|
387
|
+
self.enums.append({
|
|
388
|
+
'name': go_enum_name,
|
|
389
|
+
'symbols': symbols,
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
self.generate_unit_test('enum', go_enum_name, symbols)
|
|
393
|
+
|
|
394
|
+
return go_enum_name
|
|
395
|
+
|
|
396
|
+
def generate_interface(self, structure_schema: Dict, parent_namespace: str,
|
|
397
|
+
write_file: bool, explicit_name: str = '') -> str:
|
|
398
|
+
""" Generates a Go interface from JSON Structure abstract type """
|
|
399
|
+
interface_name = pascal(explicit_name if explicit_name else structure_schema.get('name', 'UnnamedInterface'))
|
|
400
|
+
schema_namespace = structure_schema.get('namespace', parent_namespace)
|
|
401
|
+
go_interface_name = self.go_type_name(interface_name, schema_namespace)
|
|
402
|
+
|
|
403
|
+
if go_interface_name in self.generated_types:
|
|
404
|
+
return go_interface_name
|
|
405
|
+
|
|
406
|
+
self.generated_types[go_interface_name] = "interface"
|
|
407
|
+
self.generated_structure_types[go_interface_name] = structure_schema
|
|
408
|
+
|
|
409
|
+
# Get properties to define getter methods
|
|
410
|
+
properties = structure_schema.get('properties', {})
|
|
411
|
+
required_props = structure_schema.get('required', [])
|
|
412
|
+
|
|
413
|
+
methods = []
|
|
414
|
+
for prop_name, prop_schema in properties.items():
|
|
415
|
+
go_prop_name = pascal(prop_name)
|
|
416
|
+
is_required = prop_name in required_props if not isinstance(required_props, list) or \
|
|
417
|
+
len(required_props) == 0 or not isinstance(required_props[0], list) else \
|
|
418
|
+
any(prop_name in req_set for req_set in required_props)
|
|
419
|
+
|
|
420
|
+
field_type = self.convert_structure_type_to_go(
|
|
421
|
+
interface_name, prop_name, prop_schema, schema_namespace, nullable=not is_required)
|
|
422
|
+
|
|
423
|
+
# Add nullable marker if not required and not already nullable
|
|
424
|
+
if not is_required and not field_type.startswith('*') and not field_type.startswith('[') and not field_type.startswith('map[') and field_type != 'interface{}':
|
|
425
|
+
field_type = f'*{field_type}'
|
|
426
|
+
|
|
427
|
+
# Generate getter method
|
|
428
|
+
methods.append({
|
|
429
|
+
'name': f'Get{go_prop_name}',
|
|
430
|
+
'return_type': field_type,
|
|
431
|
+
'doc': f'Get{go_prop_name} returns the {prop_name} field'
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
# Generate setter method
|
|
435
|
+
methods.append({
|
|
436
|
+
'name': f'Set{go_prop_name}',
|
|
437
|
+
'return_type': '',
|
|
438
|
+
'param_type': field_type,
|
|
439
|
+
'param_name': prop_name,
|
|
440
|
+
'doc': f'Set{go_prop_name} sets the {prop_name} field'
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
context = {
|
|
444
|
+
'doc': structure_schema.get('description', structure_schema.get('doc', interface_name)),
|
|
445
|
+
'interface_name': go_interface_name,
|
|
446
|
+
'methods': methods,
|
|
447
|
+
'base_package': self.base_package,
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
pkg_dir = os.path.join(self.output_dir, 'pkg', self.base_package)
|
|
451
|
+
if not os.path.exists(pkg_dir):
|
|
452
|
+
os.makedirs(pkg_dir, exist_ok=True)
|
|
453
|
+
file_name = os.path.join(pkg_dir, f"{go_interface_name}.go")
|
|
454
|
+
render_template('structuretogo/go_interface.jinja', file_name, **context)
|
|
455
|
+
|
|
456
|
+
return go_interface_name
|
|
457
|
+
|
|
458
|
+
def generate_choice(self, structure_schema: Dict, parent_namespace: str,
|
|
459
|
+
write_file: bool, explicit_name: str = '') -> str:
|
|
460
|
+
""" Generates a choice/union type """
|
|
461
|
+
# For simplicity, generate as interface{} for now
|
|
462
|
+
# A complete implementation would generate proper union types
|
|
463
|
+
return 'interface{}'
|
|
464
|
+
|
|
465
|
+
def generate_tuple(self, structure_schema: Dict, parent_namespace: str,
|
|
466
|
+
write_file: bool, explicit_name: str = '') -> str:
|
|
467
|
+
""" Generates a tuple type """
|
|
468
|
+
# For simplicity, generate as interface{} for now
|
|
469
|
+
return 'interface{}'
|
|
470
|
+
|
|
471
|
+
def generate_union_class(self, field_name: str, structure_types: List, parent_namespace: str) -> str:
|
|
472
|
+
""" Generates a union type """
|
|
473
|
+
# For simplicity, generate as interface{} for now
|
|
474
|
+
return 'interface{}'
|
|
475
|
+
|
|
476
|
+
def get_imports_for_fields(self, types: List[str]) -> Set[str]:
|
|
477
|
+
"""Collects necessary imports based on the Go types"""
|
|
478
|
+
imports = set()
|
|
479
|
+
for field_type in types:
|
|
480
|
+
if "time.Time" in field_type or "time.Duration" in field_type:
|
|
481
|
+
imports.add("time")
|
|
482
|
+
if "big.Int" in field_type:
|
|
483
|
+
imports.add("math/big")
|
|
484
|
+
return imports
|
|
485
|
+
|
|
486
|
+
def random_value(self, go_type: str) -> str:
|
|
487
|
+
"""Generates a random value for a given Go type"""
|
|
488
|
+
import random
|
|
489
|
+
import string
|
|
490
|
+
|
|
491
|
+
is_optional = False
|
|
492
|
+
if go_type.startswith('*'):
|
|
493
|
+
is_optional = True
|
|
494
|
+
go_type = go_type[1:]
|
|
495
|
+
|
|
496
|
+
if go_type == 'string':
|
|
497
|
+
v = '"' + ''.join(random.choices(string.ascii_letters + string.digits, k=10)) + '"'
|
|
498
|
+
elif go_type == 'bool':
|
|
499
|
+
v = 'true' if random.choice([True, False]) else 'false'
|
|
500
|
+
elif go_type == 'int':
|
|
501
|
+
v = str(random.randint(-100, 100))
|
|
502
|
+
elif go_type == 'int8':
|
|
503
|
+
v = f'int8({random.randint(-100, 100)})'
|
|
504
|
+
elif go_type == 'int16':
|
|
505
|
+
v = f'int16({random.randint(-100, 100)})'
|
|
506
|
+
elif go_type == 'int32':
|
|
507
|
+
v = f'int32({random.randint(-100, 100)})'
|
|
508
|
+
elif go_type == 'int64':
|
|
509
|
+
v = f'int64({random.randint(-100, 100)})'
|
|
510
|
+
elif go_type == 'uint':
|
|
511
|
+
v = f'uint({random.randint(0, 200)})'
|
|
512
|
+
elif go_type == 'uint8':
|
|
513
|
+
v = f'uint8({random.randint(0, 200)})'
|
|
514
|
+
elif go_type == 'uint16':
|
|
515
|
+
v = f'uint16({random.randint(0, 200)})'
|
|
516
|
+
elif go_type == 'uint32':
|
|
517
|
+
v = f'uint32({random.randint(0, 200)})'
|
|
518
|
+
elif go_type == 'uint64':
|
|
519
|
+
v = f'uint64({random.randint(0, 200)})'
|
|
520
|
+
elif go_type == 'float32':
|
|
521
|
+
v = f'float32({random.uniform(-100, 100)})'
|
|
522
|
+
elif go_type == 'float64':
|
|
523
|
+
v = f'float64({random.uniform(-100, 100)})'
|
|
524
|
+
elif go_type == '[]byte':
|
|
525
|
+
v = '[]byte("' + ''.join(random.choices(string.ascii_letters + string.digits, k=10)) + '")'
|
|
526
|
+
elif go_type.startswith('[]'):
|
|
527
|
+
inner_type = go_type[2:]
|
|
528
|
+
v = f'{go_type}{{{self.random_value(inner_type)}}}'
|
|
529
|
+
elif go_type.startswith('map['):
|
|
530
|
+
# Extract value type from map[KeyType]ValueType
|
|
531
|
+
value_type = go_type.split(']', 1)[1]
|
|
532
|
+
v = f'{go_type}{{"key": {self.random_value(value_type)}}}'
|
|
533
|
+
elif go_type in self.generated_types:
|
|
534
|
+
v = f'CreateInstance{go_type}()'
|
|
535
|
+
elif go_type == 'interface{}':
|
|
536
|
+
v = 'nil'
|
|
537
|
+
else:
|
|
538
|
+
v = 'nil'
|
|
539
|
+
|
|
540
|
+
if is_optional and v != 'nil':
|
|
541
|
+
# Create a helper function to get pointer with proper type
|
|
542
|
+
return f'func() *{go_type} {{ v := {v}; return &v }}()'
|
|
543
|
+
return v
|
|
544
|
+
|
|
545
|
+
def generate_helpers(self) -> None:
|
|
546
|
+
"""Generates helper functions for initializing structs with random values"""
|
|
547
|
+
context = {
|
|
548
|
+
'structs': self.structs,
|
|
549
|
+
'enums': self.enums,
|
|
550
|
+
'base_package': self.base_package,
|
|
551
|
+
}
|
|
552
|
+
for struct in context['structs']:
|
|
553
|
+
for field in struct['fields']:
|
|
554
|
+
if 'value' not in field:
|
|
555
|
+
field['value'] = self.random_value(field['type'])
|
|
556
|
+
helpers_file_name = os.path.join(self.output_dir, 'pkg', self.base_package, f"{self.base_package}_helpers.go")
|
|
557
|
+
render_template('structuretogo/go_helpers.jinja', helpers_file_name, **context)
|
|
558
|
+
|
|
559
|
+
def generate_unit_test(self, kind: str, name: str, fields: Any):
|
|
560
|
+
"""Generates unit tests for Go struct or enum"""
|
|
561
|
+
context = {
|
|
562
|
+
'struct_name': name,
|
|
563
|
+
'fields': fields,
|
|
564
|
+
'kind': kind,
|
|
565
|
+
'base_package': self.base_package,
|
|
566
|
+
'package_site': self.package_site,
|
|
567
|
+
'package_username': self.package_username,
|
|
568
|
+
'json_annotation': self.json_annotation,
|
|
569
|
+
'avro_annotation': self.avro_annotation
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
pkg_dir = os.path.join(self.output_dir, 'pkg', self.base_package)
|
|
573
|
+
if not os.path.exists(pkg_dir):
|
|
574
|
+
os.makedirs(pkg_dir, exist_ok=True)
|
|
575
|
+
test_file_name = os.path.join(pkg_dir, f"{name}_test.go")
|
|
576
|
+
render_template('structuretogo/go_test.jinja', test_file_name, **context)
|
|
577
|
+
|
|
578
|
+
def write_go_mod_file(self):
|
|
579
|
+
"""Writes the go.mod file for the Go project"""
|
|
580
|
+
go_mod_content = ""
|
|
581
|
+
go_mod_content += "module " + self.package_site + "/" + self.package_username + "/" + self.base_package + "\n\n"
|
|
582
|
+
go_mod_content += "go 1.21\n\n"
|
|
583
|
+
if self.avro_annotation:
|
|
584
|
+
go_mod_content += "require (\n"
|
|
585
|
+
go_mod_content += " github.com/hamba/avro/v2 v2.27.0\n"
|
|
586
|
+
go_mod_content += ")\n"
|
|
587
|
+
|
|
588
|
+
go_mod_path = os.path.join(self.output_dir, "go.mod")
|
|
589
|
+
with open(go_mod_path, 'w', encoding='utf-8') as file:
|
|
590
|
+
file.write(go_mod_content)
|
|
591
|
+
|
|
592
|
+
def write_modname_go_file(self):
|
|
593
|
+
"""Writes the modname.go file for the Go project"""
|
|
594
|
+
modname_go_content = ""
|
|
595
|
+
modname_go_content += "package " + self.base_package + "\n\n"
|
|
596
|
+
modname_go_content += "const ModName = \"" + self.base_package + "\"\n"
|
|
597
|
+
|
|
598
|
+
modname_go_path = os.path.join(self.output_dir, 'pkg', self.base_package, "module.go")
|
|
599
|
+
with open(modname_go_path, 'w', encoding='utf-8') as file:
|
|
600
|
+
file.write(modname_go_content)
|
|
601
|
+
|
|
602
|
+
def process_definitions(self, definitions: Dict, namespace_path: str) -> None:
|
|
603
|
+
""" Processes the definitions section recursively """
|
|
604
|
+
for name, definition in definitions.items():
|
|
605
|
+
if isinstance(definition, dict):
|
|
606
|
+
if 'type' in definition or 'enum' in definition:
|
|
607
|
+
# This is a type definition
|
|
608
|
+
current_namespace = namespace_path
|
|
609
|
+
# Check if this type was already generated
|
|
610
|
+
check_name = self.go_type_name(name, current_namespace)
|
|
611
|
+
if check_name not in self.generated_types:
|
|
612
|
+
if 'enum' in definition:
|
|
613
|
+
self.generate_enum(definition, name, current_namespace, write_file=True)
|
|
614
|
+
else:
|
|
615
|
+
self.generate_class_or_choice(definition, current_namespace, write_file=True, explicit_name=name)
|
|
616
|
+
else:
|
|
617
|
+
# This is a nested namespace
|
|
618
|
+
new_namespace = f"{namespace_path}.{name}" if namespace_path else name
|
|
619
|
+
self.process_definitions(definition, new_namespace)
|
|
620
|
+
|
|
621
|
+
def convert_schema(self, schema: JsonNode, output_dir: str):
|
|
622
|
+
"""Converts JSON Structure schema to Go"""
|
|
623
|
+
if not isinstance(schema, list):
|
|
624
|
+
schema = [schema]
|
|
625
|
+
if not os.path.exists(output_dir):
|
|
626
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
627
|
+
self.output_dir = output_dir
|
|
628
|
+
|
|
629
|
+
self.structs = []
|
|
630
|
+
self.enums = []
|
|
631
|
+
|
|
632
|
+
# Register all schemas with $id keywords for cross-references
|
|
633
|
+
for structure_schema in (s for s in schema if isinstance(s, dict)):
|
|
634
|
+
self.register_schema_ids(structure_schema)
|
|
635
|
+
|
|
636
|
+
for structure_schema in (s for s in schema if isinstance(s, dict)):
|
|
637
|
+
self.schema_doc = structure_schema
|
|
638
|
+
|
|
639
|
+
# Store definitions for later use
|
|
640
|
+
if 'definitions' in structure_schema:
|
|
641
|
+
self.definitions = structure_schema['definitions']
|
|
642
|
+
|
|
643
|
+
# Process root type
|
|
644
|
+
if 'enum' in structure_schema:
|
|
645
|
+
self.generate_enum(structure_schema, structure_schema.get('name', 'Enum'),
|
|
646
|
+
structure_schema.get('namespace', ''), write_file=True)
|
|
647
|
+
elif 'type' in structure_schema:
|
|
648
|
+
self.generate_class_or_choice(structure_schema, '', write_file=True)
|
|
649
|
+
elif '$root' in structure_schema:
|
|
650
|
+
root_ref = structure_schema['$root']
|
|
651
|
+
root_schema = self.resolve_ref(root_ref, structure_schema)
|
|
652
|
+
if root_schema:
|
|
653
|
+
ref_path = root_ref.split('/')
|
|
654
|
+
type_name = ref_path[-1]
|
|
655
|
+
ref_namespace = '.'.join(ref_path[2:-1]) if len(ref_path) > 3 else ''
|
|
656
|
+
self.generate_class_or_choice(root_schema, ref_namespace, write_file=True, explicit_name=type_name)
|
|
657
|
+
|
|
658
|
+
# Process remaining definitions
|
|
659
|
+
if 'definitions' in structure_schema:
|
|
660
|
+
self.process_definitions(self.definitions, '')
|
|
661
|
+
|
|
662
|
+
self.write_go_mod_file()
|
|
663
|
+
self.write_modname_go_file()
|
|
664
|
+
self.generate_helpers()
|
|
665
|
+
|
|
666
|
+
def convert(self, structure_schema_path: str, output_dir: str):
|
|
667
|
+
"""Converts JSON Structure schema to Go"""
|
|
668
|
+
if not self.base_package:
|
|
669
|
+
self.base_package = os.path.splitext(os.path.basename(structure_schema_path))[0].replace('-', '_').lower()
|
|
670
|
+
|
|
671
|
+
with open(structure_schema_path, 'r', encoding='utf-8') as file:
|
|
672
|
+
schema = json.load(file)
|
|
673
|
+
self.convert_schema(schema, output_dir)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def convert_structure_to_go(structure_schema_path: str, go_file_path: str, package_name: str = '',
|
|
677
|
+
json_annotation: bool = False, avro_annotation: bool = False,
|
|
678
|
+
package_site: str = 'github.com', package_username: str = 'username'):
|
|
679
|
+
"""Converts JSON Structure schema to Go structs
|
|
680
|
+
|
|
681
|
+
Args:
|
|
682
|
+
structure_schema_path (str): JSON Structure input schema path
|
|
683
|
+
go_file_path (str): Output Go directory path
|
|
684
|
+
package_name (str): Base package name
|
|
685
|
+
json_annotation (bool): Include JSON annotations
|
|
686
|
+
avro_annotation (bool): Include Avro annotations
|
|
687
|
+
package_site (str): Package site for Go module
|
|
688
|
+
package_username (str): Package username for Go module
|
|
689
|
+
"""
|
|
690
|
+
if not package_name:
|
|
691
|
+
package_name = os.path.splitext(os.path.basename(structure_schema_path))[0].replace('-', '_').lower()
|
|
692
|
+
|
|
693
|
+
structuretogo = StructureToGo(package_name)
|
|
694
|
+
structuretogo.json_annotation = json_annotation
|
|
695
|
+
structuretogo.avro_annotation = avro_annotation
|
|
696
|
+
structuretogo.package_site = package_site
|
|
697
|
+
structuretogo.package_username = package_username
|
|
698
|
+
structuretogo.convert(structure_schema_path, go_file_path)
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def convert_structure_schema_to_go(structure_schema: JsonNode, output_dir: str, package_name: str = '',
|
|
702
|
+
json_annotation: bool = False, avro_annotation: bool = False,
|
|
703
|
+
package_site: str = 'github.com', package_username: str = 'username'):
|
|
704
|
+
"""Converts JSON Structure schema to Go structs
|
|
705
|
+
|
|
706
|
+
Args:
|
|
707
|
+
structure_schema (JsonNode): JSON Structure schema as a dictionary or list of dictionaries
|
|
708
|
+
output_dir (str): Output directory path
|
|
709
|
+
package_name (str): Base package name
|
|
710
|
+
json_annotation (bool): Include JSON annotations
|
|
711
|
+
avro_annotation (bool): Include Avro annotations
|
|
712
|
+
package_site (str): Package site for Go module
|
|
713
|
+
package_username (str): Package username for Go module
|
|
714
|
+
"""
|
|
715
|
+
structuretogo = StructureToGo(package_name)
|
|
716
|
+
structuretogo.json_annotation = json_annotation
|
|
717
|
+
structuretogo.avro_annotation = avro_annotation
|
|
718
|
+
structuretogo.package_site = package_site
|
|
719
|
+
structuretogo.package_username = package_username
|
|
720
|
+
structuretogo.convert_schema(structure_schema, output_dir)
|