Skip to content

refactor: move serialization onto the shared traversal core - #111

Open
ehennestad wants to merge 2 commits into
add-jsonld-deserializerfrom
refactor-serializer-onto-transformer
Open

refactor: move serialization onto the shared traversal core#111
ehennestad wants to merge 2 commits into
add-jsonld-deserializerfrom
refactor-serializer-onto-transformer

Conversation

@ehennestad

@ehennestad ehennestad commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Completes the traversal rework from #69. The serializer walked the instance graph itself, with its own visited registry, its own recursion accounting and its own mixed type unwrapping, none of it shared with the resolver or the deserializer.

BaseTransformer

The accumulating counterpart to BaseVisitor. Each node is mapped to a representation and the representations of its children are composed into it.

The two protocols need different cycle semantics, which is why they are separate classes rather than one:

  • a visitor acts on each node once for the whole traversal, so a node is marked and never unmarked;
  • a transformer has to produce a value everywhere a node appears, so a node is marked only while its own subtree is being built. Meeting it again inside that subtree is a cycle and yields a reference; meeting it again in a different subtree is not a cycle and is transformed normally.

Edge hooks match BaseVisitor, so both protocols read the same way.

BaseSerializer

Now a BaseTransformer subclass carrying only the openMINDS rules: an instance's own property values, a reference for every linked value, an inline representation with no identifier for every embedded value, and a queue of referenced instances to be emitted as documents of their own.

SerializationContext is retired. Its visited registry comes from the traversal core, and its LinkedInstances map plus the threaded recursion depth are replaced by that explicit queue. The queue also fixes a smaller thing: an instance referenced from several places is emitted once rather than once per reference.

AbstractSerializer is deleted. It was a two-method stub with no subclasses, superseded by BaseSerializer and now by BaseDeserializer.

The output format is unchanged

Byte-for-byte, which the golden fixture from the first PR in this stack verifies.

That took two attempts, and both failures are worth recording because they are the kind of thing a refactor loses silently. openMINDS documents written by this library have an asymmetric shape:

  • an embedded value that can occur only once is written as a single object: "age": {...}
  • everything else is written as a list, including a linked value that can occur only once: "species": [{"@id": "..."}]

Centralizing the shape rule in one place made it natural to write "unwrap when the property is scalar", which changes every linked scalar in every document the library produces. The fixture caught it, then caught the mirror-image mistake when the rule was made uniformly "always a list". The asymmetry is now reproduced explicitly in BaseSerializer.setPropertyValue with a comment saying why.

This is a good argument for the fixture tests existing at all: the round-trip suite passed in both broken states, because the library still agreed with itself.

A regression this introduced, and the coverage gap behind it

The first version of doForLinkedEdge gathered the children of a linked property with obj.createReferences([children{:}]). Instances of different types cannot be concatenated, so a property holding more than one of its allowed types stopped serializing:

MATLAB:UnableToConvert
  converting from openminds.controlledterms.DataType to openminds.controlledterms.AccessChannel

This works on main, where the container array is passed through whole and createReferences reads .Instance.id from each element. Children are now referenced one at a time, which keeps them apart.

The round-trip suite did not catch it, because ommtest.helper.synthesizeInstance populates a property from a single allowed type and so only ever produces values of one type. A test is added for the multi-type case directly.

Tests

Two cases added to SerializationTest:

  • a circular graph serializes to two documents referencing each other, rather than being followed without end, and each node appears exactly once;
  • the embedded-scalar shape is pinned directly, since it is easy to change by accident and the fixture only covers it incidentally;
  • a property holding instances of two different allowed types serializes, covering the regression described above.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Test Results (R2022a)

759 tests  +3   757 ✅ +3   2m 12s ⏱️ -18s
 20 suites ±0     1 💤 ±0 
  1 files   ±0     1 ❌ ±0 

For more details on these failures, see this check.

Results for commit 6bf6259. ± Comparison against base commit d7b0925.

♻️ This comment has been updated with latest results.

@ehennestad
ehennestad force-pushed the refactor-serializer-onto-transformer branch from bd60069 to 9742b42 Compare August 28, 2026 01:08
@codecov

codecov Bot commented Aug 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.15068% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.74%. Comparing base (d7b0925) to head (9742b42).

Files with missing lines Patch % Lines
...de/internal/+openminds/+abstract/BaseTransformer.m 76.47% 4 Missing ⚠️
.../+openminds/+internal/+serializer/BaseSerializer.m 98.21% 1 Missing ⚠️
Additional details and impacted files
@@                     Coverage Diff                     @@
##           add-jsonld-deserializer     #111      +/-   ##
===========================================================
+ Coverage                    79.28%   79.74%   +0.45%     
===========================================================
  Files                          422      422              
  Lines                         4089     4048      -41     
===========================================================
- Hits                          3242     3228      -14     
+ Misses                         847      820      -27     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

BaseSerializer walked the instance graph itself, with its own visited
registry, its own recursion accounting and its own mixed type unwrapping,
none of it shared with the other two traversals in the codebase.

BaseTransformer is the accumulating counterpart to BaseVisitor: each node
is mapped to a representation and the representations of its children are
composed into it. Its cycle semantics differ from the visitor on purpose.
A visitor acts on each node once for the whole traversal; a transformer
has to produce a value everywhere a node appears, so a node is marked only
while its own subtree is being built. Meeting it again inside that subtree
is a cycle and yields a reference; meeting it again elsewhere is not.

BaseSerializer becomes a BaseTransformer subclass carrying only the
openMINDS rules: an instance's own properties, a reference for every
linked value, an inline representation without an identifier for every
embedded value, and a queue of referenced instances to emit as documents
of their own.

SerializationContext is retired. Its visited registry comes from the
traversal core, and its map of linked instances plus threaded recursion
depth are replaced by that explicit queue, which also stops an instance
referenced from several places being emitted more than once.

AbstractSerializer is deleted. It was a two-method stub with no
subclasses, superseded by BaseSerializer and now by BaseDeserializer.

The output format is unchanged, which the golden fixture checks. That
required reproducing one asymmetry: an embedded value that can occur only
once is written as a single object, while everything else, including a
lone linked value, is written as a list. Centralizing the shape rule made
that asymmetry easy to lose, and the fixture caught it twice while this
change was being written.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ehennestad
ehennestad force-pushed the refactor-serializer-onto-transformer branch from 93b01aa to 6bf6259 Compare August 28, 2026 12:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant