structurize 2.16.6__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.16.6.dist-info → structurize-2.17.0.dist-info}/METADATA +1 -1
- {structurize-2.16.6.dist-info → structurize-2.17.0.dist-info}/RECORD +27 -14
- {structurize-2.16.6.dist-info → structurize-2.17.0.dist-info}/WHEEL +0 -0
- {structurize-2.16.6.dist-info → structurize-2.17.0.dist-info}/entry_points.txt +0 -0
- {structurize-2.16.6.dist-info → structurize-2.17.0.dist-info}/licenses/LICENSE +0 -0
- {structurize-2.16.6.dist-info → structurize-2.17.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
# pylint: disable=line-too-long
|
|
2
|
+
|
|
3
|
+
""" StructureToProto class for converting JSON Structure schema to Protocol Buffers (.proto files) """
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import argparse
|
|
8
|
+
from typing import Literal, NamedTuple, Dict, Any, List, Optional
|
|
9
|
+
|
|
10
|
+
indent = ' '
|
|
11
|
+
|
|
12
|
+
Comment = NamedTuple('Comment', [('content', str), ('tags', Dict[str, Any])])
|
|
13
|
+
Oneof = NamedTuple('Oneof', [('comment', 'Comment'), ('name', str), ('fields', List['Field'])])
|
|
14
|
+
Field = NamedTuple('Field', [('comment', 'Comment'), ('label', str), ('type', str), ('key_type', str), ('val_type', str), ('name', str), ('number', int), ('dependencies', List[str])])
|
|
15
|
+
Enum = NamedTuple('Enum', [('comment', 'Comment'), ('name', str), ('fields', Dict[str, 'Field'])])
|
|
16
|
+
Message = NamedTuple('Message', [('comment', 'Comment'), ('name', str), ('fields', List['Field']), ('oneofs', List['Oneof']),
|
|
17
|
+
('messages', Dict[str, 'Message']), ('enums', Dict[str, 'Enum']), ('dependencies', List[str])])
|
|
18
|
+
Service = NamedTuple('Service', [('name', str), ('functions', Dict[str, 'RpcFunc'])])
|
|
19
|
+
RpcFunc = NamedTuple('RpcFunc', [('name', str), ('in_type', str), ('out_type', str), ('uri', str)])
|
|
20
|
+
ProtoFile = NamedTuple('ProtoFile',
|
|
21
|
+
[('messages', Dict[str, 'Message']), ('enums', Dict[str, 'Enum']),
|
|
22
|
+
('services', Dict[str, 'Service']), ('imports', List[str]),
|
|
23
|
+
('options', Dict[str, str]), ('package', str)])
|
|
24
|
+
ProtoFiles = NamedTuple('ProtoFiles', [('files', List['ProtoFile'])])
|
|
25
|
+
|
|
26
|
+
class StructureToProto:
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
self.naming_mode: Literal['snake', 'pascal', 'camel'] = 'pascal'
|
|
30
|
+
self.allow_optional: bool = False
|
|
31
|
+
self.default_namespace: str = ''
|
|
32
|
+
self.schema_registry: Dict[str, Dict] = {}
|
|
33
|
+
self.definitions: Dict[str, Any] = {}
|
|
34
|
+
|
|
35
|
+
def structure_primitive_to_proto_type(self, structure_type: str, dependencies: List[str]) -> str:
|
|
36
|
+
"""Map JSON Structure primitive types to Protobuf types."""
|
|
37
|
+
mapping = {
|
|
38
|
+
'null': 'google.protobuf.Empty',
|
|
39
|
+
'boolean': 'bool',
|
|
40
|
+
'string': 'string',
|
|
41
|
+
'integer': 'int32',
|
|
42
|
+
'number': 'double',
|
|
43
|
+
'int8': 'int32', # Proto doesn't have int8, use int32
|
|
44
|
+
'uint8': 'uint32',
|
|
45
|
+
'int16': 'int32', # Proto doesn't have int16, use int32
|
|
46
|
+
'uint16': 'uint32',
|
|
47
|
+
'int32': 'int32',
|
|
48
|
+
'uint32': 'uint32',
|
|
49
|
+
'int64': 'int64',
|
|
50
|
+
'uint64': 'uint64',
|
|
51
|
+
'int128': 'string', # Proto doesn't have int128, use string
|
|
52
|
+
'uint128': 'string',
|
|
53
|
+
'float8': 'float',
|
|
54
|
+
'float': 'float',
|
|
55
|
+
'float32': 'float',
|
|
56
|
+
'float64': 'double',
|
|
57
|
+
'double': 'double',
|
|
58
|
+
'binary32': 'float',
|
|
59
|
+
'binary64': 'double',
|
|
60
|
+
'decimal': 'string', # Proto doesn't have native decimal, use string
|
|
61
|
+
'binary': 'bytes',
|
|
62
|
+
'bytes': 'bytes',
|
|
63
|
+
'date': 'string', # Or use google.type.Date
|
|
64
|
+
'time': 'string', # Or use google.type.TimeOfDay
|
|
65
|
+
'datetime': 'string', # Or use google.protobuf.Timestamp
|
|
66
|
+
'timestamp': 'string',
|
|
67
|
+
'duration': 'string', # Or use google.protobuf.Duration
|
|
68
|
+
'uuid': 'string',
|
|
69
|
+
'uri': 'string',
|
|
70
|
+
'jsonpointer': 'string',
|
|
71
|
+
'any': 'google.protobuf.Any'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type_result = mapping.get(structure_type, '')
|
|
75
|
+
if not type_result:
|
|
76
|
+
dependencies.append(structure_type)
|
|
77
|
+
type_result = structure_type
|
|
78
|
+
return type_result
|
|
79
|
+
|
|
80
|
+
def compose_name(self, prefix: str, name: str, naming_mode: Literal['pascal', 'camel', 'snake', 'default', 'field'] = 'default') -> str:
|
|
81
|
+
if naming_mode == 'default':
|
|
82
|
+
naming_mode = self.naming_mode
|
|
83
|
+
if naming_mode == 'field':
|
|
84
|
+
if self.naming_mode == 'pascal':
|
|
85
|
+
naming_mode = 'camel'
|
|
86
|
+
else:
|
|
87
|
+
naming_mode = self.naming_mode
|
|
88
|
+
if naming_mode == 'snake':
|
|
89
|
+
return f"{prefix}_{name}"
|
|
90
|
+
if naming_mode == 'pascal':
|
|
91
|
+
return f"{prefix[0].upper()+prefix[1:] if prefix else ''}{name[0].upper()+name[1:] if name else ''}"
|
|
92
|
+
if naming_mode == 'camel':
|
|
93
|
+
return f"{prefix[0].lower()+prefix[1:] if prefix else ''}{name[0].upper()+name[1:] if name else ''}"
|
|
94
|
+
return prefix+name
|
|
95
|
+
|
|
96
|
+
def resolve_ref(self, ref: str, context_schema: Optional[Dict] = None) -> Optional[Dict]:
|
|
97
|
+
""" Resolves a $ref to the actual schema definition """
|
|
98
|
+
# Check if it's an absolute URI reference (schema with $id)
|
|
99
|
+
if not ref.startswith('#/'):
|
|
100
|
+
# Try to resolve from schema registry
|
|
101
|
+
if ref in self.schema_registry:
|
|
102
|
+
return self.schema_registry[ref]
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
# Handle fragment-only references (internal to document)
|
|
106
|
+
path = ref[2:].split('/')
|
|
107
|
+
schema = context_schema if context_schema else None
|
|
108
|
+
|
|
109
|
+
if schema is None:
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
for part in path:
|
|
113
|
+
if not isinstance(schema, dict) or part not in schema:
|
|
114
|
+
return None
|
|
115
|
+
schema = schema[part]
|
|
116
|
+
|
|
117
|
+
return schema
|
|
118
|
+
|
|
119
|
+
def register_schema_ids(self, schema: Dict, base_uri: str = '') -> None:
|
|
120
|
+
""" Recursively registers schemas with $id keywords """
|
|
121
|
+
if not isinstance(schema, dict):
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
# Register this schema if it has an $id
|
|
125
|
+
if '$id' in schema:
|
|
126
|
+
schema_id = schema['$id']
|
|
127
|
+
# Handle relative URIs
|
|
128
|
+
if base_uri and not schema_id.startswith(('http://', 'https://', 'urn:')):
|
|
129
|
+
from urllib.parse import urljoin
|
|
130
|
+
schema_id = urljoin(base_uri, schema_id)
|
|
131
|
+
self.schema_registry[schema_id] = schema
|
|
132
|
+
base_uri = schema_id # Update base URI for nested schemas
|
|
133
|
+
|
|
134
|
+
# Recursively process definitions
|
|
135
|
+
if 'definitions' in schema:
|
|
136
|
+
for def_name, def_schema in schema['definitions'].items():
|
|
137
|
+
if isinstance(def_schema, dict):
|
|
138
|
+
self.register_schema_ids(def_schema, base_uri)
|
|
139
|
+
|
|
140
|
+
# Recursively process properties
|
|
141
|
+
if 'properties' in schema:
|
|
142
|
+
for prop_name, prop_schema in schema['properties'].items():
|
|
143
|
+
if isinstance(prop_schema, dict):
|
|
144
|
+
self.register_schema_ids(prop_schema, base_uri)
|
|
145
|
+
|
|
146
|
+
# Recursively process items, values, etc.
|
|
147
|
+
for key in ['items', 'values', 'additionalProperties']:
|
|
148
|
+
if key in schema and isinstance(schema[key], dict):
|
|
149
|
+
self.register_schema_ids(schema[key], base_uri)
|
|
150
|
+
|
|
151
|
+
def build_enhanced_comment(self, field_schema: Dict, base_comment: str = '') -> Comment:
|
|
152
|
+
"""Build an enhanced comment with type annotations and constraints."""
|
|
153
|
+
annotations = []
|
|
154
|
+
tags = {}
|
|
155
|
+
|
|
156
|
+
# Add base description
|
|
157
|
+
if base_comment:
|
|
158
|
+
annotations.append(base_comment)
|
|
159
|
+
|
|
160
|
+
# Add const value
|
|
161
|
+
if 'const' in field_schema:
|
|
162
|
+
const_val = field_schema['const']
|
|
163
|
+
annotations.append(f"Constant value: {const_val}")
|
|
164
|
+
tags['const'] = const_val
|
|
165
|
+
|
|
166
|
+
# Add access modifiers
|
|
167
|
+
if field_schema.get('readOnly'):
|
|
168
|
+
annotations.append("Read-only")
|
|
169
|
+
tags['readOnly'] = True
|
|
170
|
+
if field_schema.get('writeOnly'):
|
|
171
|
+
annotations.append("Write-only")
|
|
172
|
+
tags['writeOnly'] = True
|
|
173
|
+
|
|
174
|
+
# Add string constraints
|
|
175
|
+
if 'maxLength' in field_schema:
|
|
176
|
+
annotations.append(f"Max length: {field_schema['maxLength']}")
|
|
177
|
+
tags['maxLength'] = field_schema['maxLength']
|
|
178
|
+
if 'minLength' in field_schema:
|
|
179
|
+
annotations.append(f"Min length: {field_schema['minLength']}")
|
|
180
|
+
tags['minLength'] = field_schema['minLength']
|
|
181
|
+
if 'pattern' in field_schema:
|
|
182
|
+
annotations.append(f"Pattern: {field_schema['pattern']}")
|
|
183
|
+
tags['pattern'] = field_schema['pattern']
|
|
184
|
+
|
|
185
|
+
# Add numeric constraints
|
|
186
|
+
if 'minimum' in field_schema:
|
|
187
|
+
annotations.append(f"Minimum: {field_schema['minimum']}")
|
|
188
|
+
tags['minimum'] = field_schema['minimum']
|
|
189
|
+
if 'maximum' in field_schema:
|
|
190
|
+
annotations.append(f"Maximum: {field_schema['maximum']}")
|
|
191
|
+
tags['maximum'] = field_schema['maximum']
|
|
192
|
+
if 'exclusiveMinimum' in field_schema:
|
|
193
|
+
annotations.append(f"Exclusive minimum: {field_schema['exclusiveMinimum']}")
|
|
194
|
+
tags['exclusiveMinimum'] = field_schema['exclusiveMinimum']
|
|
195
|
+
if 'exclusiveMaximum' in field_schema:
|
|
196
|
+
annotations.append(f"Exclusive maximum: {field_schema['exclusiveMaximum']}")
|
|
197
|
+
tags['exclusiveMaximum'] = field_schema['exclusiveMaximum']
|
|
198
|
+
|
|
199
|
+
# Add decimal precision/scale
|
|
200
|
+
if 'precision' in field_schema:
|
|
201
|
+
annotations.append(f"Precision: {field_schema['precision']}")
|
|
202
|
+
tags['precision'] = field_schema['precision']
|
|
203
|
+
if 'scale' in field_schema:
|
|
204
|
+
annotations.append(f"Scale: {field_schema['scale']}")
|
|
205
|
+
tags['scale'] = field_schema['scale']
|
|
206
|
+
|
|
207
|
+
# Add format and encoding
|
|
208
|
+
if 'format' in field_schema:
|
|
209
|
+
annotations.append(f"Format: {field_schema['format']}")
|
|
210
|
+
tags['format'] = field_schema['format']
|
|
211
|
+
if 'contentEncoding' in field_schema:
|
|
212
|
+
annotations.append(f"Encoding: {field_schema['contentEncoding']}")
|
|
213
|
+
tags['contentEncoding'] = field_schema['contentEncoding']
|
|
214
|
+
if 'contentMediaType' in field_schema:
|
|
215
|
+
annotations.append(f"Media type: {field_schema['contentMediaType']}")
|
|
216
|
+
tags['contentMediaType'] = field_schema['contentMediaType']
|
|
217
|
+
|
|
218
|
+
# Add array constraints
|
|
219
|
+
if 'minItems' in field_schema:
|
|
220
|
+
annotations.append(f"Min items: {field_schema['minItems']}")
|
|
221
|
+
tags['minItems'] = field_schema['minItems']
|
|
222
|
+
if 'maxItems' in field_schema:
|
|
223
|
+
annotations.append(f"Max items: {field_schema['maxItems']}")
|
|
224
|
+
tags['maxItems'] = field_schema['maxItems']
|
|
225
|
+
if 'uniqueItems' in field_schema:
|
|
226
|
+
annotations.append("Unique items required")
|
|
227
|
+
tags['uniqueItems'] = field_schema['uniqueItems']
|
|
228
|
+
|
|
229
|
+
# Add deprecated marker
|
|
230
|
+
if field_schema.get('deprecated'):
|
|
231
|
+
annotations.append("DEPRECATED")
|
|
232
|
+
tags['deprecated'] = True
|
|
233
|
+
if 'deprecationMessage' in field_schema:
|
|
234
|
+
annotations.append(f"Deprecation: {field_schema['deprecationMessage']}")
|
|
235
|
+
|
|
236
|
+
# Add abstract marker
|
|
237
|
+
if field_schema.get('abstract'):
|
|
238
|
+
annotations.append("Abstract type - cannot be instantiated directly")
|
|
239
|
+
tags['abstract'] = True
|
|
240
|
+
|
|
241
|
+
comment_text = ' | '.join(annotations) if annotations else ''
|
|
242
|
+
return Comment(comment_text, tags)
|
|
243
|
+
|
|
244
|
+
def resolve_extends(self, structure_schema: Dict, context_schema: Dict) -> Dict:
|
|
245
|
+
"""Resolve $extends inheritance by flattening parent properties."""
|
|
246
|
+
if '$extends' not in structure_schema:
|
|
247
|
+
return structure_schema
|
|
248
|
+
|
|
249
|
+
# Resolve the parent schema
|
|
250
|
+
parent_ref = structure_schema['$extends']
|
|
251
|
+
parent_schema = self.resolve_ref(parent_ref, context_schema)
|
|
252
|
+
|
|
253
|
+
if not parent_schema:
|
|
254
|
+
return structure_schema
|
|
255
|
+
|
|
256
|
+
# Recursively resolve parent's extends
|
|
257
|
+
parent_schema = self.resolve_extends(parent_schema, context_schema)
|
|
258
|
+
|
|
259
|
+
# Create a new schema with flattened properties
|
|
260
|
+
flattened = structure_schema.copy()
|
|
261
|
+
|
|
262
|
+
# Merge parent properties first (so child can override)
|
|
263
|
+
parent_props = parent_schema.get('properties', {})
|
|
264
|
+
current_props = flattened.get('properties', {})
|
|
265
|
+
merged_props = parent_props.copy()
|
|
266
|
+
merged_props.update(current_props)
|
|
267
|
+
flattened['properties'] = merged_props
|
|
268
|
+
|
|
269
|
+
# Merge required fields
|
|
270
|
+
parent_required = parent_schema.get('required', [])
|
|
271
|
+
current_required = flattened.get('required', [])
|
|
272
|
+
merged_required = list(set(parent_required + current_required))
|
|
273
|
+
if merged_required:
|
|
274
|
+
flattened['required'] = merged_required
|
|
275
|
+
|
|
276
|
+
# Remove $extends from flattened schema
|
|
277
|
+
if '$extends' in flattened:
|
|
278
|
+
del flattened['$extends']
|
|
279
|
+
|
|
280
|
+
return flattened
|
|
281
|
+
|
|
282
|
+
def resolve_offers_uses(self, structure_schema: Dict, context_schema: Dict) -> Dict:
|
|
283
|
+
"""Resolve $offers/$uses add-ins by flattening mixin properties."""
|
|
284
|
+
if '$uses' not in structure_schema:
|
|
285
|
+
return structure_schema
|
|
286
|
+
|
|
287
|
+
uses = structure_schema['$uses']
|
|
288
|
+
if not isinstance(uses, list):
|
|
289
|
+
uses = [uses]
|
|
290
|
+
|
|
291
|
+
# Create a new schema with flattened properties
|
|
292
|
+
flattened = structure_schema.copy()
|
|
293
|
+
current_props = flattened.get('properties', {})
|
|
294
|
+
|
|
295
|
+
# Merge properties from each add-in
|
|
296
|
+
for use_ref in uses:
|
|
297
|
+
use_schema = self.resolve_ref(use_ref, context_schema)
|
|
298
|
+
if use_schema and '$offers' in use_schema:
|
|
299
|
+
offer_props = use_schema['$offers'].get('properties', {})
|
|
300
|
+
# Add offered properties (current props take precedence)
|
|
301
|
+
for prop_name, prop_schema in offer_props.items():
|
|
302
|
+
if prop_name not in current_props:
|
|
303
|
+
current_props[prop_name] = prop_schema
|
|
304
|
+
|
|
305
|
+
flattened['properties'] = current_props
|
|
306
|
+
|
|
307
|
+
# Remove $uses from flattened schema
|
|
308
|
+
if '$uses' in flattened:
|
|
309
|
+
del flattened['$uses']
|
|
310
|
+
|
|
311
|
+
return flattened
|
|
312
|
+
|
|
313
|
+
def convert_field(self, message: Message, structure_field: dict, index: int, proto_files: ProtoFiles, context_schema: Dict) -> Field | Oneof | Enum | Message:
|
|
314
|
+
"""Convert a JSON Structure property to a Protobuf field."""
|
|
315
|
+
field_name = structure_field.get('name', f'field{index}')
|
|
316
|
+
|
|
317
|
+
# Build enhanced comment with annotations
|
|
318
|
+
base_desc = structure_field.get('description', structure_field.get('doc', ''))
|
|
319
|
+
comment = self.build_enhanced_comment(structure_field, base_desc)
|
|
320
|
+
|
|
321
|
+
return self.convert_field_type(message, field_name, structure_field, comment, index, proto_files, context_schema)
|
|
322
|
+
|
|
323
|
+
def convert_record_type(self, structure_record: dict, comment: Comment, proto_files: ProtoFiles, context_schema: Dict) -> Message:
|
|
324
|
+
"""Convert a JSON Structure object to a Protobuf message."""
|
|
325
|
+
# Resolve $extends inheritance
|
|
326
|
+
structure_record = self.resolve_extends(structure_record, context_schema)
|
|
327
|
+
|
|
328
|
+
# Resolve $offers/$uses add-ins
|
|
329
|
+
structure_record = self.resolve_offers_uses(structure_record, context_schema)
|
|
330
|
+
|
|
331
|
+
# Build enhanced comment with abstract/deprecated markers
|
|
332
|
+
message_comment = self.build_enhanced_comment(structure_record, comment.content)
|
|
333
|
+
|
|
334
|
+
local_message = Message(message_comment, structure_record.get('name', 'UnnamedMessage'), [], [], {}, {}, [])
|
|
335
|
+
properties = structure_record.get('properties', {})
|
|
336
|
+
required_props = structure_record.get('required', [])
|
|
337
|
+
|
|
338
|
+
offs = 1
|
|
339
|
+
for i, (prop_name, prop_schema) in enumerate(properties.items()):
|
|
340
|
+
field_dict = {'name': prop_name}
|
|
341
|
+
if isinstance(prop_schema, dict):
|
|
342
|
+
field_dict.update(prop_schema)
|
|
343
|
+
else:
|
|
344
|
+
field_dict['type'] = prop_schema
|
|
345
|
+
|
|
346
|
+
field = self.convert_field(local_message, field_dict, i+offs, proto_files, context_schema)
|
|
347
|
+
if isinstance(field, Oneof):
|
|
348
|
+
for f in field.fields:
|
|
349
|
+
local_message.dependencies.extend(f.dependencies)
|
|
350
|
+
local_message.oneofs.append(field)
|
|
351
|
+
offs += len(field.fields)-1
|
|
352
|
+
elif isinstance(field, Enum):
|
|
353
|
+
enum = Enum(field.comment, self.compose_name(field.name, 'enum'), field.fields)
|
|
354
|
+
local_message.enums[enum.name] = enum
|
|
355
|
+
local_message.fields.append(Field(field.comment, '', enum.name, '', '', prop_name, i+offs, []))
|
|
356
|
+
elif isinstance(field, Message):
|
|
357
|
+
inner_message = Message(field.comment, self.compose_name(prop_name, 'type'), field.fields, field.oneofs, field.messages, field.enums, [])
|
|
358
|
+
local_message.messages[inner_message.name] = inner_message
|
|
359
|
+
local_message.fields.append(Field(field.comment, '', inner_message.name, '', '', prop_name, i+offs, []))
|
|
360
|
+
local_message.dependencies.extend(field.dependencies)
|
|
361
|
+
else:
|
|
362
|
+
local_message.dependencies.extend(field.dependencies)
|
|
363
|
+
local_message.fields.append(field)
|
|
364
|
+
return local_message
|
|
365
|
+
|
|
366
|
+
def convert_field_type(self, message: Message, field_name: str, field_type_schema: Dict | str | list, comment: Comment, index: int, proto_files: ProtoFiles, context_schema: Dict) -> Field | Oneof | Enum | Message:
|
|
367
|
+
"""Convert a JSON Structure field type to a Protobuf field type."""
|
|
368
|
+
label = ''
|
|
369
|
+
|
|
370
|
+
# Handle list of types (union)
|
|
371
|
+
if isinstance(field_type_schema, list):
|
|
372
|
+
# Handling union types (including nullable fields)
|
|
373
|
+
non_null_types = [t for t in field_type_schema if t != 'null']
|
|
374
|
+
if len(non_null_types) == 1:
|
|
375
|
+
if self.allow_optional:
|
|
376
|
+
label = 'optional'
|
|
377
|
+
field_type_schema = non_null_types[0]
|
|
378
|
+
elif len(non_null_types) > 0:
|
|
379
|
+
oneof_fields = []
|
|
380
|
+
for i, t in enumerate(non_null_types):
|
|
381
|
+
field = self.convert_field_type(message, self.compose_name(field_name, 'choice', 'field'), t, comment, i+index, proto_files, context_schema)
|
|
382
|
+
if isinstance(field, Field):
|
|
383
|
+
if field.type == 'map' or field.type == 'array':
|
|
384
|
+
local_message = Message(comment, self.compose_name(field.name, field.type), [], [], {}, {}, field.dependencies)
|
|
385
|
+
local_message.fields.append(field)
|
|
386
|
+
new_field = Field(field.comment, '', local_message.name, '', '', self.compose_name(field.name, field.type, 'field'), i+index, field.dependencies)
|
|
387
|
+
message.messages[local_message.name] = local_message
|
|
388
|
+
oneof_fields.append(new_field)
|
|
389
|
+
else:
|
|
390
|
+
field = Field(field.comment, field.label, field.type, field.key_type, field.val_type, self.compose_name(field_name, (field.type.split('.')[-1]), 'field'), i+index, field.dependencies)
|
|
391
|
+
oneof_fields.append(field)
|
|
392
|
+
elif isinstance(field, Enum):
|
|
393
|
+
enum = Enum(field.comment, self.compose_name(field.name, "options"), field.fields)
|
|
394
|
+
message.enums[enum.name] = enum
|
|
395
|
+
field = Field(field.comment, '', enum.name, '', '', field.name, i+index, [])
|
|
396
|
+
oneof_fields.append(field)
|
|
397
|
+
elif isinstance(field, Message):
|
|
398
|
+
local_message = Message(field.comment, self.compose_name(field.name, 'type'), field.fields, field.oneofs, field.messages, field.enums, field.dependencies)
|
|
399
|
+
message.messages[local_message.name] = local_message
|
|
400
|
+
field = Field(field.comment, '', local_message.name, '', '', field.name, i+index, field.dependencies)
|
|
401
|
+
oneof_fields.append(field)
|
|
402
|
+
oneof = Oneof(comment, field_name, oneof_fields)
|
|
403
|
+
return oneof
|
|
404
|
+
else:
|
|
405
|
+
raise ValueError(f"Field {field_name} is a union type without any non-null types")
|
|
406
|
+
|
|
407
|
+
# Handle dict types (complex structures)
|
|
408
|
+
if isinstance(field_type_schema, dict):
|
|
409
|
+
# Handle $ref
|
|
410
|
+
if '$ref' in field_type_schema:
|
|
411
|
+
ref_schema = self.resolve_ref(field_type_schema['$ref'], context_schema)
|
|
412
|
+
if ref_schema:
|
|
413
|
+
return self.convert_field_type(message, field_name, ref_schema, comment, index, proto_files, context_schema)
|
|
414
|
+
else:
|
|
415
|
+
# Reference not found, use string as fallback
|
|
416
|
+
deps: List[str] = []
|
|
417
|
+
return Field(comment, label, 'string', '', '', field_name, index, deps)
|
|
418
|
+
|
|
419
|
+
# Handle enum keyword
|
|
420
|
+
if 'enum' in field_type_schema:
|
|
421
|
+
enum_values = field_type_schema['enum']
|
|
422
|
+
enum_name = self.compose_name(field_name, 'Enum')
|
|
423
|
+
enum_fields = {str(val): Field(comment, '', str(val), '', '', str(val), i, []) for i, val in enumerate(enum_values)}
|
|
424
|
+
return Enum(comment, enum_name, enum_fields)
|
|
425
|
+
|
|
426
|
+
# Get the type from the schema
|
|
427
|
+
if 'type' not in field_type_schema:
|
|
428
|
+
# No type specified, use Any
|
|
429
|
+
deps1: List[str] = []
|
|
430
|
+
return Field(comment, label, 'google.protobuf.Any', '', '', field_name, index, deps1)
|
|
431
|
+
|
|
432
|
+
struct_type = field_type_schema['type']
|
|
433
|
+
|
|
434
|
+
# Handle type as a list (union type declared inline in "type" field)
|
|
435
|
+
if isinstance(struct_type, list):
|
|
436
|
+
# This is a union type specified as "type": ["string", "null"]
|
|
437
|
+
# Recursively call convert_field_type with the list
|
|
438
|
+
return self.convert_field_type(message, field_name, struct_type, comment, index, proto_files, context_schema)
|
|
439
|
+
|
|
440
|
+
# Handle object type
|
|
441
|
+
if struct_type == 'object':
|
|
442
|
+
return self.convert_record_type(field_type_schema, comment, proto_files, context_schema)
|
|
443
|
+
|
|
444
|
+
# Handle array type
|
|
445
|
+
elif struct_type == 'array':
|
|
446
|
+
items_schema = field_type_schema.get('items', {'type': 'any'})
|
|
447
|
+
converted_field_type = self.convert_field_type(message, self.compose_name(field_name, "item"), items_schema, comment, 1, proto_files, context_schema)
|
|
448
|
+
if isinstance(converted_field_type, Field):
|
|
449
|
+
# If the item is an array or map, we need to wrap it in a message
|
|
450
|
+
if converted_field_type.type in ('array', 'map'):
|
|
451
|
+
item_message = Message(comment, self.compose_name(field_name, 'ItemType'), [converted_field_type], [], {}, {}, converted_field_type.dependencies)
|
|
452
|
+
message.messages[item_message.name] = item_message
|
|
453
|
+
return Field(comment, 'repeated', 'array', '', item_message.name, field_name, index, [])
|
|
454
|
+
else:
|
|
455
|
+
return Field(comment, 'repeated', 'array', '', converted_field_type.type, field_name, index, converted_field_type.dependencies)
|
|
456
|
+
elif isinstance(converted_field_type, Enum):
|
|
457
|
+
enum = Enum(converted_field_type.comment, self.compose_name(converted_field_type.name, 'enum'), converted_field_type.fields)
|
|
458
|
+
message.enums[enum.name] = enum
|
|
459
|
+
return Field(comment, 'repeated', 'array', '', enum.name, field_name, index, [])
|
|
460
|
+
elif isinstance(converted_field_type, Message):
|
|
461
|
+
local_message = Message(converted_field_type.comment, self.compose_name(converted_field_type.name, 'type'), converted_field_type.fields, converted_field_type.oneofs, converted_field_type.messages, converted_field_type.enums, converted_field_type.dependencies)
|
|
462
|
+
message.messages[local_message.name] = local_message
|
|
463
|
+
return Field(comment, 'repeated', 'array', '', local_message.name, field_name, index, [])
|
|
464
|
+
|
|
465
|
+
# Handle set type (same as array in proto)
|
|
466
|
+
elif struct_type == 'set':
|
|
467
|
+
items_schema = field_type_schema.get('items', {'type': 'any'})
|
|
468
|
+
converted_field_type = self.convert_field_type(message, self.compose_name(field_name, "item"), items_schema, comment, index, proto_files, context_schema)
|
|
469
|
+
if isinstance(converted_field_type, Field):
|
|
470
|
+
return Field(comment, 'repeated', 'array', '', converted_field_type.type, field_name, index, converted_field_type.dependencies)
|
|
471
|
+
elif isinstance(converted_field_type, Enum):
|
|
472
|
+
enum = Enum(converted_field_type.comment, self.compose_name(converted_field_type.name, 'enum'), converted_field_type.fields)
|
|
473
|
+
message.enums[enum.name] = enum
|
|
474
|
+
return Field(comment, 'repeated', 'array', '', enum.name, field_name, index, [])
|
|
475
|
+
elif isinstance(converted_field_type, Message):
|
|
476
|
+
local_message = Message(converted_field_type.comment, self.compose_name(converted_field_type.name, 'type'), converted_field_type.fields, converted_field_type.oneofs, converted_field_type.messages, converted_field_type.enums, converted_field_type.dependencies)
|
|
477
|
+
message.messages[local_message.name] = local_message
|
|
478
|
+
return Field(comment, 'repeated', 'array', '', local_message.name, field_name, index, [])
|
|
479
|
+
|
|
480
|
+
# Handle map type
|
|
481
|
+
elif struct_type == 'map':
|
|
482
|
+
values_schema = field_type_schema.get('values', {'type': 'any'})
|
|
483
|
+
converted_field_type = self.convert_field_type(message, self.compose_name(field_name, 'item', 'field'), values_schema, comment, index, proto_files, context_schema)
|
|
484
|
+
if isinstance(converted_field_type, Field):
|
|
485
|
+
return Field(comment, label, 'map', 'string', converted_field_type.type, field_name, index, converted_field_type.dependencies)
|
|
486
|
+
elif isinstance(converted_field_type, Enum):
|
|
487
|
+
enum = Enum(converted_field_type.comment, self.compose_name(converted_field_type.name, 'enum'), converted_field_type.fields)
|
|
488
|
+
message.enums[enum.name] = enum
|
|
489
|
+
return Field(comment, label, 'map', 'string', enum.name, field_name, index, [])
|
|
490
|
+
elif isinstance(converted_field_type, Message):
|
|
491
|
+
local_message = Message(converted_field_type.comment, self.compose_name(converted_field_type.name, 'type'), converted_field_type.fields, converted_field_type.oneofs, converted_field_type.messages, converted_field_type.enums, [])
|
|
492
|
+
message.messages[local_message.name] = local_message
|
|
493
|
+
return Field(comment, label, 'map', 'string', local_message.name, field_name, index, local_message.dependencies)
|
|
494
|
+
|
|
495
|
+
# Handle choice type (discriminated union)
|
|
496
|
+
elif struct_type == 'choice':
|
|
497
|
+
# Choice type becomes a oneof in protobuf
|
|
498
|
+
choices = field_type_schema.get('choices', {})
|
|
499
|
+
oneof_fields = []
|
|
500
|
+
for i, (choice_name, choice_schema) in enumerate(choices.items()):
|
|
501
|
+
choice_field = self.convert_field_type(message, choice_name, choice_schema, comment, i+index, proto_files, context_schema)
|
|
502
|
+
if isinstance(choice_field, Field):
|
|
503
|
+
oneof_fields.append(Field(choice_field.comment, '', choice_field.type, '', '', choice_name, i+index, choice_field.dependencies))
|
|
504
|
+
elif isinstance(choice_field, Message):
|
|
505
|
+
local_message = Message(choice_field.comment, self.compose_name(choice_name, 'type'), choice_field.fields, choice_field.oneofs, choice_field.messages, choice_field.enums, choice_field.dependencies)
|
|
506
|
+
message.messages[local_message.name] = local_message
|
|
507
|
+
oneof_fields.append(Field(choice_field.comment, '', local_message.name, '', '', choice_name, i+index, []))
|
|
508
|
+
return Oneof(comment, field_name, oneof_fields)
|
|
509
|
+
|
|
510
|
+
# Handle tuple type
|
|
511
|
+
elif struct_type == 'tuple':
|
|
512
|
+
# Tuple becomes a message with numbered fields
|
|
513
|
+
tuple_order = field_type_schema.get('tuple', [])
|
|
514
|
+
properties = field_type_schema.get('properties', {})
|
|
515
|
+
tuple_message = Message(comment, self.compose_name(field_name, 'Tuple'), [], [], {}, {}, [])
|
|
516
|
+
for i, prop_name in enumerate(tuple_order):
|
|
517
|
+
if prop_name in properties:
|
|
518
|
+
prop_schema = properties[prop_name]
|
|
519
|
+
tuple_field = self.convert_field_type(tuple_message, prop_name, prop_schema, Comment('', {}), i+1, proto_files, context_schema)
|
|
520
|
+
if isinstance(tuple_field, Field):
|
|
521
|
+
tuple_message.fields.append(tuple_field)
|
|
522
|
+
return tuple_message
|
|
523
|
+
|
|
524
|
+
# Handle primitive types with format specifications
|
|
525
|
+
else:
|
|
526
|
+
deps2: List[str] = []
|
|
527
|
+
proto_type = self.structure_primitive_to_proto_type(struct_type, deps2)
|
|
528
|
+
return Field(comment, label, proto_type, '', '', field_name, index, deps2)
|
|
529
|
+
|
|
530
|
+
# Handle string types (primitive type names)
|
|
531
|
+
elif isinstance(field_type_schema, str):
|
|
532
|
+
deps3: List[str] = []
|
|
533
|
+
proto_type = self.structure_primitive_to_proto_type(field_type_schema, deps3)
|
|
534
|
+
return Field(comment, label, proto_type, '', '', field_name, index, deps3)
|
|
535
|
+
|
|
536
|
+
raise ValueError(f"Unknown field type {field_type_schema}")
|
|
537
|
+
|
|
538
|
+
def structure_schema_to_proto_message(self, structure_schema: dict, proto_files: ProtoFiles) -> str:
|
|
539
|
+
"""Convert a JSON Structure schema to a Protobuf message definition."""
|
|
540
|
+
comment = Comment('', {})
|
|
541
|
+
if 'doc' in structure_schema or 'description' in structure_schema:
|
|
542
|
+
comment = Comment(structure_schema.get('description', structure_schema.get('doc', '')), {})
|
|
543
|
+
|
|
544
|
+
namespace = structure_schema.get("namespace", '')
|
|
545
|
+
if not namespace:
|
|
546
|
+
namespace = self.default_namespace
|
|
547
|
+
|
|
548
|
+
struct_type = structure_schema.get('type', 'object')
|
|
549
|
+
|
|
550
|
+
if struct_type == 'object':
|
|
551
|
+
message = self.convert_record_type(structure_schema, comment, proto_files, structure_schema)
|
|
552
|
+
file = next((f for f in proto_files.files if f.package == namespace), None)
|
|
553
|
+
if not file:
|
|
554
|
+
file = ProtoFile({}, {}, {}, [], {}, namespace)
|
|
555
|
+
proto_files.files.append(file)
|
|
556
|
+
file.messages[message.name] = message
|
|
557
|
+
elif struct_type == 'enum' or 'enum' in structure_schema:
|
|
558
|
+
enum_name = structure_schema.get('name', 'UnnamedEnum')
|
|
559
|
+
enum_values = structure_schema.get('enum', [])
|
|
560
|
+
enum_fields = {str(val): Field(comment, '', str(val), '', '', str(val), i, []) for i, val in enumerate(enum_values)}
|
|
561
|
+
enum = Enum(comment, enum_name, enum_fields)
|
|
562
|
+
file = next((f for f in proto_files.files if f.package == namespace), None)
|
|
563
|
+
if not file:
|
|
564
|
+
file = ProtoFile({}, {}, {}, [], {}, namespace)
|
|
565
|
+
proto_files.files.append(file)
|
|
566
|
+
file.enums[enum_name] = enum
|
|
567
|
+
|
|
568
|
+
return structure_schema.get("name", "UnnamedSchema")
|
|
569
|
+
|
|
570
|
+
def structure_schema_to_proto_messages(self, structure_schema_input, proto_files: ProtoFiles):
|
|
571
|
+
"""Convert JSON Structure schema(s) to Protobuf message definitions."""
|
|
572
|
+
if not isinstance(structure_schema_input, list):
|
|
573
|
+
structure_schema_list = [structure_schema_input]
|
|
574
|
+
else:
|
|
575
|
+
structure_schema_list = structure_schema_input
|
|
576
|
+
|
|
577
|
+
# Register all schemas first
|
|
578
|
+
for structure_schema in structure_schema_list:
|
|
579
|
+
if isinstance(structure_schema, dict):
|
|
580
|
+
self.register_schema_ids(structure_schema)
|
|
581
|
+
|
|
582
|
+
# Then convert them
|
|
583
|
+
for structure_schema in structure_schema_list:
|
|
584
|
+
if isinstance(structure_schema, dict):
|
|
585
|
+
# Store definitions for later use
|
|
586
|
+
if 'definitions' in structure_schema:
|
|
587
|
+
self.definitions = structure_schema['definitions']
|
|
588
|
+
# Process definitions - pass the full schema as context so $ref can be resolved
|
|
589
|
+
for def_name, def_schema in structure_schema['definitions'].items():
|
|
590
|
+
if isinstance(def_schema, dict):
|
|
591
|
+
def_schema_copy = def_schema.copy()
|
|
592
|
+
if 'name' not in def_schema_copy:
|
|
593
|
+
def_schema_copy['name'] = def_name
|
|
594
|
+
if 'namespace' not in def_schema_copy:
|
|
595
|
+
def_schema_copy['namespace'] = structure_schema.get('namespace', self.default_namespace)
|
|
596
|
+
# Pass structure_schema as context for resolving $ref
|
|
597
|
+
self.structure_schema_to_proto_message_with_context(def_schema_copy, proto_files, structure_schema)
|
|
598
|
+
|
|
599
|
+
# Process root schema
|
|
600
|
+
self.structure_schema_to_proto_message(structure_schema, proto_files)
|
|
601
|
+
|
|
602
|
+
def structure_schema_to_proto_message_with_context(self, structure_schema: dict, proto_files: ProtoFiles, context_schema: Dict):
|
|
603
|
+
"""Convert a JSON Structure schema to a Protobuf message definition with context for $ref resolution."""
|
|
604
|
+
comment = Comment('', {})
|
|
605
|
+
if 'doc' in structure_schema or 'description' in structure_schema:
|
|
606
|
+
comment = Comment(structure_schema.get('description', structure_schema.get('doc', '')), {})
|
|
607
|
+
|
|
608
|
+
namespace = structure_schema.get("namespace", '')
|
|
609
|
+
if not namespace:
|
|
610
|
+
namespace = self.default_namespace
|
|
611
|
+
|
|
612
|
+
struct_type = structure_schema.get('type', 'object')
|
|
613
|
+
|
|
614
|
+
if struct_type == 'object':
|
|
615
|
+
message = self.convert_record_type(structure_schema, comment, proto_files, context_schema)
|
|
616
|
+
file = next((f for f in proto_files.files if f.package == namespace), None)
|
|
617
|
+
if not file:
|
|
618
|
+
file = ProtoFile({}, {}, {}, [], {}, namespace)
|
|
619
|
+
proto_files.files.append(file)
|
|
620
|
+
file.messages[message.name] = message
|
|
621
|
+
elif struct_type == 'enum' or 'enum' in structure_schema:
|
|
622
|
+
enum_name = structure_schema.get('name', 'UnnamedEnum')
|
|
623
|
+
enum_values = structure_schema.get('enum', [])
|
|
624
|
+
enum_fields = {str(val): Field(comment, '', str(val), '', '', str(val), i, []) for i, val in enumerate(enum_values)}
|
|
625
|
+
enum = Enum(comment, enum_name, enum_fields)
|
|
626
|
+
file = next((f for f in proto_files.files if f.package == namespace), None)
|
|
627
|
+
if not file:
|
|
628
|
+
file = ProtoFile({}, {}, {}, [], {}, namespace)
|
|
629
|
+
proto_files.files.append(file)
|
|
630
|
+
file.enums[enum_name] = enum
|
|
631
|
+
|
|
632
|
+
return structure_schema.get("name", "UnnamedSchema")
|
|
633
|
+
|
|
634
|
+
def save_proto_to_file(self, proto_files: ProtoFiles, proto_path):
|
|
635
|
+
"""Save the Protobuf schema to a file."""
|
|
636
|
+
for proto in proto_files.files:
|
|
637
|
+
# gather dependencies that are within the package
|
|
638
|
+
deps: List[str] = []
|
|
639
|
+
for message in proto.messages.values():
|
|
640
|
+
for dep in message.dependencies:
|
|
641
|
+
if '.' in dep:
|
|
642
|
+
deps.append(dep.rsplit('.', 1)[0])
|
|
643
|
+
deps = list(set(deps))
|
|
644
|
+
|
|
645
|
+
proto.imports.extend([d for d in deps if d != proto.package])
|
|
646
|
+
proto_file_path = os.path.join(proto_path, f"{proto.package if proto.package else 'default'}.proto")
|
|
647
|
+
# create the directory for the proto file if it doesn't exist
|
|
648
|
+
proto_dir = os.path.dirname(proto_file_path)
|
|
649
|
+
if not os.path.exists(proto_dir):
|
|
650
|
+
os.makedirs(proto_dir, exist_ok=True)
|
|
651
|
+
with open(proto_file_path, 'w', encoding='utf-8') as proto_file:
|
|
652
|
+
# dump the ProtoFile structure in proto syntax
|
|
653
|
+
proto_str = 'syntax = "proto3";\n\n'
|
|
654
|
+
if proto.package:
|
|
655
|
+
proto_str += f'package {proto.package};\n\n'
|
|
656
|
+
|
|
657
|
+
for import_package in proto.imports:
|
|
658
|
+
proto_str += f'import "{import_package}.proto";\n'
|
|
659
|
+
if len(proto.imports):
|
|
660
|
+
proto_str += "\n"
|
|
661
|
+
for enum_name, enum in proto.enums.items():
|
|
662
|
+
proto_str += f"enum {enum_name} {{\n"
|
|
663
|
+
for _, field in enum.fields.items():
|
|
664
|
+
proto_str += f"{indent}{field.name} = {field.number};\n"
|
|
665
|
+
proto_str += "}\n\n"
|
|
666
|
+
for message in proto.messages.values():
|
|
667
|
+
proto_str += self.render_message(message)
|
|
668
|
+
for service in proto.services.values():
|
|
669
|
+
proto_str += f"service {service.name} {{\n"
|
|
670
|
+
for function_name, func in service.functions.items():
|
|
671
|
+
proto_str += f"{indent}rpc {func.name} ({func.in_type}) returns ({func.out_type}) {{\n"
|
|
672
|
+
proto_str += f"{indent}{indent}option (google.api.http) = {{\n"
|
|
673
|
+
proto_str += f"{indent}{indent}{indent}post: \"{func.uri}\"\n"
|
|
674
|
+
proto_str += f"{indent}{indent}}};\n"
|
|
675
|
+
proto_str += f"{indent}}};\n"
|
|
676
|
+
proto_str += "}\n\n"
|
|
677
|
+
proto_file.write(proto_str)
|
|
678
|
+
|
|
679
|
+
def render_message(self, message, level=0) -> str:
|
|
680
|
+
proto_str = ''
|
|
681
|
+
|
|
682
|
+
# Add message-level comment if present
|
|
683
|
+
if message.comment.content:
|
|
684
|
+
comment_lines = message.comment.content.split(' | ')
|
|
685
|
+
for line in comment_lines:
|
|
686
|
+
proto_str += f"{indent*level}// {line}\n"
|
|
687
|
+
|
|
688
|
+
proto_str += f"{indent*level}message {message.name} {{\n"
|
|
689
|
+
|
|
690
|
+
# Add deprecated option if message is deprecated
|
|
691
|
+
if message.comment.tags.get('deprecated'):
|
|
692
|
+
proto_str += f"{indent*level}{indent}option deprecated = true;\n"
|
|
693
|
+
|
|
694
|
+
# Render nested messages and enums FIRST (protobuf convention)
|
|
695
|
+
for local_message in message.messages.values():
|
|
696
|
+
proto_str += self.render_message(local_message, level+1)
|
|
697
|
+
for enum in message.enums.values():
|
|
698
|
+
# Add enum-level comment if present
|
|
699
|
+
if enum.comment.content:
|
|
700
|
+
for line in enum.comment.content.split(' | '):
|
|
701
|
+
proto_str += f"{indent*level}{indent}// {line}\n"
|
|
702
|
+
proto_str += f"{indent*level}{indent}enum {enum.name} {{\n"
|
|
703
|
+
if enum.comment.tags.get('deprecated'):
|
|
704
|
+
proto_str += f"{indent*level}{indent}{indent}option deprecated = true;\n"
|
|
705
|
+
for _, field in enum.fields.items():
|
|
706
|
+
proto_str += f"{indent*level}{indent}{indent}{field.label}{' ' if field.label else ''}{field.name} = {field.number};\n"
|
|
707
|
+
proto_str += f"{indent*level}{indent}}}\n"
|
|
708
|
+
|
|
709
|
+
# Then render fields and oneofs
|
|
710
|
+
fieldsAndOneofs = message.fields+message.oneofs
|
|
711
|
+
fieldsAndOneofs.sort(key=lambda f: f.number if isinstance(f, Field) else f.fields[0].number)
|
|
712
|
+
for fo in fieldsAndOneofs:
|
|
713
|
+
if isinstance(fo, Field):
|
|
714
|
+
field = fo
|
|
715
|
+
# Add field-level comment if present
|
|
716
|
+
if field.comment.content:
|
|
717
|
+
for line in field.comment.content.split(' | '):
|
|
718
|
+
proto_str += f"{indent*level}{indent}// {line}\n"
|
|
719
|
+
|
|
720
|
+
# Render field with deprecated option if needed
|
|
721
|
+
deprecated_option = ' [deprecated = true]' if field.comment.tags.get('deprecated') else ''
|
|
722
|
+
|
|
723
|
+
if field.type == "map":
|
|
724
|
+
proto_str += f"{indent*level}{indent}{field.label}{' ' if field.label else ''}map<{field.key_type}, {field.val_type}> {field.name} = {field.number}{deprecated_option};\n"
|
|
725
|
+
elif field.type == "array":
|
|
726
|
+
proto_str += f"{indent*level}{indent}{field.label}{' ' if field.label else ''}{field.val_type} {field.name} = {field.number}{deprecated_option};\n"
|
|
727
|
+
else:
|
|
728
|
+
proto_str += f"{indent*level}{indent}{field.label}{' ' if field.label else ''}{field.type} {field.name} = {field.number}{deprecated_option};\n"
|
|
729
|
+
else:
|
|
730
|
+
oneof = fo
|
|
731
|
+
# Add oneof-level comment if present
|
|
732
|
+
if oneof.comment.content:
|
|
733
|
+
for line in oneof.comment.content.split(' | '):
|
|
734
|
+
proto_str += f"{indent*level}{indent}// {line}\n"
|
|
735
|
+
proto_str += f"{indent*level}{indent}oneof {oneof.name} {{\n"
|
|
736
|
+
for field in oneof.fields:
|
|
737
|
+
# Add field comment in oneof
|
|
738
|
+
if field.comment.content:
|
|
739
|
+
for line in field.comment.content.split(' | '):
|
|
740
|
+
proto_str += f"{indent*level}{indent}{indent}// {line}\n"
|
|
741
|
+
deprecated_option = ' [deprecated = true]' if field.comment.tags.get('deprecated') else ''
|
|
742
|
+
proto_str += f"{indent*level}{indent}{indent}{field.label}{' ' if field.label else ''}{field.type} {field.name} = {field.number}{deprecated_option};\n"
|
|
743
|
+
proto_str += f"{indent*level}{indent}}}\n"
|
|
744
|
+
|
|
745
|
+
proto_str += f"{indent*level}}}\n"
|
|
746
|
+
if level == 0:
|
|
747
|
+
proto_str += "\n"
|
|
748
|
+
return proto_str
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def convert_structure_to_proto(self, structure_schema_path, proto_file_path):
|
|
752
|
+
"""Convert JSON Structure schema file to Protobuf .proto file."""
|
|
753
|
+
with open(structure_schema_path, 'r', encoding='utf-8') as structure_file:
|
|
754
|
+
structure_schema = json.load(structure_file)
|
|
755
|
+
proto_files = ProtoFiles([])
|
|
756
|
+
self.structure_schema_to_proto_messages(structure_schema, proto_files)
|
|
757
|
+
self.save_proto_to_file(proto_files, proto_file_path)
|
|
758
|
+
|
|
759
|
+
def convert_structure_to_proto(structure_schema_path, proto_file_path, naming_mode: Literal['snake', 'pascal', 'camel'] = 'pascal', allow_optional: bool = False):
|
|
760
|
+
structuretoproto = StructureToProto()
|
|
761
|
+
structuretoproto.naming_mode = naming_mode
|
|
762
|
+
structuretoproto.allow_optional = allow_optional
|
|
763
|
+
structuretoproto.default_namespace = os.path.splitext(os.path.basename(proto_file_path))[0].replace('-', '_')
|
|
764
|
+
structuretoproto.convert_structure_to_proto(structure_schema_path, proto_file_path)
|