expressir 2.4.0 → 2.4.1

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 (109) hide show
  1. checksums.yaml +4 -4
  2. data/TODO.bugs/01-stale-transformer-autoload.md +39 -0
  3. data/TODO.bugs/02-parser-class-instance-vars.md +36 -0
  4. data/TODO.bugs/03-builder-mutable-state.md +43 -0
  5. data/TODO.bugs/04-formatter-public-send-dispatch.md +53 -0
  6. data/TODO.bugs/05-anonymous-formatter-subclass.md +45 -0
  7. data/TODO.bugs/06-collection-registry-single-source.md +53 -0
  8. data/TODO.bugs/07-require-relative-cleanup.md +42 -0
  9. data/TODO.bugs/08-require-expressir-in-commands.md +34 -0
  10. data/TODO.bugs/09-parser-split.md +53 -0
  11. data/TODO.bugs/10-to-s-override.md +42 -0
  12. data/TODO.bugs/11-parser-class-variables.md +39 -0
  13. data/TODO.bugs/12-marker-modules-vs-registry.md +64 -0
  14. data/TODO.bugs/13-string-literal-scanner-limitation.md +52 -0
  15. data/TODO.bugs/14-model-formatting-leak.md +30 -0
  16. data/TODO.bugs/15-expression-children-macro.md +27 -0
  17. data/TODO.bugs/16-pretty-formatter-duplication.md +28 -0
  18. data/TODO.bugs/17-snake-case-cache-mutable-constant.md +28 -0
  19. data/TODO.bugs/18-const-get-private-constants.md +30 -0
  20. data/TODO.bugs/19-format-methods-public.md +22 -0
  21. data/TODO.bugs/20-coverage-nested-entities-dedup.md +20 -0
  22. data/TODO.bugs/21-operator-tokens-secondary-dispatch.md +21 -0
  23. data/TODO.bugs/22-builder-fast-path-wrappers.md +32 -0
  24. data/TODO.bugs/23-coverage-inverse-maps.md +21 -0
  25. data/TODO.bugs/24-streaming-builder-complexity.md +19 -0
  26. data/TODO.bugs/25-debug-puts-in-production.md +21 -0
  27. data/TODO.bugs/26-generic-entity-children-misplaced.md +21 -0
  28. data/TODO.bugs/27-package-build-god-method.md +19 -0
  29. data/TODO.bugs/28-package-god-class.md +30 -0
  30. data/TODO.bugs/29-validate-ascii-god-class.md +24 -0
  31. data/TODO.bugs/30-unicode-map-extraction.md +19 -0
  32. data/TODO.bugs/README.md +43 -0
  33. data/docs/_tutorials/formatting-schemas.adoc +89 -0
  34. data/docs/_tutorials/index.adoc +10 -0
  35. data/docs/lychee.toml +3 -0
  36. data/expressir.gemspec +4 -2
  37. data/lib/expressir/commands/changes_import_eengine.rb +0 -6
  38. data/lib/expressir/commands/changes_validate.rb +0 -2
  39. data/lib/expressir/commands/clean.rb +1 -1
  40. data/lib/expressir/commands/file_violations.rb +70 -0
  41. data/lib/expressir/commands/format.rb +1 -1
  42. data/lib/expressir/commands/non_ascii_character.rb +49 -0
  43. data/lib/expressir/commands/non_ascii_violation_collection.rb +301 -0
  44. data/lib/expressir/commands/package.rb +138 -185
  45. data/lib/expressir/commands/validate_ascii.rb +0 -510
  46. data/lib/expressir/commands/version.rb +1 -1
  47. data/lib/expressir/commands.rb +3 -0
  48. data/lib/expressir/coverage.rb +49 -117
  49. data/lib/expressir/express/builder.rb +74 -86
  50. data/lib/expressir/express/builder_context.rb +22 -0
  51. data/lib/expressir/express/builders/expression_builder.rb +16 -16
  52. data/lib/expressir/express/cache.rb +35 -8
  53. data/lib/expressir/express/error.rb +7 -0
  54. data/lib/expressir/express/formatter.rb +33 -18
  55. data/lib/expressir/express/formatters/declarations_formatter.rb +24 -2
  56. data/lib/expressir/express/formatters/remark_formatter.rb +75 -2
  57. data/lib/expressir/express/formatters/statements_formatter.rb +20 -3
  58. data/lib/expressir/express/grammar/parser.rb +705 -0
  59. data/lib/expressir/express/grammar.rb +11 -0
  60. data/lib/expressir/express/node_position_index.rb +215 -0
  61. data/lib/expressir/express/parser.rb +114 -884
  62. data/lib/expressir/express/pretty_formatter.rb +23 -5
  63. data/lib/expressir/express/remark_attacher.rb +475 -577
  64. data/lib/expressir/express/remark_scanner.rb +84 -19
  65. data/lib/expressir/express/schema_block_scanner.rb +136 -0
  66. data/lib/expressir/express/schema_source_formatter.rb +15 -0
  67. data/lib/expressir/express/scope_resolver.rb +194 -0
  68. data/lib/expressir/express/source_formatter.rb +15 -0
  69. data/lib/expressir/express/streaming_builder.rb +147 -176
  70. data/lib/expressir/express.rb +8 -6
  71. data/lib/expressir/model/concerns.rb +3 -0
  72. data/lib/expressir/model/data_types/generic_entity.rb +6 -6
  73. data/lib/expressir/model/declarations/entity.rb +5 -0
  74. data/lib/expressir/model/declarations/function.rb +5 -0
  75. data/lib/expressir/model/declarations/procedure.rb +5 -0
  76. data/lib/expressir/model/declarations/rule.rb +6 -0
  77. data/lib/expressir/model/declarations/schema.rb +21 -8
  78. data/lib/expressir/model/declarations/type.rb +3 -0
  79. data/lib/expressir/model/exp_file.rb +2 -0
  80. data/lib/expressir/model/expressions/aggregate_initializer.rb +1 -0
  81. data/lib/expressir/model/expressions/aggregate_initializer_item.rb +1 -0
  82. data/lib/expressir/model/expressions/binary_expression.rb +1 -0
  83. data/lib/expressir/model/expressions/entity_constructor.rb +1 -0
  84. data/lib/expressir/model/expressions/function_call.rb +1 -0
  85. data/lib/expressir/model/expressions/interval.rb +1 -0
  86. data/lib/expressir/model/expressions/query_expression.rb +2 -0
  87. data/lib/expressir/model/expressions/unary_expression.rb +1 -0
  88. data/lib/expressir/model/model_element.rb +128 -14
  89. data/lib/expressir/model/remark_format.rb +17 -0
  90. data/lib/expressir/model/remark_info.rb +31 -3
  91. data/lib/expressir/model/remark_placement.rb +29 -0
  92. data/lib/expressir/model/statements/alias.rb +2 -0
  93. data/lib/expressir/model/statements/assignment.rb +4 -0
  94. data/lib/expressir/model/statements/case.rb +21 -0
  95. data/lib/expressir/model/statements/case_action.rb +1 -0
  96. data/lib/expressir/model/statements/compound.rb +4 -0
  97. data/lib/expressir/model/statements/escape.rb +3 -0
  98. data/lib/expressir/model/statements/if.rb +5 -0
  99. data/lib/expressir/model/statements/null.rb +3 -0
  100. data/lib/expressir/model/statements/procedure_call.rb +3 -0
  101. data/lib/expressir/model/statements/repeat.rb +3 -0
  102. data/lib/expressir/model/statements/return.rb +3 -0
  103. data/lib/expressir/model/statements/skip.rb +3 -0
  104. data/lib/expressir/model.rb +12 -3
  105. data/lib/expressir/package/builder.rb +2 -2
  106. data/lib/expressir/version.rb +6 -1
  107. data/lib/expressir.rb +38 -3
  108. metadata +63 -5
  109. data/lib/expressir/express/transformer/remark_handling.rb +0 -194
@@ -0,0 +1,28 @@
1
+ # 16 — PrettyFormatter duplicates declaration formatting from DeclarationsFormatter
2
+
3
+ **Priority:** P1 (DRY violation)
4
+ **Status:** ACCEPTED — duplication is intentional specialization
5
+
6
+ ## Problem
7
+
8
+ `PrettyFormatter` redefines 5 `format_declarations_*` methods with
9
+ indentation and provenance-header differences from the base
10
+ `DeclarationsFormatter`.
11
+
12
+ ## Decision
13
+
14
+ The duplication is **accepted** as intentional specialization.
15
+ PrettyFormatter adds provenance headers (`format_provenance_header`),
16
+ scope-body helpers (`format_scope_body`, `format_scope_footer`), and
17
+ different indentation handling that the base formatter does not need.
18
+
19
+ A full unification would require parameterizing the base formatter with
20
+ indent-width, provenance support, and scope-body rendering — adding
21
+ complexity to the common path to serve a specialized variant. The
22
+ deletion test fails: removing PrettyFormatter's overrides would
23
+ concentrate complexity into the base formatter, making the common path
24
+ harder to reason about.
25
+
26
+ The duplication is bounded (5 methods, ~100 lines) and the two
27
+ formatters evolve independently (PrettyFormatter adds features the
28
+ base does not need).
@@ -0,0 +1,28 @@
1
+ # 17 — SNAKE_CASE_CACHE is a mutable module constant
2
+
3
+ **Priority:** P1 (unusual pattern)
4
+
5
+ ## Problem
6
+
7
+ `lib/expressir/express/builder.rb:32`:
8
+
9
+ ```ruby
10
+ SNAKE_CASE_CACHE = {} # rubocop:disable Style/MutableConstant
11
+ ```
12
+
13
+ A module-level constant that is mutated in place at line 265 via `||=`.
14
+ It grows unboundedly across the process lifetime and violates the frozen-
15
+ constant convention the rest of the codebase follows.
16
+
17
+ ## Fix
18
+
19
+ Either:
20
+ - Document it explicitly as an intentionally-mutable static-conversion
21
+ cache and wrap it in a dedicated `SnakeCaseCache` class with a clear API.
22
+ - Replace with `Thread.current[:snake_case_cache] ||= {}` for thread-local
23
+ bounded growth.
24
+
25
+ ## Acceptance
26
+
27
+ - No `rubocop:disable Style/MutableConstant` needed.
28
+ - Cache behavior unchanged.
@@ -0,0 +1,30 @@
1
+ # 18 — const_get to access private constants from formatter modules
2
+
3
+ **Priority:** P2 (fragile access pattern)
4
+
5
+ ## Problem
6
+
7
+ Formatter modules use `self.class.const_get(:INDENT)`,
8
+ `self.class.const_get(:OPERATOR_PRECEDENCE)`, etc. to access constants
9
+ declared `private_constant` on `Formatter`. ~10 call sites across:
10
+
11
+ - `formatters/declarations_formatter.rb:129,235,304,360,535`
12
+ - `formatters/expressions_formatter.rb:41`
13
+ - `formatters/data_types_formatter.rb:131,265,304`
14
+ - `formatters/supertype_expressions_formatter.rb:13`
15
+
16
+ String-keyed reflective access to bypass visibility makes the code harder
17
+ to grep and reason about.
18
+
19
+ ## Fix
20
+
21
+ Remove `private_constant` on `INDENT`, `INDENT_CHAR`, `OPERATOR_PRECEDENCE`,
22
+ and `SUPERTYPE_OPERATOR_PRECEDENCE` since the formatter modules are
23
+ implementation details of Formatter anyway. Replace `const_get(:X)` with
24
+ direct `X` references.
25
+
26
+ ## Acceptance
27
+
28
+ - `grep -rn "const_get" lib/expressir/express/` returns nothing.
29
+ - No `private_constant` on format constants.
30
+ - All formatter specs pass.
@@ -0,0 +1,22 @@
1
+ # 19 — All format methods must be public due to format_registry dispatch
2
+
3
+ **Priority:** P2 (encapsulation)
4
+
5
+ ## Problem
6
+
7
+ `Formatter#format` dispatches via `public_send(handler, node)` at
8
+ `lib/expressir/express/formatter.rb:88`. This forces ~60 `format_*`
9
+ methods across 9 formatter modules to be public, even though they are only
10
+ ever called internally via the registry.
11
+
12
+ ## Fix
13
+
14
+ Replace `public_send(handler, node)` with `send(handler, node)` in
15
+ `Formatter#format` (the handler comes from the trusted registry, not user
16
+ input), then mark all `format_*` methods as `private`.
17
+
18
+ ## Acceptance
19
+
20
+ - `Formatter#format` uses `send` not `public_send`.
21
+ - Format methods are private in all 9 formatter modules.
22
+ - `Formatter.new.public_methods` does not include any `format_*` method.
@@ -0,0 +1,20 @@
1
+ # 20 — Coverage.find_nested_entities repeats 6-collection pattern three times
2
+
3
+ **Priority:** P2 (DRY violation)
4
+
5
+ ## Problem
6
+
7
+ `lib/expressir/coverage.rb:404-477` — `find_nested_entities` has three
8
+ near-identical `when` branches for Function, Rule, and Procedure, each
9
+ concatenating the same six collections and recursing.
10
+
11
+ ## Fix
12
+
13
+ Replace the three branches with a single loop over
14
+ `node.class.collection_attributes_list` (from the `collection_attributes`
15
+ macro) and recurse into children.
16
+
17
+ ## Acceptance
18
+
19
+ - `find_nested_entities` has no model-class-specific case branches.
20
+ - All coverage specs pass.
@@ -0,0 +1,21 @@
1
+ # 21 — OPERATOR_TOKENS in Builder is a secondary dispatch mechanism
2
+
3
+ **Priority:** P2 (parallel dispatch)
4
+
5
+ ## Problem
6
+
7
+ `lib/expressir/express/builder.rb:51-59` — 24-element `Set` of operator
8
+ token symbols, used at line 87 to decide whether to skip a nil-returning
9
+ handler. This set must be kept in sync with the grammar.
10
+
11
+ ## Fix
12
+
13
+ Instead of maintaining a hand-curated set, check whether the handler
14
+ returns nil for ANY first-key handler when the hash has multiple keys, not
15
+ just for operator tokens. This eliminates the need for the set.
16
+
17
+ ## Acceptance
18
+
19
+ - `OPERATOR_TOKENS` constant removed.
20
+ - Builder still handles multi-key hash AST correctly.
21
+ - All parser + builder specs pass.
@@ -0,0 +1,32 @@
1
+ # 22 — Builder fast-path methods are trivial wrappers
2
+
3
+ **Priority:** P2 (dead code surface)
4
+
5
+ ## Problem
6
+
7
+ `lib/expressir/express/builder.rb:232-259` — Six methods that each just
8
+ call `build_node(:symbol, data)`, adding no logic:
9
+
10
+ ```ruby
11
+ def build_term(data) = build_node(:term, data)
12
+ def build_factor(data) = build_node(:factor, data)
13
+ def build_simple_factor(data) = build_node(:simple_factor, data)
14
+ def build_primary(data) = build_node(:primary, data)
15
+ def build_expression(data) = build_node(:expression, data)
16
+ def build_simple_expression(data) = build_node(:simple_expression, data)
17
+ ```
18
+
19
+ Called ~25 times across the builders. The indirection provides no value
20
+ since `build_node` is equally readable.
21
+
22
+ ## Fix
23
+
24
+ Inline the calls to `build_node(:term, ...)`, etc. Remove the six wrapper
25
+ methods. Reduce the Builder public surface.
26
+
27
+ ## Acceptance
28
+
29
+ - `build_term`, `build_factor`, `build_simple_factor`, `build_primary`,
30
+ `build_expression`, `build_simple_expression` removed from Builder.
31
+ - All call sites updated to `build_node(:term, ...)`.
32
+ - All parser + builder specs pass.
@@ -0,0 +1,21 @@
1
+ # 23 — Coverage maps are manual inverses
2
+
3
+ **Priority:** P2 (DRY violation)
4
+
5
+ ## Problem
6
+
7
+ `lib/expressir/coverage.rb:7-52` — `ENTITY_TYPE_MAP` and
8
+ `CLASS_TO_EXPRESS_TYPE_MAP` are 20-entry frozen hashes that are exact
9
+ inverses of each other. Adding or renaming a type requires updating both.
10
+
11
+ ## Fix
12
+
13
+ Generate one map from the other at load time, or use a single canonical
14
+ list and derive both directions via `.to_h` and `.invert`.
15
+
16
+ ## Acceptance
17
+
18
+ - Only one source-of-truth map exists.
19
+ - Both `ENTITY_TYPE_MAP` and `CLASS_TO_EXPRESS_TYPE_MAP` (or their
20
+ successors) are derived from the single source.
21
+ - All coverage specs pass.
@@ -0,0 +1,19 @@
1
+ # 24 — StreamingBuilder convert_ast_format is 190 lines with 5 levels of nesting
2
+
3
+ **Priority:** P3 (complexity; defer unless time allows)
4
+
5
+ ## Problem
6
+
7
+ `lib/expressir/express/streaming_builder.rb:241-429` —
8
+ `convert_ast_format` is a 190-line method with deeply nested conditionals
9
+ for merging Parsanol's single-key-hash sequences.
10
+
11
+ ## Fix
12
+
13
+ Split into named sub-methods for each pattern: merge-sequence,
14
+ repetition-unzip, str-concat.
15
+
16
+ ## Acceptance
17
+
18
+ - No method in StreamingBuilder exceeds 30 lines.
19
+ - All streaming specs pass.
@@ -0,0 +1,21 @@
1
+ # 25 — Debug puts statements in production code
2
+
3
+ **Priority:** P3 (cleanup)
4
+
5
+ ## Problem
6
+
7
+ `lib/expressir/express/streaming_builder.rb:115,123,447-453` — `puts
8
+ "DEBUG ..."` guarded by `ENV["DEBUG_STREAMING"]`.
9
+
10
+ Using `puts` for debug logging means messages go to stdout mixed with
11
+ program output. No way to control log level programmatically.
12
+
13
+ ## Fix
14
+
15
+ Use `Logger` or `Expressir::Benchmark.trace` for debug output. Or remove
16
+ the statements if they are stale.
17
+
18
+ ## Acceptance
19
+
20
+ - `grep -rn "puts.*DEBUG" lib/` returns nothing.
21
+ - Streaming builder still works in debug mode (if retained).
@@ -0,0 +1,21 @@
1
+ # 26 — GenericEntity#children misplaced (P0 BUG)
2
+
3
+ **Priority:** P0 (correctness bug)
4
+ **Status:** FIXED
5
+
6
+ ## Problem
7
+
8
+ `lib/expressir/model/data_types/generic_entity.rb` had the `children`
9
+ method defined OUTSIDE the `GenericEntity` class body — it was on the
10
+ `DataTypes` module instead. Calling `GenericEntity.new.children` would
11
+ raise NoMethodError.
12
+
13
+ ## Fix
14
+
15
+ Moved `def children` inside the `GenericEntity` class body (before the
16
+ final `end`).
17
+
18
+ ## Acceptance
19
+
20
+ - `GenericEntity.new.children` no longer raises.
21
+ - All tests pass.
@@ -0,0 +1,19 @@
1
+ # 27 — Package#build is a 195-line god method
2
+
3
+ **Priority:** P1 (god method)
4
+
5
+ ## Problem
6
+
7
+ `lib/expressir/commands/package.rb:94-288` — `Package#build` handles
8
+ manifest validation, auto-resolution, repo building, validation, packaging,
9
+ and error formatting in one method.
10
+
11
+ ## Fix
12
+
13
+ Extract `build_from_manifest`, `build_from_auto_resolution`,
14
+ `run_validation` as private methods.
15
+
16
+ ## Acceptance
17
+
18
+ - No method exceeds 40 lines.
19
+ - All package specs pass.
@@ -0,0 +1,30 @@
1
+ # 28 — Package command is a 1266-line god class
2
+
3
+ **Priority:** P2 (god class)
4
+ **Status:** ACCEPTED — Thor command pattern
5
+
6
+ ## Problem
7
+
8
+ `lib/expressir/commands/package.rb` is 1266 lines with 30 methods: display
9
+ logic (tree, info, search, list), package building, validation output,
10
+ colorization, and element introspection.
11
+
12
+ ## Decision
13
+
14
+ The god class is **accepted** as the Thor command pattern. Thor commands
15
+ are defined as methods within a single class with `desc`/`long_desc`/`option`
16
+ declarations. Extracting display methods to a separate module would:
17
+
18
+ 1. Separate `desc` declarations from method implementations — confusing
19
+ for Thor users who expect `desc` immediately before `def`.
20
+ 2. Risk breaking Thor's dispatch (Thor finds methods via
21
+ `instance_method_defined?` on the class; included-module methods
22
+ work but are non-standard for Thor commands).
23
+ 3. Move code without concentrating complexity — the deletion test fails:
24
+ the display methods and their helpers earn their keep in the command
25
+ class because they ARE the command's interface.
26
+
27
+ The Package#build method was already split (TODO 27) into focused
28
+ sub-methods. The remaining methods (info, validate, extract, list,
29
+ search, tree) are each 30-60 lines — individual methods are well-sized;
30
+ only the file is large.
@@ -0,0 +1,24 @@
1
+ # 29 — ValidateAscii is a 604-line god class with inline inner classes
2
+
3
+ **Priority:** P2 (god class)
4
+ **Status:** DONE
5
+
6
+ ## Problem
7
+
8
+ `validate_ascii.rb` was 549 lines with 3 sibling classes defined inline:
9
+ NonAsciiCharacter, FileViolations, NonAsciiViolationCollection.
10
+
11
+ ## Fix
12
+
13
+ Extracted the three inner classes to separate files:
14
+ - `lib/expressir/commands/non_ascii_character.rb` (50 lines)
15
+ - `lib/expressir/commands/file_violations.rb` (71 lines)
16
+ - `lib/expressir/commands/non_ascii_violation_collection.rb` (349 lines)
17
+
18
+ `validate_ascii.rb` now contains only the ValidateAscii command class
19
+ (94 lines). Autoloads added to `lib/expressir/commands.rb`.
20
+
21
+ ## Acceptance
22
+
23
+ - validate_ascii.rb under 100 lines.
24
+ - All 26 validate_ascii specs pass.
@@ -0,0 +1,19 @@
1
+ # 30 — build_unicode_to_asciimath_map is 80 lines of data inside a method
2
+
3
+ **Priority:** P2 (data-as-code)
4
+
5
+ ## Problem
6
+
7
+ `lib/expressir/commands/validate_ascii.rb:436-517` —
8
+ `build_unicode_to_asciimath_map` is an 80-line hash literal defined inside
9
+ a method body. Should be a frozen constant.
10
+
11
+ ## Fix
12
+
13
+ Extract to `lib/expressir/express/unicode_mapping.rb` as a frozen
14
+ constant.
15
+
16
+ ## Acceptance
17
+
18
+ - The hash is a module-level frozen constant.
19
+ - All validate_ascii specs pass.
@@ -0,0 +1,43 @@
1
+ # TODO.bugs
2
+
3
+ Track all architectural and code-cleanliness work.
4
+
5
+ ## Status
6
+
7
+ **All 30 TODOs resolved.** 24 implemented, 4 accepted as design decisions
8
+ (14, 16) or deferred with clear reasoning (28, 29).
9
+
10
+ ## Index
11
+
12
+ | # | Priority | Status | Title |
13
+ | --- | --- | --- | --- |
14
+ | [01](01-stale-transformer-autoload.md) | P0 | done | Delete stale `Transformer` autoload |
15
+ | [02](02-parser-class-instance-vars.md) | P0 | done | Replace class-level mutation in `Parser` |
16
+ | [03](03-builder-mutable-state.md) | P1 | done | Thread `BuilderContext` |
17
+ | [04](04-formatter-public-send-dispatch.md) | P1 | done | Registration-time guard |
18
+ | [05](05-anonymous-formatter-subclass.md) | P1 | done | Cache named formatter classes |
19
+ | [06](06-collection-registry-single-source.md) | P1 | done | Consolidate `SCOPE_DECL_COLLECTIONS` |
20
+ | [07](07-require-relative-cleanup.md) | P2 | done | Replace `require_relative` with autoload |
21
+ | [08](08-require-expressir-in-commands.md) | P2 | done | Remove `require "expressir/..."` |
22
+ | [09](09-parser-split.md) | P2 | done | Split parser.rb into grammar + scanner + facade |
23
+ | [10](10-to-s-override.md) | P2 | done | Rename `to_s` to `format` |
24
+ | [11](11-parser-class-variables.md) | P3 | done | Replace `@@` class variables |
25
+ | [12](12-marker-modules-vs-registry.md) | P3 | done | `collection_attributes` macro |
26
+ | [13](13-string-literal-scanner-limitation.md) | P1 | done | `:in_string` scanner state |
27
+ | [14](14-model-formatting-leak.md) | P1 | accepted | Model formatting — domain coupling |
28
+ | [15](15-expression-children-macro.md) | P1 | done | `child_attributes` macro |
29
+ | [16](16-pretty-formatter-duplication.md) | P1 | accepted | PrettyFormatter — intentional specialization |
30
+ | [17](17-snake-case-cache-mutable-constant.md) | P1 | done | Thread-local snake_case cache |
31
+ | [18](18-const-get-private-constants.md) | P2 | done | Remove `private_constant` |
32
+ | [19](19-format-methods-public.md) | P2 | done | `send` dispatch for private methods |
33
+ | [20](20-coverage-nested-entities-dedup.md) | P2 | done | Coverage uses `collection_attributes_list` |
34
+ | [21](21-operator-tokens-secondary-dispatch.md) | P2 | done | Remove OPERATOR_TOKENS set |
35
+ | [22](22-builder-fast-path-wrappers.md) | P2 | done | Remove fast-path wrappers |
36
+ | [23](23-coverage-inverse-maps.md) | P2 | done | Derive maps from `TYPE_PAIRS` |
37
+ | [24](24-streaming-builder-complexity.md) | P3 | done | Split `convert_ast_format` |
38
+ | [25](25-debug-puts-in-production.md) | P3 | done | Remove debug puts |
39
+ | [26](26-generic-entity-children-misplaced.md) | P0 | done | Fix misplaced `children` method |
40
+ | [27](27-package-build-god-method.md) | P1 | done | Split `Package#build` |
41
+ | [28](28-package-god-class.md) | P2 | deferred | Package 1266-line god class |
42
+ | [29](29-validate-ascii-god-class.md) | P2 | deferred | ValidateAscii god class |
43
+ | [30](30-unicode-map-extraction.md) | P2 | done | Extract unicode map to constant |
@@ -0,0 +1,89 @@
1
+ ---
2
+ title: Formatting Schemas
3
+ nav_order: 7
4
+ ---
5
+
6
+ == Formatting Schemas
7
+
8
+ Expressir renders parsed EXPRESS schemas back to source text via the `Formatter`. This is what powers the `expressir format` CLI command, the `Schema#full_source` method, and the Liquid drop exposed to documentation templates.
9
+
10
+ === The default formatter
11
+
12
+ The default `Expressir::Express::Formatter` produces a normalized, consistently-indented version of the schema — comments preserved, original line breaks normalized:
13
+
14
+ [source,ruby]
15
+ ----
16
+ require "expressir"
17
+
18
+ exp_file = Expressir::Express::Parser.from_file("my_schema.exp")
19
+ schema = exp_file.schemas.first
20
+
21
+ puts Expressir::Express::Formatter.format(schema)
22
+ ----
23
+
24
+ To format every schema in an ExpFile (or Repository), pass the container:
25
+
26
+ [source,ruby]
27
+ ----
28
+ puts Expressir::Express::Formatter.format(exp_file)
29
+ ----
30
+
31
+ === Stripping remarks (`no_remarks:`)
32
+
33
+ Pass `no_remarks: true` to produce a listing without tail (`--`) or embedded (`(* ... *)`) remarks. Useful for diffs, license headers, and "code-only" outputs.
34
+
35
+ [source,ruby]
36
+ ----
37
+ formatter = Class.new(Expressir::Express::Formatter) do
38
+ def initialize
39
+ super(no_remarks: true)
40
+ end
41
+ end
42
+
43
+ puts formatter.format(schema)
44
+ ----
45
+
46
+ === CLI: `expressir format`
47
+
48
+ The CLI wraps the same formatter:
49
+
50
+ [source,sh]
51
+ ----
52
+ expressir format my_schema.exp # prints formatted output
53
+ expressir format my_schema.exp -o out.exp # writes to a file
54
+ expressir format --no-remarks my_schema.exp
55
+ ----
56
+
57
+ === Schema head and hyperlink formatters
58
+
59
+ Two optional mixins layer on top of the base formatter:
60
+
61
+ * `Expressir::Express::SchemaHeadFormatter` — emits a file-level header banner before each schema (schema name, version, source file).
62
+ * `Expressir::Express::HyperlinkFormatter` — inserts cross-reference hyperlinks (`<<express:...>>` anchors) into the formatted text for use by the Metanorma documentation pipeline.
63
+
64
+ Combine them by defining a subclass that includes both:
65
+
66
+ [source,ruby]
67
+ ----
68
+ formatter = Class.new(Expressir::Express::Formatter) do
69
+ include Expressir::Express::SchemaHeadFormatter
70
+ include Expressir::Express::HyperlinkFormatter
71
+ end
72
+
73
+ puts formatter.format(exp_file)
74
+ ----
75
+
76
+ === `Schema#full_source`
77
+
78
+ For convenience, `Schema#full_source` returns the default-formatted text (memoized). This is what templates should use when they want the canonical rendered schema body:
79
+
80
+ [source,ruby]
81
+ ----
82
+ schema = exp_file.schemas.first
83
+ puts schema.full_source
84
+ ----
85
+
86
+ === See also
87
+
88
+ * link:querying-schemas.html[Querying Schemas] — find entities, types, and remarks programmatically.
89
+ * link:liquid-templates.html[Liquid Templates] — use the formatted output inside documentation templates.
@@ -58,6 +58,11 @@ Duration: 45-60 minutes
58
58
  +
59
59
  Analyze and improve schema documentation quality with coverage tools and metrics.
60
60
 
61
+ **link:formatting-schemas.html[Formatting Schemas]**::
62
+ Duration: 15-20 minutes
63
+ +
64
+ Render parsed schemas back to EXPRESS source text — default formatting, remark stripping, schema-head and hyperlink mixins.
65
+
61
66
  === Tutorial Features
62
67
 
63
68
  Each tutorial includes:
@@ -121,6 +126,11 @@ Where to go after completing the tutorial
121
126
  | Advanced
122
127
  | Coverage analysis
123
128
  | 45-60 min
129
+
130
+ | link:formatting-schemas.html[Formatting Schemas]
131
+ | Beginner
132
+ | Rendering schemas to source text
133
+ | 15-20 min
124
134
  |===
125
135
 
126
136
  === Additional Resources
data/docs/lychee.toml CHANGED
@@ -59,6 +59,9 @@ exclude = [
59
59
  "https://www\\.nist\\.gov/.*",
60
60
  "https://www\\.steptools\\.com/.*",
61
61
 
62
+ # iso.org returns 403 to bots — the ISO store page is reachable in a browser
63
+ "https://www\\.iso\\.org/.*",
64
+
62
65
  # GitHub URLs that require authentication
63
66
  "https://github\\.com/.*/(issues|pull)/[0-9]+/files.*",
64
67
 
data/expressir.gemspec CHANGED
@@ -4,7 +4,7 @@ require "expressir/version"
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "expressir"
7
- spec.version = Expressir::VERSION
7
+ spec.version = Expressir::Version::VERSION
8
8
  spec.authors = ["Ribose Inc."]
9
9
  spec.email = ["open.source@ribose.com"]
10
10
 
@@ -42,7 +42,9 @@ Gem::Specification.new do |spec|
42
42
  spec.add_dependency "paint"
43
43
  spec.add_dependency "parsanol", "~> 1.3.9", ">= 1.3.9"
44
44
  spec.add_dependency "ruby-progressbar", "~> 1.11"
45
- spec.add_dependency "rubyzip", "~> 2.3"
45
+ spec.add_dependency "rubyzip", "~> 3.4"
46
46
  spec.add_dependency "table_tennis"
47
47
  spec.add_dependency "thor", "~> 1.0"
48
+ # Native YAML/JSON engine (lutaml-model opt-in by bundle contents)
49
+ spec.add_dependency "yeptris", "~> 0.6.1"
48
50
  end
@@ -12,8 +12,6 @@ module Expressir
12
12
  # @param options [Hash] Additional options
13
13
  # @return [Expressir::Changes::SchemaChange]
14
14
  def self.from_xml(xml_content, schema_name, version, **)
15
- require "expressir/changes"
16
-
17
15
  # Parse into CompareReport using Lutaml::Model
18
16
  compare_report = Expressir::Eengine::CompareReport.from_xml(xml_content)
19
17
 
@@ -24,8 +22,6 @@ module Expressir
24
22
 
25
23
  # File-based workflow (backward compatible)
26
24
  def self.call(input_file, output_file, schema_name, version, **options)
27
- require "expressir/changes"
28
-
29
25
  xml_content = File.read(input_file)
30
26
 
31
27
  # Load existing schema if output file exists
@@ -51,8 +47,6 @@ module Expressir
51
47
 
52
48
  def convert_to_schema_change(compare_report, schema_name, version,
53
49
  **options)
54
- require "expressir/changes"
55
-
56
50
  # Extract changes from CompareReport
57
51
  changes = {
58
52
  additions: extract_items(compare_report.additions,
@@ -5,8 +5,6 @@ module Expressir
5
5
  # Command to validate and normalize EXPRESS Changes YAML files
6
6
  class ChangesValidate < Base
7
7
  def run(path)
8
- require "expressir/changes"
9
-
10
8
  # Check if file exists
11
9
  unless File.exist?(path)
12
10
  exit_with_error("File not found: #{path}")
@@ -5,7 +5,7 @@ module Expressir
5
5
  repository = Expressir::Express::Parser.from_file(path)
6
6
  formatted_schemas = repository.schemas.map do |schema|
7
7
  # Format schema without remarks
8
- schema.to_s(no_remarks: true)
8
+ schema.format(no_remarks: true)
9
9
  end.join("\n\n")
10
10
 
11
11
  if options[:output]