Skip to content

Commit 4f46e40

Browse files
committed
Fix handling of objects with repeated subobjects, "each"
* Add"start_index" argument in "each" in jsonc format that informs the position of the shared sub objects
1 parent 95e934d commit 4f46e40

6 files changed

Lines changed: 717 additions & 35 deletions

File tree

‎src/objdictgen/jsonod.py‎

Lines changed: 44 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ class ValidationError(Exception):
120120
# node.ParamDictionary[index] = { N: { ..dict..}, ..dict.. }
121121
FIELDS_PARAMS = {'comment', 'save', 'buffer_size'}
122122
FIELDS_PARAMS_PROMOTE = {'callback'}
123+
FIELDS_PARAMS_EACH = {"start_index"}
123124

124125
# Fields representing the dictionary value
125126
FIELDS_VALUE = {'value'}
@@ -421,7 +422,7 @@ def generate_node(contents: str|TODJson, validate: bool = True) -> Node:
421422
if validate:
422423
validate_fromdict(jd, objtypes_i2s, objtypes_s2i)
423424

424-
return node_fromdict(jd, objtypes_s2i)
425+
return node_fromdict(jd, objtypes_s2i, validate=validate)
425426

426427

427428
def node_todict(node: Node, sort=False, rich=True, internal=False, validate=True) -> TODJson:
@@ -484,7 +485,6 @@ def node_todict(node: Node, sort=False, rich=True, internal=False, validate=True
484485
finally:
485486
# Add in a fancyer index (do it here after index is finished being used)
486487
if rich:
487-
index = obj["index"]
488488
obj["index"] = f'@@"0x{index:04X}" // {index}@@'
489489

490490
dictionary.append(obj)
@@ -598,9 +598,12 @@ def indexentry_to_jsondict(ientry: TIndexEntry) -> TODObjJson:
598598
obj['struct'] = struct
599599
obj['sub'] = obj.pop('values', []) # type: ignore[typeddict-item] # values is about to be renamed
600600

601-
# Move subindex[1] to 'each' on objecs that contain 'nbmax'
602-
if len(obj['sub']) > 1 and 'nbmax' in obj['sub'][1]:
603-
obj['each'] = obj['sub'].pop(1) # type: ignore[typeddict-item]
601+
# Move subindex[-1] to 'each' on objecs that contain 'nbmax'
602+
if len(obj['sub']) > 1 and 'nbmax' in obj['sub'][-1]:
603+
obj['each'] = obj['sub'].pop(-1) # type: ignore[typeddict-item]
604+
start_index = len(obj["sub"])
605+
if start_index > 1:
606+
obj["each"]["start_index"] = start_index
604607

605608
# Baseobj should have been emptied
606609
if odobj != {}:
@@ -892,20 +895,18 @@ def validate_indexentry(ientry: TIndexEntry):
892895
nbmaxok = True
893896

894897
elif struct in (OD.ARRAY, OD.NARRAY):
895-
if len(nbmax) == 2:
898+
if len(nbmax) == 2: # Array only have length + repeat
896899
lenok = True
897-
if sum(nbmax) == 1 and nbmax[1]:
900+
if sum(nbmax) == 1 and nbmax[-1]:
898901
nbmaxok = True
899902

900903
elif struct in (OD.RECORD, OD.NRECORD):
901-
if sum(nbmax) and len(nbmax) > 1 and nbmax[1]:
904+
if len(nbmax) >= 2: # Record can have more than one item before repeat
905+
lenok = True
906+
if sum(nbmax) == 1 and nbmax[-1]:
902907
nbmaxok = True
903-
if len(nbmax) == 2:
904-
lenok = True
905-
elif sum(nbmax) == 0:
908+
if sum(nbmax) == 0:
906909
nbmaxok = True
907-
if len(nbmax) > 1:
908-
lenok = True
909910
else:
910911
raise ValidationError(f"Unknown struct '{struct}'")
911912

@@ -915,7 +916,7 @@ def validate_indexentry(ientry: TIndexEntry):
915916
raise ValidationError(f"Unexpexted count of subindexes in mapping object, found {len(nbmax)}")
916917

917918

918-
def node_fromdict(jd: TODJson, objtypes_s2i: dict[str, int]) -> Node:
919+
def node_fromdict(jd: TODJson, objtypes_s2i: dict[str, int], validate: bool = True) -> Node:
919920
""" Convert a dict jd into a Node """
920921

921922
# Create the node and fill the most basic data
@@ -983,15 +984,16 @@ def node_fromdict(jd: TODJson, objtypes_s2i: dict[str, int]) -> Node:
983984
elif 'built-in' in groups:
984985
refobj = maps.MAPPING_DICTIONARY.get(index)
985986

986-
diff = deepdiff.DeepDiff(refobj, ientry['object'], view='tree')
987-
if diff:
988-
log.debug("Index 0x%04x (%s) Difference between built-in object and imported:", index, index)
989-
for line in diff.pretty().splitlines():
990-
log.debug(' %s', line)
991-
raise ValidationError(
992-
f"Built-in object index 0x{index:04x} ({index}) "
993-
"does not match against system parameters"
994-
)
987+
if validate:
988+
diff = deepdiff.DeepDiff(refobj, ientry['object'], view='tree')
989+
if diff:
990+
log.debug("Index 0x%04x (%s) Difference between built-in object and imported:", index, index)
991+
for line in diff.pretty().splitlines():
992+
log.debug(' %s', line)
993+
raise ValidationError(
994+
f"Built-in object index 0x{index:04x} ({index}) "
995+
"does not match against system parameters"
996+
)
995997

996998
return node
997999

@@ -1093,7 +1095,9 @@ def rearrange_for_node(obj: TODObjJson, objtypes_s2i: dict[str, int]) -> TIndexE
10931095

10941096
# Move back the each object
10951097
if 'each' in obj:
1096-
subitems.append(obj.pop('each')) # type: ignore[arg-type]
1098+
each = obj.pop('each')
1099+
each.pop("start_index", None) # Remove the start_index if present
1100+
subitems.append(each) # type: ignore[arg-type]
10971101

10981102
# Check if the object is a repeat object
10991103
repeat = obj.pop('repeat', False)
@@ -1173,7 +1177,7 @@ def validate_fromdict(jsonobj: TODJson, objtypes_i2s: dict[int, str], objtypes_s
11731177
# Verify that we have the expected members
11741178
member_compare(jsonobj.keys(), must=FIELDS_DATA_MUST, optional=FIELDS_DATA_OPT)
11751179

1176-
def _validate_sub(obj, idx=0, is_var=False, is_repeat=False, is_each=False):
1180+
def _validate_sub(obj, idx=0, is_var=False, is_repeat=False, is_each=False, is_index_each=False):
11771181

11781182
# Validated: (See FIELDS_MAPVAPS_*, FIELDS_PARAMS and FIELDS_VALUE)
11791183
# ----------
@@ -1209,7 +1213,7 @@ def _validate_sub(obj, idx=0, is_var=False, is_repeat=False, is_each=False):
12091213

12101214
# Set what parameters should be present, optional or not present
12111215
if idx == -1: # Checking "each" section. No object or value
1212-
params = 'no'
1216+
params = "each"
12131217

12141218
elif is_repeat: # Object repeat = defined elsewhere. No definition needed.
12151219
defs = 'no'
@@ -1219,10 +1223,9 @@ def _validate_sub(obj, idx=0, is_var=False, is_repeat=False, is_each=False):
12191223
elif is_var: # VAR type, guaranteed idx==0 here
12201224
value = 'opt'
12211225

1222-
elif is_each: # Param have "each". Should never have any defs in idx > 0
1223-
if idx > 0:
1224-
defs = 'no'
1225-
value = 'must'
1226+
elif is_each and idx >= is_index_each: # Param have "each" and the index is in the "each" items
1227+
defs = 'no'
1228+
value = 'must'
12261229

12271230
else: # All other (not each, not repeat, not VAR)
12281231
if idx > 0:
@@ -1240,6 +1243,8 @@ def _validate_sub(obj, idx=0, is_var=False, is_repeat=False, is_each=False):
12401243
# must |= FIELDS_PARAMS
12411244
if params == 'opt':
12421245
opts |= FIELDS_PARAMS
1246+
if params == 'each':
1247+
opts |= FIELDS_PARAMS_EACH
12431248
if value == 'must':
12441249
must |= FIELDS_VALUE
12451250
if value == 'opt':
@@ -1335,7 +1340,10 @@ def _validate_dictionary(index, obj):
13351340
for idx, sub in enumerate(subitems):
13361341
try:
13371342
is_var = struct in (OD.VAR, OD.NVAR)
1338-
_validate_sub(sub, idx, is_var=is_var, is_repeat=is_repeat, is_each='each' in obj)
1343+
_validate_sub(
1344+
sub, idx, is_var=is_var, is_repeat=is_repeat, is_each="each" in obj,
1345+
is_index_each=obj.get("each", {}).get("start_index", 1),
1346+
)
13391347
except Exception as exc:
13401348
exc_amend(exc, f"sub[{idx}]: ")
13411349
raise
@@ -1347,9 +1355,11 @@ def _validate_dictionary(index, obj):
13471355
if struct in (OD.VAR, OD.NVAR):
13481356
raise ValidationError("Unexpected 'each' use in VAR/NVAR object")
13491357

1350-
# Having 'each' requires use of only one sub item with 'name' in it
1351-
if not (sum(has_name) == 1 and has_name[0]):
1352-
raise ValidationError("Unexpected subitems. Subitem 0 must contain name")
1358+
# When each is present, the other items must have a name field in them
1359+
if sum(has_name) != sub.get("start_index", 1):
1360+
raise ValidationError(
1361+
f"Unexpected subitems. Subitems 0..{sub['start_index'] - 1} must contain name"
1362+
)
13531363

13541364
try:
13551365
_validate_sub(sub, idx=-1)

‎src/objdictgen/schema/od.schema.json‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@
187187
"pdo": { "$ref": "#pdo" },
188188
"nbmin": { "$ref": "#nbmin" },
189189
"nbmax": { "$ref": "#nbmax" },
190+
"start_index": { "$ref": "#index" },
190191
"default": { "$ref": "#value" }
191192
},
192193
"additionalProperties": false,
@@ -305,6 +306,11 @@
305306
"type": "integer"
306307
},
307308

309+
"start_index": {
310+
"$id": "#start_index",
311+
"type": "integer"
312+
},
313+
308314
"pdo": {
309315
"$id": "#pdo",
310316
"description": "Set if object is usable in a PDO",
@@ -411,4 +417,4 @@
411417
}
412418

413419
}
414-
}
420+
}

‎src/objdictgen/typing.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ def GetDefaultStringSize(self) -> int:
198198
"pdo": bool,
199199
"nbmin": int,
200200
"nbmax": int,
201+
"start_index": int,
201202
"default": TODValue,
202203
})
203204
"""JSON object dictionary "each" type definition."""

0 commit comments

Comments
 (0)