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.
@@ -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)