Skip to content

Commit 0b8cc2d

Browse files
authored
Add support for jsonc files (#40)
* Add support for .jsonc in GUI * Accept either json or jsonc input * Generate pure json output when json is selected
1 parent 22153bc commit 0b8cc2d

10 files changed

+124
-43
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ _tmp*
2121
tests/od/extra-compare*
2222
/*.od
2323
/*.json
24+
/*.jsonc
2425

2526
objdictgen.*
2627
fw-can-shared

README.md

+7-10
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ Laerdal Medical fork for the canfestival library:
1818
objdictgen is a tool to parse, view and manipulate files containing object
1919
dictionary (OD). An object dictionary is entries with data and configuration
2020
in CANopen devices. The `odg` executable is installed. It supports
21-
reading and writing OD files in `.json` format, in legacy XML `.od` and `.eds`
22-
files. It can generate c code for use with the canfestival library.
21+
reading and writing OD files in `.json`/`.jsonc` format, in legacy XML `.od`
22+
and `.eds` files. It can generate c code for use with the canfestival library.
2323

2424

2525
## Install
@@ -76,22 +76,19 @@ descriptions, help with values and validate the file.
7676
"json.schemas": [
7777
{
7878
"fileMatch": [
79-
"**.json"
79+
"**.jsonc"
8080
],
8181
"url": "./src/objdictgen/schema/od.schema.json"
8282
}
8383
],
84-
"files.associations": {
85-
"*.json": "jsonc"
86-
}
8784
```
8885

8986
## Conversion
9087

9188
The recommended way to convert existing/legacy `.od` files to the new JSON
9289
format is:
9390

94-
$ odg generate <file.od> <file.json> --fix --drop-unused [--nosort]
91+
$ odg generate <file.od> <file.jsonc> --fix --drop-unused [--nosort]
9592

9693
The `--fix` option might be necessary if the OD-file contains internal
9794
inconsistencies. It is safe to run this option as it will not delete any active
@@ -102,10 +99,10 @@ parameter that might be used in the file.
10299
## Motivation
103100

104101
The biggest improvement with the new tool over the original implementation is
105-
the introduction of a new `.json` based format to store the object dictionary.
106-
The JSON format is well-known and easy to read. The tool supports jsonc,
102+
the introduction of a new `.jsonc` based format to store the object dictionary.
103+
The JSON format is well-known and easy to read. The tool use jsonc,
107104
allowing comments in the json file. `odg` will process the file in a repeatable
108-
manner, making it possible support diffing of the `.json` file output. `odg`
105+
manner, making it possible support diffing of the `.jsonc` file output. `odg`
109106
remains 100% compatible with the legacy `.od` format on both input and output.
110107

111108
The original objdictedit and objdictgen tool were written in legacy python 2 and

src/objdictgen/__main__.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ def main(debugopts: DebugOpts, args: Sequence[str]|None = None):
220220
subp.add_argument('-x', '--exclude', action="append", help="OD Index to exclude.")
221221
subp.add_argument('-f', '--fix', action="store_true",
222222
help="Fix any inconsistency errors in OD before generate output")
223-
subp.add_argument('-t', '--type', choices=['od', 'eds', 'json', 'c'],
223+
subp.add_argument('-t', '--type', choices=['od', 'eds', 'json', 'jsonc', 'c'],
224224
help="Select output file type")
225225
subp.add_argument('--drop-unused', action="store_true", help="Remove unused parameters")
226226
subp.add_argument('--internal', action="store_true",
@@ -351,8 +351,9 @@ def main(debugopts: DebugOpts, args: Sequence[str]|None = None):
351351

352352
# Write the data
353353
od.DumpFile(opts.out,
354-
filetype=opts.type, sort=not opts.no_sort,
355-
internal=opts.internal, validate=not opts.novalidate
354+
filetype=opts.type,
355+
# These additional options are only used for JSON output
356+
sort=not opts.no_sort, internal=opts.internal, validate=not opts.novalidate
356357
)
357358

358359

src/objdictgen/jsonod.py

+21-3
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,8 @@ def compare_profile(profilename: TPath, params: ODMapping, menu: TProfileMenu|No
357357
return False, False
358358

359359

360-
def generate_jsonc(node: "Node", compact=False, sort=False, internal=False, validate=True) -> str:
360+
def generate_jsonc(node: "Node", compact=False, sort=False, internal=False,
361+
validate=True, jsonc=True) -> str:
361362
""" Export a JSONC string representation of the node """
362363

363364
# Get the dict representation
@@ -374,20 +375,37 @@ def generate_jsonc(node: "Node", compact=False, sort=False, internal=False, vali
374375
text = json.dumps(jd, separators=(',', ': '), indent=2)
375376

376377
# Convert the special __ fields to jsonc comments
378+
# Syntax: "__<field>: <value>"
377379
text = re.sub(
378380
r'^(\s*)"__(\w+)": "(.*)",?$',
379-
r'\1// "\2": "\3"',
381+
# In regular json files, __* fields are omitted from output
382+
# In jsonc files, the __* entry is converted to a comment:
383+
# "// <field>: <value>"
384+
r'\1// "\2": "\3"' if jsonc else '',
380385
text,
381386
flags=re.MULTILINE,
382387
)
383388

389+
if jsonc:
390+
# In jsonc the field is converted to "<field>, // <comment>"
391+
repl = lambda m: m[1].replace('\\"', '"') + m[3] + m[2]
392+
else:
393+
# In json the field is converted to "<field>,"
394+
repl = lambda m: m[1].replace('\\"', '"') + m[3]
395+
384396
# Convert the special @@ fields to jsonc comments
397+
# Syntax: "@@<field>, // <comment>@@"
385398
text = re.sub(
386399
r'"@@(.*?)(\s*//.*?)?@@"(.*)$',
387-
lambda m: m[1].replace('\\"', '"') + m[3] + m[2],
400+
repl,
388401
text,
389402
flags=re.MULTILINE,
390403
)
404+
405+
# In case the json contains empty lines, remove them
406+
if not jsonc:
407+
text = "\n".join(line for line in text.splitlines() if line.strip())
408+
391409
return text
392410

393411

src/objdictgen/node.py

+8-6
Original file line numberDiff line numberDiff line change
@@ -196,14 +196,14 @@ def LoadJson(contents: str) -> "Node":
196196
""" Import a new Node from a JSON string """
197197
return jsonod.generate_node(contents)
198198

199-
def DumpFile(self, filepath: TPath, filetype: str|None = "json", **kwargs):
199+
def DumpFile(self, filepath: TPath, filetype: str|None = "jsonc", **kwargs):
200200
""" Save node into file """
201201

202202
# Attempt to determine the filetype from the filepath
203203
if not filetype:
204204
filetype = Path(filepath).suffix[1:]
205205
if not filetype:
206-
filetype = "json"
206+
filetype = "jsonc"
207207

208208
if filetype == 'od':
209209
log.debug("Writing XML OD '%s'", filepath)
@@ -219,9 +219,11 @@ def DumpFile(self, filepath: TPath, filetype: str|None = "json", **kwargs):
219219
f.write(content)
220220
return
221221

222-
if filetype == 'json':
222+
if filetype in ('json', 'jsonc'):
223223
log.debug("Writing JSON OD '%s'", filepath)
224-
jdata = self.DumpJson(**kwargs)
224+
kw = kwargs.copy()
225+
kw['jsonc'] = filetype == 'jsonc'
226+
jdata = self.DumpJson(**kw)
225227
with open(filepath, "w", encoding="utf-8") as f:
226228
f.write(jdata)
227229
return
@@ -234,10 +236,10 @@ def DumpFile(self, filepath: TPath, filetype: str|None = "json", **kwargs):
234236

235237
raise ValueError("Unknown file suffix, unable to write file")
236238

237-
def DumpJson(self, compact=False, sort=False, internal=False, validate=True) -> str:
239+
def DumpJson(self, compact=False, sort=False, internal=False, validate=True, jsonc=True) -> str:
238240
""" Dump the node into a JSON string """
239241
return jsonod.generate_jsonc(
240-
self, compact=compact, sort=sort, internal=internal, validate=validate
242+
self, compact=compact, sort=sort, internal=internal, validate=validate, jsonc=jsonc
241243
)
242244

243245
def asdict(self) -> dict[str, Any]:

src/objdictgen/ui/objdictedit.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ def OnOpenMenu(self, event): # pylint: disable=unused-argument
424424

425425
with wx.FileDialog(
426426
self, "Choose a file", directory, "",
427-
"OD files (*.json;*.od;*.eds)|*.json;*.od;*.eds|All files|*.*",
427+
wildcard="OD JSON file (*.jsonc;*.json)|*.jsonc;*.json|Legacy OD file (*.od)|*.od|EDS file (*.eds)|*.eds|All files|*.*",
428428
style=wx.FD_OPEN | wx.FD_CHANGE_DIR,
429429
) as dialog:
430430
if dialog.ShowModal() != wx.ID_OK:
@@ -475,7 +475,7 @@ def SaveAs(self):
475475

476476
with wx.FileDialog(
477477
self, "Choose a file", directory, filename,
478-
wildcard="OD JSON file (*.json)|*.json;|OD file (*.od)|*.od;|EDS file (*.eds)|*.eds",
478+
wildcard="OD JSON file (*.jsonc;*.json)|*.jsonc;*.json|Legacy OD file (*.od)|*.od|EDS file (*.eds)|*.eds",
479479
style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT | wx.FD_CHANGE_DIR,
480480
) as dialog:
481481
if dialog.ShowModal() != wx.ID_OK:

tests/conftest.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ def pytest_generate_tests(metafunc):
200200
# Add "_suffix" fixture
201201
if "_suffix" in metafunc.fixturenames:
202202
metafunc.parametrize(
203-
"_suffix", ['od', 'json', 'eds'], indirect=False, scope="session"
203+
"_suffix", ['od', 'jsonc', 'json', 'eds'], indirect=False, scope="session"
204204
)
205205

206206
# Make a list of all .od files in tests/od
@@ -211,6 +211,7 @@ def pytest_generate_tests(metafunc):
211211
jsonfiles = []
212212
for d in oddirs:
213213
jsonfiles += ODPath.nfactory(d.glob('*.json'))
214+
jsonfiles += ODPath.nfactory(d.glob('*.jsonc'))
214215

215216
edsfiles = []
216217
for d in oddirs:
@@ -228,15 +229,15 @@ def odids(odlist):
228229
)
229230

230231
# Add "odjson" fixture
231-
# Fixture for each of the .od and .json files in the test directory
232+
# Fixture for each of the .od and .json[c] files in the test directory
232233
if "odjson" in metafunc.fixturenames:
233234
data = sorted(odfiles + jsonfiles)
234235
metafunc.parametrize(
235236
"odjson", data, ids=odids(data), indirect=False, scope="session"
236237
)
237238

238239
# Add "odjsoneds" fixture
239-
# Fixture for each of the .od, .json, and .eds files in the test directory
240+
# Fixture for each of the .od, .json[c], and .eds files in the test directory
240241
if "odjsoneds" in metafunc.fixturenames:
241242
data = sorted(odfiles + jsonfiles + edsfiles)
242243
metafunc.parametrize(

tests/od/jsonod-comments.json

+3-9
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@
1212
"default_string_size": 24,
1313
"dictionary": [
1414
{
15-
"index": "0x1003", // 4099
15+
"index": "0x1003",
1616
"name": "Pre-defined Error Field",
1717
"struct": "array",
1818
"group": "built-in",
1919
"mandatory": false,
2020
"profile_callback": true,
2121
"each": {
2222
"name": "Standard Error Field",
23-
"type": "UNSIGNED32", // 7
23+
"type": "UNSIGNED32",
2424
"access": "ro",
2525
"pdo": false,
2626
"nbmin": 1,
@@ -29,23 +29,17 @@
2929
"sub": [
3030
{
3131
"name": "Number of Errors",
32-
"type": "UNSIGNED8", // 5
32+
"type": "UNSIGNED8",
3333
"access": "rw",
3434
"pdo": false
3535
},
3636
{
37-
// "name": "Standard Error Field"
38-
// "type": "UNSIGNED32" // 7
3937
"value": 0
4038
},
4139
{
42-
// "name": "Standard Error Field"
43-
// "type": "UNSIGNED32" // 7
4440
"value": 0
4541
},
4642
{
47-
// "name": "Standard Error Field"
48-
// "type": "UNSIGNED32" // 7
4943
"value": 0
5044
}
5145
]

tests/od/jsonod-comments.jsonc

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"$id": "od data",
3+
"$version": "1",
4+
"$description": "Canfestival object dictionary data",
5+
"$tool": "odg 3.5",
6+
"$date": "2024-08-13T11:36:24.609168",
7+
"name": "JSON comments",
8+
"description": "Test for JSON comments",
9+
"type": "slave",
10+
"id": 0,
11+
"profile": "None",
12+
"default_string_size": 24,
13+
"dictionary": [
14+
{
15+
"index": "0x1003", // 4099
16+
"name": "Pre-defined Error Field",
17+
"struct": "array",
18+
"group": "built-in",
19+
"mandatory": false,
20+
"profile_callback": true,
21+
"each": {
22+
"name": "Standard Error Field",
23+
"type": "UNSIGNED32", // 7
24+
"access": "ro",
25+
"pdo": false,
26+
"nbmin": 1,
27+
"nbmax": 254
28+
},
29+
"sub": [
30+
{
31+
"name": "Number of Errors",
32+
"type": "UNSIGNED8", // 5
33+
"access": "rw",
34+
"pdo": false
35+
},
36+
{
37+
// "name": "Standard Error Field"
38+
// "type": "UNSIGNED32" // 7
39+
"value": 0
40+
},
41+
{
42+
// "name": "Standard Error Field"
43+
// "type": "UNSIGNED32" // 7
44+
"value": 0
45+
},
46+
{
47+
// "name": "Standard Error Field"
48+
// "type": "UNSIGNED32" // 7
49+
"value": 0
50+
}
51+
]
52+
}
53+
]
54+
}

tests/test_jsonod.py

+20-7
Original file line numberDiff line numberDiff line change
@@ -135,15 +135,28 @@ def test_jsonod_timezone():
135135
def test_jsonod_comments(odpath):
136136
""" Test that the json file exports comments correctly. """
137137

138-
fname = odpath / "jsonod-comments.json"
139-
m1 = Node.LoadFile(fname)
140-
with open(fname, "r") as f:
141-
od = f.read()
138+
fname_jsonc = odpath / "jsonod-comments.jsonc"
139+
fname_json = odpath / "jsonod-comments.json"
142140

143-
out = generate_jsonc(m1, compact=False, sort=False, internal=False, validate=True)
141+
m1 = Node.LoadFile(fname_jsonc)
142+
143+
with open(fname_jsonc, "r") as f:
144+
jsonc_data = f.read()
145+
with open(fname_json, "r") as f:
146+
json_data = f.read()
147+
148+
out = generate_jsonc(m1, compact=False, sort=False, internal=False, validate=True, jsonc=True)
149+
150+
# Compare the jsonc data with the generated data
151+
for a, b in zip(jsonc_data.splitlines(), out.splitlines()):
152+
if '"$date"' in a or '"$tool"' in a:
153+
continue
154+
assert a == b
155+
156+
out = generate_jsonc(m1, compact=False, sort=False, internal=False, validate=True, jsonc=False)
144157

145-
for a, b in zip(od.splitlines(), out.splitlines()):
146-
print(a)
158+
# Compare the json data with the generated data
159+
for a, b in zip(json_data.splitlines(), out.splitlines()):
147160
if '"$date"' in a or '"$tool"' in a:
148161
continue
149162
assert a == b

0 commit comments

Comments
 (0)