Add Interval strong types with JSON and EF Core support (closes #60)#77
Add Interval strong types with JSON and EF Core support (closes #60)#77KaliCZ wants to merge 3 commits into
Conversation
CoverageLines: 4115 / 5502 (74.8%) Branches: 2153 / 2860 (75.3%) Files changed in this PR
StrongTypes — lines 93.3% (1705/1827), branches 88.5% (1088/1230)Booleans — lines 100.0% (14/14), branches 84.6% (22/26)
Collections — lines 90.2% (276/306), branches 85.1% (114/134)
Digits — lines 97.7% (85/87), branches 93.8% (30/32)
Emails — lines 98.6% (68/69), branches 92.5% (37/40)
Enums — lines 100.0% (58/58), branches 95.5% (21/22)
Exceptions — lines 78.9% (15/19), branches 50.0% (7/14)
Intervals — lines 94.9% (185/195), branches 91.0% (91/100)
Maybe — lines 92.1% (151/164), branches 83.9% (94/112)
Nullables — lines 100.0% (12/12), branches 83.3% (20/24)
Numbers — lines 100.0% (84/84), branches 94.4% (17/18)
Result — lines 98.5% (333/338), branches 95.6% (417/436)
Strings — lines 84.7% (133/157), branches 84.4% (54/64)
generated — lines 89.8% (291/324), branches 78.8% (164/208)
StrongTypes.Analyzers — lines 94.7% (322/340), branches 86.8% (132/152)(root) — lines 94.7% (322/340), branches 86.8% (132/152)
StrongTypes.Api — lines 97.0% (352/363), branches 86.3% (88/102)(root) — lines 100.0% (11/11), branches n/a (0/0)
Controllers — lines 97.0% (225/232), branches 86.3% (88/102)
Data — lines 100.0% (75/75), branches n/a (0/0)
Entities — lines 75.0% (12/16), branches n/a (0/0)
Models — lines 100.0% (29/29), branches n/a (0/0)
StrongTypes.AspNetCore — lines 89.3% (151/169), branches 84.5% (71/84)(root) — lines 89.3% (151/169), branches 84.5% (71/84)
StrongTypes.AspNetCore.TestApi — lines 90.7% (39/43), branches 92.5% (37/40)(root) — lines 100.0% (6/6), branches n/a (0/0)
Controllers — lines 89.2% (33/37), branches 92.5% (37/40)
StrongTypes.EfCore — lines 90.6% (125/138), branches 73.2% (41/56)(root) — lines 90.6% (125/138), branches 73.2% (41/56)
StrongTypes.FsCheck — lines 80.6% (116/144), branches 100.0% (2/2)(root) — lines 80.6% (116/144), branches 100.0% (2/2)
StrongTypes.OpenApi.Core — lines 91.5% (442/483), branches 83.5% (299/358)(root) — lines 92.1% (269/292), branches 86.6% (175/202)
Inlining — lines 90.6% (173/191), branches 79.5% (124/156)
StrongTypes.OpenApi.Microsoft — lines 39.1% (441/1127), branches 40.6% (173/426)(root) — lines 94.6% (246/260), branches 74.6% (97/130)
Binding — lines 98.1% (103/105), branches 78.6% (55/70)
Collections — lines 100.0% (19/19), branches 100.0% (8/8)
Digits — lines 100.0% (11/11), branches 100.0% (2/2)
Emails — lines 100.0% (11/11), branches 100.0% (2/2)
Inlining — lines 100.0% (7/7), branches 50.0% (1/2)
Intervals — lines 100.0% (15/15), branches 100.0% (2/2)
Maybe — lines 100.0% (12/12), branches 100.0% (2/2)
Numbers — lines 100.0% (8/8), branches 100.0% (2/2)
Strings — lines 100.0% (9/9), branches 100.0% (2/2)
obj/Debug/net10.0/Microsoft.AspNetCore.OpenApi.SourceGenerators/Microsoft.AspNetCore.OpenApi.SourceGenerators.XmlCommentGenerator — lines 0.0% (0/670), branches 0.0% (0/204)
StrongTypes.OpenApi.Swashbuckle — lines 94.3% (248/263), branches 76.7% (155/202)(root) — lines 98.6% (68/69), branches 90.3% (56/62)
Binding — lines 88.3% (106/120), branches 64.3% (72/112)
Collections — lines 100.0% (6/6), branches 100.0% (4/4)
Digits — lines 100.0% (10/10), branches 100.0% (4/4)
Emails — lines 100.0% (10/10), branches 100.0% (4/4)
Inlining — lines 100.0% (2/2), branches n/a (0/0)
Intervals — lines 100.0% (17/17), branches 100.0% (4/4)
Maybe — lines 100.0% (13/13), branches 75.0% (3/4)
Numbers — lines 100.0% (8/8), branches 100.0% (4/4)
Strings — lines 100.0% (8/8), branches 100.0% (4/4)
StrongTypes.OpenApi.TestApi.Microsoft — lines 41.0% (163/398), branches 31.6% (65/206)(root) — lines 100.0% (14/14), branches 100.0% (2/2)
obj/Debug/net10.0/Microsoft.AspNetCore.OpenApi.SourceGenerators/Microsoft.AspNetCore.OpenApi.SourceGenerators.XmlCommentGenerator — lines 38.8% (149/384), branches 30.9% (63/204)
StrongTypes.OpenApi.TestApi.Shared — lines 0.0% (0/196), branches n/a (0/0)(root) — lines 0.0% (0/196), branches n/a (0/0)
StrongTypes.OpenApi.TestApi.Swashbuckle — lines 100.0% (11/11), branches 100.0% (2/2)(root) — lines 100.0% (11/11), branches 100.0% (2/2)
|
|
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## main #77 +/- ##
==========================================
- Coverage 89.07% 87.56% -1.52%
==========================================
Files 85 92 +7
Lines 1877 2091 +214
Branches 408 447 +39
==========================================
+ Hits 1672 1831 +159
- Misses 134 177 +43
- Partials 71 83 +12
🚀 New features to boost your workflow:
|
Introduce four interval struct types covering every nullability combination so the C# compiler enforces "closed interval ⇒ non-nullable endpoints": - ClosedInterval<T>: (T Start, T End) both bounded - IntervalFrom<T>: (T Start, T? End) "from X" - IntervalUntil<T>: (T? Start, T End) "until Y" - Interval<T>: (T? Start, T? End) fully open All four enforce Start <= End wherever both endpoints are present, follow the TryCreate / Create factory pattern, and ship a Deconstruct method that enables the 4-arm switch from issue #60. Persistence: - IntervalJsonConverterFactory handles all four shapes through System.Text.Json. - IntervalJsonValueConverter<TInterval> stores the interval as a JSON string column in EF Core; alternatively, callers can map to two scalar columns via EF Core's standard ComplexProperty (no custom converter needed). Tests: - FsCheck arbitraries for each type (always satisfying start <= end). - Property tests cover validation, deconstruction, equality, and JSON round-trip across all four nullability shapes. https://claude.ai/code/session_01Qc2Xf1PTwgi9jPSBGbv8f6
Reviewing PR #77 (interval strong types) on top of latest main surfaced gaps in error reporting, integration coverage, and docs. This addresses them. Implementation: - IntervalJsonConverterFactory: nested endpoint reads now rethrow path-less so System.Text.Json reattaches the property path. A null/type-mismatch endpoint was surfacing as the document root ("$") instead of "$.value" — the same bug #106 fixed for the numeric converter. Error messages also no longer leak the arity-suffixed CLR name ("ClosedInterval`1"). Unit tests (StrongTypes.Tests): - Contains over open/closed bounds and ToString for every variant, JSON edge cases (missing/extra/reordered properties, non-object tokens, a DateOnly/DateTime endpoint), error-message quality, and the path-less rethrow. Added generator-branch coverage facts per testing.md. API integration tests (StrongTypes.Api*): - Four interval entities/controllers mapped to a single JSON column via the shipped HasIntervalJsonConversion. Dedicated wire-to-DB suite covering the round-trip on both providers, null handling, update, and invalid payloads keyed at "$.value" (the #106 contract). OpenAPI (StrongTypes.OpenApi.*): - Interval schema transformer (Microsoft) and filter (Swashbuckle) render each variant as { Start, End } with required: [Start, End] and per-variant endpoint nullability, inlined like the other wrappers. CloneWireShape now preserves `required`. Tested on all three pipeline configs. Docs: - README, SKILL.md + new references/intervals.md, and the EfCore/OpenApi references and package readmes. Corrected the ComplexProperty claim: EF Core cannot bind the interval's private constructor as a complex type, and column materialization would bypass the Start <= End invariant — the JSON column is the supported shape. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
c63f864 to
e6f0bf5
Compare
|
Rebased onto latest
Full build passes with |
WPF two-way binding goes through ParsableTypeConverter<T> (T : IParsable<T>), which the type-description provider only synthesises for the scalar strong types (NonEmptyString, Email, Digit, numeric wrappers). The intervals — like Maybe<T> and NonEmptyEnumerable<T> — are composite, have no single-TextBox string form, and are correctly not registered. The readme and skill claimed "every strong type"; tighten both to say string-round-trippable types and point composite types at field-by-field binding. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
Implements #60. Introduces four interval struct types covering every nullability combination so the C# compiler enforces "closed interval ⇒ non-nullable endpoints" — there is no runtime check needed because the type system already won't let a caller pass
nullfor the wrong endpoint.StartEndClosedInterval<T>TTIntervalFrom<T>TT?IntervalUntil<T>T?TInterval<T>T?T?All four are
readonly structwithwhere T : struct, IComparable<T>, follow theTryCreate/Createfactory pattern, enforceStart <= Endwhenever both endpoints are present, and ship aDeconstructmethod so the 4-arm switch from #60 works:JSON
IntervalJsonConverterFactoryhandles all four shapes throughSystem.Text.Json. Wire format is{"Start": …, "End": …}and honours the activeJsonNamingPolicy. JSON values that violate the wrapper invariant (e.g.start > end, ornullfor a non-nullable endpoint) throwJsonException.EF Core
Two persistence shapes are offered:
IntervalJsonValueConverter<TInterval>plusentity.HasIntervalJsonConversion(e => e.Interval).entity.ComplexProperty(e => e.Interval). The interval'sStartandEndbecome two columns whose nullability follows the interval variant automatically (closed → bothNOT NULL,IntervalFrom→ startNOT NULL/end nullable, etc.).Tests
Start <= End).Start > End, null for a non-nullable endpoint, missing properties).Test plan
dotnet buildsucceeds withTreatWarningsAsErrors=true.StrongTypes.Testspasses (theIntervals/folder and the newGeneratorsarbitraries).Interval<DateTime>.dotnet buildlocally — the dev environment doesn't have the .NET SDK installed, so CI is the first place this code is exercised.https://claude.ai/code/session_01Qc2Xf1PTwgi9jPSBGbv8f6
Generated by Claude Code