Skip to content

FragmentAdapter trait and DefaultFragmentAdapter; opt-in fragmentation#457

Merged
leynos merged 7 commits intomainfrom
feature-fragment-adapter-trait-tmoc7b
Feb 15, 2026
Merged

FragmentAdapter trait and DefaultFragmentAdapter; opt-in fragmentation#457
leynos merged 7 commits intomainfrom
feature-fragment-adapter-trait-tmoc7b

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Feb 12, 2026

Summary

  • Introduces a public FragmentAdapter abstraction to centralize fragmentation policy and handling.
  • Adds a default adapter implementation bridging existing Fragmenter/Reassembler internals.
  • Fragmentation is now opt-in via app/builder defaults (no fragmentation by default).
  • Wires the adapter into the app path (src/app/fragmentation_state.rs, frame_handling/*) and updates builder defaults accordingly.
  • Includes scaffolding for unit/integration/behavioural tests around the adapter contract.
  • Updates documentation surface to reflect explicit opt-in fragmentation and the new adapter contract.

Changes

  • New public API surface:
    • src/fragment/adapter.rs (FragmentAdapter trait and DefaultFragmentAdapter)
    • src/fragment/mod.rs (expose the adapter module)
  • Integration points:
    • Wire adapter into app path (src/app/fragmentation_state.rs, frame_handling/*)
    • Modify builder defaults to disable fragmentation by default and to allow explicit opt-in
  • Testing:
    • Add unit tests for FragmentAdapter contract and error handling
    • Add integration/behavioural test scaffolding to cover opt-in flow, duplicates, out-of-order handling, zero-length fragments, and overflow paths
  • Documentation:
    • Prepare updates to design/user docs to reflect explicit opt-in fragmentation and new adapter contract

API surface

  • Trait: FragmentAdapter with bounds: Send + Sync
  • Core methods (illustrative):
    • fragment(packet: E) -> Result<Vec, FragmentationError>
    • reassemble(packet: E) -> Result<Option, Self::Error>
    • purge_expired() -> Vec
  • Error taxonomy placeholders (to be refined): FragmentationError, FragmentAdapterError, etc.
  • Public purge capability exposed via the adapter contract

Rationale

  • Aligns with roadmap requirement to harden fragmentation via an explicit public adapter contract.
  • Reduces implicit defaults by making fragmentation opt-in at app/builder boundaries.
  • Enables deterministic policies for duplicates and out-of-order fragments under a single contract.

Migration / Compatibility

  • WireframeApp::new() will not fragment by default anymore; fragmentation must be enabled via builder/config.
  • Public API changes are additive and scoped to fragmentation features; existing non-fragmentation paths remain unaffected.

Tests plan

  • Unit tests for the FragmentAdapter trait: happy path, error cases, and purge behavior
  • Integration tests for opt-in fragmentation path: ensure large/interleaved streams reassemble correctly
  • Behavioural tests (rstest-bdd v0.5.0) scaffolding to cover policy decisions (duplicates suppression, out-of-order rejection, zero-length case, overflow)

Validation and gates

  • Build passes (fmt, lint, test)
  • Fragmentation opt-in path verified via integration tests
  • Documentation reflects the new FragmentAdapter contract and opt-in behavior
  • Roadmap/documentation updates prepared for 9.2.1 milestone

Documentation updates

  • Update docs/design/generic-message-fragmentation-and-re-assembly-design.md to describe the FragmentAdapter contract and opt-in model
  • Update docs/users-guide.md to describe the explicit fragmentation opt-in surface and adapter responsibilities
  • Prepare notes for docs/roadmap.md to reflect completion status of 9.2.1 items

Notes for reviewers

  • This work lays the groundwork for fragmentation hardening with a public adapter contract.
  • Exact trait method signatures and associated types will be refined in Stage A (contract definition), with implementation refined in Stage B/C.
  • Dependency upgrades (e.g., rstest-bdd v0.5.0) will be addressed in follow-up PRs once the interface stabilizes.

📎 Task: https://www.devboxer.com/task/63aac90e-7ba4-4cf7-9a8d-d3661b768afc

📎 Task: https://www.devboxer.com/task/5836b8c0-0e9b-4447-a190-84233efc8219

…t hardening

- Introduces a comprehensive ExecPlan draft for roadmap item 9.2.1 focused on
  the FragmentAdapter trait and fragmentation opt-in hardening.
- Covers purpose, constraints, tolerances, risks, progress, decisions, and plans.
- Defines public API shape, behavioral policies, and testing strategy.
- Guides staged implementation and documentation updates.
- Serves as a reference point for fragmentation-related development and QA.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Feb 12, 2026

Reviewer's Guide

Introduces a public FragmentAdapter trait and DefaultFragmentAdapter implementation, rewires app fragmentation to use this adapter with fragmentation disabled by default and explicitly opt-in via builder APIs, hardens fragment-series policies (duplicate suppression, out-of-order handling, zero-length support) and reassembler behaviour, expands unit/integration/BDD tests for the new contract (including interleaved streams), and updates documentation and roadmap to reflect the new adapter contract, opt-in semantics, purge ownership, and upgraded rstest-bdd dev-dependencies.

Sequence diagram for inbound and outbound fragmentation with FragmentAdapter

sequenceDiagram
    actor Developer
    participant Builder as WireframeAppBuilder
    participant App as WireframeApp
    participant Handler
    participant Adapter as FragmentAdapter
    participant Codec as FrameCodec
    participant Socket
    participant Timer as ReadTimeoutScheduler

    rect rgb(235,235,255)
        Developer->>Builder: new()
        Builder-->>Developer: App (fragmentation None)

        Developer->>Builder: enable_fragmentation()
        Builder->>Builder: derive FragmentationConfig from codec.max_frame_length()
        Builder->>Builder: create DefaultFragmentAdapter
        Builder-->>Developer: App (fragmentation Some(DefaultFragmentAdapter))
    end

    rect rgb(235,255,235)
        Handler->>App: send_response(envelope)
        App->>Adapter: fragment(envelope)
        Adapter-->>App: fragments Vec~Envelope~
        loop for each fragment
            App->>Codec: encode(fragment)
            Codec-->>App: frame
            App->>Socket: write(frame)
        end
    end

    rect rgb(255,235,235)
        Socket-->>Codec: frame
        Codec-->>App: packet
        App->>Adapter: reassemble(packet)
        Adapter-->>App: Option~packet~
        alt complete message
            App-->>Handler: deliver reassembled envelope
        else more fragments required
            App-->>Handler: no callback yet
        end
    end

    rect rgb(255,255,210)
        Timer-->>App: read timeout tick
        App->>Adapter: purge_expired()
        Adapter-->>App: Vec~MessageId~
        App->>App: log purged message ids
    end
Loading

Class diagram for FragmentAdapter trait and DefaultFragmentAdapter implementation

classDiagram
    direction LR

    class Fragmentable {
    <<trait>>
    }

    class FragmentAdapter {
    <<trait>>
    +fragment(packet: E) Result~Vec~E~~
    +reassemble(packet: E) Result~Option~E~~
    +purge_expired() Vec~MessageId~
    }

    class DefaultFragmentAdapter {
    +fragmenter: Fragmenter
    +reassembler: Reassembler
    +new(config: FragmentationConfig) DefaultFragmentAdapter
    +fragment(packet: E) Result~Vec~E~~
    +reassemble(packet: E) Result~Option~E~~
    +purge_expired() Vec~MessageId~
    -fragment_inner(packet: E) Result~Vec~E~~
    -reassemble_inner(packet: E) Result~Option~E~~
    -purge_expired_inner() Vec~MessageId~
    }

    class Fragmenter {
    +new(fragment_payload_cap: usize) Fragmenter
    }

    class Reassembler {
    +new(max_message_size: usize, reassembly_timeout: Duration) Reassembler
    +push(header: FragmentHeader, payload: &[u8]) Result~Option~ReassembledMessage~~
    +purge_expired() Vec~MessageId~
    }

    class FragmentAdapterError {
    <<enum>>
    +Decode(DecodeError)
    +Reassembly(ReassemblyError)
    }

    class FragmentStatus {
    <<enum>>
    +Incomplete
    +Duplicate
    +Complete
    }

    class FragmentSeries {
    +message_id: MessageId
    +next_index: FragmentIndex
    +complete: bool
    +accept(fragment: FragmentHeader) Result~FragmentStatus, FragmentError~
    }

    class FragmentationConfig {
    +fragment_payload_cap: usize
    +max_message_size: usize
    +reassembly_timeout: Duration
    }

    class FragmentationState {
    <<type_alias>>
    }

    class FragmentProcessError {
    <<type_alias>>
    }

    FragmentAdapter <|.. DefaultFragmentAdapter : implements
    DefaultFragmentAdapter --> Fragmenter : uses
    DefaultFragmentAdapter --> Reassembler : uses

    FragmentAdapter --> Fragmentable : E constraint
    FragmentAdapter --> FragmentAdapterError
    FragmentAdapter --> FragmentationError
    FragmentAdapter --> MessageId

    FragmentSeries --> FragmentStatus

    DefaultFragmentAdapter --> FragmentationConfig

    FragmentationState --> DefaultFragmentAdapter : alias of
    FragmentProcessError --> FragmentAdapterError : alias of
Loading

Flow diagram for builder fragmentation configuration and codec changes

flowchart LR
    A[WireframeApp_default\nfragmentation None] --> B{Developer calls enable_fragmentation}
    B -->|Yes| C[Compute FragmentationConfig\nfrom codec.max_frame_length]
    C --> D[Create DefaultFragmentAdapter]
    D --> E[Store Some_DefaultFragmentAdapter\ninto App.fragmentation]
    B -->|No| F[Fragmentation remains disabled]

    E --> G{Developer calls with_codec}
    F --> G

    G --> H[Rebuild app with new codec]
    H --> I[Reset fragmentation to None\nrequires explicit reconfiguration]

    I --> J{Developer calls buffer_capacity}
    J --> K[Update LengthDelimitedFrameCodec\nwith new capacity]
    K --> L[Fragmentation remains None until\nexplicit enable_fragmentation or fragmentation_Some_cfg]

    subgraph Explicit_config
        M[Developer calls fragmentation_Some_cfg]
        M --> N[Create DefaultFragmentAdapter\nfrom provided config]
        N --> O[Store Some_DefaultFragmentAdapter\ninto App.fragmentation]
    end

    I --> M
Loading

File-Level Changes

Change Details Files
Add public FragmentAdapter trait, DefaultFragmentAdapter implementation, and export them as the central fragmentation API.
  • Create src/fragment/adapter.rs defining FragmentAdapter<E: Fragmentable>, FragmentAdapterError, and DefaultFragmentAdapter that internally uses Fragmenter and Reassembler and FragmentParts-based conversion.
  • Expose adapter module from src/fragment/mod.rs and re-export FragmentAdapter, FragmentAdapterError, and DefaultFragmentAdapter through the fragment module and crate root.
  • Replace previous connection-scoped FragmentationState struct with a type alias to DefaultFragmentAdapter and a corresponding FragmentProcessError alias to FragmentAdapterError so app frame handling uses the new adapter contract without changing call sites.
src/fragment/adapter.rs
src/fragment/mod.rs
src/lib.rs
src/app/fragmentation_state.rs
Make fragmentation explicitly opt-in at the WireframeApp builder level and adjust codec/buffer configuration to not auto-enable fragmentation.
  • Change WireframeApp::default/new implementation to initialize fragmentation as None instead of default_fragmentation(max_frame_length).
  • Add enable_fragmentation() builder method that derives fragmentation defaults from the active codec's max_frame_length and sets the fragmentation field.
  • Update with_codec(...) to clear fragmentation (set to None) instead of recomputing defaults, and update buffer_capacity(...) to stop resetting fragmentation when adjusting LengthDelimitedFrameCodec capacity.
  • Add builder tests asserting that defaults keep fragmentation disabled, enable_fragmentation() sets it, and with_codec() clears it again.
src/app/builder/core.rs
src/app/builder/config.rs
src/app/builder/codec.rs
Refine fragment-series and reassembler behaviour to explicitly suppress duplicates, handle zero-length fragments, and preserve out-of-order rejection semantics, and surface duplicate status via FragmentStatus.
  • Extend FragmentStatus enum with a Duplicate variant representing suppressed duplicate fragments.
  • Change FragmentSeries::accept to treat repeated indices (< next_index) as FragmentStatus::Duplicate, keep next_index unchanged, and treat indices > next_index as IndexMismatch errors; update rustdoc accordingly.
  • Update Reassembler::push to handle FragmentStatus::Duplicate by returning Ok(None) and not altering buffered state, and add targeted unit tests for series_suppresses_duplicate_fragment, reassembler_suppresses_duplicate_fragment, and reassembler_accepts_zero_length_fragments.
src/fragment/error.rs
src/fragment/series.rs
src/fragment/reassembler.rs
src/fragment/tests.rs
Expand integration and behavioural tests to cover opt-in fragmentation semantics, duplicate suppression, zero-length fragments, and interleaved streams at the transport and reassembler levels.
  • Add a new test fragmentation_is_opt_in_by_default in tests/fragment_transport.rs asserting that a default WireframeApp does not emit FRAG-wrapped responses, plus transport tests for duplicate_fragment_is_suppressed_and_reassembles and interleaved_fragment_streams_reassemble_independently using fragment_envelope helper and Fragmenter.
  • Add TestPacket implementing Fragmentable and a default_fragment_adapter_exposes_purge_api test that exercises DefaultFragmentAdapter::reassemble and purge_expired.
  • Adjust fragment transport rejection tests to drop the duplicate mutation case now that duplicates are suppressed rather than rejected.
  • Extend rstest-bdd feature file with scenarios for duplicate suppression, zero-length fragments, and interleaved messages, and wire them through new scenarios in tests/scenarios/fragment_scenarios.rs.
tests/fragment_transport.rs
tests/fragment_transport/mod.rs
tests/fragment_transport/rejection.rs
src/fragment/tests.rs
tests/features/fragment.feature
tests/scenarios/fragment_scenarios.rs
Update documentation and roadmap to describe the FragmentAdapter contract, opt-in behaviour, purge ownership, layering order, and upgraded rstest-bdd versions.
  • Revise generic-message-fragmentation-and-re-assembly-design.md to describe FragmentAdapter trait and DefaultFragmentAdapter, duplicate suppression policy, caller-owned purge via purge_expired, opt-in behaviour, and updated timeout handling.
  • Update users-guide and multiple hardening/ADR docs to say WireframeApp keeps fragmentation disabled by default, requires enable_fragmentation()/fragmentation(Some(cfg)), and define serializer ↔ fragmentation ↔ codec layering order.
  • Mark roadmap 9.2.1 and its sub-items as completed, add a detailed exec plan document for 9.2.1 under docs/execplans, and align resilience/maturity docs with opt-in fragmentation and purge ownership.
  • Upgrade rstest-bdd and rstest-bdd-macros dev-dependencies from 0.4.0 to 0.5.0 in Cargo.toml and update rstest-bdd users guide docs to reference 0.5.0.
docs/generic-message-fragmentation-and-re-assembly-design.md
docs/users-guide.md
docs/hardening-wireframe-a-guide-to-production-resilience.md
docs/multi-packet-and-streaming-responses-design.md
docs/the-road-to-wireframe-1-0-feature-set-philosophy-and-capability-maturity.md
docs/adr-004-pluggable-protocol-codecs.md
docs/roadmap.md
docs/rstest-bdd-users-guide.md
docs/execplans/9-1-3-fragment-adapter-trait.md
Cargo.toml
Cargo.lock

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Introduce a public FragmentAdapter trait and DefaultFragmentAdapter; make fragmentation opt‑in via builder APIs (enable_fragmentation / fragmentation(Some(cfg))); expose explicit purge_expired control; implement duplicate‑fragment suppression and updated reassembly logic; bump rstest‑bdd deps to 0.5.0.

Changes

Cohort / File(s) Summary
Dependency Updates
Cargo.toml, docs/rstest-bdd-users-guide.md
Bump rstest-bdd and rstest-bdd-macros to v0.5.0 and update docs/feature flags.
Public FragmentAdapter Trait & Re-exports
src/fragment/adapter.rs, src/fragment/mod.rs, src/lib.rs
Add public FragmentAdapter<E: Fragmentable> trait, DefaultFragmentAdapter, FragmentAdapterError; re-export at crate root.
Fragment Processing Logic
src/fragment/error.rs, src/fragment/series.rs, src/fragment/reassembler.rs
Add FragmentStatus::Duplicate; treat duplicate indices as no‑ops; out‑of‑order/ahead indices return errors; reassembler suppresses duplicate fragments without mutating state.
Fragmenter & Adapter Integration
src/fragment/..., src/fragment/adapter.rs
DefaultFragmentAdapter composes Fragmenter + Reassembler; handles decode/encode mapping; exposes fragment, reassemble, and purge_expired.
Builder API and App Wiring
src/app/builder/config.rs, src/app/builder/codec.rs, src/app/builder/core.rs
Remove automatic fragmentation derivation; add enable_fragmentation() for explicit opt‑in; changing codec or buffer_capacity clears fragmentation.
Fragmentation State Typing
src/app/fragmentation_state.rs
Make FragmentationState a crate‑local alias to crate::fragment::DefaultFragmentAdapter; alias FragmentProcessError to crate::fragment::FragmentAdapterError.
Tests & Public Test Helpers
tests/fragment_transport.rs, tests/*, src/fragment/tests/*
Expose Fragmenter and Packet for tests; split tests into adapter/fragmenter/header_series/reassembler modules; add integration tests for opt‑in fragmentation, duplicate suppression, zero‑length fragments, interleaved reassembly and purge semantics.
Behaviour Scenarios & Stepdefs
tests/features/fragment.feature, tests/scenarios/fragment_scenarios.rs, tests/fragment_transport/rejection.rs, tests/steps/fragment_steps.rs
Add scenarios for duplicate suppression, zero‑length fragments, interleaved messages; remove duplicate mutation in rejection tests; add singular/plural buffered message step helpers.
Documentation & Planning
docs/generic-message-fragmentation-and-re-assembly-design.md, docs/execplans/..., docs/hardening-wireframe-a-guide-to-production-resilience.md, docs/*
Document opt‑in fragmentation, explicit purge_expired ownership, FragmentAdapter trait and DefaultFragmentAdapter, updated data‑flow ordering (serializer → fragmentation → codec; codec decode → reassembly → deserialiser), and mark roadmap/execplan progress.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant App as WireframeApp\n(builder/runtime)
  participant Adapter as DefaultFragmentAdapter\n(FragmentAdapter)
  participant Codec as Codec/Framing
  participant Handler as ApplicationHandler

  App->>Adapter: fragment(packet) [outbound when opt-in]
  Adapter->>Adapter: split payload -> fragments
  Adapter->>Codec: emit per-fragment frames
  Codec->>App: send frames

  %% inbound path
  Codec->>Adapter: deliver frame -> decode header+payload
  Adapter->>Adapter: reassemble(fragment) -> check duplicate / index
  alt complete
    Adapter->>Handler: deliver reassembled packet
  else incomplete
    Adapter-->>Adapter: buffer partial state
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

Fragments scatter, then gather whole once more,
Opt‑in hands stitch bytes back to the core.
Suppress repeat echoes, purge the stale frame,
Reassemble stories, each message by name. ✨

Use the new FragmentAdapter API for explicit fragmentation control and call `purge_expired` from your runtime tick when using `DefaultFragmentAdapter`. 
🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (37 files):

⚔️ docs/adr-004-pluggable-protocol-codecs.md (content)
⚔️ docs/generic-message-fragmentation-and-re-assembly-design.md (content)
⚔️ docs/hardening-wireframe-a-guide-to-production-resilience.md (content)
⚔️ docs/multi-packet-and-streaming-responses-design.md (content)
⚔️ docs/roadmap.md (content)
⚔️ docs/the-road-to-wireframe-1-0-feature-set-philosophy-and-capability-maturity.md (content)
⚔️ docs/users-guide.md (content)
⚔️ docs/wireframe-client-design.md (content)
⚔️ src/app/builder/codec.rs (content)
⚔️ src/app/builder/config.rs (content)
⚔️ src/app/builder/core.rs (content)
⚔️ src/app/fragmentation_state.rs (content)
⚔️ src/client/error.rs (content)
⚔️ src/client/messaging.rs (content)
⚔️ src/client/mod.rs (content)
⚔️ src/client/runtime.rs (content)
⚔️ src/client/tests/error_handling.rs (content)
⚔️ src/client/tests/messaging.rs (content)
⚔️ src/fragment/error.rs (content)
⚔️ src/fragment/mod.rs (content)
⚔️ src/fragment/reassembler.rs (content)
⚔️ src/fragment/series.rs (content)
⚔️ src/fragment/tests.rs (content)
⚔️ src/lib.rs (content)
⚔️ tests/client_runtime.rs (content)
⚔️ tests/features/client_lifecycle.feature (content)
⚔️ tests/features/client_runtime.feature (content)
⚔️ tests/features/fragment.feature (content)
⚔️ tests/fixtures/client_runtime.rs (content)
⚔️ tests/fragment_transport.rs (content)
⚔️ tests/fragment_transport/mod.rs (content)
⚔️ tests/fragment_transport/rejection.rs (content)
⚔️ tests/scenarios/client_runtime_scenarios.rs (content)
⚔️ tests/scenarios/fragment_scenarios.rs (content)
⚔️ tests/steps/client_lifecycle_steps.rs (content)
⚔️ tests/steps/client_runtime_steps.rs (content)
⚔️ tests/steps/fragment_steps.rs (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarises the main changes: introducing FragmentAdapter trait, DefaultFragmentAdapter implementation, and converting fragmentation from default-enabled to opt-in.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, covering the new adapter abstraction, opt-in model, API surface, testing approach, and documentation updates.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature-fragment-adapter-trait-tmoc7b

Comment @coderabbitai help to get the list of available commands and usage tips.

…e FragmentAdapter API

- Introduce a public `FragmentAdapter` trait and `DefaultFragmentAdapter` implementation to unify fragmentation and reassembly behavior.
- Shift fragmentation from default-enabled to explicit opt-in via `enable_fragmentation()` or `fragmentation(Some(cfg))` on `WireframeApp` builder.
- Make `with_codec()` clear fragmentation config to require reconfiguration aligned with codec frame budget.
- Add explicit duplicate fragment suppression (non-fatal) and preserve out-of-order rejection with deterministic cleanup.
- Expose caller-driven purge API via `FragmentAdapter::purge_expired()` for manual timeout eviction control.
- Update related docs, design ADRs, and user guides to reflect new fragmentation control and policies.
- Add comprehensive unit, integration, and behavioral tests verifying opt-in semantics, duplicate suppression, zero-length fragment support, interleaved reassembly, and purge API usage.

This change improves resource control, clarity, and flexibility of transport fragmentation handling, preventing unintended overhead and enabling tailored fragment handling strategies.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
@leynos leynos changed the title Implement FragmentAdapter trait and tests Implement FragmentAdapter with opt-in fragmentation and tests Feb 12, 2026
@leynos leynos marked this pull request as ready for review February 12, 2026 09:59
sourcery-ai[bot]

This comment was marked as resolved.

@coderabbitai coderabbitai Bot added the codex label Feb 12, 2026
coderabbitai[bot]

This comment was marked as resolved.

…t types

- Updated FragmentAdapter trait to remove generic parameter on trait and instead use generic methods for fragment and reassemble.
- Adjusted DefaultFragmentAdapter implementation accordingly.
- Improved error handling with #[from] attributes on FragmentAdapterError variants.
- Revised related documentation to use "adapter" instead of "adaptor" and align terminology.
- Split large fragment tests module into focused submodules for clarity and maintainability.
- Added new well-structured unit tests for fragment adapter, fragmenter, header series, and reassembler.
- Updated builder and codec modules to require explicit opt-in for fragmentation when frame budgets or codec change.

This refactor improves the fragment adapter API ergonomics and type safety, and enhances test coverage and documentation clarity.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
coderabbitai[bot]

This comment was marked as resolved.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Feb 13, 2026

@coderabbitai Have the following now been resolved?

Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `src/fragment/series.rs:82-86` </location>
<code_context>
         }

-        if fragment.fragment_index() != self.next_index {
+        if fragment.fragment_index() < self.next_index {
+            return Ok(FragmentStatus::Duplicate);
+        }
+
+        if fragment.fragment_index() > self.next_index {
             return Err(FragmentError::IndexMismatch {
                 expected: self.next_index,
</code_context>

<issue_to_address>
**issue:** Duplicate handling does not cover duplicates after series completion, which conflicts with the method’s documentation.

The docstring promises `FragmentStatus::Duplicate` and no state change whenever a fragment reuses an accepted index. Because `is_complete` currently runs before the `< self.next_index` check, a repeat of the final fragment after completion returns `SeriesComplete` instead. To align behavior with the docs, consider checking `< self.next_index` (and any completion gating) before `is_complete`. If that behavior is intentional, update the docs to clarify that duplicates are only reported while the series is incomplete.
</issue_to_address>

### Comment 2
<location> `src/fragment/tests.rs:165-174` </location>
<code_context>
 #[derive(Debug, Encode, BorrowDecode)]
 struct DummyMessage(Vec<u8>);

+#[derive(Clone, Debug, PartialEq, Eq)]
+struct TestPacket {
+    id: u32,
+    correlation_id: Option<u64>,
+    payload: Vec<u8>,
+}
+
+impl Fragmentable for TestPacket {
+    fn into_fragment_parts(self) -> FragmentParts {
+        FragmentParts::new(self.id, self.correlation_id, self.payload)
+    }
+
+    fn from_fragment_parts(parts: FragmentParts) -> Self {
+        Self {
+            id: parts.id(),
</code_context>

<issue_to_address>
**suggestion (testing):** Add focused unit tests for `DefaultFragmentAdapter::fragment` and `reassemble` using `TestPacket`

`TestPacket` and its `Fragmentable` impl plus `default_fragment_adapter_exposes_purge_api` are a good start, but they don’t yet validate the `FragmentAdapter` surface end-to-end. Please add adapter-level tests that:

- Check `DefaultFragmentAdapter::fragment(TestPacket)` yields multiple fragments when payload > `fragment_payload_cap`, preserving `id`/`correlation_id` on each.
- Feed those fragments into `DefaultFragmentAdapter::reassemble` and assert you get exactly one `Some(TestPacket)` with the original payload, and that subsequent calls for that message id return `Ok(None)`.
- Cover the pass-through case where `decode_fragment_payload` returns `Ok(None)` (non-fragment payload) and confirm `Ok(Some(packet))` with the payload unchanged.

These can live next to `default_fragment_adapter_exposes_purge_api` and reuse `TestPacket` for full round-trip coverage instead of depending only on `Reassembler` tests.

Suggested implementation:

```rust
fn assert_fragment(batch: &FragmentBatch, index: usize, payload: &[u8], is_last: bool) {
    let fragment = &batch.fragments()[index];

    // Depending on the existing API, these accessors may need to be adjusted.
    // The intent is:
    // - Assert the raw fragment payload bytes
    // - Assert whether this fragment is marked as the last one in the series
    assert_eq!(fragment.payload(), payload);
    assert_eq!(fragment.is_last(), is_last);
}

#[test]
fn default_fragment_adapter_fragments_and_reassembles_test_packet() {
    // Use a small cap so we are guaranteed to get multiple fragments
    let fragment_payload_cap = 4usize;

    // Assuming there is a constructor like this; if not, wire this up to the
    // existing helper used by `default_fragment_adapter_exposes_purge_api`.
    let mut adapter = DefaultFragmentAdapter::<TestPacket>::new(fragment_payload_cap);

    let full_payload: Vec<u8> = (0u8..10).collect();
    let packet = TestPacket {
        id: 42,
        correlation_id: Some(777),
        payload: full_payload.clone(),
    };

    // Fragment the packet and assert that we get multiple fragments, all of which
    // preserve the id and correlation_id.
    let batch = adapter
        .fragment(packet.clone())
        .expect("fragmenting TestPacket should succeed");

    let fragments = batch.fragments();
    assert!(
        fragments.len() > 1,
        "Expected multiple fragments when payload exceeds cap"
    );

    for (idx, fragment) in fragments.iter().enumerate() {
        // Depending on the existing API, these accessors may need to be adjusted.
        assert_eq!(fragment.id(), packet.id, "fragment id should match packet id");
        assert_eq!(
            fragment.correlation_id(),
            packet.correlation_id,
            "fragment correlation_id should match packet correlation_id"
        );

        let is_last = idx == fragments.len() - 1;
        // Reuse the helper to validate payload slice and last/continuation flag.
        let expected_start = idx * fragment_payload_cap;
        let expected_end = usize::min(expected_start + fragment_payload_cap, full_payload.len());
        let expected_slice = &full_payload[expected_start..expected_end];

        assert_fragment(&batch, idx, expected_slice, is_last);
    }

    // Now feed the fragments back into the adapter and ensure we get exactly one
    // reassembled TestPacket with the original payload, and subsequent calls
    // for the same fragments return Ok(None).
    let mut reassembled: Vec<TestPacket> = Vec::new();

    for fragment in fragments.iter().cloned() {
        let result = adapter
            .reassemble(fragment)
            .expect("reassembling fragment should not error");

        if let Some(pkt) = result {
            reassembled.push(pkt);
        }
    }

    assert_eq!(
        reassembled.len(),
        1,
        "Expected exactly one reassembled TestPacket"
    );
    assert_eq!(
        reassembled[0],
        packet,
        "Reassembled TestPacket should match the original"
    );

    // Re-feeding the same fragments should be a no-op and always return Ok(None).
    for fragment in fragments.iter().cloned() {
        let result = adapter
            .reassemble(fragment)
            .expect("reassembling already-completed series should not error");
        assert!(
            result.is_none(),
            "Expected Ok(None) after series is already complete"
        );
    }
}

#[test]
fn default_fragment_adapter_passes_through_non_fragment_payload() {
    let fragment_payload_cap = 1024usize;
    let mut adapter = DefaultFragmentAdapter::<TestPacket>::new(fragment_payload_cap);

    // Construct a payload that is *not* encoded as a fragment so that
    // `decode_fragment_payload` returns Ok(None).
    let payload: Vec<u8> = b"non-fragment payload".to_vec();

    // Assuming `Fragment::new` and `MessageId::new` exist and are used elsewhere
    // in this module; adjust to match the actual constructors if they differ.
    let message_id = MessageId::new(123);
    let fragment = Fragment::new(message_id, payload.clone(), true);

    let result = adapter
        .reassemble(fragment)
        .expect("reassembling non-fragment payload should not error");

    // Pass-through behavior: adapter should wrap the payload into a TestPacket
    // with id from the MessageId, no correlation_id, and unchanged payload.
    let packet = result.expect("Expected Ok(Some(TestPacket)) for non-fragment payload");

    assert_eq!(packet.id, message_id.get());
    assert_eq!(packet.correlation_id, None);
    assert_eq!(packet.payload, payload);
}

```

The above implementation assumes several APIs based on typical patterns in this module:

1. `DefaultFragmentAdapter::<TestPacket>::new(cap: usize)` — if your adapter is constructed differently (e.g., via `Default::default()` or a helper like `default_fragment_adapter()`), update the constructor calls accordingly.
2. Fragment accessors used in the tests:
   - `fragment.payload() -> &[u8]`
   - `fragment.is_last() -> bool`
   - `fragment.id() -> u32`
   - `fragment.correlation_id() -> Option<u64>`
   Adjust these to the real methods/fields on your `Fragment` type.
3. The `Fragment`/`MessageId` APIs:
   - `Fragment::new(message_id, payload, is_last)`
   - `MessageId::new(u32)` and `MessageId::get() -> u32`
   If your types expose different constructors or getters, adapt the pass-through test to use them.
4. If `FragmentBatch` is generic or constructed differently in existing tests, you may need to adjust its type parameters or how it is obtained to compile cleanly with the new helper and tests.

Once those names are aligned with your actual types, these tests will give you the focused, adapter-level coverage you described: multi-fragment behavior, reassembly semantics, and the pass-through `decode_fragment_payload -> Ok(None)` path.
</issue_to_address>

### Comment 3
<location> `docs/generic-message-fragmentation-and-re-assembly-design.md:44-46` </location>
<code_context>
-subsequent codecs in the chain.
+The feature is exposed as a public `FragmentAdapter` trait plus a default
+implementation (`DefaultFragmentAdapter`) that wraps `Fragmenter` and
+`Reassembler`. Protocol-specific stacks can still layer additional strategy
+codecs around this adaptor.

 ```plaintext
</code_context>

<issue_to_address>
**suggestion (typo):** Use consistent spelling for "adapter" to match the `FragmentAdapter` type name.

This sentence ends with "this adaptor" while the type and other references use `FragmentAdapter`. Please change "adaptor" to "adapter" here for consistent terminology.

```suggestion
implementation (`DefaultFragmentAdapter`) that wraps `Fragmenter` and
`Reassembler`. Protocol-specific stacks can still layer additional strategy
codecs around this adapter.
```
</issue_to_address>

### Comment 4
<location> `src/fragment/adapter.rs:35` </location>
<code_context>
+}
+
+/// Adapter contract for transport-level fragmentation and reassembly.
+pub trait FragmentAdapter<E: Fragmentable>: Send + Sync {
+    /// Attempt to fragment a packet for outbound transport.
+    ///
</code_context>

<issue_to_address>
**issue (complexity):** Consider simplifying the adapter by removing the inner passthrough methods and changing the generic trait into a non-generic trait with method-level generics to reduce indirection and type complexity.

You can reduce the complexity here without changing behaviour by:

1. **Dropping the inner methods**, and  
2. **Simplifying the trait generics to method-level generics.**

### 1. Remove `*_inner` indirection

`fragment_inner`, `reassemble_inner`, and `purge_expired_inner` are pure pass‑throughs. You can keep the same API surface and behaviour by inlining their logic into the public methods and the trait impl.

```rust
#[derive(Debug)]
pub struct DefaultFragmentAdapter {
    fragmenter: Fragmenter,
    reassembler: Reassembler,
}

impl DefaultFragmentAdapter {
    #[must_use]
    pub fn new(config: FragmentationConfig) -> Self {
        Self {
            fragmenter: Fragmenter::new(config.fragment_payload_cap),
            reassembler: Reassembler::new(
                config.max_message_size,
                config.reassembly_timeout,
            ),
        }
    }

    pub fn fragment<E: Fragmentable>(
        &self,
        packet: E,
    ) -> Result<Vec<E>, FragmentationError> {
        fragment_packet(&self.fragmenter, packet)
    }

    pub fn reassemble<E: Fragmentable>(
        &mut self,
        packet: E,
    ) -> Result<Option<E>, FragmentAdapterError> {
        let parts = packet.into_fragment_parts();
        let id = parts.id();
        let correlation_id = parts.correlation_id();
        let payload = parts.into_payload();

        match decode_fragment_payload(&payload) {
            Ok(Some((header, fragment_payload))) => {
                match self.reassembler.push(header, fragment_payload) {
                    Ok(Some(message)) => {
                        let rebuilt = FragmentParts::new(
                            id,
                            correlation_id,
                            message.into_payload(),
                        );
                        Ok(Some(E::from_fragment_parts(rebuilt)))
                    }
                    Ok(None) => Ok(None),
                    Err(err) => Err(FragmentAdapterError::Reassembly(err)),
                }
            }
            Ok(None) => {
                let passthrough =
                    FragmentParts::new(id, correlation_id, payload);
                Ok(Some(E::from_fragment_parts(passthrough)))
            }
            Err(err) => Err(FragmentAdapterError::Decode(err)),
        }
    }

    pub fn purge_expired(&mut self) -> Vec<MessageId> {
        self.reassembler.purge_expired()
    }
}
```

The trait impl below can then call these directly.

### 2. Simplify trait generics: method‑level generics only

Currently you have a *generic trait* plus a *non‑generic implementor with generic methods*:

```rust
pub trait FragmentAdapter<E: Fragmentable>: Send + Sync {
    fn fragment(&self, packet: E) -> Result<Vec<E>, FragmentationError>;
    fn reassemble(&mut self, packet: E)
        -> Result<Option<E>, FragmentAdapterError>;
    fn purge_expired(&mut self) -> Vec<MessageId>;
}

impl<E: Fragmentable> FragmentAdapter<E> for DefaultFragmentAdapter { ... }
```

You can remove a whole layer of generic complexity by making the trait non‑generic and moving the generic to the methods, matching what `DefaultFragmentAdapter` already does:

```rust
pub trait FragmentAdapter: Send + Sync {
    fn fragment<E: Fragmentable>(
        &self,
        packet: E,
    ) -> Result<Vec<E>, FragmentationError>;

    fn reassemble<E: Fragmentable>(
        &mut self,
        packet: E,
    ) -> Result<Option<E>, FragmentAdapterError>;

    fn purge_expired(&mut self) -> Vec<MessageId>;
}

impl FragmentAdapter for DefaultFragmentAdapter {
    fn fragment<E: Fragmentable>(
        &self,
        packet: E,
    ) -> Result<Vec<E>, FragmentationError> {
        self.fragment(packet)
    }

    fn reassemble<E: Fragmentable>(
        &mut self,
        packet: E,
    ) -> Result<Option<E>, FragmentAdapterError> {
        self.reassemble(packet)
    }

    fn purge_expired(&mut self) -> Vec<MessageId> {
        self.purge_expired()
    }
}
```

This keeps all existing functionality (including support for multiple `Fragmentable` packet types with a single adapter instance) while:

- Eliminating the extra “inner” layer, and  
- Avoiding the combination of a generic trait and a blanket impl, which simplifies type inference and makes the adapter easier to reason about.
</issue_to_address>

### Comment 5
<location> `src/app/builder/core.rs:168` </location>
<code_context>
+    use super::WireframeApp;
+    use crate::{app::Envelope, codec::LengthDelimitedFrameCodec, serializer::BincodeSerializer};
+
+    #[test]
+    fn builder_defaults_fragmentation_to_disabled() {
+        let app = WireframeApp::<BincodeSerializer, (), Envelope, LengthDelimitedFrameCodec>::new()
</code_context>

<issue_to_address>
**suggestion (review_instructions):** These new tests duplicate setup without using `rstest` fixtures for shared setup as required.

The three tests in this module each construct the same `WireframeApp::<BincodeSerializer, (), Envelope, LengthDelimitedFrameCodec>::new().expect("build app")` setup inline. Per the instructions, shared setup should be factored into an `rstest` fixture (and, if appropriate, the assertions could be made parameterised cases). Consider extracting the common construction into a fixture using `#[rstest]` to comply with the testing guidelines.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.rs`

**Instructions:**
Use `rstest` fixtures for shared setup.

</details>
</issue_to_address>

@coderabbitai

This comment was marked as resolved.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
@leynos leynos changed the title Implement FragmentAdapter with opt-in fragmentation and tests Add FragmentAdapter trait and DefaultFragmentAdapter; opt-in fragmentation Feb 13, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@docs/execplans/9-1-3-fragment-adapter-trait.md`:
- Line 18: The sentences that read "public `FragmentAdapter` trait and aligns
runtime behaviour so fragmentation is" and the other occurrences join two
independent clauses with "so" but lack the required comma; update each sentence
(including the one mentioning the FragmentAdapter trait and the two other
occurrences later in the document) to insert a comma before "so" so they read
"...behaviour, so fragmentation..." (and similarly add a comma before "so" in
the other two sentences).
- Line 352: The phrase uses the non-standard "users guide"; update the text that
reads "Documentation parity: users guide and design docs describe the same
behaviour" to use the conventional "user guide" instead. Locate the exact string
"users guide" in the document (e.g., within the sentence starting "Documentation
parity:") and replace it with "user guide", preserving the rest of the sentence
and punctuation.

In `@docs/users-guide.md`:
- Around line 842-844: Replace the US spelling "deserialization" with the
British English "deserialisation" in the documentation text (specifically the
phrase "codec decode → fragment reassembly → deserialization" and any other
occurrences in this file) to comply with the project's en-GB-oxendict
requirement; update the inbound processing line to read "codec decode → fragment
reassembly → deserialisation" and run a quick search in docs/users-guide.md to
change any other instances of "deserialization".

Comment thread docs/execplans/9-1-3-fragment-adapter-trait.md Outdated
Comment thread docs/execplans/9-1-3-fragment-adapter-trait.md Outdated
Comment thread docs/users-guide.md
…an docs

Correct multiple instances of "deserialization" to "deserialisation" and "documentation" to "documentation" to align with UK English spelling conventions in users-guide.md and 9-1-3-fragment-adapter-trait.md.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
@leynos leynos changed the title Add FragmentAdapter trait and DefaultFragmentAdapter; opt-in fragmentation FragmentAdapter trait and DefaultFragmentAdapter; opt-in fragmentation Feb 14, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/users-guide.md (1)

367-367: ⚠️ Potential issue | 🟡 Minor

Use consistent terminology: "adapter" not "adaptor".

Replace "adaptor" with "adapter" to match the FragmentAdapter type name.

✏️ Proposed fix
-logging. This keeps transport experiments lightweight while the full adaptor
+logging. This keeps transport experiments lightweight while the full adapter
 layer evolves. The helper is fallible—`FragmentationError` surfaces encoding
🤖 Fix all issues with AI agents
In `@docs/execplans/9-1-3-fragment-adapter-trait.md`:
- Around line 28-29: Add a comma before "while" in the sentence "interleaved
fragment streams reassemble correctly while duplicate and out-of-order series
follow documented policies" so the two independent clauses are correctly
punctuated; update the sentence in the
docs/execplans/9-1-3-fragment-adapter-trait.md content where that phrase appears
(look for the sentence starting with "interleaved fragment streams reassemble
correctly") to read "...reassemble correctly, while duplicate and out-of-order
series follow documented policies."
- Around line 45-46: The filename token
"generic-message-fragmentation-and-re-assembly-design.md" in the bullet is being
split across lines; update the bullet so the filename remains atomic by either
letting that filename exceed the 80‑column wrap or rephrasing the sentence to
keep the full filename on one line (for example: "Record decisions in the
companion doc generic-message-fragmentation-and-re-assembly-design.md and other
related composition guidance."). Ensure the exact filename string is not
hyphen-wrapped or broken into multiple lines in the markdown.

Comment thread docs/execplans/9-1-3-fragment-adapter-trait.md Outdated
Comment thread docs/execplans/9-1-3-fragment-adapter-trait.md Outdated
Corrected instances of "adaptor" to "adapter" for consistent terminology in the users guide documentation.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Feb 15, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 15, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@docs/users-guide.md`:
- Line 121: Replace the British -ise spelling "deserialisation" with
en-GB-oxendict "-ize" form "deserialization" everywhere in the document
(instances currently written as "deserialisation" at the noted locations),
ensuring consistency in surrounding punctuation and footnote markers (e.g., keep
"[^6][^7]" intact) and update all other occurrences mentioned (the other two
instances) to "deserialization".

Comment thread docs/users-guide.md Outdated
- Corrected comma usage for clarity in fragmentation design doc.
- Fixed spelling from 'deserialisation' to 'deserialization' in multiple places.
- Improved wording and formatting in execution plans and user guide docs.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Review comment resolved: all instances of 'deserialisation' have been updated to 'deserialization'.

@leynos leynos merged commit fe8ef9a into main Feb 15, 2026
6 checks passed
@leynos leynos deleted the feature-fragment-adapter-trait-tmoc7b branch February 15, 2026 00:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant