structurize 2.18.2__py3-none-any.whl → 2.20.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/_version.py +3 -3
- avrotize/avrotocsharp.py +21 -2
- avrotize/avrotojava.py +16 -6
- avrotize/avrotots.py +96 -0
- avrotize/cddltostructure.py +1841 -0
- avrotize/commands.json +226 -0
- avrotize/constants.py +71 -4
- avrotize/dependencies/cpp/vcpkg/vcpkg.json +19 -0
- avrotize/dependencies/typescript/node22/package.json +16 -0
- avrotize/dependency_version.py +432 -0
- avrotize/structuretocddl.py +597 -0
- avrotize/structuretocsharp.py +311 -21
- avrotize/structuretojava.py +853 -0
- {structurize-2.18.2.dist-info → structurize-2.20.0.dist-info}/METADATA +1 -1
- {structurize-2.18.2.dist-info → structurize-2.20.0.dist-info}/RECORD +19 -13
- {structurize-2.18.2.dist-info → structurize-2.20.0.dist-info}/WHEEL +0 -0
- {structurize-2.18.2.dist-info → structurize-2.20.0.dist-info}/entry_points.txt +0 -0
- {structurize-2.18.2.dist-info → structurize-2.20.0.dist-info}/licenses/LICENSE +0 -0
- {structurize-2.18.2.dist-info → structurize-2.20.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1841 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CDDL (Concise Data Definition Language) to JSON Structure converter.
|
|
3
|
+
|
|
4
|
+
CDDL is defined in RFC 8610 and is a schema language primarily used for
|
|
5
|
+
expressing CBOR and JSON data structures.
|
|
6
|
+
|
|
7
|
+
RFC 8610 Compliance Summary:
|
|
8
|
+
============================
|
|
9
|
+
|
|
10
|
+
Fully Implemented (RFC 8610 Sections 2-3):
|
|
11
|
+
- Primitive types: uint, nint, int, float16/32/64, tstr, bstr, bool, nil, any
|
|
12
|
+
- Maps/objects with member keys and values
|
|
13
|
+
- Arrays with homogeneous and heterogeneous (tuple) items
|
|
14
|
+
- Choice/union types (type1 / type2)
|
|
15
|
+
- Occurrence indicators: ? (optional), * (zero-or-more), + (one-or-more)
|
|
16
|
+
- Literal values (strings, integers, floats, booleans)
|
|
17
|
+
- Range constraints (min..max, min...max)
|
|
18
|
+
- Type references and nested types
|
|
19
|
+
- Groups and group composition
|
|
20
|
+
- CBOR tags (#6.n(type)) - underlying type extracted
|
|
21
|
+
- Choice from groups (&group)
|
|
22
|
+
- Comments (handled by parser)
|
|
23
|
+
- Generic type parameters (<T>): Parameterized type definitions with instantiation
|
|
24
|
+
- Unwrap operator (~): Extracting group/array content
|
|
25
|
+
|
|
26
|
+
Control Operators (RFC 8610 Section 3.8):
|
|
27
|
+
- .size: Maps to minLength/maxLength for strings, minItems/maxItems for arrays
|
|
28
|
+
- .regexp: Maps to pattern validation (ECMAScript regex)
|
|
29
|
+
- .default: Maps to default value
|
|
30
|
+
- .lt, .le, .gt, .ge: Maps to minimum/maximum/exclusiveMinimum/exclusiveMaximum
|
|
31
|
+
- .eq: Maps to const value
|
|
32
|
+
- .bits, .cbor, .cborseq: Base type extracted (CBOR-specific, no JSON equivalent)
|
|
33
|
+
- .within, .and: Type intersection (limited support, base type used)
|
|
34
|
+
|
|
35
|
+
Not Implemented:
|
|
36
|
+
- CDDL sockets ($name, $$name): Extensibility points (parser limitation)
|
|
37
|
+
- .ne operator: No direct JSON Structure equivalent
|
|
38
|
+
|
|
39
|
+
Notes on Type Mapping:
|
|
40
|
+
- CBOR-specific types (bstr, tstr) map to JSON equivalents (bytes, string)
|
|
41
|
+
- CBOR tags are unwrapped to their underlying types
|
|
42
|
+
- Integer types (uint, nint, int) all map to int64 for JSON compatibility
|
|
43
|
+
- Float types map to float (float16/32) or double (float64)
|
|
44
|
+
"""
|
|
45
|
+
# pylint: disable=too-many-lines,too-many-instance-attributes,too-many-arguments
|
|
46
|
+
# pylint: disable=too-many-positional-arguments,too-many-locals,too-many-branches
|
|
47
|
+
# pylint: disable=too-many-statements,too-many-return-statements,too-many-nested-blocks
|
|
48
|
+
# pylint: disable=no-else-return
|
|
49
|
+
|
|
50
|
+
import json
|
|
51
|
+
import logging
|
|
52
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
53
|
+
|
|
54
|
+
from cddlparser import parse as cddl_parse
|
|
55
|
+
from cddlparser.ast import (
|
|
56
|
+
Rule, Type, Typename, Map, Array, Value, Range, Operator, Tag,
|
|
57
|
+
ChoiceFrom, CDDLNode
|
|
58
|
+
)
|
|
59
|
+
from json_structure import SchemaValidator, ValidationError, ValidationSeverity
|
|
60
|
+
|
|
61
|
+
from avrotize.common import avro_name
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Configure module logger
|
|
65
|
+
logger = logging.getLogger(__name__)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Default namespace for JSON Structure schema
|
|
69
|
+
DEFAULT_NAMESPACE = 'example.com'
|
|
70
|
+
|
|
71
|
+
# Maximum recursion depth for type conversion (prevents stack overflow)
|
|
72
|
+
MAX_CONVERSION_DEPTH = 100
|
|
73
|
+
|
|
74
|
+
# Type aliases for better type safety
|
|
75
|
+
JsonStructureType = Dict[str, Any]
|
|
76
|
+
AstNode = Any # Union of all cddlparser AST node types
|
|
77
|
+
CommentList = List[Any]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class CddlConversionError(Exception):
|
|
81
|
+
"""
|
|
82
|
+
Exception raised when CDDL to JSON Structure conversion fails.
|
|
83
|
+
|
|
84
|
+
Attributes:
|
|
85
|
+
message: Human-readable error description
|
|
86
|
+
context: Optional context about where the error occurred
|
|
87
|
+
cause: Optional underlying exception that caused this error
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, message: str, context: Optional[str] = None,
|
|
91
|
+
cause: Optional[Exception] = None) -> None:
|
|
92
|
+
self.message = message
|
|
93
|
+
self.context = context
|
|
94
|
+
self.cause = cause
|
|
95
|
+
full_message = message
|
|
96
|
+
if context:
|
|
97
|
+
full_message = f"{message} (context: {context})"
|
|
98
|
+
super().__init__(full_message)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class CddlCycleError(CddlConversionError):
|
|
102
|
+
"""
|
|
103
|
+
Exception raised when a circular type reference is detected.
|
|
104
|
+
|
|
105
|
+
Attributes:
|
|
106
|
+
cycle_path: List of type names forming the cycle
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def __init__(self, cycle_path: List[str]) -> None:
|
|
110
|
+
self.cycle_path = cycle_path
|
|
111
|
+
cycle_str = ' -> '.join(cycle_path)
|
|
112
|
+
super().__init__(f"Circular type reference detected: {cycle_str}")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# CDDL primitive types to JSON Structure type mapping
|
|
116
|
+
CDDL_PRIMITIVE_TYPES: Dict[str, Dict[str, Any]] = {
|
|
117
|
+
# Integer types
|
|
118
|
+
'uint': {'type': 'int64'}, # Unsigned integer (CBOR major type 0)
|
|
119
|
+
'nint': {'type': 'int64'}, # Negative integer (CBOR major type 1)
|
|
120
|
+
'int': {'type': 'int64'}, # Signed integer (uint or nint)
|
|
121
|
+
'integer': {'type': 'int64'}, # Alias for int
|
|
122
|
+
|
|
123
|
+
# Floating-point types
|
|
124
|
+
'float16': {'type': 'float'}, # Half precision
|
|
125
|
+
'float32': {'type': 'float'}, # Single precision
|
|
126
|
+
'float64': {'type': 'double'}, # Double precision
|
|
127
|
+
'float': {'type': 'double'}, # Generic float (maps to double)
|
|
128
|
+
|
|
129
|
+
# String types
|
|
130
|
+
'bstr': {'type': 'binary'}, # Byte string (base64 encoded)
|
|
131
|
+
'bytes': {'type': 'binary'}, # Alias for bstr
|
|
132
|
+
'tstr': {'type': 'string'}, # Text string (UTF-8)
|
|
133
|
+
'text': {'type': 'string'}, # Alias for tstr
|
|
134
|
+
|
|
135
|
+
# Boolean and null
|
|
136
|
+
'bool': {'type': 'boolean'},
|
|
137
|
+
'true': {'type': 'boolean'},
|
|
138
|
+
'false': {'type': 'boolean'},
|
|
139
|
+
'nil': {'type': 'null'},
|
|
140
|
+
'null': {'type': 'null'},
|
|
141
|
+
|
|
142
|
+
# Any type
|
|
143
|
+
'any': {'type': 'any'},
|
|
144
|
+
|
|
145
|
+
# Additional CBOR types
|
|
146
|
+
'undefined': {'type': 'null'}, # CBOR undefined maps to null in JSON Structure
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class CddlToStructureConverter:
|
|
151
|
+
"""
|
|
152
|
+
Converts CDDL schema documents to JSON Structure format.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def __init__(self) -> None:
|
|
156
|
+
self.root_namespace = DEFAULT_NAMESPACE
|
|
157
|
+
self.type_registry: Dict[str, Dict[str, Any]] = {}
|
|
158
|
+
self.definitions: Dict[str, Dict[str, Any]] = {}
|
|
159
|
+
self.generic_types: Dict[str, Dict[str, Any]] = {} # type name -> generic info
|
|
160
|
+
self.current_generic_bindings: Dict[str, Dict[str, Any]] = {} # type param bindings
|
|
161
|
+
self.generic_template_names: Set[str] = set() # generic templates (not concrete)
|
|
162
|
+
# Cycle detection state
|
|
163
|
+
self._conversion_stack: List[str] = [] # Stack of type names being converted
|
|
164
|
+
self._conversion_depth: int = 0 # Current recursion depth
|
|
165
|
+
# Counter for auto-generated type names
|
|
166
|
+
self._auto_name_counter: int = 0
|
|
167
|
+
|
|
168
|
+
def _create_inline_definition(
|
|
169
|
+
self, type_def: Dict[str, Any], base_name: str
|
|
170
|
+
) -> Dict[str, Any]:
|
|
171
|
+
"""
|
|
172
|
+
Move an inline compound type to definitions and return a $ref.
|
|
173
|
+
|
|
174
|
+
JSON Structure requires compound types (object, tuple, array, map, etc.)
|
|
175
|
+
inside unions to be referenced via $ref. This method handles the extraction.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
type_def: The inline type definition to extract
|
|
179
|
+
base_name: Base name to use for generating the definition name
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
A $ref pointing to the new definition, or the original type if not compound
|
|
183
|
+
"""
|
|
184
|
+
type_value = type_def.get('type')
|
|
185
|
+
# All compound types that need extraction in union contexts
|
|
186
|
+
if type_value not in ('object', 'tuple', 'choice', 'array', 'map', 'set'):
|
|
187
|
+
return type_def
|
|
188
|
+
|
|
189
|
+
# Generate a unique name for the inline type
|
|
190
|
+
self._auto_name_counter += 1
|
|
191
|
+
type_name = f"{base_name}_{type_value}_{self._auto_name_counter}"
|
|
192
|
+
type_name = avro_name(type_name)
|
|
193
|
+
|
|
194
|
+
# Add name to the type and store in definitions
|
|
195
|
+
type_def['name'] = type_name
|
|
196
|
+
self.definitions[type_name] = type_def
|
|
197
|
+
|
|
198
|
+
# Return a $ref
|
|
199
|
+
return {'$ref': f'#/definitions/{type_name}'}
|
|
200
|
+
|
|
201
|
+
def _has_unresolved_refs(self, type_def: Dict[str, Any]) -> bool:
|
|
202
|
+
"""
|
|
203
|
+
Check if a type definition has unresolved $ref pointers.
|
|
204
|
+
|
|
205
|
+
Unresolved refs are typically generic type parameters (like T, E) that
|
|
206
|
+
weren't substituted during instantiation.
|
|
207
|
+
"""
|
|
208
|
+
def check(obj: Any) -> bool:
|
|
209
|
+
if isinstance(obj, dict):
|
|
210
|
+
if '$ref' in obj:
|
|
211
|
+
ref = obj['$ref']
|
|
212
|
+
# Check if this is a reference to a definitions entry
|
|
213
|
+
if ref.startswith('#/definitions/'):
|
|
214
|
+
ref_name = ref[14:] # Remove '#/definitions/'
|
|
215
|
+
# Check if ref exists in definitions or type_registry
|
|
216
|
+
# Type registry has original names, definitions has transformed names
|
|
217
|
+
if ref_name in self.definitions:
|
|
218
|
+
return False # Resolved
|
|
219
|
+
if ref_name in self.type_registry:
|
|
220
|
+
return False # Resolved (original name)
|
|
221
|
+
# Check if the transformed name exists
|
|
222
|
+
if avro_name(ref_name) in self.definitions:
|
|
223
|
+
return False # Resolved with transformed name
|
|
224
|
+
# Check for generic type parameters (typically single uppercase letters)
|
|
225
|
+
if len(ref_name) <= 2 and ref_name.isupper():
|
|
226
|
+
return True # Likely an unresolved generic parameter
|
|
227
|
+
# If not found anywhere, it's unresolved
|
|
228
|
+
return True
|
|
229
|
+
elif ref.startswith('#/'):
|
|
230
|
+
# Other internal refs - allow them
|
|
231
|
+
return False
|
|
232
|
+
for v in obj.values():
|
|
233
|
+
if check(v):
|
|
234
|
+
return True
|
|
235
|
+
elif isinstance(obj, list):
|
|
236
|
+
for item in obj:
|
|
237
|
+
if check(item):
|
|
238
|
+
return True
|
|
239
|
+
return False
|
|
240
|
+
return check(type_def)
|
|
241
|
+
|
|
242
|
+
def _extract_description(self, node: Any) -> Optional[str]:
|
|
243
|
+
"""
|
|
244
|
+
Extract description from comments attached to a node.
|
|
245
|
+
|
|
246
|
+
CDDL comments start with ';' and are attached to AST nodes by the parser.
|
|
247
|
+
This method extracts and cleans up those comments to use as descriptions.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
node: Any AST node that might have comments attached
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
A cleaned description string, or None if no comments
|
|
254
|
+
"""
|
|
255
|
+
if not hasattr(node, 'comments') or not node.comments:
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
# Extract comment text from each comment token
|
|
259
|
+
comment_lines = []
|
|
260
|
+
for comment in node.comments:
|
|
261
|
+
if hasattr(comment, 'literal'):
|
|
262
|
+
# Remove the leading semicolon and whitespace
|
|
263
|
+
text = comment.literal.strip()
|
|
264
|
+
if text.startswith(';'):
|
|
265
|
+
text = text[1:].strip()
|
|
266
|
+
if text:
|
|
267
|
+
comment_lines.append(text)
|
|
268
|
+
|
|
269
|
+
if not comment_lines:
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
# Join multiple comment lines with spaces
|
|
273
|
+
return ' '.join(comment_lines)
|
|
274
|
+
|
|
275
|
+
def convert_cddl_to_structure(
|
|
276
|
+
self, cddl_content: str, namespace: Optional[str] = None
|
|
277
|
+
) -> Dict[str, Any]:
|
|
278
|
+
"""
|
|
279
|
+
Convert CDDL content to JSON Structure format.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
cddl_content: The CDDL schema as a string
|
|
283
|
+
namespace: Optional namespace for the schema
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Dict containing the JSON Structure schema
|
|
287
|
+
"""
|
|
288
|
+
if namespace:
|
|
289
|
+
self.root_namespace = namespace
|
|
290
|
+
|
|
291
|
+
# Parse the CDDL content
|
|
292
|
+
ast = cddl_parse(cddl_content)
|
|
293
|
+
|
|
294
|
+
# Clear type registry and cycle detection state
|
|
295
|
+
self.type_registry.clear()
|
|
296
|
+
self.definitions.clear()
|
|
297
|
+
self.generic_types.clear()
|
|
298
|
+
self.current_generic_bindings.clear()
|
|
299
|
+
self.generic_template_names.clear()
|
|
300
|
+
self._conversion_stack.clear()
|
|
301
|
+
self._conversion_depth = 0
|
|
302
|
+
self._auto_name_counter = 0
|
|
303
|
+
|
|
304
|
+
# First pass: collect all type names and generic definitions
|
|
305
|
+
for rule in ast.rules:
|
|
306
|
+
if hasattr(rule, 'getChildren'):
|
|
307
|
+
children = rule.getChildren()
|
|
308
|
+
if len(children) >= 2:
|
|
309
|
+
typename_node = children[0]
|
|
310
|
+
if hasattr(typename_node, 'name'):
|
|
311
|
+
self.type_registry[typename_node.name] = {}
|
|
312
|
+
# Check for generic parameters
|
|
313
|
+
generic_params = self._extract_generic_parameters(typename_node)
|
|
314
|
+
if generic_params:
|
|
315
|
+
self.generic_types[typename_node.name] = {
|
|
316
|
+
'params': generic_params,
|
|
317
|
+
'type_node': children[1] if len(children) > 1 else None
|
|
318
|
+
}
|
|
319
|
+
# Mark this as a generic template (not a concrete type)
|
|
320
|
+
self.generic_template_names.add(avro_name(typename_node.name))
|
|
321
|
+
|
|
322
|
+
# Second pass: process all rules
|
|
323
|
+
for rule in ast.rules:
|
|
324
|
+
self._process_rule(rule)
|
|
325
|
+
|
|
326
|
+
# Build the output structure
|
|
327
|
+
structure_schema: Dict[str, Any] = {
|
|
328
|
+
"$schema": "https://json-structure.org/meta/extended/v0/#",
|
|
329
|
+
"$id": f"https://{self.root_namespace.replace('.', '/')}/schema.json"
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
# Add definitions if any (excluding generic templates and types with unresolved refs)
|
|
333
|
+
concrete_definitions = {
|
|
334
|
+
name: defn for name, defn in self.definitions.items()
|
|
335
|
+
if name not in self.generic_template_names and not self._has_unresolved_refs(defn)
|
|
336
|
+
}
|
|
337
|
+
if concrete_definitions:
|
|
338
|
+
structure_schema['definitions'] = concrete_definitions
|
|
339
|
+
|
|
340
|
+
# If there's a root type (first rule that's not a generic template), add it to the schema
|
|
341
|
+
if ast.rules and concrete_definitions:
|
|
342
|
+
first_rule_name = self._get_rule_name(ast.rules[0])
|
|
343
|
+
# Skip generic templates to find first concrete type
|
|
344
|
+
for rule in ast.rules:
|
|
345
|
+
first_rule_name = self._get_rule_name(rule)
|
|
346
|
+
if first_rule_name and first_rule_name not in self.generic_template_names:
|
|
347
|
+
break
|
|
348
|
+
if first_rule_name and first_rule_name in concrete_definitions:
|
|
349
|
+
root_def = concrete_definitions[first_rule_name]
|
|
350
|
+
# Copy root type properties to schema root
|
|
351
|
+
for key, value in root_def.items():
|
|
352
|
+
if key not in structure_schema:
|
|
353
|
+
structure_schema[key] = value
|
|
354
|
+
|
|
355
|
+
# Scan for extension usage
|
|
356
|
+
uses = self._scan_for_uses(structure_schema)
|
|
357
|
+
if uses:
|
|
358
|
+
structure_schema['$uses'] = uses
|
|
359
|
+
|
|
360
|
+
return structure_schema
|
|
361
|
+
|
|
362
|
+
def _get_rule_name(self, rule: Rule) -> Optional[str]:
|
|
363
|
+
"""Extract the name from a rule."""
|
|
364
|
+
if hasattr(rule, 'getChildren'):
|
|
365
|
+
children = rule.getChildren()
|
|
366
|
+
if len(children) >= 1:
|
|
367
|
+
typename_node = children[0]
|
|
368
|
+
if hasattr(typename_node, 'name'):
|
|
369
|
+
return avro_name(typename_node.name)
|
|
370
|
+
return None
|
|
371
|
+
|
|
372
|
+
def validate_structure_schema(self, structure_schema: Dict[str, Any],
|
|
373
|
+
source_text: Optional[str] = None) -> List[ValidationError]:
|
|
374
|
+
"""
|
|
375
|
+
Validate a JSON Structure schema using the json-structure SDK.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
structure_schema: The JSON Structure schema to validate
|
|
379
|
+
source_text: Optional source text for better error locations
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
List of validation errors (empty if valid)
|
|
383
|
+
|
|
384
|
+
Raises:
|
|
385
|
+
CddlConversionError: If the schema is invalid
|
|
386
|
+
"""
|
|
387
|
+
validator = SchemaValidator()
|
|
388
|
+
errors = validator.validate(structure_schema, source_text)
|
|
389
|
+
|
|
390
|
+
# Log any errors
|
|
391
|
+
for error in errors:
|
|
392
|
+
if error.severity == ValidationSeverity.ERROR:
|
|
393
|
+
logger.error("Schema validation error: %s at %s", error.message, error.path)
|
|
394
|
+
else:
|
|
395
|
+
logger.warning("Schema validation warning: %s at %s", error.message, error.path)
|
|
396
|
+
|
|
397
|
+
return errors
|
|
398
|
+
|
|
399
|
+
def _process_rule(self, rule: Rule) -> None:
|
|
400
|
+
"""Process a CDDL rule and add it to definitions."""
|
|
401
|
+
if not hasattr(rule, 'getChildren'):
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
children = rule.getChildren()
|
|
405
|
+
if len(children) < 2:
|
|
406
|
+
return
|
|
407
|
+
|
|
408
|
+
# First child is the typename, second is the type
|
|
409
|
+
typename_node = children[0]
|
|
410
|
+
type_node = children[1] if len(children) > 1 else None
|
|
411
|
+
|
|
412
|
+
if not hasattr(typename_node, 'name') or type_node is None:
|
|
413
|
+
return
|
|
414
|
+
|
|
415
|
+
rule_name = avro_name(typename_node.name)
|
|
416
|
+
original_name = typename_node.name
|
|
417
|
+
|
|
418
|
+
# Extract description from comments on the typename
|
|
419
|
+
description = self._extract_description(typename_node)
|
|
420
|
+
|
|
421
|
+
# Convert the type
|
|
422
|
+
structure_type = self._convert_type(type_node, rule_name)
|
|
423
|
+
|
|
424
|
+
if structure_type:
|
|
425
|
+
if isinstance(structure_type, dict):
|
|
426
|
+
structure_type['name'] = rule_name
|
|
427
|
+
# Add description if present
|
|
428
|
+
if description:
|
|
429
|
+
structure_type['description'] = description
|
|
430
|
+
# Add altnames if original name differs
|
|
431
|
+
if original_name != rule_name:
|
|
432
|
+
structure_type['altnames'] = {'cddl': original_name}
|
|
433
|
+
self.definitions[rule_name] = structure_type
|
|
434
|
+
self.type_registry[typename_node.name] = structure_type
|
|
435
|
+
|
|
436
|
+
def _convert_type(self, type_node: Any, context_name: str = '') -> Dict[str, Any]:
|
|
437
|
+
"""
|
|
438
|
+
Convert a CDDL type node to JSON Structure type.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
type_node: The AST node to convert
|
|
442
|
+
context_name: Name context for error messages and nested type naming
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
JSON Structure type definition
|
|
446
|
+
|
|
447
|
+
Raises:
|
|
448
|
+
CddlConversionError: If conversion depth exceeds MAX_CONVERSION_DEPTH
|
|
449
|
+
"""
|
|
450
|
+
if type_node is None:
|
|
451
|
+
return {'type': 'any'}
|
|
452
|
+
|
|
453
|
+
# Check recursion depth
|
|
454
|
+
self._conversion_depth += 1
|
|
455
|
+
if self._conversion_depth > MAX_CONVERSION_DEPTH:
|
|
456
|
+
self._conversion_depth -= 1
|
|
457
|
+
logger.warning("Maximum conversion depth exceeded for context: %s", context_name)
|
|
458
|
+
raise CddlConversionError(
|
|
459
|
+
f"Maximum conversion depth ({MAX_CONVERSION_DEPTH}) exceeded",
|
|
460
|
+
context=context_name
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
try:
|
|
464
|
+
node_type = type(type_node).__name__
|
|
465
|
+
|
|
466
|
+
if node_type == 'Type':
|
|
467
|
+
return self._convert_type_node(type_node, context_name)
|
|
468
|
+
elif node_type == 'Typename':
|
|
469
|
+
return self._convert_typename(type_node)
|
|
470
|
+
elif node_type == 'Map':
|
|
471
|
+
return self._convert_map(type_node, context_name)
|
|
472
|
+
elif node_type == 'Array':
|
|
473
|
+
return self._convert_array(type_node, context_name)
|
|
474
|
+
elif node_type == 'Group':
|
|
475
|
+
return self._convert_group(type_node, context_name)
|
|
476
|
+
elif node_type == 'GroupChoice':
|
|
477
|
+
return self._convert_group_choice(type_node, context_name)
|
|
478
|
+
elif node_type == 'GroupEntry':
|
|
479
|
+
return self._convert_group_entry(type_node, context_name)
|
|
480
|
+
elif node_type == 'Value':
|
|
481
|
+
return self._convert_value(type_node)
|
|
482
|
+
elif node_type == 'Range':
|
|
483
|
+
return self._convert_range(type_node)
|
|
484
|
+
elif node_type == 'Operator':
|
|
485
|
+
return self._convert_operator(type_node, context_name)
|
|
486
|
+
elif node_type == 'Tag':
|
|
487
|
+
return self._convert_tag(type_node, context_name)
|
|
488
|
+
elif node_type == 'ChoiceFrom':
|
|
489
|
+
return self._convert_choice_from(type_node, context_name)
|
|
490
|
+
else:
|
|
491
|
+
# Default fallback
|
|
492
|
+
logger.debug("Unknown node type '%s', using 'any'", node_type)
|
|
493
|
+
return {'type': 'any'}
|
|
494
|
+
finally:
|
|
495
|
+
self._conversion_depth -= 1
|
|
496
|
+
|
|
497
|
+
def _convert_type_node(self, type_node: Type, context_name: str = '') -> Dict[str, Any]:
|
|
498
|
+
"""Convert a Type node."""
|
|
499
|
+
if not hasattr(type_node, 'getChildren'):
|
|
500
|
+
return {'type': 'any'}
|
|
501
|
+
|
|
502
|
+
children = type_node.getChildren()
|
|
503
|
+
if not children:
|
|
504
|
+
return {'type': 'any'}
|
|
505
|
+
|
|
506
|
+
# If there are multiple children, this might be a choice (union)
|
|
507
|
+
if len(children) > 1:
|
|
508
|
+
# Check if this is a choice type (type1 / type2)
|
|
509
|
+
converted_types = []
|
|
510
|
+
for child in children:
|
|
511
|
+
converted = self._convert_type(child, context_name)
|
|
512
|
+
if converted:
|
|
513
|
+
converted_types.append(converted)
|
|
514
|
+
|
|
515
|
+
if len(converted_types) > 1:
|
|
516
|
+
# Check if all types are string constants - if so, convert to enum
|
|
517
|
+
all_string_consts = all(
|
|
518
|
+
isinstance(ct, dict) and
|
|
519
|
+
ct.get('type') == 'string' and
|
|
520
|
+
'const' in ct and
|
|
521
|
+
len(ct) == 2
|
|
522
|
+
for ct in converted_types
|
|
523
|
+
)
|
|
524
|
+
if all_string_consts:
|
|
525
|
+
# Convert to string enum
|
|
526
|
+
enum_values = [ct['const'] for ct in converted_types]
|
|
527
|
+
return {'type': 'string', 'enum': enum_values}
|
|
528
|
+
|
|
529
|
+
# Create a union type per JSON Structure spec Section 3.5.1
|
|
530
|
+
# Union elements should be type names (strings) or $ref schemas
|
|
531
|
+
# Inline compound types must be extracted to definitions
|
|
532
|
+
union_elements: List[Any] = []
|
|
533
|
+
for ct in converted_types:
|
|
534
|
+
if '$ref' in ct:
|
|
535
|
+
# Keep $ref as-is (it's a valid union element)
|
|
536
|
+
union_elements.append(ct)
|
|
537
|
+
elif 'type' in ct and isinstance(ct['type'], str):
|
|
538
|
+
type_val = ct['type']
|
|
539
|
+
# Check if it's a compound type that needs extraction
|
|
540
|
+
# object, tuple, choice, array, map are all compound types
|
|
541
|
+
if type_val in ('object', 'tuple', 'choice', 'array', 'map', 'set'):
|
|
542
|
+
# Extract to definitions and use $ref
|
|
543
|
+
extracted = self._create_inline_definition(ct, f"{context_name}_option")
|
|
544
|
+
union_elements.append(extracted)
|
|
545
|
+
else:
|
|
546
|
+
# Check if it's a simple primitive type with no other attributes
|
|
547
|
+
other_keys = set(ct.keys()) - {'type'}
|
|
548
|
+
if not other_keys:
|
|
549
|
+
# Simple type - use just the type name
|
|
550
|
+
union_elements.append(type_val)
|
|
551
|
+
else:
|
|
552
|
+
# Type with constraints - keep full schema
|
|
553
|
+
union_elements.append(ct)
|
|
554
|
+
else:
|
|
555
|
+
# Keep as-is for other types
|
|
556
|
+
union_elements.append(ct)
|
|
557
|
+
return {'type': union_elements}
|
|
558
|
+
elif len(converted_types) == 1:
|
|
559
|
+
return converted_types[0]
|
|
560
|
+
|
|
561
|
+
# Single child
|
|
562
|
+
return self._convert_type(children[0], context_name)
|
|
563
|
+
|
|
564
|
+
def _convert_typename(self, typename_node: Typename) -> Dict[str, Any]:
|
|
565
|
+
"""Convert a Typename node."""
|
|
566
|
+
if not hasattr(typename_node, 'name'):
|
|
567
|
+
return {'type': 'any'}
|
|
568
|
+
|
|
569
|
+
type_name = typename_node.name
|
|
570
|
+
|
|
571
|
+
# Check for unwrap operator (~)
|
|
572
|
+
has_unwrap = self._has_unwrap_operator(typename_node)
|
|
573
|
+
|
|
574
|
+
# Check if this is a type parameter that's currently bound
|
|
575
|
+
if type_name in self.current_generic_bindings:
|
|
576
|
+
result = dict(self.current_generic_bindings[type_name])
|
|
577
|
+
if has_unwrap:
|
|
578
|
+
result = self._apply_unwrap(result)
|
|
579
|
+
return result
|
|
580
|
+
|
|
581
|
+
# Check if it's a primitive type
|
|
582
|
+
if type_name in CDDL_PRIMITIVE_TYPES:
|
|
583
|
+
return dict(CDDL_PRIMITIVE_TYPES[type_name])
|
|
584
|
+
|
|
585
|
+
# Check for generic arguments
|
|
586
|
+
generic_args = self._extract_generic_arguments(typename_node)
|
|
587
|
+
|
|
588
|
+
# Check if this is a reference to a generic type with arguments
|
|
589
|
+
if generic_args and type_name in self.generic_types:
|
|
590
|
+
result = self._instantiate_generic_type(type_name, generic_args)
|
|
591
|
+
if has_unwrap:
|
|
592
|
+
result = self._apply_unwrap(result)
|
|
593
|
+
return result
|
|
594
|
+
|
|
595
|
+
# Check if it's a reference to another type
|
|
596
|
+
normalized_name = avro_name(type_name)
|
|
597
|
+
ref_result: Dict[str, Any]
|
|
598
|
+
if type_name in self.type_registry or normalized_name in self.definitions:
|
|
599
|
+
ref_result = {'$ref': f'#/definitions/{normalized_name}'}
|
|
600
|
+
else:
|
|
601
|
+
# Unknown type - treat as reference
|
|
602
|
+
ref_result = {'$ref': f'#/definitions/{normalized_name}'}
|
|
603
|
+
|
|
604
|
+
if has_unwrap:
|
|
605
|
+
ref_result = self._apply_unwrap(ref_result)
|
|
606
|
+
return ref_result
|
|
607
|
+
|
|
608
|
+
def _has_unwrap_operator(self, typename_node: Any) -> bool:
|
|
609
|
+
"""Check if a typename node has the unwrap operator (~)."""
|
|
610
|
+
# Check for 'unwrapped' attribute which contains the TILDE token
|
|
611
|
+
if hasattr(typename_node, 'unwrapped') and typename_node.unwrapped is not None:
|
|
612
|
+
return True
|
|
613
|
+
# Fallback: check children for TILDE token
|
|
614
|
+
if hasattr(typename_node, 'getChildren'):
|
|
615
|
+
for child in typename_node.getChildren():
|
|
616
|
+
if hasattr(child, 'kind') and 'TILDE' in str(child.kind):
|
|
617
|
+
return True
|
|
618
|
+
return False
|
|
619
|
+
|
|
620
|
+
def _apply_unwrap(self, type_def: Dict[str, Any]) -> Dict[str, Any]:
|
|
621
|
+
"""
|
|
622
|
+
Apply the unwrap operator (~) to a type.
|
|
623
|
+
|
|
624
|
+
The unwrap operator extracts the content from a group or array.
|
|
625
|
+
In JSON Structure, we mark this with a special annotation.
|
|
626
|
+
"""
|
|
627
|
+
# For referenced types, we need to resolve and unwrap
|
|
628
|
+
if '$ref' in type_def:
|
|
629
|
+
ref_name = type_def['$ref'].split('/')[-1]
|
|
630
|
+
if ref_name in self.definitions:
|
|
631
|
+
resolved = self.definitions[ref_name]
|
|
632
|
+
# If it's an object, return its properties inline
|
|
633
|
+
if resolved.get('type') == 'object' and 'properties' in resolved:
|
|
634
|
+
return dict(resolved)
|
|
635
|
+
# If it's an array, return items type
|
|
636
|
+
if resolved.get('type') == 'array' and 'items' in resolved:
|
|
637
|
+
return resolved['items']
|
|
638
|
+
|
|
639
|
+
# For inline types, just return as-is (unwrap is contextual)
|
|
640
|
+
return type_def
|
|
641
|
+
|
|
642
|
+
def _extract_generic_parameters(self, typename_node: Any) -> List[str]:
|
|
643
|
+
"""Extract generic type parameters from a typename node (e.g., optional<T> -> ['T'])."""
|
|
644
|
+
params = []
|
|
645
|
+
if hasattr(typename_node, 'getChildren'):
|
|
646
|
+
for child in typename_node.getChildren():
|
|
647
|
+
if type(child).__name__ == 'GenericParameters':
|
|
648
|
+
for pc in child.getChildren():
|
|
649
|
+
if type(pc).__name__ == 'Typename' and hasattr(pc, 'name'):
|
|
650
|
+
params.append(pc.name)
|
|
651
|
+
return params
|
|
652
|
+
|
|
653
|
+
def _extract_generic_arguments(self, typename_node: Any) -> List[Any]:
|
|
654
|
+
"""Extract generic type arguments from a typename node.
|
|
655
|
+
|
|
656
|
+
Returns AST nodes for the arguments so they can be properly converted
|
|
657
|
+
with current bindings in _convert_typename.
|
|
658
|
+
"""
|
|
659
|
+
args = []
|
|
660
|
+
if hasattr(typename_node, 'getChildren'):
|
|
661
|
+
for child in typename_node.getChildren():
|
|
662
|
+
if type(child).__name__ == 'GenericArguments':
|
|
663
|
+
for ac in child.getChildren():
|
|
664
|
+
# Return the AST node itself for proper conversion
|
|
665
|
+
args.append(ac)
|
|
666
|
+
return args
|
|
667
|
+
|
|
668
|
+
def _resolve_generic_argument(self, arg_node: Any) -> Dict[str, Any]:
|
|
669
|
+
"""Resolve a generic argument node to a JSON Structure type."""
|
|
670
|
+
if type(arg_node).__name__ == 'Typename':
|
|
671
|
+
if hasattr(arg_node, 'name'):
|
|
672
|
+
arg_name = arg_node.name
|
|
673
|
+
# Check if it's a bound type parameter
|
|
674
|
+
if arg_name in self.current_generic_bindings:
|
|
675
|
+
return dict(self.current_generic_bindings[arg_name])
|
|
676
|
+
# Check for primitives
|
|
677
|
+
if arg_name in CDDL_PRIMITIVE_TYPES:
|
|
678
|
+
return dict(CDDL_PRIMITIVE_TYPES[arg_name])
|
|
679
|
+
# Otherwise it's a reference to a defined type
|
|
680
|
+
normalized_name = avro_name(arg_name)
|
|
681
|
+
return {'$ref': f'#/definitions/{normalized_name}'}
|
|
682
|
+
# Fallback: try to convert it
|
|
683
|
+
return self._convert_type(arg_node, '')
|
|
684
|
+
|
|
685
|
+
def _instantiate_generic_type(
|
|
686
|
+
self, type_name: str, type_arg_nodes: List[Any]
|
|
687
|
+
) -> Dict[str, Any]:
|
|
688
|
+
"""
|
|
689
|
+
Instantiate a generic type with concrete type arguments.
|
|
690
|
+
|
|
691
|
+
Args:
|
|
692
|
+
type_name: Name of the generic type (e.g., 'optional', 'pair')
|
|
693
|
+
type_arg_nodes: AST nodes for the type arguments
|
|
694
|
+
|
|
695
|
+
Returns:
|
|
696
|
+
JSON Structure type definition with type arguments substituted
|
|
697
|
+
|
|
698
|
+
Raises:
|
|
699
|
+
CddlCycleError: If a circular generic instantiation is detected
|
|
700
|
+
"""
|
|
701
|
+
generic_info = self.generic_types.get(type_name)
|
|
702
|
+
if not generic_info:
|
|
703
|
+
return {'type': 'any'}
|
|
704
|
+
|
|
705
|
+
# Check for cycles in generic instantiation
|
|
706
|
+
if type_name in self._conversion_stack:
|
|
707
|
+
cycle_start = self._conversion_stack.index(type_name)
|
|
708
|
+
cycle_path = self._conversion_stack[cycle_start:] + [type_name]
|
|
709
|
+
logger.warning(
|
|
710
|
+
"Circular generic instantiation detected: %s", ' -> '.join(cycle_path)
|
|
711
|
+
)
|
|
712
|
+
# Return a reference instead of raising to allow recursive types
|
|
713
|
+
normalized_name = avro_name(type_name)
|
|
714
|
+
return {'$ref': f'#/definitions/{normalized_name}'}
|
|
715
|
+
|
|
716
|
+
params = generic_info.get('params', [])
|
|
717
|
+
type_node = generic_info.get('type_node')
|
|
718
|
+
|
|
719
|
+
if not type_node or len(params) != len(type_arg_nodes):
|
|
720
|
+
# Parameter count mismatch - return reference
|
|
721
|
+
normalized_name = avro_name(type_name)
|
|
722
|
+
return {'$ref': f'#/definitions/{normalized_name}'}
|
|
723
|
+
|
|
724
|
+
# Resolve type arguments with current bindings before creating new bindings
|
|
725
|
+
resolved_args = [self._resolve_generic_argument(arg) for arg in type_arg_nodes]
|
|
726
|
+
|
|
727
|
+
# Create bindings for type parameters
|
|
728
|
+
old_bindings = self.current_generic_bindings.copy()
|
|
729
|
+
for param, arg in zip(params, resolved_args):
|
|
730
|
+
self.current_generic_bindings[param] = arg
|
|
731
|
+
|
|
732
|
+
# Push to conversion stack for cycle detection
|
|
733
|
+
self._conversion_stack.append(type_name)
|
|
734
|
+
try:
|
|
735
|
+
# Convert the type with the current bindings
|
|
736
|
+
result = self._convert_type(type_node, type_name)
|
|
737
|
+
return result
|
|
738
|
+
finally:
|
|
739
|
+
# Pop from conversion stack and restore previous bindings
|
|
740
|
+
self._conversion_stack.pop()
|
|
741
|
+
self.current_generic_bindings = old_bindings
|
|
742
|
+
|
|
743
|
+
def _convert_map(self, map_node: Map, context_name: str = '') -> Dict[str, Any]:
|
|
744
|
+
"""Convert a Map node to JSON Structure object or map."""
|
|
745
|
+
properties: Dict[str, Any] = {}
|
|
746
|
+
required: List[str] = []
|
|
747
|
+
extends_refs: List[str] = []
|
|
748
|
+
computed_key_info: Optional[Dict[str, Any]] = None
|
|
749
|
+
|
|
750
|
+
# Process map contents
|
|
751
|
+
if hasattr(map_node, 'getChildren'):
|
|
752
|
+
for child in map_node.getChildren():
|
|
753
|
+
child_type = type(child).__name__
|
|
754
|
+
if child_type == 'GroupChoice':
|
|
755
|
+
computed_key_info = self._process_group_choice_for_object(
|
|
756
|
+
child, properties, required, extends_refs, context_name
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
# If we have a computed key (like * tstr => int) and no explicit properties,
|
|
760
|
+
# this is a JSON Structure map type
|
|
761
|
+
if computed_key_info and not properties and not extends_refs:
|
|
762
|
+
result: Dict[str, Any] = {'type': 'map'}
|
|
763
|
+
if computed_key_info.get('keys'):
|
|
764
|
+
result['keys'] = computed_key_info['keys']
|
|
765
|
+
if computed_key_info.get('values'):
|
|
766
|
+
result['values'] = computed_key_info['values']
|
|
767
|
+
return result
|
|
768
|
+
|
|
769
|
+
# Otherwise it's a regular object
|
|
770
|
+
result = {'type': 'object'}
|
|
771
|
+
if extends_refs:
|
|
772
|
+
# Use $extends for unwrapped types - only first one supported
|
|
773
|
+
# JSON Structure only supports single inheritance via $extends
|
|
774
|
+
result['$extends'] = extends_refs[0]
|
|
775
|
+
if len(extends_refs) > 1:
|
|
776
|
+
logger.warning(
|
|
777
|
+
"Multiple unwrap operators found, only using first: %s. "
|
|
778
|
+
"JSON Structure $extends only supports single inheritance.",
|
|
779
|
+
extends_refs[0]
|
|
780
|
+
)
|
|
781
|
+
if properties:
|
|
782
|
+
result['properties'] = properties
|
|
783
|
+
if required:
|
|
784
|
+
result['required'] = required
|
|
785
|
+
|
|
786
|
+
return result
|
|
787
|
+
|
|
788
|
+
def _process_group_choice_for_object(
|
|
789
|
+
self,
|
|
790
|
+
group_choice: CDDLNode,
|
|
791
|
+
properties: Dict[str, Any],
|
|
792
|
+
required: List[str],
|
|
793
|
+
extends_refs: List[str],
|
|
794
|
+
context_name: str
|
|
795
|
+
) -> Optional[Dict[str, Any]]:
|
|
796
|
+
"""Process GroupChoice for object properties.
|
|
797
|
+
|
|
798
|
+
Returns computed key info if a computed key entry (like * tstr => int) is found.
|
|
799
|
+
"""
|
|
800
|
+
if not hasattr(group_choice, 'getChildren'):
|
|
801
|
+
return None
|
|
802
|
+
|
|
803
|
+
computed_key_info: Optional[Dict[str, Any]] = None
|
|
804
|
+
|
|
805
|
+
for child in group_choice.getChildren():
|
|
806
|
+
child_type = type(child).__name__
|
|
807
|
+
if child_type == 'GroupEntry':
|
|
808
|
+
entry_computed = self._process_group_entry_for_object(
|
|
809
|
+
child, properties, required, extends_refs, context_name
|
|
810
|
+
)
|
|
811
|
+
if entry_computed:
|
|
812
|
+
computed_key_info = entry_computed
|
|
813
|
+
elif child_type == 'GroupChoice':
|
|
814
|
+
# Nested group choice
|
|
815
|
+
nested_computed = self._process_group_choice_for_object(
|
|
816
|
+
child, properties, required, extends_refs, context_name
|
|
817
|
+
)
|
|
818
|
+
if nested_computed:
|
|
819
|
+
computed_key_info = nested_computed
|
|
820
|
+
|
|
821
|
+
return computed_key_info
|
|
822
|
+
|
|
823
|
+
def _process_group_entry_for_object(
|
|
824
|
+
self,
|
|
825
|
+
entry: CDDLNode,
|
|
826
|
+
properties: Dict[str, Any],
|
|
827
|
+
required: List[str],
|
|
828
|
+
extends_refs: List[str],
|
|
829
|
+
context_name: str
|
|
830
|
+
) -> Optional[Dict[str, Any]]:
|
|
831
|
+
"""Process a GroupEntry for object properties.
|
|
832
|
+
|
|
833
|
+
Returns computed key info if this is a computed key entry (like * tstr => int).
|
|
834
|
+
"""
|
|
835
|
+
if not hasattr(entry, 'getChildren'):
|
|
836
|
+
return None
|
|
837
|
+
|
|
838
|
+
children = entry.getChildren()
|
|
839
|
+
|
|
840
|
+
# Parse occurrence, memberkey, and type from children
|
|
841
|
+
occurrence_indicator = None
|
|
842
|
+
member_key = None
|
|
843
|
+
member_type = None
|
|
844
|
+
unwrap_typename = None
|
|
845
|
+
|
|
846
|
+
for child in children:
|
|
847
|
+
child_type = type(child).__name__
|
|
848
|
+
if child_type == 'Occurrence':
|
|
849
|
+
occurrence_indicator = self._parse_occurrence(child)
|
|
850
|
+
elif child_type == 'Memberkey':
|
|
851
|
+
member_key = self._parse_memberkey(child)
|
|
852
|
+
elif child_type == 'Typename':
|
|
853
|
+
# Check if this is an unwrap operator usage (~typename)
|
|
854
|
+
if self._has_unwrap_operator(child):
|
|
855
|
+
unwrap_typename = child
|
|
856
|
+
else:
|
|
857
|
+
member_type = self._convert_type(child, context_name)
|
|
858
|
+
elif child_type == 'Type':
|
|
859
|
+
# Check if this Type contains an unwrapped Typename
|
|
860
|
+
if hasattr(child, 'getChildren'):
|
|
861
|
+
type_children = child.getChildren()
|
|
862
|
+
if len(type_children) == 1 and type(type_children[0]).__name__ == 'Typename':
|
|
863
|
+
typename = type_children[0]
|
|
864
|
+
if self._has_unwrap_operator(typename):
|
|
865
|
+
unwrap_typename = typename
|
|
866
|
+
continue
|
|
867
|
+
member_type = self._convert_type(child, context_name)
|
|
868
|
+
elif child_type in ('Map', 'Array', 'Group', 'Value'):
|
|
869
|
+
member_type = self._convert_type(child, context_name)
|
|
870
|
+
|
|
871
|
+
# Handle unwrap operator (~) - use $extends to reference the base type
|
|
872
|
+
if unwrap_typename is not None and member_key is None:
|
|
873
|
+
ref_name = getattr(unwrap_typename, 'name', None)
|
|
874
|
+
if ref_name:
|
|
875
|
+
normalized_ref = avro_name(ref_name)
|
|
876
|
+
# Add $ref to extends_refs for $extends
|
|
877
|
+
ref = f"#/definitions/{normalized_ref}"
|
|
878
|
+
if ref not in extends_refs:
|
|
879
|
+
extends_refs.append(ref)
|
|
880
|
+
return None
|
|
881
|
+
|
|
882
|
+
if member_key is None:
|
|
883
|
+
return None
|
|
884
|
+
|
|
885
|
+
prop_name = member_key.get('name', '')
|
|
886
|
+
original_name = member_key.get('original_name', prop_name)
|
|
887
|
+
is_computed = member_key.get('computed', False)
|
|
888
|
+
|
|
889
|
+
# Handle computed keys (patterns like * tstr => int)
|
|
890
|
+
if is_computed:
|
|
891
|
+
key_type_name = member_key.get('key_type', 'string')
|
|
892
|
+
# Map CDDL key type to JSON Structure type
|
|
893
|
+
if key_type_name in ('tstr', 'text'):
|
|
894
|
+
keys_type = {'type': 'string'}
|
|
895
|
+
elif key_type_name in ('bstr', 'bytes'):
|
|
896
|
+
keys_type = {'type': 'binary'}
|
|
897
|
+
elif key_type_name in ('int', 'uint', 'nint'):
|
|
898
|
+
keys_type = {'type': 'int64'}
|
|
899
|
+
else:
|
|
900
|
+
keys_type = {'type': 'string'}
|
|
901
|
+
|
|
902
|
+
values_type = member_type if member_type else {'type': 'any'}
|
|
903
|
+
|
|
904
|
+
return {
|
|
905
|
+
'keys': keys_type,
|
|
906
|
+
'values': values_type
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if not prop_name:
|
|
910
|
+
return None
|
|
911
|
+
|
|
912
|
+
normalized_name = avro_name(prop_name)
|
|
913
|
+
|
|
914
|
+
if member_type:
|
|
915
|
+
prop_schema = member_type.copy() if isinstance(member_type, dict) else member_type
|
|
916
|
+
else:
|
|
917
|
+
prop_schema = {'type': 'any'}
|
|
918
|
+
|
|
919
|
+
# Handle occurrence indicators
|
|
920
|
+
if occurrence_indicator:
|
|
921
|
+
if occurrence_indicator.get('optional'):
|
|
922
|
+
# Optional field - no need to add to required
|
|
923
|
+
pass
|
|
924
|
+
elif occurrence_indicator.get('min', 1) >= 1:
|
|
925
|
+
required.append(normalized_name)
|
|
926
|
+
|
|
927
|
+
# Handle array occurrences (* or +)
|
|
928
|
+
if occurrence_indicator.get('array'):
|
|
929
|
+
prop_schema = {
|
|
930
|
+
'type': 'array',
|
|
931
|
+
'items': prop_schema
|
|
932
|
+
}
|
|
933
|
+
if occurrence_indicator.get('min'):
|
|
934
|
+
prop_schema['minItems'] = occurrence_indicator['min']
|
|
935
|
+
if occurrence_indicator.get('max'):
|
|
936
|
+
prop_schema['maxItems'] = occurrence_indicator['max']
|
|
937
|
+
else:
|
|
938
|
+
# No occurrence indicator means required
|
|
939
|
+
required.append(normalized_name)
|
|
940
|
+
|
|
941
|
+
# Add altnames if original name differs
|
|
942
|
+
if original_name != normalized_name:
|
|
943
|
+
if isinstance(prop_schema, dict):
|
|
944
|
+
prop_schema['altnames'] = {'cddl': original_name}
|
|
945
|
+
|
|
946
|
+
# Add description if present in member_key or occurrence indicator
|
|
947
|
+
if isinstance(prop_schema, dict):
|
|
948
|
+
desc = member_key.get('description')
|
|
949
|
+
if not desc and occurrence_indicator:
|
|
950
|
+
desc = occurrence_indicator.get('description')
|
|
951
|
+
if desc:
|
|
952
|
+
prop_schema['description'] = desc
|
|
953
|
+
|
|
954
|
+
properties[normalized_name] = prop_schema
|
|
955
|
+
return None
|
|
956
|
+
|
|
957
|
+
def _parse_occurrence(self, occurrence: CDDLNode) -> Dict[str, Any]:
|
|
958
|
+
"""Parse an Occurrence node."""
|
|
959
|
+
result: Dict[str, Any] = {}
|
|
960
|
+
|
|
961
|
+
if not hasattr(occurrence, 'getChildren'):
|
|
962
|
+
return {'optional': True}
|
|
963
|
+
|
|
964
|
+
# Check for comments on the occurrence tokens
|
|
965
|
+
if hasattr(occurrence, 'tokens') and occurrence.tokens:
|
|
966
|
+
for token in occurrence.tokens:
|
|
967
|
+
if hasattr(token, 'comments') and token.comments:
|
|
968
|
+
description = self._extract_description_from_comments(token.comments)
|
|
969
|
+
if description:
|
|
970
|
+
result['description'] = description
|
|
971
|
+
break
|
|
972
|
+
|
|
973
|
+
for child in occurrence.getChildren():
|
|
974
|
+
# Check for occurrence tokens
|
|
975
|
+
if hasattr(child, 'kind'):
|
|
976
|
+
token_kind = str(child.kind)
|
|
977
|
+
if 'QUEST' in token_kind: # ?
|
|
978
|
+
result['optional'] = True
|
|
979
|
+
elif 'ASTERISK' in token_kind: # *
|
|
980
|
+
result['array'] = True
|
|
981
|
+
result['min'] = 0
|
|
982
|
+
elif 'PLUS' in token_kind: # +
|
|
983
|
+
result['array'] = True
|
|
984
|
+
result['min'] = 1
|
|
985
|
+
elif hasattr(child, 'value'):
|
|
986
|
+
# Handle numeric occurrences like n*m
|
|
987
|
+
pass
|
|
988
|
+
|
|
989
|
+
if not result or ('optional' not in result and 'array' not in result):
|
|
990
|
+
result['optional'] = True
|
|
991
|
+
|
|
992
|
+
return result
|
|
993
|
+
|
|
994
|
+
def _extract_description_from_comments(self, comments: List[Any]) -> Optional[str]:
|
|
995
|
+
"""Extract description from a list of comment tokens."""
|
|
996
|
+
if not comments:
|
|
997
|
+
return None
|
|
998
|
+
|
|
999
|
+
comment_lines = []
|
|
1000
|
+
for comment in comments:
|
|
1001
|
+
if hasattr(comment, 'literal'):
|
|
1002
|
+
text = comment.literal.strip()
|
|
1003
|
+
if text.startswith(';'):
|
|
1004
|
+
text = text[1:].strip()
|
|
1005
|
+
if text:
|
|
1006
|
+
comment_lines.append(text)
|
|
1007
|
+
|
|
1008
|
+
return ' '.join(comment_lines) if comment_lines else None
|
|
1009
|
+
|
|
1010
|
+
def _parse_memberkey(self, memberkey: CDDLNode) -> Optional[Dict[str, Any]]:
|
|
1011
|
+
"""Parse a Memberkey node."""
|
|
1012
|
+
if not hasattr(memberkey, 'getChildren'):
|
|
1013
|
+
return None
|
|
1014
|
+
|
|
1015
|
+
result: Dict[str, Any] = {}
|
|
1016
|
+
|
|
1017
|
+
for child in memberkey.getChildren():
|
|
1018
|
+
child_type = type(child).__name__
|
|
1019
|
+
if child_type == 'Typename':
|
|
1020
|
+
if hasattr(child, 'name'):
|
|
1021
|
+
name = child.name
|
|
1022
|
+
# Check if this is a computed key (like tstr => any)
|
|
1023
|
+
if name in ('tstr', 'text', 'bstr', 'bytes', 'int', 'uint'):
|
|
1024
|
+
result['computed'] = True
|
|
1025
|
+
result['key_type'] = name
|
|
1026
|
+
else:
|
|
1027
|
+
result['name'] = avro_name(name)
|
|
1028
|
+
result['original_name'] = name
|
|
1029
|
+
# Extract description from comments on the typename
|
|
1030
|
+
description = self._extract_description(child)
|
|
1031
|
+
if description:
|
|
1032
|
+
result['description'] = description
|
|
1033
|
+
elif child_type == 'Value':
|
|
1034
|
+
# Could be string key or integer key
|
|
1035
|
+
if hasattr(child, 'value'):
|
|
1036
|
+
value = child.value
|
|
1037
|
+
# Remove quotes if present (string key)
|
|
1038
|
+
if value.startswith('"') and value.endswith('"'):
|
|
1039
|
+
value = value[1:-1]
|
|
1040
|
+
result['name'] = avro_name(value)
|
|
1041
|
+
result['original_name'] = value
|
|
1042
|
+
else:
|
|
1043
|
+
# Try to parse as integer key
|
|
1044
|
+
try:
|
|
1045
|
+
int_key = int(value)
|
|
1046
|
+
result['name'] = f'_{value}'
|
|
1047
|
+
result['original_name'] = value
|
|
1048
|
+
result['integer_key'] = int_key
|
|
1049
|
+
except ValueError:
|
|
1050
|
+
result['name'] = avro_name(value)
|
|
1051
|
+
result['original_name'] = value
|
|
1052
|
+
|
|
1053
|
+
return result if result else None
|
|
1054
|
+
|
|
1055
|
+
def _convert_array(self, array_node: Array, context_name: str = '') -> Dict[str, Any]:
|
|
1056
|
+
"""Convert an Array node to JSON Structure array or tuple."""
|
|
1057
|
+
items_types: List[Dict[str, Any]] = []
|
|
1058
|
+
is_tuple = False
|
|
1059
|
+
|
|
1060
|
+
if hasattr(array_node, 'getChildren'):
|
|
1061
|
+
for child in array_node.getChildren():
|
|
1062
|
+
child_type = type(child).__name__
|
|
1063
|
+
if child_type == 'GroupChoice':
|
|
1064
|
+
# Get the items types from the group
|
|
1065
|
+
items_types, is_tuple = self._get_array_items_types(child, context_name)
|
|
1066
|
+
elif child_type in ('Type', 'Typename'):
|
|
1067
|
+
items_types = [self._convert_type(child, context_name)]
|
|
1068
|
+
|
|
1069
|
+
if is_tuple and len(items_types) > 1:
|
|
1070
|
+
# Fixed-length array with different types = tuple
|
|
1071
|
+
# Use the JSON Structure tuple format
|
|
1072
|
+
properties: Dict[str, Any] = {}
|
|
1073
|
+
tuple_order: List[str] = []
|
|
1074
|
+
for idx, item_type in enumerate(items_types):
|
|
1075
|
+
prop_name = f"_{idx}"
|
|
1076
|
+
properties[prop_name] = item_type
|
|
1077
|
+
tuple_order.append(prop_name)
|
|
1078
|
+
return {
|
|
1079
|
+
'type': 'tuple',
|
|
1080
|
+
'properties': properties,
|
|
1081
|
+
'tuple': tuple_order
|
|
1082
|
+
}
|
|
1083
|
+
elif items_types:
|
|
1084
|
+
# Regular array
|
|
1085
|
+
if len(items_types) == 1:
|
|
1086
|
+
items_type = items_types[0]
|
|
1087
|
+
# Extract inline compound types to definitions
|
|
1088
|
+
items_type = self._create_inline_definition(items_type, f"{context_name}_item")
|
|
1089
|
+
else:
|
|
1090
|
+
# Multiple types that aren't a tuple - use union for items
|
|
1091
|
+
# Extract each inline compound type to definitions
|
|
1092
|
+
extracted_types = [
|
|
1093
|
+
self._create_inline_definition(t, f"{context_name}_item")
|
|
1094
|
+
for t in items_types
|
|
1095
|
+
]
|
|
1096
|
+
items_type = {'type': extracted_types}
|
|
1097
|
+
return {
|
|
1098
|
+
'type': 'array',
|
|
1099
|
+
'items': items_type
|
|
1100
|
+
}
|
|
1101
|
+
else:
|
|
1102
|
+
return {
|
|
1103
|
+
'type': 'array',
|
|
1104
|
+
'items': {'type': 'any'}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
def _get_array_items_types(
|
|
1108
|
+
self, group_choice: CDDLNode, context_name: str
|
|
1109
|
+
) -> Tuple[List[Dict[str, Any]], bool]:
|
|
1110
|
+
"""Extract items types from a GroupChoice in an array context.
|
|
1111
|
+
|
|
1112
|
+
Returns:
|
|
1113
|
+
Tuple of (list of item types, is_tuple flag)
|
|
1114
|
+
is_tuple is True if this looks like a fixed-size tuple definition
|
|
1115
|
+
"""
|
|
1116
|
+
if not hasattr(group_choice, 'getChildren'):
|
|
1117
|
+
return [{'type': 'any'}], False
|
|
1118
|
+
|
|
1119
|
+
children = group_choice.getChildren()
|
|
1120
|
+
if not children:
|
|
1121
|
+
return [{'type': 'any'}], False
|
|
1122
|
+
|
|
1123
|
+
# Collect all types from group entries
|
|
1124
|
+
types = []
|
|
1125
|
+
all_entries_are_single = True
|
|
1126
|
+
|
|
1127
|
+
for child in children:
|
|
1128
|
+
child_type = type(child).__name__
|
|
1129
|
+
if child_type == 'GroupEntry':
|
|
1130
|
+
entry_type = self._get_group_entry_type(child, context_name)
|
|
1131
|
+
if entry_type:
|
|
1132
|
+
types.append(entry_type)
|
|
1133
|
+
# Check if entry has occurrence markers (*, +, ?)
|
|
1134
|
+
if self._has_occurrence_marker(child):
|
|
1135
|
+
all_entries_are_single = False
|
|
1136
|
+
elif child_type in ('Type', 'Typename'):
|
|
1137
|
+
types.append(self._convert_type(child, context_name))
|
|
1138
|
+
|
|
1139
|
+
# It's a tuple if we have multiple distinct types AND no occurrence markers
|
|
1140
|
+
is_tuple = len(types) > 1 and all_entries_are_single
|
|
1141
|
+
|
|
1142
|
+
return types, is_tuple
|
|
1143
|
+
|
|
1144
|
+
def _has_occurrence_marker(self, entry: CDDLNode) -> bool:
|
|
1145
|
+
"""Check if a GroupEntry has occurrence markers (*, +, ?)."""
|
|
1146
|
+
if hasattr(entry, 'getChildren'):
|
|
1147
|
+
for child in entry.getChildren():
|
|
1148
|
+
if type(child).__name__ == 'Occurrence':
|
|
1149
|
+
return True
|
|
1150
|
+
return False
|
|
1151
|
+
|
|
1152
|
+
def _get_group_entry_type(self, entry: CDDLNode, context_name: str) -> Optional[Dict[str, Any]]:
|
|
1153
|
+
"""Get the type from a GroupEntry."""
|
|
1154
|
+
if not hasattr(entry, 'getChildren'):
|
|
1155
|
+
return None
|
|
1156
|
+
|
|
1157
|
+
for child in entry.getChildren():
|
|
1158
|
+
child_type = type(child).__name__
|
|
1159
|
+
if child_type in ('Type', 'Typename', 'Map', 'Array', 'Group', 'Value'):
|
|
1160
|
+
return self._convert_type(child, context_name)
|
|
1161
|
+
|
|
1162
|
+
return None
|
|
1163
|
+
|
|
1164
|
+
def _convert_group(self, group_node: CDDLNode, context_name: str = '') -> Dict[str, Any]:
|
|
1165
|
+
"""Convert a Group node."""
|
|
1166
|
+
# Check if this is an integer-keyed group (should become a tuple)
|
|
1167
|
+
int_key_entries = self._extract_integer_key_entries(group_node)
|
|
1168
|
+
if int_key_entries:
|
|
1169
|
+
return self._convert_to_tuple(int_key_entries, context_name)
|
|
1170
|
+
|
|
1171
|
+
# Check if this is a sequence of unnamed entries (should become a tuple)
|
|
1172
|
+
unnamed_entries = self._extract_unnamed_entries(group_node)
|
|
1173
|
+
if unnamed_entries:
|
|
1174
|
+
# Convert to tuple format - assign sequential indices
|
|
1175
|
+
indexed_entries = [(i, False, entry) for i, entry in enumerate(unnamed_entries)]
|
|
1176
|
+
return self._convert_to_tuple(indexed_entries, context_name)
|
|
1177
|
+
|
|
1178
|
+
# Otherwise, treat as an object
|
|
1179
|
+
return self._convert_map_like(group_node, context_name)
|
|
1180
|
+
|
|
1181
|
+
def _extract_unnamed_entries(self, node: Any) -> Optional[List[Any]]:
|
|
1182
|
+
"""Extract unnamed entries from a group (entries with only types, no member keys)."""
|
|
1183
|
+
entries: List[Any] = []
|
|
1184
|
+
has_named = False
|
|
1185
|
+
|
|
1186
|
+
if not hasattr(node, 'getChildren'):
|
|
1187
|
+
return None
|
|
1188
|
+
|
|
1189
|
+
for child in node.getChildren():
|
|
1190
|
+
if type(child).__name__ == 'GroupChoice':
|
|
1191
|
+
for gc_child in child.getChildren():
|
|
1192
|
+
if type(gc_child).__name__ == 'GroupEntry':
|
|
1193
|
+
has_memberkey = False
|
|
1194
|
+
type_node = None
|
|
1195
|
+
for entry_child in gc_child.getChildren():
|
|
1196
|
+
child_type = type(entry_child).__name__
|
|
1197
|
+
if child_type == 'Memberkey':
|
|
1198
|
+
has_memberkey = True
|
|
1199
|
+
elif child_type in ('Type', 'Typename'):
|
|
1200
|
+
type_node = entry_child
|
|
1201
|
+
if has_memberkey:
|
|
1202
|
+
has_named = True
|
|
1203
|
+
elif type_node:
|
|
1204
|
+
entries.append(type_node)
|
|
1205
|
+
|
|
1206
|
+
# Only return if all entries are unnamed
|
|
1207
|
+
if entries and not has_named:
|
|
1208
|
+
return entries
|
|
1209
|
+
return None
|
|
1210
|
+
|
|
1211
|
+
def _extract_integer_key_entries(self, node: Any) -> Optional[List[Tuple[int, bool, Any]]]:
|
|
1212
|
+
"""
|
|
1213
|
+
Extract integer-keyed entries from a group.
|
|
1214
|
+
Returns list of (key, is_optional, type_node) tuples, or None if not integer-keyed.
|
|
1215
|
+
"""
|
|
1216
|
+
entries: List[Tuple[int, bool, Any]] = []
|
|
1217
|
+
|
|
1218
|
+
if not hasattr(node, 'getChildren'):
|
|
1219
|
+
return None
|
|
1220
|
+
|
|
1221
|
+
for child in node.getChildren():
|
|
1222
|
+
if type(child).__name__ == 'GroupChoice':
|
|
1223
|
+
result = self._extract_int_keys_from_group_choice(child)
|
|
1224
|
+
if result is None:
|
|
1225
|
+
return None # Mixed keys, not a pure integer-keyed group
|
|
1226
|
+
entries.extend(result)
|
|
1227
|
+
|
|
1228
|
+
if not entries:
|
|
1229
|
+
return None
|
|
1230
|
+
|
|
1231
|
+
# Sort by key and check for sequential keys starting from 1
|
|
1232
|
+
entries.sort(key=lambda x: x[0])
|
|
1233
|
+
|
|
1234
|
+
return entries
|
|
1235
|
+
|
|
1236
|
+
def _extract_int_keys_from_group_choice(
|
|
1237
|
+
self, group_choice: Any
|
|
1238
|
+
) -> Optional[List[Tuple[int, bool, Any]]]:
|
|
1239
|
+
"""Extract integer keys from a GroupChoice node."""
|
|
1240
|
+
entries: List[Tuple[int, bool, Any]] = []
|
|
1241
|
+
|
|
1242
|
+
if not hasattr(group_choice, 'getChildren'):
|
|
1243
|
+
return None
|
|
1244
|
+
|
|
1245
|
+
for child in group_choice.getChildren():
|
|
1246
|
+
if type(child).__name__ == 'GroupEntry':
|
|
1247
|
+
result = self._extract_int_key_from_entry(child)
|
|
1248
|
+
if result is None:
|
|
1249
|
+
return None # Not an integer key
|
|
1250
|
+
entries.append(result)
|
|
1251
|
+
elif type(child).__name__ == 'GroupChoice':
|
|
1252
|
+
nested = self._extract_int_keys_from_group_choice(child)
|
|
1253
|
+
if nested is None:
|
|
1254
|
+
return None
|
|
1255
|
+
entries.extend(nested)
|
|
1256
|
+
|
|
1257
|
+
return entries
|
|
1258
|
+
|
|
1259
|
+
def _extract_int_key_from_entry(self, entry: Any) -> Optional[Tuple[int, bool, Any]]:
|
|
1260
|
+
"""Extract integer key, optionality, and type from a GroupEntry."""
|
|
1261
|
+
if not hasattr(entry, 'getChildren'):
|
|
1262
|
+
return None
|
|
1263
|
+
|
|
1264
|
+
children = entry.getChildren()
|
|
1265
|
+
is_optional = False
|
|
1266
|
+
int_key = None
|
|
1267
|
+
type_node = None
|
|
1268
|
+
|
|
1269
|
+
for child in children:
|
|
1270
|
+
child_type = type(child).__name__
|
|
1271
|
+
if child_type == 'Occurrence':
|
|
1272
|
+
# Check for optional marker
|
|
1273
|
+
is_optional = self._is_optional_occurrence(child)
|
|
1274
|
+
elif child_type == 'Memberkey':
|
|
1275
|
+
int_key = self._get_integer_key(child)
|
|
1276
|
+
if int_key is None:
|
|
1277
|
+
return None # Not an integer key
|
|
1278
|
+
elif child_type in ('Type', 'Typename', 'Map', 'Array', 'Group', 'Value'):
|
|
1279
|
+
type_node = child
|
|
1280
|
+
|
|
1281
|
+
if int_key is not None and type_node is not None:
|
|
1282
|
+
return (int_key, is_optional, type_node)
|
|
1283
|
+
|
|
1284
|
+
return None
|
|
1285
|
+
|
|
1286
|
+
def _is_optional_occurrence(self, occurrence: Any) -> bool:
|
|
1287
|
+
"""Check if an occurrence node indicates optional (?)."""
|
|
1288
|
+
# The ? marker means n=0, m=1 (zero or one)
|
|
1289
|
+
# Check the n attribute - if n=0, it's optional
|
|
1290
|
+
if hasattr(occurrence, 'n') and occurrence.n == 0:
|
|
1291
|
+
return True
|
|
1292
|
+
# Fallback: check tokens for QUEST
|
|
1293
|
+
if hasattr(occurrence, 'tokens'):
|
|
1294
|
+
for token in occurrence.tokens:
|
|
1295
|
+
if 'QUEST' in repr(token):
|
|
1296
|
+
return True
|
|
1297
|
+
return False
|
|
1298
|
+
|
|
1299
|
+
def _get_integer_key(self, memberkey: Any) -> Optional[int]:
|
|
1300
|
+
"""Get integer key from a Memberkey node, or None if not an integer key."""
|
|
1301
|
+
if not hasattr(memberkey, 'getChildren'):
|
|
1302
|
+
return None
|
|
1303
|
+
|
|
1304
|
+
for child in memberkey.getChildren():
|
|
1305
|
+
if type(child).__name__ == 'Value' and hasattr(child, 'value'):
|
|
1306
|
+
try:
|
|
1307
|
+
return int(child.value)
|
|
1308
|
+
except ValueError:
|
|
1309
|
+
return None
|
|
1310
|
+
|
|
1311
|
+
return None
|
|
1312
|
+
|
|
1313
|
+
def _convert_to_tuple(
|
|
1314
|
+
self, entries: List[Tuple[int, bool, Any]], context_name: str
|
|
1315
|
+
) -> Dict[str, Any]:
|
|
1316
|
+
"""Convert integer-keyed entries to a tuple type per JSON Structure spec.
|
|
1317
|
+
|
|
1318
|
+
JSON Structure tuples use:
|
|
1319
|
+
- 'properties': named properties with schemas
|
|
1320
|
+
- 'tuple': array of property names defining the order
|
|
1321
|
+
- All declared properties are implicitly REQUIRED
|
|
1322
|
+
|
|
1323
|
+
For optional elements, we omit them from the 'tuple' array but keep them
|
|
1324
|
+
in properties, or we can include them and note that JSON Structure tuples
|
|
1325
|
+
don't support optional elements directly.
|
|
1326
|
+
"""
|
|
1327
|
+
# Sort entries by key
|
|
1328
|
+
entries.sort(key=lambda x: x[0])
|
|
1329
|
+
|
|
1330
|
+
# Build properties map and tuple order array
|
|
1331
|
+
properties: Dict[str, Any] = {}
|
|
1332
|
+
tuple_order: List[str] = []
|
|
1333
|
+
|
|
1334
|
+
for key, is_optional, type_node in entries:
|
|
1335
|
+
# Generate property name from the integer key
|
|
1336
|
+
prop_name = f"_{key}"
|
|
1337
|
+
item_type = self._convert_type(type_node, context_name)
|
|
1338
|
+
|
|
1339
|
+
# Store the original CDDL key as altname
|
|
1340
|
+
if isinstance(item_type, dict):
|
|
1341
|
+
item_type['altnames'] = {'cddl': str(key)}
|
|
1342
|
+
|
|
1343
|
+
properties[prop_name] = item_type
|
|
1344
|
+
|
|
1345
|
+
# Only include non-optional items in tuple order
|
|
1346
|
+
# (optional items can still be present but are not required)
|
|
1347
|
+
if not is_optional:
|
|
1348
|
+
tuple_order.append(prop_name)
|
|
1349
|
+
|
|
1350
|
+
result: Dict[str, Any] = {
|
|
1351
|
+
'type': 'tuple',
|
|
1352
|
+
'properties': properties,
|
|
1353
|
+
'tuple': [f"_{e[0]}" for e in entries] # Include all in order for the tuple layout
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
# If some items are optional, add required to indicate which are mandatory
|
|
1357
|
+
if tuple_order and len(tuple_order) < len(entries):
|
|
1358
|
+
result['required'] = tuple_order
|
|
1359
|
+
|
|
1360
|
+
return result
|
|
1361
|
+
|
|
1362
|
+
def _convert_map_like(self, node: Any, context_name: str = '') -> Dict[str, Any]:
|
|
1363
|
+
"""Convert a map-like structure to object."""
|
|
1364
|
+
properties: Dict[str, Any] = {}
|
|
1365
|
+
required: List[str] = []
|
|
1366
|
+
extends_refs: List[str] = []
|
|
1367
|
+
|
|
1368
|
+
if hasattr(node, 'getChildren'):
|
|
1369
|
+
for child in node.getChildren():
|
|
1370
|
+
child_type = type(child).__name__
|
|
1371
|
+
if child_type == 'GroupChoice':
|
|
1372
|
+
self._process_group_choice_for_object(
|
|
1373
|
+
child, properties, required, extends_refs, context_name
|
|
1374
|
+
)
|
|
1375
|
+
|
|
1376
|
+
result: Dict[str, Any] = {'type': 'object'}
|
|
1377
|
+
if extends_refs:
|
|
1378
|
+
# JSON Structure only supports single inheritance via $extends
|
|
1379
|
+
result['$extends'] = extends_refs[0]
|
|
1380
|
+
if len(extends_refs) > 1:
|
|
1381
|
+
logger.warning(
|
|
1382
|
+
"Multiple unwrap operators found, only using first: %s. "
|
|
1383
|
+
"JSON Structure $extends only supports single inheritance.",
|
|
1384
|
+
extends_refs[0]
|
|
1385
|
+
)
|
|
1386
|
+
if properties:
|
|
1387
|
+
result['properties'] = properties
|
|
1388
|
+
if required:
|
|
1389
|
+
result['required'] = required
|
|
1390
|
+
|
|
1391
|
+
return result
|
|
1392
|
+
|
|
1393
|
+
def _convert_group_entry(self, entry: CDDLNode, context_name: str = '') -> Dict[str, Any]:
|
|
1394
|
+
"""Convert a GroupEntry node used as a top-level type (e.g., for inline groups)."""
|
|
1395
|
+
# A GroupEntry at top level typically contains a Type with a Group inside
|
|
1396
|
+
if not hasattr(entry, 'getChildren'):
|
|
1397
|
+
return {'type': 'any'}
|
|
1398
|
+
|
|
1399
|
+
for child in entry.getChildren():
|
|
1400
|
+
child_type = type(child).__name__
|
|
1401
|
+
if child_type == 'Type':
|
|
1402
|
+
return self._convert_type(child, context_name)
|
|
1403
|
+
elif child_type == 'Group':
|
|
1404
|
+
return self._convert_group(child, context_name)
|
|
1405
|
+
elif child_type == 'GroupChoice':
|
|
1406
|
+
return self._convert_group_choice(child, context_name)
|
|
1407
|
+
|
|
1408
|
+
return {'type': 'any'}
|
|
1409
|
+
|
|
1410
|
+
def _convert_group_choice(
|
|
1411
|
+
self, group_choice: CDDLNode, context_name: str = ''
|
|
1412
|
+
) -> Dict[str, Any]:
|
|
1413
|
+
"""Convert a GroupChoice node."""
|
|
1414
|
+
# Process as an object with properties
|
|
1415
|
+
properties: Dict[str, Any] = {}
|
|
1416
|
+
required: List[str] = []
|
|
1417
|
+
extends_refs: List[str] = []
|
|
1418
|
+
|
|
1419
|
+
self._process_group_choice_for_object(
|
|
1420
|
+
group_choice, properties, required, extends_refs, context_name
|
|
1421
|
+
)
|
|
1422
|
+
|
|
1423
|
+
result: Dict[str, Any] = {'type': 'object'}
|
|
1424
|
+
if extends_refs:
|
|
1425
|
+
# JSON Structure only supports single inheritance via $extends
|
|
1426
|
+
result['$extends'] = extends_refs[0]
|
|
1427
|
+
if len(extends_refs) > 1:
|
|
1428
|
+
logger.warning(
|
|
1429
|
+
"Multiple unwrap operators found, only using first: %s. "
|
|
1430
|
+
"JSON Structure $extends only supports single inheritance.",
|
|
1431
|
+
extends_refs[0]
|
|
1432
|
+
)
|
|
1433
|
+
if properties:
|
|
1434
|
+
result['properties'] = properties
|
|
1435
|
+
if required:
|
|
1436
|
+
result['required'] = required
|
|
1437
|
+
|
|
1438
|
+
return result
|
|
1439
|
+
|
|
1440
|
+
def _convert_value(self, value_node: Value) -> Dict[str, Any]:
|
|
1441
|
+
"""Convert a Value node (literal value)."""
|
|
1442
|
+
if not hasattr(value_node, 'value'):
|
|
1443
|
+
return {'type': 'any'}
|
|
1444
|
+
|
|
1445
|
+
value = value_node.value
|
|
1446
|
+
|
|
1447
|
+
# Determine type based on value
|
|
1448
|
+
if value.startswith('"') and value.endswith('"'):
|
|
1449
|
+
# String literal - create enum with single value
|
|
1450
|
+
string_val = value[1:-1]
|
|
1451
|
+
return {
|
|
1452
|
+
'type': 'string',
|
|
1453
|
+
'const': string_val
|
|
1454
|
+
}
|
|
1455
|
+
elif value in ('true', 'false'):
|
|
1456
|
+
return {
|
|
1457
|
+
'type': 'boolean',
|
|
1458
|
+
'const': value == 'true'
|
|
1459
|
+
}
|
|
1460
|
+
elif value.isdigit() or (value.startswith('-') and value[1:].isdigit()):
|
|
1461
|
+
return {
|
|
1462
|
+
'type': 'int64',
|
|
1463
|
+
'const': int(value)
|
|
1464
|
+
}
|
|
1465
|
+
elif '.' in value:
|
|
1466
|
+
try:
|
|
1467
|
+
float_val = float(value)
|
|
1468
|
+
return {
|
|
1469
|
+
'type': 'double',
|
|
1470
|
+
'const': float_val
|
|
1471
|
+
}
|
|
1472
|
+
except ValueError:
|
|
1473
|
+
pass
|
|
1474
|
+
|
|
1475
|
+
return {'type': 'string', 'const': value}
|
|
1476
|
+
|
|
1477
|
+
def _convert_range(self, range_node: Range) -> Dict[str, Any]:
|
|
1478
|
+
"""Convert a Range node (min..max or min...max)."""
|
|
1479
|
+
result: Dict[str, Any] = {'type': 'int64'}
|
|
1480
|
+
|
|
1481
|
+
if hasattr(range_node, 'getChildren'):
|
|
1482
|
+
children = range_node.getChildren()
|
|
1483
|
+
values: List[Any] = []
|
|
1484
|
+
for child in children:
|
|
1485
|
+
if hasattr(child, 'value'):
|
|
1486
|
+
try:
|
|
1487
|
+
values.append(int(child.value))
|
|
1488
|
+
except ValueError:
|
|
1489
|
+
try:
|
|
1490
|
+
values.append(float(child.value))
|
|
1491
|
+
result['type'] = 'double'
|
|
1492
|
+
except ValueError:
|
|
1493
|
+
pass
|
|
1494
|
+
|
|
1495
|
+
if len(values) >= 2:
|
|
1496
|
+
result['minimum'] = values[0]
|
|
1497
|
+
result['maximum'] = values[1]
|
|
1498
|
+
elif len(values) == 1:
|
|
1499
|
+
result['minimum'] = values[0]
|
|
1500
|
+
|
|
1501
|
+
return result
|
|
1502
|
+
|
|
1503
|
+
def _convert_operator(self, operator_node: Operator, context_name: str = '') -> Dict[str, Any]:
|
|
1504
|
+
"""
|
|
1505
|
+
Convert an Operator node (type constraints like .size, .regexp, .default).
|
|
1506
|
+
|
|
1507
|
+
Supported control operators:
|
|
1508
|
+
- .size: Maps to minLength/maxLength for strings, minItems/maxItems for arrays
|
|
1509
|
+
- .regexp: Maps to pattern (ECMAScript regex)
|
|
1510
|
+
- .default: Maps to default value
|
|
1511
|
+
- .bits, .cbor, .cborseq: Base type extracted (CBOR-specific, no JSON Structure equivalent)
|
|
1512
|
+
- .within, .and: Type intersection (limited support)
|
|
1513
|
+
|
|
1514
|
+
See RFC 8610 Section 3.8 for full control operator specification.
|
|
1515
|
+
"""
|
|
1516
|
+
base_type: Dict[str, Any] = {'type': 'any'}
|
|
1517
|
+
operator_name: Optional[str] = None
|
|
1518
|
+
controller_value: Any = None
|
|
1519
|
+
|
|
1520
|
+
# Extract operator name and base type
|
|
1521
|
+
if hasattr(operator_node, 'name') and hasattr(operator_node.name, 'literal'):
|
|
1522
|
+
operator_name = operator_node.name.literal
|
|
1523
|
+
|
|
1524
|
+
if hasattr(operator_node, 'type'):
|
|
1525
|
+
# Use _convert_type which handles all node types properly
|
|
1526
|
+
if operator_node.type:
|
|
1527
|
+
base_type = self._convert_type(operator_node.type, context_name)
|
|
1528
|
+
else:
|
|
1529
|
+
base_type = {'type': 'any'}
|
|
1530
|
+
|
|
1531
|
+
# Extract controller (constraint argument)
|
|
1532
|
+
if hasattr(operator_node, 'controller'):
|
|
1533
|
+
controller_value = self._extract_controller_value(operator_node.controller)
|
|
1534
|
+
|
|
1535
|
+
# Apply operator-specific mappings
|
|
1536
|
+
if operator_name == 'size':
|
|
1537
|
+
base_type = self._apply_size_constraint(base_type, controller_value)
|
|
1538
|
+
elif operator_name == 'regexp':
|
|
1539
|
+
base_type = self._apply_regexp_constraint(base_type, controller_value)
|
|
1540
|
+
elif operator_name == 'default':
|
|
1541
|
+
base_type = self._apply_default_value(base_type, controller_value)
|
|
1542
|
+
elif operator_name == 'lt':
|
|
1543
|
+
# .lt (less than) -> exclusiveMaximum
|
|
1544
|
+
if isinstance(controller_value, (int, float)):
|
|
1545
|
+
base_type['exclusiveMaximum'] = controller_value
|
|
1546
|
+
elif operator_name == 'le':
|
|
1547
|
+
# .le (less or equal) -> maximum
|
|
1548
|
+
if isinstance(controller_value, (int, float)):
|
|
1549
|
+
base_type['maximum'] = controller_value
|
|
1550
|
+
elif operator_name == 'gt':
|
|
1551
|
+
# .gt (greater than) -> exclusiveMinimum
|
|
1552
|
+
if isinstance(controller_value, (int, float)):
|
|
1553
|
+
base_type['exclusiveMinimum'] = controller_value
|
|
1554
|
+
elif operator_name == 'ge':
|
|
1555
|
+
# .ge (greater or equal) -> minimum
|
|
1556
|
+
if isinstance(controller_value, (int, float)):
|
|
1557
|
+
base_type['minimum'] = controller_value
|
|
1558
|
+
elif operator_name == 'eq':
|
|
1559
|
+
# .eq (equals) -> const
|
|
1560
|
+
base_type['const'] = controller_value
|
|
1561
|
+
elif operator_name == 'ne':
|
|
1562
|
+
# .ne (not equals) - no direct JSON Structure equivalent, add as comment
|
|
1563
|
+
pass
|
|
1564
|
+
elif operator_name in ('within', 'and'):
|
|
1565
|
+
# Type intersection - limited support, just use base type
|
|
1566
|
+
pass
|
|
1567
|
+
elif operator_name in ('bits', 'cbor', 'cborseq'):
|
|
1568
|
+
# CBOR-specific operators - just use base type
|
|
1569
|
+
pass
|
|
1570
|
+
|
|
1571
|
+
return base_type
|
|
1572
|
+
|
|
1573
|
+
def _extract_controller_value(self, controller: Any) -> Any:
|
|
1574
|
+
"""Extract the value from an operator's controller (argument)."""
|
|
1575
|
+
if controller is None:
|
|
1576
|
+
return None
|
|
1577
|
+
|
|
1578
|
+
controller_type = type(controller).__name__
|
|
1579
|
+
|
|
1580
|
+
# Handle direct Value node (e.g., .size 32, .ge 0)
|
|
1581
|
+
if controller_type == 'Value':
|
|
1582
|
+
return self._parse_value_literal(controller)
|
|
1583
|
+
|
|
1584
|
+
# Handle direct Range node
|
|
1585
|
+
if controller_type == 'Range':
|
|
1586
|
+
return self._extract_range_values(controller)
|
|
1587
|
+
|
|
1588
|
+
# Handle direct Typename node
|
|
1589
|
+
if controller_type == 'Typename' and hasattr(controller, 'name'):
|
|
1590
|
+
return controller.name
|
|
1591
|
+
|
|
1592
|
+
# Handle Type node containing children (e.g., .size (1..100))
|
|
1593
|
+
if hasattr(controller, 'getChildren'):
|
|
1594
|
+
children = controller.getChildren()
|
|
1595
|
+
for child in children:
|
|
1596
|
+
child_type = type(child).__name__
|
|
1597
|
+
if child_type == 'Range':
|
|
1598
|
+
# Extract min/max from range
|
|
1599
|
+
return self._extract_range_values(child)
|
|
1600
|
+
if child_type == 'Value':
|
|
1601
|
+
return self._parse_value_literal(child)
|
|
1602
|
+
if child_type == 'Typename':
|
|
1603
|
+
if hasattr(child, 'name'):
|
|
1604
|
+
return child.name
|
|
1605
|
+
|
|
1606
|
+
return None
|
|
1607
|
+
|
|
1608
|
+
def _extract_range_values(self, range_node: Any) -> Dict[str, Any]:
|
|
1609
|
+
"""Extract min and max from a Range node."""
|
|
1610
|
+
result: Dict[str, Any] = {}
|
|
1611
|
+
values = []
|
|
1612
|
+
is_exclusive = False
|
|
1613
|
+
|
|
1614
|
+
if hasattr(range_node, 'getChildren'):
|
|
1615
|
+
for child in range_node.getChildren():
|
|
1616
|
+
child_type = type(child).__name__
|
|
1617
|
+
if child_type == 'Value':
|
|
1618
|
+
values.append(self._parse_value_literal(child))
|
|
1619
|
+
elif hasattr(child, 'kind') and 'EXCLRANGE' in str(child.kind):
|
|
1620
|
+
is_exclusive = True
|
|
1621
|
+
|
|
1622
|
+
if len(values) >= 2:
|
|
1623
|
+
result['min'] = values[0]
|
|
1624
|
+
result['max'] = values[1]
|
|
1625
|
+
result['exclusive'] = is_exclusive
|
|
1626
|
+
elif len(values) == 1:
|
|
1627
|
+
result['min'] = values[0]
|
|
1628
|
+
result['exclusive'] = is_exclusive
|
|
1629
|
+
|
|
1630
|
+
return result
|
|
1631
|
+
|
|
1632
|
+
def _parse_value_literal(self, value_node: Any) -> Any:
|
|
1633
|
+
"""Parse a Value node and return the Python value."""
|
|
1634
|
+
if not hasattr(value_node, 'value'):
|
|
1635
|
+
return None
|
|
1636
|
+
|
|
1637
|
+
value = value_node.value
|
|
1638
|
+
|
|
1639
|
+
# String value
|
|
1640
|
+
if value.startswith('"') and value.endswith('"'):
|
|
1641
|
+
return value[1:-1]
|
|
1642
|
+
|
|
1643
|
+
# Boolean
|
|
1644
|
+
if value == 'true':
|
|
1645
|
+
return True
|
|
1646
|
+
if value == 'false':
|
|
1647
|
+
return False
|
|
1648
|
+
|
|
1649
|
+
# Integer
|
|
1650
|
+
try:
|
|
1651
|
+
return int(value)
|
|
1652
|
+
except ValueError:
|
|
1653
|
+
pass
|
|
1654
|
+
|
|
1655
|
+
# Float
|
|
1656
|
+
try:
|
|
1657
|
+
return float(value)
|
|
1658
|
+
except ValueError:
|
|
1659
|
+
pass
|
|
1660
|
+
|
|
1661
|
+
return value
|
|
1662
|
+
|
|
1663
|
+
def _apply_size_constraint(self, base_type: Dict[str, Any], constraint: Any) -> Dict[str, Any]:
|
|
1664
|
+
"""Apply .size constraint to a type."""
|
|
1665
|
+
if constraint is None:
|
|
1666
|
+
return base_type
|
|
1667
|
+
|
|
1668
|
+
type_name = base_type.get('type', 'any')
|
|
1669
|
+
|
|
1670
|
+
# Determine if this is a string or array type
|
|
1671
|
+
is_string_type = type_name in ('string', 'binary')
|
|
1672
|
+
is_array_type = type_name == 'array'
|
|
1673
|
+
|
|
1674
|
+
if isinstance(constraint, dict):
|
|
1675
|
+
# Range constraint
|
|
1676
|
+
min_val = constraint.get('min')
|
|
1677
|
+
max_val = constraint.get('max')
|
|
1678
|
+
|
|
1679
|
+
if is_string_type:
|
|
1680
|
+
if min_val is not None:
|
|
1681
|
+
base_type['minLength'] = min_val
|
|
1682
|
+
if max_val is not None:
|
|
1683
|
+
base_type['maxLength'] = max_val
|
|
1684
|
+
elif is_array_type:
|
|
1685
|
+
if min_val is not None:
|
|
1686
|
+
base_type['minItems'] = min_val
|
|
1687
|
+
if max_val is not None:
|
|
1688
|
+
base_type['maxItems'] = max_val
|
|
1689
|
+
else:
|
|
1690
|
+
# For other types, might be constraining bit/byte size
|
|
1691
|
+
# Map to general min/max
|
|
1692
|
+
if min_val is not None:
|
|
1693
|
+
base_type['minimum'] = min_val
|
|
1694
|
+
if max_val is not None:
|
|
1695
|
+
base_type['maximum'] = max_val
|
|
1696
|
+
elif isinstance(constraint, int):
|
|
1697
|
+
# Exact size constraint
|
|
1698
|
+
if is_string_type:
|
|
1699
|
+
base_type['minLength'] = constraint
|
|
1700
|
+
base_type['maxLength'] = constraint
|
|
1701
|
+
elif is_array_type:
|
|
1702
|
+
base_type['minItems'] = constraint
|
|
1703
|
+
base_type['maxItems'] = constraint
|
|
1704
|
+
|
|
1705
|
+
return base_type
|
|
1706
|
+
|
|
1707
|
+
def _apply_regexp_constraint(self, base_type: Dict[str, Any], pattern: Any) -> Dict[str, Any]:
|
|
1708
|
+
"""Apply .regexp constraint to a string type."""
|
|
1709
|
+
if pattern and isinstance(pattern, str):
|
|
1710
|
+
base_type['pattern'] = pattern
|
|
1711
|
+
return base_type
|
|
1712
|
+
|
|
1713
|
+
def _apply_default_value(self, base_type: Dict[str, Any], default_val: Any) -> Dict[str, Any]:
|
|
1714
|
+
"""Apply .default value to a type."""
|
|
1715
|
+
if default_val is not None:
|
|
1716
|
+
base_type['default'] = default_val
|
|
1717
|
+
return base_type
|
|
1718
|
+
|
|
1719
|
+
def _convert_tag(self, tag_node: Tag, context_name: str = '') -> Dict[str, Any]:
|
|
1720
|
+
"""Convert a Tag node (CBOR tags like #6.n)."""
|
|
1721
|
+
# CBOR tags don't have a direct JSON Structure equivalent
|
|
1722
|
+
# Just convert the underlying type
|
|
1723
|
+
if hasattr(tag_node, 'getChildren'):
|
|
1724
|
+
for child in tag_node.getChildren():
|
|
1725
|
+
child_type = type(child).__name__
|
|
1726
|
+
if child_type in ('Type', 'Typename', 'Map', 'Array'):
|
|
1727
|
+
return self._convert_type(child, context_name)
|
|
1728
|
+
|
|
1729
|
+
return {'type': 'any'}
|
|
1730
|
+
|
|
1731
|
+
def _convert_choice_from(
|
|
1732
|
+
self, choice_from: ChoiceFrom, _context_name: str = ''
|
|
1733
|
+
) -> Dict[str, Any]:
|
|
1734
|
+
"""Convert a ChoiceFrom node (&group or &enum)."""
|
|
1735
|
+
# This is typically used for enumerations
|
|
1736
|
+
result: Dict[str, Any] = {'type': 'string'}
|
|
1737
|
+
|
|
1738
|
+
if hasattr(choice_from, 'getChildren'):
|
|
1739
|
+
for child in choice_from.getChildren():
|
|
1740
|
+
if hasattr(child, 'name'):
|
|
1741
|
+
# Reference to an enumeration group
|
|
1742
|
+
normalized_name = avro_name(child.name)
|
|
1743
|
+
return {'$ref': f'#/definitions/{normalized_name}'}
|
|
1744
|
+
|
|
1745
|
+
return result
|
|
1746
|
+
|
|
1747
|
+
def _scan_for_uses(self, structure_schema: Dict[str, Any]) -> List[str]:
|
|
1748
|
+
"""Scan the structure schema for extension feature usage."""
|
|
1749
|
+
uses = set()
|
|
1750
|
+
|
|
1751
|
+
def scan(obj: Any) -> None:
|
|
1752
|
+
if isinstance(obj, dict):
|
|
1753
|
+
for k, v in obj.items():
|
|
1754
|
+
if k == 'altnames':
|
|
1755
|
+
uses.add('JSONStructureAlternateNames')
|
|
1756
|
+
if k in {'unit', 'currency', 'symbol'}:
|
|
1757
|
+
uses.add('JSONStructureUnits')
|
|
1758
|
+
if k in {'pattern', 'minLength', 'maxLength', 'minimum', 'maximum',
|
|
1759
|
+
'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf',
|
|
1760
|
+
'const', 'enum', 'required', 'minItems', 'maxItems', 'default'}:
|
|
1761
|
+
uses.add('JSONStructureValidation')
|
|
1762
|
+
if k in {'if', 'then', 'else', 'dependentRequired', 'dependentSchemas',
|
|
1763
|
+
'anyOf', 'allOf', 'oneOf', 'not'}:
|
|
1764
|
+
uses.add('JSONStructureConditionalComposition')
|
|
1765
|
+
# Note: $extends is a core keyword per Section 3.10.2, not an add-in
|
|
1766
|
+
scan(v)
|
|
1767
|
+
elif isinstance(obj, list):
|
|
1768
|
+
for item in obj:
|
|
1769
|
+
scan(item)
|
|
1770
|
+
|
|
1771
|
+
scan(structure_schema)
|
|
1772
|
+
return sorted(uses)
|
|
1773
|
+
|
|
1774
|
+
|
|
1775
|
+
def convert_cddl_to_structure(cddl_content: str, namespace: str = DEFAULT_NAMESPACE,
|
|
1776
|
+
validate: bool = True) -> str:
|
|
1777
|
+
"""
|
|
1778
|
+
Convert CDDL content to JSON Structure format.
|
|
1779
|
+
|
|
1780
|
+
Args:
|
|
1781
|
+
cddl_content: The CDDL schema as a string
|
|
1782
|
+
namespace: The namespace for the schema
|
|
1783
|
+
validate: If True, validate the output schema using json-structure SDK
|
|
1784
|
+
|
|
1785
|
+
Returns:
|
|
1786
|
+
JSON Structure schema as a string
|
|
1787
|
+
|
|
1788
|
+
Raises:
|
|
1789
|
+
CddlConversionError: If validation is enabled and the schema is invalid
|
|
1790
|
+
"""
|
|
1791
|
+
converter = CddlToStructureConverter()
|
|
1792
|
+
converter.root_namespace = namespace
|
|
1793
|
+
result = converter.convert_cddl_to_structure(cddl_content, namespace)
|
|
1794
|
+
|
|
1795
|
+
# Validate the output schema
|
|
1796
|
+
if validate:
|
|
1797
|
+
result_str = json.dumps(result, indent=2)
|
|
1798
|
+
errors = converter.validate_structure_schema(result, result_str)
|
|
1799
|
+
error_errors = [e for e in errors if e.severity == ValidationSeverity.ERROR]
|
|
1800
|
+
if error_errors:
|
|
1801
|
+
error_messages = '; '.join(f"{e.message} at {e.path}" for e in error_errors)
|
|
1802
|
+
raise CddlConversionError(
|
|
1803
|
+
f"Generated schema validation failed: {error_messages}",
|
|
1804
|
+
context="output validation"
|
|
1805
|
+
)
|
|
1806
|
+
|
|
1807
|
+
return json.dumps(result, indent=2)
|
|
1808
|
+
|
|
1809
|
+
|
|
1810
|
+
def convert_cddl_to_structure_files(
|
|
1811
|
+
cddl_file_path: str,
|
|
1812
|
+
structure_schema_path: str,
|
|
1813
|
+
namespace: Optional[str] = None,
|
|
1814
|
+
validate: bool = True
|
|
1815
|
+
) -> None:
|
|
1816
|
+
"""
|
|
1817
|
+
Convert a CDDL file to JSON Structure format.
|
|
1818
|
+
|
|
1819
|
+
Args:
|
|
1820
|
+
cddl_file_path: Path to the input CDDL file
|
|
1821
|
+
structure_schema_path: Path to the output JSON Structure file
|
|
1822
|
+
namespace: Optional namespace for the schema
|
|
1823
|
+
validate: If True, validate the output schema using json-structure SDK
|
|
1824
|
+
|
|
1825
|
+
Raises:
|
|
1826
|
+
CddlConversionError: If validation is enabled and the schema is invalid
|
|
1827
|
+
"""
|
|
1828
|
+
# Use default namespace if None provided
|
|
1829
|
+
if namespace is None:
|
|
1830
|
+
namespace = DEFAULT_NAMESPACE
|
|
1831
|
+
|
|
1832
|
+
# Read the CDDL file
|
|
1833
|
+
with open(cddl_file_path, 'r', encoding='utf-8') as f:
|
|
1834
|
+
cddl_content = f.read()
|
|
1835
|
+
|
|
1836
|
+
# Convert to JSON Structure (validation happens in convert_cddl_to_structure)
|
|
1837
|
+
result = convert_cddl_to_structure(cddl_content, namespace, validate)
|
|
1838
|
+
|
|
1839
|
+
# Write the result
|
|
1840
|
+
with open(structure_schema_path, 'w', encoding='utf-8') as f:
|
|
1841
|
+
f.write(result)
|