jsmn-tools 0.1.0__tar.gz

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.
Files changed (154) hide show
  1. jsmn_tools-0.1.0/.clang-format +14 -0
  2. jsmn_tools-0.1.0/.clangd +2 -0
  3. jsmn_tools-0.1.0/.cmake-format.yaml +16 -0
  4. jsmn_tools-0.1.0/.coveragerc +7 -0
  5. jsmn_tools-0.1.0/.github/workflows/ci.yml +50 -0
  6. jsmn_tools-0.1.0/.github/workflows/publish.yml +17 -0
  7. jsmn_tools-0.1.0/.gitignore +61 -0
  8. jsmn_tools-0.1.0/.importlinter +10 -0
  9. jsmn_tools-0.1.0/.mypy.ini +16 -0
  10. jsmn_tools-0.1.0/.prettierrc.yaml +8 -0
  11. jsmn_tools-0.1.0/CLAUDE.md +47 -0
  12. jsmn_tools-0.1.0/LICENSE +21 -0
  13. jsmn_tools-0.1.0/PKG-INFO +9 -0
  14. jsmn_tools-0.1.0/README.md +43 -0
  15. jsmn_tools-0.1.0/cmake/modules/FindUnity.cmake +47 -0
  16. jsmn_tools-0.1.0/cmake/modules/JsmnToolsConfig.cmake +45 -0
  17. jsmn_tools-0.1.0/codegen/src/jsmn_tools/__init__.py +1 -0
  18. jsmn_tools-0.1.0/codegen/src/jsmn_tools/__main__.py +5 -0
  19. jsmn_tools-0.1.0/codegen/src/jsmn_tools/bundle.py +128 -0
  20. jsmn_tools-0.1.0/codegen/src/jsmn_tools/cli.py +105 -0
  21. jsmn_tools-0.1.0/codegen/src/jsmn_tools/lang/jsmn/__init__.py +21 -0
  22. jsmn_tools-0.1.0/codegen/src/jsmn_tools/lang/jsmn/descriptor.py +282 -0
  23. jsmn_tools-0.1.0/codegen/src/jsmn_tools/lang/jsmn/filters.py +210 -0
  24. jsmn_tools-0.1.0/codegen/src/jsmn_tools/lang/jsmn/flatten.py +245 -0
  25. jsmn_tools-0.1.0/codegen/src/jsmn_tools/lang/jsmn/ir.py +202 -0
  26. jsmn_tools-0.1.0/codegen/src/jsmn_tools/lang/jsmn/mangle.py +68 -0
  27. jsmn_tools-0.1.0/codegen/src/jsmn_tools/lang/jsmn/primitives.py +16 -0
  28. jsmn_tools-0.1.0/codegen/src/jsmn_tools/lang/jsmn/render.py +285 -0
  29. jsmn_tools-0.1.0/codegen/src/jsmn_tools/lang/jsmn/runtime/jsmn.h +471 -0
  30. jsmn_tools-0.1.0/codegen/src/jsmn_tools/lang/jsmn/runtime/runtime.c +761 -0
  31. jsmn_tools-0.1.0/codegen/src/jsmn_tools/lang/jsmn/runtime/runtime.h +136 -0
  32. jsmn_tools-0.1.0/codegen/src/jsmn_tools/lang/jsmn/templates/jsmn_generated.c +149 -0
  33. jsmn_tools-0.1.0/codegen/src/jsmn_tools/lang/jsmn/templates/jsmn_generated.h +83 -0
  34. jsmn_tools-0.1.0/codegen/src/jsmn_tools/node/__init__.py +44 -0
  35. jsmn_tools-0.1.0/codegen/src/jsmn_tools/node/behavior.py +52 -0
  36. jsmn_tools-0.1.0/codegen/src/jsmn_tools/node/location.py +40 -0
  37. jsmn_tools-0.1.0/codegen/src/jsmn_tools/node/node.py +132 -0
  38. jsmn_tools-0.1.0/codegen/src/jsmn_tools/node/ref.py +42 -0
  39. jsmn_tools-0.1.0/codegen/src/jsmn_tools/spec/__init__.py +26 -0
  40. jsmn_tools-0.1.0/codegen/src/jsmn_tools/spec/json_schema.py +68 -0
  41. jsmn_tools-0.1.0/codegen/src/jsmn_tools/spec/openapi_3_1.py +171 -0
  42. jsmn_tools-0.1.0/codegen/src/jsmn_tools/walk/__init__.py +32 -0
  43. jsmn_tools-0.1.0/codegen/src/jsmn_tools/walk/diff.py +71 -0
  44. jsmn_tools-0.1.0/codegen/src/jsmn_tools/walk/join.py +90 -0
  45. jsmn_tools-0.1.0/codegen/src/jsmn_tools/walk/merge.py +109 -0
  46. jsmn_tools-0.1.0/codegen/src/jsmn_tools/walk/normalize.py +39 -0
  47. jsmn_tools-0.1.0/codegen/src/jsmn_tools/walk/walk.py +33 -0
  48. jsmn_tools-0.1.0/codegen/tests/e2e/CMakeLists.txt +79 -0
  49. jsmn_tools-0.1.0/codegen/tests/e2e/README.md +9 -0
  50. jsmn_tools-0.1.0/codegen/tests/e2e/gen_test.h +92 -0
  51. jsmn_tools-0.1.0/codegen/tests/e2e/listify.h +217 -0
  52. jsmn_tools-0.1.0/codegen/tests/e2e/templates/jsmn.h.jinja2 +1 -0
  53. jsmn_tools-0.1.0/codegen/tests/e2e/templates/jsmn_generated.c.jinja2 +2 -0
  54. jsmn_tools-0.1.0/codegen/tests/e2e/templates/jsmn_generated.h.jinja2 +4 -0
  55. jsmn_tools-0.1.0/codegen/tests/e2e/templates/runtime.c.jinja2 +3 -0
  56. jsmn_tools-0.1.0/codegen/tests/e2e/templates/runtime.h.jinja2 +3 -0
  57. jsmn_tools-0.1.0/codegen/tests/e2e/test_cursor.c +98 -0
  58. jsmn_tools-0.1.0/codegen/tests/e2e/test_descriptor.c +110 -0
  59. jsmn_tools-0.1.0/codegen/tests/e2e/test_fmt.c +124 -0
  60. jsmn_tools-0.1.0/codegen/tests/e2e/test_generated.c +410 -0
  61. jsmn_tools-0.1.0/codegen/tests/e2e/test_tok.c +123 -0
  62. jsmn_tools-0.1.0/codegen/tests/e2e/test_tok_ex.c +112 -0
  63. jsmn_tools-0.1.0/codegen/tests/e2e/util.h +30 -0
  64. jsmn_tools-0.1.0/codegen/tests/fixtures/flatten/README.md +49 -0
  65. jsmn_tools-0.1.0/codegen/tests/fixtures/flatten/spec_a.yaml +205 -0
  66. jsmn_tools-0.1.0/codegen/tests/fixtures/flatten/spec_b.yaml +39 -0
  67. jsmn_tools-0.1.0/codegen/tests/fixtures/join/merge_conflict_a.yaml +14 -0
  68. jsmn_tools-0.1.0/codegen/tests/fixtures/join/merge_conflict_b.yaml +14 -0
  69. jsmn_tools-0.1.0/codegen/tests/fixtures/join/merge_union_a.yaml +14 -0
  70. jsmn_tools-0.1.0/codegen/tests/fixtures/join/merge_union_b.yaml +19 -0
  71. jsmn_tools-0.1.0/codegen/tests/fixtures/join/openapi_sets_a.yaml +29 -0
  72. jsmn_tools-0.1.0/codegen/tests/fixtures/join/openapi_sets_b.yaml +29 -0
  73. jsmn_tools-0.1.0/codegen/tests/fixtures/join/ordered_a.yaml +13 -0
  74. jsmn_tools-0.1.0/codegen/tests/fixtures/join/ordered_b.yaml +13 -0
  75. jsmn_tools-0.1.0/codegen/tests/fixtures/join/refs.yaml +23 -0
  76. jsmn_tools-0.1.0/codegen/tests/fixtures/join/schema_dispatch_a.yaml +28 -0
  77. jsmn_tools-0.1.0/codegen/tests/fixtures/join/schema_dispatch_b.yaml +28 -0
  78. jsmn_tools-0.1.0/codegen/tests/fixtures/join/schema_sets_a.yaml +21 -0
  79. jsmn_tools-0.1.0/codegen/tests/fixtures/join/schema_sets_b.yaml +21 -0
  80. jsmn_tools-0.1.0/codegen/tests/fixtures/location/pointer_resolve.yaml +37 -0
  81. jsmn_tools-0.1.0/codegen/tests/fixtures/render/jsmn-every-type.openapi.yaml +24 -0
  82. jsmn_tools-0.1.0/codegen/tests/fixtures/render/jsmn-vla.openapi.yaml +122 -0
  83. jsmn_tools-0.1.0/codegen/tests/fixtures/render/jsmn.openapi.yaml +53 -0
  84. jsmn_tools-0.1.0/codegen/tests/fixtures/runtime/arrays.yaml +159 -0
  85. jsmn_tools-0.1.0/codegen/tests/fixtures/runtime/objects.yaml +51 -0
  86. jsmn_tools-0.1.0/codegen/tests/fixtures/runtime/optionals.yaml +50 -0
  87. jsmn_tools-0.1.0/codegen/tests/fixtures/runtime/primitives.yaml +47 -0
  88. jsmn_tools-0.1.0/codegen/tests/fixtures/templates/bool.h +12 -0
  89. jsmn_tools-0.1.0/codegen/tests/fixtures/templates/bool.yml +2 -0
  90. jsmn_tools-0.1.0/codegen/tests/fixtures/templates/bugfix-dependency-order.h +72 -0
  91. jsmn_tools-0.1.0/codegen/tests/fixtures/templates/bugfix-dependency-order.yml +13 -0
  92. jsmn_tools-0.1.0/codegen/tests/fixtures/templates/bugfix-null-property.h +53 -0
  93. jsmn_tools-0.1.0/codegen/tests/fixtures/templates/bugfix-null-property.yml +6 -0
  94. jsmn_tools-0.1.0/codegen/tests/fixtures/templates/generic.h +80 -0
  95. jsmn_tools-0.1.0/codegen/tests/fixtures/templates/generic.yml +47 -0
  96. jsmn_tools-0.1.0/codegen/tests/fixtures/templates/null.h +12 -0
  97. jsmn_tools-0.1.0/codegen/tests/fixtures/templates/null.yml +2 -0
  98. jsmn_tools-0.1.0/codegen/tests/fixtures/templates/number.h +54 -0
  99. jsmn_tools-0.1.0/codegen/tests/fixtures/templates/number.yml +24 -0
  100. jsmn_tools-0.1.0/codegen/tests/fixtures/templates/object.h +103 -0
  101. jsmn_tools-0.1.0/codegen/tests/fixtures/templates/object.yml +69 -0
  102. jsmn_tools-0.1.0/codegen/tests/fixtures/templates/object_optionals.h +201 -0
  103. jsmn_tools-0.1.0/codegen/tests/fixtures/templates/object_optionals.yml +57 -0
  104. jsmn_tools-0.1.0/codegen/tests/fixtures/templates/object_optionals_prefix.h +201 -0
  105. jsmn_tools-0.1.0/codegen/tests/fixtures/templates/object_optionals_prefix.yml +57 -0
  106. jsmn_tools-0.1.0/codegen/tests/fixtures/templates/object_prefix.h +103 -0
  107. jsmn_tools-0.1.0/codegen/tests/fixtures/templates/object_prefix.yml +69 -0
  108. jsmn_tools-0.1.0/codegen/tests/fixtures/templates/string.h +12 -0
  109. jsmn_tools-0.1.0/codegen/tests/fixtures/templates/string.yml +4 -0
  110. jsmn_tools-0.1.0/codegen/tests/fixtures/templates/variable_length_arrays.h +97 -0
  111. jsmn_tools-0.1.0/codegen/tests/fixtures/templates/variable_length_arrays.yml +114 -0
  112. jsmn_tools-0.1.0/codegen/tests/fixtures/templates/variable_length_arrays_optionals.h +178 -0
  113. jsmn_tools-0.1.0/codegen/tests/fixtures/templates/variable_length_arrays_optionals.yml +113 -0
  114. jsmn_tools-0.1.0/codegen/tests/fixtures/walk/components.yaml +24 -0
  115. jsmn_tools-0.1.0/codegen/tests/fixtures/walk/paths.yaml +43 -0
  116. jsmn_tools-0.1.0/codegen/tests/fixtures/workspace/network/.jsmn-tools.yaml +10 -0
  117. jsmn_tools-0.1.0/codegen/tests/fixtures/workspace/network/schemas/ethernet.openapi.yaml +34 -0
  118. jsmn_tools-0.1.0/codegen/tests/fixtures/workspace/network/schemas/wifi.openapi.yaml +29 -0
  119. jsmn_tools-0.1.0/codegen/tests/fixtures/workspace/sdk/.jsmn-tools.yaml +10 -0
  120. jsmn_tools-0.1.0/codegen/tests/fixtures/workspace/sdk/schemas/auth.openapi.yaml +31 -0
  121. jsmn_tools-0.1.0/codegen/tests/fixtures/workspace/sdk/schemas/common.openapi.yaml +30 -0
  122. jsmn_tools-0.1.0/codegen/tests/fixtures/workspace/sensors/.jsmn-tools.yaml +10 -0
  123. jsmn_tools-0.1.0/codegen/tests/fixtures/workspace/sensors/schemas/humidity.openapi.yaml +30 -0
  124. jsmn_tools-0.1.0/codegen/tests/fixtures/workspace/sensors/schemas/temperature.openapi.yaml +30 -0
  125. jsmn_tools-0.1.0/codegen/tests/integration/__init__.py +0 -0
  126. jsmn_tools-0.1.0/codegen/tests/integration/__snapshots__/test_jsmn_render.ambr +2065 -0
  127. jsmn_tools-0.1.0/codegen/tests/integration/test_flatten_openapi_3_1.py +149 -0
  128. jsmn_tools-0.1.0/codegen/tests/integration/test_join_openapi_3_1.py +122 -0
  129. jsmn_tools-0.1.0/codegen/tests/integration/test_jsmn_render.py +68 -0
  130. jsmn_tools-0.1.0/codegen/tests/integration/test_normalize_openapi_3_1.py +49 -0
  131. jsmn_tools-0.1.0/codegen/tests/integration/test_spec_location.py +60 -0
  132. jsmn_tools-0.1.0/codegen/tests/integration/test_walk_openapi_3_1.py +44 -0
  133. jsmn_tools-0.1.0/codegen/tests/unit/__init__.py +0 -0
  134. jsmn_tools-0.1.0/codegen/tests/unit/test_bundle.py +29 -0
  135. jsmn_tools-0.1.0/codegen/tests/unit/test_lang_jsmn.py +147 -0
  136. jsmn_tools-0.1.0/codegen/tests/unit/test_spec_diff.py +91 -0
  137. jsmn_tools-0.1.0/codegen/tests/unit/test_spec_location.py +147 -0
  138. jsmn_tools-0.1.0/codegen/tests/unit/test_spec_ref.py +46 -0
  139. jsmn_tools-0.1.0/docs/_static/custom.css +267 -0
  140. jsmn_tools-0.1.0/docs/_static/logo.png +0 -0
  141. jsmn_tools-0.1.0/docs/_templates/base.html +12 -0
  142. jsmn_tools-0.1.0/docs/ai/audits/audits.md +1 -0
  143. jsmn_tools-0.1.0/docs/ai/index.md +12 -0
  144. jsmn_tools-0.1.0/docs/ai/prompts/prompts.md +1 -0
  145. jsmn_tools-0.1.0/docs/conf.py +91 -0
  146. jsmn_tools-0.1.0/docs/index.md +11 -0
  147. jsmn_tools-0.1.0/pyproject.toml +45 -0
  148. jsmn_tools-0.1.0/pyrightconfig.json +14 -0
  149. jsmn_tools-0.1.0/pytest.ini +2 -0
  150. jsmn_tools-0.1.0/radon.cfg +9 -0
  151. jsmn_tools-0.1.0/rebrand.md +135 -0
  152. jsmn_tools-0.1.0/ruff.toml +32 -0
  153. jsmn_tools-0.1.0/schema-tools-compare.md +99 -0
  154. jsmn_tools-0.1.0/uv.lock +1419 -0
@@ -0,0 +1,14 @@
1
+ BasedOnStyle: LLVM
2
+ ColumnLimit: 80
3
+ # Return type on its own line, brace on new line for functions
4
+ BreakAfterReturnType: All
5
+ BreakBeforeBraces: Linux
6
+ BinPackParameters: false
7
+ BinPackArguments: false
8
+ AllowAllParametersOfDeclarationOnNextLine: false
9
+ InsertBraces: true
10
+ IndentWidth: 4
11
+ AlignConsecutiveDeclarations:
12
+ Enabled: true
13
+ AcrossEmptyLines: false
14
+ AcrossComments: true
@@ -0,0 +1,2 @@
1
+ CompileFlags:
2
+ CompilationDatabase: build/
@@ -0,0 +1,16 @@
1
+ line_width: 100
2
+ tab_size: 4
3
+ enable_markup: false
4
+ max_subgroups_hwrap: 3
5
+ max_pargs_hwrap: 6
6
+ separate_ctrl_name_with_space: false
7
+ separate_fn_name_with_space: false
8
+ dangle_parens: true
9
+ dangle_align: prefix
10
+ min_prefix_chars: 4
11
+ max_prefix_chars: 10
12
+ max_lines_hwrap: 2
13
+ command_case: canonical
14
+ keyword_case: upper
15
+ enable_sort: true
16
+ autosort: true
@@ -0,0 +1,7 @@
1
+ [run]
2
+ source = jsmn_tools
3
+
4
+ [report]
5
+ omit =
6
+ codegen/src/jsmn_tools/__main__.py
7
+ codegen/src/jsmn_tools/cli.py
@@ -0,0 +1,50 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ name: Tests & Quality
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: astral-sh/setup-uv@v4
15
+ - run: uv sync
16
+
17
+ - name: Pytest with coverage
18
+ run: uv run pytest --cov=jsmn_tools --cov-report=term-missing --cov-fail-under=90
19
+
20
+ - name: Radon complexity
21
+ run: |
22
+ output=$(uv run radon cc codegen/src/jsmn_tools -n D --no-assert)
23
+ if echo "$output" | grep -qE "^\s+[FMCD]"; then
24
+ echo "::error::Complexity check failed — blocks rated D or worse:"
25
+ echo "$output"
26
+ exit 1
27
+ fi
28
+ echo "Radon check passed (threshold: D)"
29
+
30
+ e2e:
31
+ name: E2E (${{ matrix.os }})
32
+ runs-on: ${{ matrix.os }}
33
+ strategy:
34
+ matrix:
35
+ os: [ubuntu-latest, windows-latest]
36
+ steps:
37
+ - uses: actions/checkout@v4
38
+ - uses: astral-sh/setup-uv@v4
39
+ - uses: ilammy/msvc-dev-cmd@v1
40
+ if: runner.os == 'Windows'
41
+ - run: uv sync
42
+
43
+ - name: Configure
44
+ run: uv run cmake -B build -S codegen/tests/e2e -G Ninja
45
+
46
+ - name: Build
47
+ run: cmake --build build
48
+
49
+ - name: Test
50
+ run: ctest --test-dir build --output-on-failure
@@ -0,0 +1,17 @@
1
+ name: Publish
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ environment: pypi
11
+ permissions:
12
+ id-token: write
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: astral-sh/setup-uv@v4
16
+ - run: uv build
17
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,61 @@
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Build outputs - TypeScript
5
+ dist/
6
+ *.tsbuildinfo
7
+
8
+ # Build outputs - Python
9
+ __pycache__/
10
+ *.py[cod]
11
+ *$py.class
12
+ *.so
13
+ .Python
14
+ build/
15
+ develop-eggs/
16
+ eggs/
17
+ *.egg-info/
18
+ *.egg
19
+ .installed.cfg
20
+ pip-log.txt
21
+ pip-delete-this-directory.txt
22
+
23
+ # Build outputs - CMake
24
+ CMakeCache.txt
25
+ CMakeFiles/
26
+ cmake_install.cmake
27
+ compile_commands.json
28
+ Makefile
29
+ !**/CMakeLists.txt
30
+ .cache
31
+
32
+ # Virtual environments
33
+ .venv/
34
+ venv/
35
+ ENV/
36
+
37
+ # IDE / Editor
38
+ .vscode/
39
+ .idea/
40
+ *.swp
41
+ *.swo
42
+ *~
43
+
44
+ # OS
45
+ .DS_Store
46
+ Thumbs.db
47
+
48
+ # Testing / Coverage
49
+ coverage/
50
+ .coverage
51
+ htmlcov/
52
+ .pytest_cache/
53
+
54
+ # Sphinx
55
+ docs/_build/
56
+
57
+ # Claude
58
+ .claude/
59
+
60
+ # AI local artifacts (conversations, drafts with PII)
61
+ docs/ai/local/
@@ -0,0 +1,10 @@
1
+ [importlinter]
2
+ root_package = jsmn_tools
3
+
4
+ [importlinter:contract:layers]
5
+ name = node <- walk <- spec layering
6
+ type = layers
7
+ layers =
8
+ jsmn_tools.spec
9
+ jsmn_tools.walk
10
+ jsmn_tools.node
@@ -0,0 +1,16 @@
1
+ [mypy]
2
+ python_version = 3.12
3
+ files = codegen/src, codegen/tests
4
+ exclude = codegen/tests/e2e
5
+ strict = True
6
+ warn_return_any = True
7
+ warn_unused_ignores = True
8
+ disallow_untyped_defs = True
9
+ disallow_incomplete_defs = True
10
+ check_untyped_defs = True
11
+ no_implicit_optional = True
12
+ warn_redundant_casts = True
13
+ warn_unused_configs = True
14
+ show_error_codes = True
15
+ disallow_any_generics = False
16
+ warn_unreachable = True
@@ -0,0 +1,8 @@
1
+ proseWrap: always
2
+ printWidth: 80
3
+ tabWidth: 2
4
+ useTabs: false
5
+ semi: true
6
+ singleQuote: false
7
+ trailingComma: es5
8
+ bracketSpacing: true
@@ -0,0 +1,47 @@
1
+ # Jsmn Tools
2
+
3
+ ## Intro
4
+
5
+ jsmn-tools is a code generation project. jsmn-tools generates code by
6
+ crawling OpenAPI specifications and AsyncAPI specifications for "JSON Schemas".
7
+ Code generation must be explicitly declared via OpenAPI and AsyncAPI extension.
8
+
9
+ More details are in the design document. You should only need to consult the
10
+ design document when you are assigned a task.
11
+
12
+ ## Design Pushback
13
+
14
+ If you identify a design flaw in what I'm asking for, stop and flag it before
15
+ implementing. Explain the concern and propose an alternative. Don't implement
16
+ something you know is wrong just because I asked for it.
17
+
18
+ Specific things to watch for:
19
+ - Adding indirection (wrappers, abstractions, registries) without a concrete
20
+ second consumer
21
+ - Premature generalization — solving hypothetical future requirements instead of
22
+ the actual task
23
+ - Layering workarounds on top of a bad foundation instead of fixing the
24
+ foundation
25
+ - Duplicating logic that should be factored, or factoring logic that only has
26
+ one call site
27
+
28
+ When in doubt, push back with a short explanation and an alternative. I'd rather
29
+ have a 30-second conversation than undo a bad design later.
30
+
31
+ ## Code Structure
32
+
33
+ ```
34
+ codegen/
35
+ src/jsmn_tools/
36
+ cli.py # CLI entry point
37
+ bundle.py # Bundle logic for code generation
38
+ node/ # Node types, behaviors, locations, refs
39
+ spec/ # Spec structure definitions (OpenAPI 3.1, JSON Schema)
40
+ walk/ # Tree operations (walk, normalize, diff, merge, join)
41
+ lang/jsmn/ # C code generation (IR types, flatten, templates, runtime)
42
+ tests/
43
+ fixtures/ # Test fixtures (specs, templates)
44
+ unit/ # Unit tests
45
+ integration/ # Integration tests
46
+ e2e/ # Full end to end C integration test (via CMake)
47
+ ```
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Altronix
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: jsmn-tools
3
+ Version: 0.1.0
4
+ License-File: LICENSE
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: jinja2>=3.1.6
7
+ Requires-Dist: jsonschema>=4.23
8
+ Requires-Dist: referencing>=0.35
9
+ Requires-Dist: ruamel-yaml>=0.19.1
@@ -0,0 +1,43 @@
1
+ # jsmn-tools
2
+
3
+ [![CI](https://github.com/TomzBench/jsmn-tools/actions/workflows/ci.yml/badge.svg)](https://github.com/TomzBench/jsmn-tools/actions/workflows/ci.yml)
4
+
5
+ A code generator for JSON parsing in C.
6
+
7
+ ## Testing
8
+
9
+ ```bash
10
+ uv run pytest # unit + integration
11
+ uv run pytest --cov=jsmn_tools # with coverage
12
+ uv run pytest codegen/tests/unit # unit only
13
+ uv run radon cc codegen/src -a -nb # cyclomatic complexity
14
+ uv run radon mi codegen/src -nb # maintainability index
15
+ ```
16
+
17
+ # Building the docs
18
+
19
+ ## Prerequisites
20
+
21
+ - Python >= 3.11
22
+ - [uv](https://docs.astral.sh/uv/)
23
+
24
+ ## Setup
25
+
26
+ ```bash
27
+ uv venv
28
+ uv sync --extra docs
29
+ ```
30
+
31
+ ## Build
32
+
33
+ ```bash
34
+ uv run sphinx-build -b html docs docs/_build
35
+ ```
36
+
37
+ ## Preview
38
+
39
+ ```bash
40
+ uv run python -m http.server 8000 --directory docs/_build
41
+ ```
42
+
43
+ Open [http://localhost:8000](http://localhost:8000).
@@ -0,0 +1,47 @@
1
+ # FindUnity.cmake
2
+ #
3
+ # Find or fetch the ThrowTheSwitch Unity test framework.
4
+ #
5
+ # First attempts find_package in CONFIG mode (for system-installed Unity).
6
+ # Falls back to FetchContent from GitHub if not found.
7
+ #
8
+ # Options:
9
+ # UNITY_VERSION - Git tag to fetch (default: v2.6.1)
10
+ # UNITY_FIND_REQUIRED - If TRUE, failure is fatal (standard find_package behavior)
11
+ #
12
+ # Imported targets:
13
+ # unity::framework - The target to link against
14
+
15
+ if(TARGET unity::framework)
16
+ return()
17
+ endif()
18
+
19
+ # Try system-installed Unity first (exports unity::framework)
20
+ find_package(unity CONFIG QUIET)
21
+ if(unity_FOUND)
22
+ set(Unity_FOUND TRUE)
23
+ return()
24
+ endif()
25
+
26
+ # Fall back to FetchContent
27
+ include(FetchContent)
28
+
29
+ if(NOT DEFINED UNITY_VERSION)
30
+ set(UNITY_VERSION "v2.6.1")
31
+ endif()
32
+
33
+ FetchContent_Declare(
34
+ unity
35
+ GIT_REPOSITORY https://github.com/ThrowTheSwitch/Unity.git
36
+ GIT_TAG ${UNITY_VERSION}
37
+ GIT_SHALLOW TRUE
38
+ )
39
+
40
+ FetchContent_MakeAvailable(unity)
41
+
42
+ # Suppress MSVC Spectre mitigation warnings (C5045) on Unity sources
43
+ if(MSVC AND TARGET unity)
44
+ target_compile_options(unity PRIVATE /wd5045)
45
+ endif()
46
+
47
+ set(Unity_FOUND TRUE)
@@ -0,0 +1,45 @@
1
+ set(JsmnTools_FOUND TRUE)
2
+ set(JsmnTools_VERSION "0.1.0")
3
+ find_program(JsmnTools_EXECUTABLE jsmn-tools-codegen REQUIRED)
4
+
5
+ macro(_append_if cond var)
6
+ if(${cond})
7
+ list(APPEND ${var} ${ARGN})
8
+ endif()
9
+ endmacro()
10
+
11
+ function(jsmn_tools_generate)
12
+ cmake_parse_arguments(ARG "" "TARGET;PREFIX" "SPECS;TEMPLATES;ENV" ${ARGN})
13
+
14
+ # Build --template pairs and collect outputs
15
+ list(LENGTH ARG_TEMPLATES templates_len)
16
+ math(EXPR templates_end "${templates_len} - 1")
17
+ foreach(i RANGE 0 ${templates_end} 2)
18
+ math(EXPR j "${i} + 1")
19
+ list(GET ARG_TEMPLATES ${i} tpl)
20
+ list(GET ARG_TEMPLATES ${j} out)
21
+ list(APPEND _template_args --template "${tpl}" "${out}")
22
+ list(APPEND _depends "${tpl}")
23
+ list(APPEND _outputs "${out}")
24
+ endforeach()
25
+
26
+ # Build --env args (user-defined template variables)
27
+ foreach(e IN LISTS ARG_ENV)
28
+ list(APPEND _env_args --env "${e}")
29
+ endforeach()
30
+
31
+ # Build command args
32
+ set(_args ${ARG_SPECS} ${_template_args} ${_env_args})
33
+ _append_if(ARG_PREFIX _args --prefix "${ARG_PREFIX}")
34
+
35
+ add_custom_command(
36
+ OUTPUT ${_outputs}
37
+ COMMAND ${JsmnTools_EXECUTABLE} generate ${_args}
38
+ DEPENDS ${ARG_SPECS} ${_depends}
39
+ COMMENT "jsmn-tools: generating outputs"
40
+ )
41
+
42
+ if(ARG_TARGET)
43
+ add_custom_target(${ARG_TARGET} DEPENDS ${_outputs})
44
+ endif()
45
+ endfunction()
@@ -0,0 +1 @@
1
+ """jsmn-tools code generator."""
@@ -0,0 +1,5 @@
1
+ """Allow running as `python -m jsmn_tools`."""
2
+
3
+ from jsmn_tools.cli import main
4
+
5
+ main()
@@ -0,0 +1,128 @@
1
+ """Workspace parsing utilities"""
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import NamedTuple, TypedDict, cast
6
+
7
+ from jsonschema import validate # type: ignore[import-untyped]
8
+ from referencing import Registry, Resource
9
+ from referencing.jsonschema import DRAFT202012
10
+ from ruamel.yaml import YAML
11
+
12
+ # safe yaml loader for schemas
13
+ yaml = YAML(typ="safe")
14
+
15
+ # regex for finding jsmn-tools file
16
+ RE_CONFIG = re.compile(r"^\.?(jsmnTools|JsmnTools|jsmn-tools).ya?ml$")
17
+
18
+ # regex for validating $id URIs: scheme://module/resource/vN
19
+ RE_URI = re.compile(
20
+ r"^[a-zA-Z][a-zA-Z0-9-]*://[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+/v\d+$"
21
+ )
22
+
23
+
24
+ # validator support
25
+ SCHEMA = yaml.load("""
26
+ type: object
27
+ required: [resources]
28
+ properties:
29
+ resources:
30
+ type: array
31
+ items:
32
+ type: object
33
+ required: [name, version]
34
+ properties:
35
+ name:
36
+ type: string
37
+ version:
38
+ type: number
39
+ openapi:
40
+ type: array
41
+ items:
42
+ type: string
43
+ asyncapi:
44
+ type: array
45
+ items:
46
+ type: string
47
+ if:
48
+ type: array
49
+ items:
50
+ type: string
51
+ """)
52
+
53
+
54
+ class ResourceConfig(TypedDict):
55
+ name: str
56
+ version: int
57
+ openapi: list[Path]
58
+ asyncapi: list[Path]
59
+
60
+
61
+ class Config(TypedDict):
62
+ name: str
63
+ resources: list[ResourceConfig]
64
+
65
+
66
+ class Uri(NamedTuple):
67
+ module: str
68
+ resource: str
69
+ version: int
70
+
71
+
72
+ def _parse_workspace(path: Path | str, config: Path) -> Config:
73
+ root = Path(path).absolute()
74
+ name = root.name.split(".")[0]
75
+ doc = yaml.load(root / config)
76
+ validate(instance=doc, schema=SCHEMA)
77
+ doc["name"] = name
78
+ for res in doc["resources"]:
79
+ if "openapi" in res:
80
+ res["openapi"] = [root / path for path in res["openapi"]]
81
+ else:
82
+ res["openapi"] = []
83
+ if "asyncapi" in res:
84
+ res["asyncapi"] = [root / path for path in res["asyncapi"]]
85
+ else:
86
+ res["asyncapi"] = []
87
+ return cast("Config", doc)
88
+
89
+
90
+ class InvalidUriError(Exception):
91
+ """Raised when a schema $id does not match the expected URI format."""
92
+
93
+
94
+ def _read_resource(
95
+ loc: str | Path,
96
+ scheme: str,
97
+ module: str,
98
+ res: str,
99
+ version: int,
100
+ ) -> Resource:
101
+ content = yaml.load(loc)
102
+ if "$id" not in content:
103
+ content["$id"] = f"{scheme}://{module}/{res}/v{version}"
104
+ elif not RE_URI.match(content["$id"]):
105
+ # TODO need to collect errors instead of failing fast
106
+ raise InvalidUriError("Invalid $id: \"content['$id']\"")
107
+ return Resource.from_contents(content, default_specification=DRAFT202012)
108
+
109
+
110
+ def bundle(scheme: str, workspace: list[Path] | list[str]) -> Registry:
111
+ """ """
112
+
113
+ # iterate over workspace directories, parsing specifications
114
+ configs = [
115
+ _parse_workspace(module, path)
116
+ for module in workspace
117
+ for path in Path(module).iterdir()
118
+ if RE_CONFIG.match(path.name)
119
+ ]
120
+
121
+ # flatten resources into a keyable registry
122
+ content = [
123
+ _read_resource(spec, scheme, cfg["name"], res["name"], res["version"])
124
+ for cfg in configs
125
+ for res in cfg["resources"]
126
+ for spec in res["openapi"]
127
+ ]
128
+ return content @ Registry()
@@ -0,0 +1,105 @@
1
+ """CLI entry point for jsmn tools cli"""
2
+
3
+ import argparse
4
+ import sys
5
+ from importlib.resources import files
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from referencing import Registry, Resource
10
+ from referencing.jsonschema import DRAFT202012
11
+ from ruamel.yaml import YAML
12
+
13
+ from jsmn_tools.lang.jsmn.flatten import flatten_with_resolver
14
+ from jsmn_tools.lang.jsmn.render import Renderer
15
+
16
+ JSMN_RUNTIME_DIR = files("jsmn_tools").joinpath("lang", "jsmn", "runtime")
17
+ RUNTIME_FILES = ("runtime.c", "runtime.h", "jsmn.h")
18
+
19
+ yaml = YAML(typ="safe")
20
+
21
+
22
+ def parse_spec(p: str) -> Resource:
23
+ y = yaml.load(Path(p))
24
+ return Resource.from_contents(y, default_specification=DRAFT202012)
25
+
26
+
27
+ def _die(fmt: str, *args: object) -> None:
28
+ print(f"error: {fmt % args}" if args else f"error: {fmt}", file=sys.stderr)
29
+ sys.exit(1)
30
+
31
+
32
+ def _cmake_dir() -> str:
33
+ pkg = files("jsmn_tools").joinpath("cmake")
34
+ if Path(str(pkg)).is_dir():
35
+ return str(pkg)
36
+ # Editable install: cmake/ lives at repo root, not inside the package.
37
+ repo = Path(__file__).resolve().parents[3]
38
+ fallback = repo / "cmake" / "modules"
39
+ if fallback.is_dir():
40
+ return str(fallback)
41
+ raise FileNotFoundError("Cannot locate jsmn tools cmake modules")
42
+
43
+
44
+ def _generate(args: argparse.Namespace) -> None:
45
+ resources = [parse_spec(s) for s in args.specs]
46
+ extra_env = dict(e.split("=", 1) for e in args.env)
47
+ registry: Registry[Any] = resources @ Registry()
48
+ specs = [r.contents for r in registry.values()]
49
+ compiled = flatten_with_resolver(*specs, resolver=registry.resolver())
50
+ opts: dict[str, Any] = {"extra_env": extra_env}
51
+ if args.prefix:
52
+ opts["prefix"] = args.prefix
53
+ renderer = Renderer(compiled.decls, **opts)
54
+ for src, out in args.templates:
55
+ tpl = Path(src).read_text(encoding="utf-8")
56
+ Path(out).write_text(renderer.render(tpl), encoding="utf-8")
57
+
58
+
59
+ def main() -> None:
60
+ parser = argparse.ArgumentParser(prog="jsmn-tools", description="jsmn tools")
61
+ subparsers = parser.add_subparsers(dest="command")
62
+
63
+ # Top level commands
64
+ parser.add_argument(
65
+ "--cmake-dir",
66
+ action="store_true",
67
+ help="Print cmake module install path",
68
+ )
69
+
70
+ # Code generator sub command
71
+ gen_parser = subparsers.add_parser("generate", help="template renderer")
72
+ gen_parser.add_argument("specs", nargs="*", help="YAML spec files")
73
+ gen_parser.add_argument(
74
+ "--template",
75
+ nargs=2,
76
+ action="append",
77
+ metavar=("TEMPLATE", "OUTPUT"),
78
+ dest="templates",
79
+ default=[],
80
+ help="Template and output filename pair",
81
+ )
82
+ gen_parser.add_argument(
83
+ "--env",
84
+ action="append",
85
+ metavar="KEY=VALUE",
86
+ default=[],
87
+ help="User-defined template variable",
88
+ )
89
+ gen_parser.add_argument(
90
+ "--prefix",
91
+ help="Function/type prefix (default: jsmn_)",
92
+ )
93
+
94
+ args = parser.parse_args()
95
+ if args.cmake_dir:
96
+ print(_cmake_dir())
97
+ elif args.command == "generate":
98
+ _generate(args)
99
+ else:
100
+ parser.print_help()
101
+ _die("no command specified")
102
+
103
+
104
+ if __name__ == "__main__":
105
+ main()