feat!: arvo 1.0 — PrimitiveValue trait, remove SQLx, serde validation#98
Merged
codegresscom merged 15 commits intomainfrom Apr 23, 2026
Merged
feat!: arvo 1.0 — PrimitiveValue trait, remove SQLx, serde validation#98codegresscom merged 15 commits intomainfrom
codegresscom merged 15 commits intomainfrom
Conversation
added 15 commits
April 21, 2026 20:21
… bugs Bug fixes: - PhoneNumber: calling_code() fallback "+0" replaced with proper error; construction now fails for unknown country codes instead of silently producing an invalid E.164 number - Url::host(): strip port from host when URL contains an explicit port number (e.g. example.com:8080 → example.com); IPv6 bracket form preserved New methods: - TimeRange: contains(DateTime<Utc>), overlaps(TimeRange) - BusinessHours: is_open_at(NaiveTime) - BoundingBox: contains(Coordinate) - BirthDate: is_minor() - UnixTimestamp: as_datetime() → DateTime<Utc> - Locale: language(), region() - Money: add(), sub(), neg() (fulfils ROADMAP "immutable arithmetic helpers") - IpV4Address: is_loopback(), is_private() - Port: is_well_known(), is_registered(), is_ephemeral() - Percentage: as_fraction() - HexColor: to_rgb() - CardExpiryDate: months_until() Docs: all new methods documented in docs/*.md
contact/mod.rs: export PhoneNumberInput and PhoneNumberOutput (PhoneNumberInput is a struct required to construct PhoneNumber — its absence was a real API gap) geo/mod.rs: export LatitudeInput/Output, LongitudeInput/Output, TimeZoneInput/Output, CountryRegionInput/Output (all were defined in source files but not re-exported from the module) net/mod.rs: export Input/Output type aliases for all 10 net types (Url, Domain, IpV4Address, IpV6Address, IpAddress, Port, MacAddress, MimeType, HttpStatusCode, ApiKey) lib.rs: expand prelude to cover all 8 domain modules (finance, geo, temporal, net, measurement, contact composites) so that `use arvo::prelude::*` actually brings all types into scope; fix feature flag list in crate-level docstring
CountryCodeInput/Output, EmailAddressInput/Output, PostalAddressOutput, and WebsiteOutput were defined in their source files but not re-exported from contact/mod.rs, making them inaccessible to crate consumers.
Every Input/Output type alias and Input struct now re-exported via prelude so that `use arvo::prelude::*` brings the complete public API into scope — consistent across all 8 feature modules.
Every VO now implements TryFrom for its Input type, enabling ergonomic construction with the ? operator and consistent with the existing TryFrom<&str> pattern already present in some types. Implemented conversions by Input type: - TryFrom<&str>: CreditCardNumber, ApiKey - TryFrom<f64>: Latitude, Longitude, Percentage, Probability - TryFrom<i64>: PositiveInt, NonNegativeInt, UnixTimestamp - TryFrom<u16>: Port, HttpStatusCode - TryFrom<Decimal>: PositiveDecimal, NonNegativeDecimal - TryFrom<NaiveDate>: BirthDate, ExpiryDate - TryFrom<*Input struct>: PhoneNumber, PostalAddress, Money, ExchangeRate, Coordinate, BoundingBox, BusinessHours, TimeRange, Length, Weight, Temperature, Volume, Area, Energy, Frequency, Power, Pressure, Speed BoundedString already had TryFrom<&str> (with const generics).
Replace redundant TryFrom<InputType> wrappers (which just delegated to new()) with TryFrom<&str> implementations that parse the canonical string representation into the input type before calling Self::new(). Phone and postal address intentionally have no TryFrom due to parsing ambiguity.
Cover happy path, invalid format, and invalid value for all 29 types that received TryFrom<&str> implementations.
All Value Objects now implement sqlx::Type + Encode + Decode for Postgres via the opt-in `sql` feature. Simple newtypes use transparent mapping to their native DB type; composite types (Money, Coordinate, measurements, etc.) round-trip as TEXT via their canonical string and TryFrom<&str>. Port and HttpStatusCode map to INT4. PhoneNumber and PostalAddress are intentionally excluded (canonical string is not reversible). README gains a "Parsing from strings" section documenting TryFrom<&str> and a "SQL support" section with storage mapping table and usage example.
Replace serde(transparent) with serde(try_from = "T", into = "T") on all simple newtypes so that deserialization goes through new() and domain validation is enforced. Add TryFrom<T> and From<VO> for T impls for each inner type category (String, f64, i64, u16, Decimal, NaiveDate). All 689 tests now pass including the serde_deserialize_validates suite.
…rage - README: clarify that serde deserialisation validates via new() (not transparent) - implementing.md: add serde try_from pattern and sqlx checklist items - contact.md: document PhoneNumber and PostalAddress serde/sql/TryFrom exceptions
Split the ValueObject trait into two: - ValueObject: base trait for all VOs (new, into_inner) - PrimitiveValue: subtrait for simple newtypes (value() -> &Primitive) Composite types retain value() as a concrete inherent method. All 42 simple newtypes implement PrimitiveValue with typed Primitive (String, f64, i64, u16, Decimal, NaiveDate). Remove SQLx support entirely — users integrate with their ORM directly via into_inner() and individual accessors. Remove sql feature flag and all sqlx impl blocks (~260 lines). Remove XxxOutput type aliases from all VO files and re-exports. Export PrimitiveValue from prelude alongside ValueObject. All 689 tests pass.
- README: replace SQL support section with ORM integration guide; document PrimitiveValue trait; add 0.x → 1.0 migration table - docs/value-objects.md: document PrimitiveValue subtrait and when to use it - docs/implementing.md: update checklist and examples for new trait design; add ORM integration examples with sqlx and SeaORM - docs/contact.md: remove SQLx notes from PhoneNumber and PostalAddress
The sql feature was removed in the SQLx refactor but the test-sql CI job still referenced it. Drop the job and its Postgres service entirely. once_cell::sync::Lazy was introduced in v1.3.0 but Cargo.toml pinned version = "1", resolving to v1.0.1 under minimal-versions and breaking the check. Replace with std::sync::LazyLock (stable since Rust 1.80, within our 1.85 MSRV) and remove the once_cell dependency.
serde(try_from = "...") was introduced in 1.0.116; the previous version = "1" resolved to v1.0.99 under minimal-versions, which panicked with "unknown serde container attribute try_from".
codegresscom
approved these changes
Apr 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ValueObjecttrait — extractedvalue()into a newPrimitiveValuesubtrait. Simple newtypes implement both; composite types implement onlyValueObjectand expose data through dedicated accessors. This correctly models the semantic difference between the two kinds of value objects.value()/into_inner()directly, which also enables multi-column storage for composite types in ORMs (SeaORM, Diesel). Rationale: orphan rules would prevent users from doing this themselves, but keeping SQLx in arvo forced single-column TEXT storage for composites — the opposite of idiomatic ORM usage.serde(transparent)withserde(try_from = "T", into = "T")on all simple newtypes so deserialization routes throughnew()and domain rules are always enforced.XxxOutputtype aliases — these were redundant with the concrete primitive types and cluttered the public API.Breaking changes
PrimitiveValuesubtrait addedValueObjectfor any custom simple newtypesValueObject::value()moved toPrimitiveValueT: ValueObjectbounds toT: PrimitiveValueif calling.value()genericallytype Outputremoved fromValueObject<T as ValueObject>::Outputwith the concrete typeXxxOutputtype aliases removedEmailAddressOutputwithStringsqlfeature removed.value()/.into_inner()to bind; implement sqlx traits in your own crate if neededTest plan
cargo test --features full,serde)serde_deserialize_validatestests pass for all 62 types🤖 Generated with Claude Code