You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
reactive_stores store rows (#[derive(Patch)]) can only hold field types that
implement PatchField, and the orphan rule means web — which owns neither the
trait nor the newtypes — cannot write those impls. The result is documented
carve-outs where validated domain values are erased to primitives inside web
store DTOs:
web/src/audiences/api.rs:100-103 — AudienceSummary.name: String, holding a
value that is an AudienceName at construction, edge-converted with a comment
explaining the PatchField gap.
The trait itself is not the obstacle: in the pinned reactive_stores 0.4.3, PatchField is public and unsealed, with one method
(patch_field(&mut self, new: Self, path: &StorePath, notify: &mut dyn FnMut(&StorePath), _keys: Option<&KeyMap>)), and the crate's own leaf impls for String/i64/etc. are just compare-assign-notify — trivially writable for any PartialEq type, which every newtype trailer already provides. Upstream can't
offer a blanket impl<T: PartialEq> (it would conflict with their struct
impls), which is why the leaf set is a closed enumeration.
Direction
The orphan-rule-legal home is common: add reactive_stores as an optional,
feature-gated dependency (it is already in the workspace lock at 0.4.3, so no
vendor churn) and provide the leaf PatchField impls there — either hand-written
for the store-facing newtypes, or folded into the StrNewtype/IdNewtype
derive trailer behind the feature so the carve-out pattern is retired, not one
instance.
This changes common's dependency posture (version-coupling to the leptos
release train), which is a design decision worth recording — an ADR (or an
ADR-0063 addendum) before the mechanical work.
Then:
AudienceSummary.name: AudienceName; delete the edge-convert and its comment.
Alternatives considered and rejected in the audit discussion: a web-local
wrapper type (legal, but every field becomes Wrapper<T>), and upstreaming a
leaf-impl macro to leptos-rs (timeline outside our control).
Acceptance
The dependency decision recorded (ADR or addendum) before implementation.
Feature-gated PatchField support in common (derive-trailer or explicit
impls), with the impl behavior covered (macros crate is coverage-measured if
the derive route is taken).
Problem
reactive_storesstore rows (#[derive(Patch)]) can only hold field types thatimplement
PatchField, and the orphan rule meansweb— which owns neither thetrait nor the newtypes — cannot write those impls. The result is documented
carve-outs where validated domain values are erased to primitives inside web
store DTOs:
web/src/audiences/api.rs:100-103—AudienceSummary.name: String, holding avalue that is an
AudienceNameat construction, edge-converted with a commentexplaining the
PatchFieldgap.AudienceSummary.audience_id: i64instead ofAudienceId, same root cause.The trait itself is not the obstacle: in the pinned
reactive_stores0.4.3,PatchFieldis public and unsealed, with one method(
patch_field(&mut self, new: Self, path: &StorePath, notify: &mut dyn FnMut(&StorePath), _keys: Option<&KeyMap>)), and the crate's own leaf impls forString/i64/etc. are just compare-assign-notify — trivially writable for anyPartialEqtype, which every newtype trailer already provides. Upstream can'toffer a blanket
impl<T: PartialEq>(it would conflict with their structimpls), which is why the leaf set is a closed enumeration.
Direction
The orphan-rule-legal home is
common: addreactive_storesas an optional,feature-gated dependency (it is already in the workspace lock at 0.4.3, so no
vendor churn) and provide the leaf
PatchFieldimpls there — either hand-writtenfor the store-facing newtypes, or folded into the
StrNewtype/IdNewtypederive trailer behind the feature so the carve-out pattern is retired, not one
instance.
This changes
common's dependency posture (version-coupling to the leptosrelease train), which is a design decision worth recording — an ADR (or an
ADR-0063 addendum) before the mechanical work.
Then:
AudienceSummary.name: AudienceName; delete the edge-convert and its comment.i64carve-out: the key field additionally needsHash/Eq/Debug, whichIdNewtypealready carries — verify types: newtype AudienceId and thread it through storage/web #475 had nofurther reasons before flipping
audience_id: AudienceId.Alternatives considered and rejected in the audit discussion: a
web-localwrapper type (legal, but every field becomes
Wrapper<T>), and upstreaming aleaf-impl macro to leptos-rs (timeline outside our control).
Acceptance
PatchFieldsupport incommon(derive-trailer or explicitimpls), with the impl behavior covered (macros crate is coverage-measured if
the derive route is taken).
AudienceSummary.nametypedAudienceName; the types: newtype AudienceId and thread it through storage/web #475 key decision re-examinedand either flipped or re-documented with the concrete blocker.
cargo xtask validate --no-e2eclean.Related
Family: #475 (
AudienceId+ the keyed carve-out), #350 (AudienceName), #503(render-prop newtypes), ADR-0063. From the 2026-07-21 post-#560 newtype audit.