Skip to content

feat: add RoutingError for non-mapping route failures#48

Merged
nathanjmcdougall merged 6 commits into
mainfrom
feat/routing-error
May 26, 2026
Merged

feat: add RoutingError for non-mapping route failures#48
nathanjmcdougall merged 6 commits into
mainfrom
feat/routing-error

Conversation

@nathanjmcdougall
Copy link
Copy Markdown
Collaborator

@nathanjmcdougall nathanjmcdougall commented May 26, 2026

Summary

Introduces RoutingError(PatchError) — a typed exception raised when a key path passes through a non-mapping node (scalar or list) during add(), upsert(), or merge().

Previously these failures surfaced as a bare PatchError (or, in merge(), a NodeTypeError with a hand-crafted message). Callers had no way to distinguish routing failures from other patch errors without string-matching Rust internals.

Changes

errors.py

  • New RoutingError(PatchError) — placed between KeyMissingError and NodeTypeError. No TypeError mixin.

document.py

  • _PatchErrorKind: two new members — NON_MAPPING_ROUTE and EXPECTED_MAPPING.
  • _classify_patch_error: refactored to a loop (fixes PLR0911).
  • _is_routing_error(kind): helper returning True for all three routing-related kinds.
  • _create_at and add(): routing errors re-raised as RoutingError with message Route passes through a non-mapping node at <path>.
  • merge(): drops the ancestor-hunting try/except — RoutingError propagates from upsert → _create_at.

__init__.py

  • RoutingError added to imports and __all__.

Tests

  • test_errors.py: TestRoutingErrorHierarchy and TestRoutingErrorBehaviour.
  • test_edge_cases.py: four new TestPatchErrorStringPins entries for the two new Rust substrings.
  • test_merge.py: updated to expect RoutingError.
  • test_public_api.py: test_error_classes extended with RoutingError.

Breaking change

doc.merge("a", "b", "c", value=...) where a.b is a scalar previously raised NodeTypeError; it now raises RoutingError. Both are PatchError subclasses, so code catching PatchError is unaffected.

Spec

doc/specs/2026-05-26-routing-error-design.md

@nathanjmcdougall nathanjmcdougall changed the title Feat/routing error feat: add RoutingError for non-mapping route failures May 26, 2026
The previous loop iterated _PatchErrorKind in enum definition order,
making the ordering load-bearing but invisible to future maintainers.

Replace with an explicit _ordered tuple inside the function. The tuple
literal documents that ordering is intentional: more-specific substrings
must precede any shorter ones that are a prefix of them. Adding a new
kind now requires a deliberate insertion at the right position, not just
a new enum member.
Add two missing cases to TestRoutingErrorBehaviour:

- test_add_through_list_raises_routing_error: add() through a list node
  was untested; only the scalar-node case existed.
- test_upsert_scalar_root_raises_routing_error: exercises the
  _create_at(parent_keys=(), ...) branch on a non-empty document and
  pins the '<root>' token in RoutingError messages.
- test_merge_through_list_raises: pins RoutingError when merge() routes
  through a list node (complements existing scalar test).
- test_sync_through_scalar_raises_routing_error: pins that sync()
  propagates RoutingError via its upsert delegation path.
@nathanjmcdougall nathanjmcdougall requested a review from Copilot May 26, 2026 04:26
@nathanjmcdougall nathanjmcdougall marked this pull request as ready for review May 26, 2026 04:26
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a dedicated RoutingError exception to represent “route passes through a non-mapping node” failures during write operations, allowing callers to reliably distinguish routing problems from other patch/type errors without matching Rust-layer error strings.

Changes:

  • Introduces RoutingError(PatchError) and exports it via the package public API (__init__.py / __all__).
  • Updates Document.add() / _create_at() (and therefore upsert(), sync(), and merge()’s missing-path delegation) to re-raise routing-related patch failures as RoutingError with a stable, human-readable message.
  • Extends the internal patch-error classifier with two new Rust substring pins and updates tests to assert the new exception type and behavior (including merge() now raising RoutingError for these cases).

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.

Show a summary per file
File Description
tests/test_sync.py Adds a regression test asserting sync() surfaces routing failures as RoutingError.
tests/test_public_api.py Ensures RoutingError is part of the public API contract (subclassing + export).
tests/test_merge.py Updates merge() routing-failure expectations to RoutingError and adds list-routing coverage.
tests/test_errors.py Adds hierarchy and behavior tests for RoutingError across upsert()/add() scenarios.
tests/test_edge_cases.py Pins new Rust-layer substrings and validates classification for routing-related failures.
src/yamltrip/errors.py Defines the new RoutingError(PatchError) type.
src/yamltrip/document.py Adds routing error kinds + helper, refactors classifier, and re-raises routing failures as RoutingError from add() / _create_at(); simplifies merge().
src/yamltrip/init.py Exports RoutingError and includes it in __all__.
doc/specs/2026-05-26-routing-error-design.md Documents the design, detection strategy, raise sites, and breaking-change behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@nathanjmcdougall nathanjmcdougall merged commit 0ba93e3 into main May 26, 2026
19 checks passed
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.

2 participants