Add BoundedInt<TBounds> with custom-bounds witness pattern (closes #59)#79
Open
KaliCZ wants to merge 4 commits into
Open
Add BoundedInt<TBounds> with custom-bounds witness pattern (closes #59)#79KaliCZ wants to merge 4 commits into
KaliCZ wants to merge 4 commits into
Conversation
CoverageLines: 3923 / 5326 (73.7%) Branches: 2072 / 2800 (74.0%) Files changed in this PR
StrongTypes — lines 91.5% (1568/1714), branches 86.7% (1014/1170)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)
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% (94/94), branches 95.5% (21/22)
Result — lines 98.5% (333/338), branches 95.6% (417/436)
Strings — lines 84.7% (133/157), branches 84.4% (54/64)
generated — lines 83.1% (329/396), branches 72.5% (177/244)
StrongTypes.Analyzers — lines 94.7% (323/341), branches 86.8% (132/152)(root) — lines 94.7% (323/341), branches 86.8% (132/152)
StrongTypes.Api — lines 96.8% (332/343), branches 86.3% (88/102)(root) — lines 100.0% (11/11), branches n/a (0/0)
Controllers — lines 96.9% (222/229), branches 86.3% (88/102)
Data — lines 100.0% (56/56), branches n/a (0/0)
Entities — lines 77.8% (14/18), 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 88.8% (119/134), branches 70.0% (42/60)(root) — lines 88.8% (119/134), branches 70.0% (42/60)
StrongTypes.FsCheck — lines 77.4% (96/124), branches n/a (0/0)(root) — lines 77.4% (96/124), branches n/a (0/0)
StrongTypes.OpenApi.Core — lines 90.6% (445/491), branches 82.0% (292/356)(root) — lines 90.8% (275/303), branches 84.2% (170/202)
Inlining — lines 90.4% (170/188), branches 79.2% (122/154)
StrongTypes.OpenApi.Microsoft — lines 39.3% (435/1108), branches 40.8% (174/426)(root) — lines 95.0% (247/260), branches 75.4% (98/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)
Maybe — lines 100.0% (12/12), branches 100.0% (2/2)
Numbers — lines 100.0% (16/16), branches 100.0% (4/4)
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/658), branches 0.0% (0/204)
StrongTypes.OpenApi.Swashbuckle — lines 94.1% (239/254), 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)
Maybe — lines 100.0% (13/13), branches 75.0% (3/4)
Numbers — lines 100.0% (16/16), branches 100.0% (8/8)
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 1.0% (2/196), branches n/a (0/0)(root) — lines 1.0% (2/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 #79 +/- ##
==========================================
+ Coverage 89.07% 89.10% +0.02%
==========================================
Files 85 86 +1
Lines 1877 1890 +13
Branches 408 409 +1
==========================================
+ Hits 1672 1684 +12
- Misses 134 135 +1
Partials 71 71
🚀 New features to boost your workflow:
|
Introduces an int constrained to [TBounds.Min, TBounds.Max] via a small witness type implementing IBounds<int>. Bounds travel with the type, so e.g. `BoundedInt<PageSizeBounds>` makes the 1..100 invariant part of the signature. Also opens up the JSON converter factory to any struct marked with [NumericWrapper], so users can spin up their own validated numeric wrappers with just the attribute and a `Value` / `TryCreate` pair — the source generator and JSON converter take care of the rest. EF Core convention and the missing-EfCore-package analyzer now also recognize BoundedInt<>.
When a [NumericWrapper] type uses a concrete value type (e.g. int) as its underlying type, IEquatable<T>.Equals(T) and IComparable<T>.CompareTo(T) require the unannotated parameter type. Emitting Equals(int? other) / CompareTo(int? other) is Nullable<int>, which doesn't satisfy the interface contracts. For generic-parameter underlyings (e.g. T : INumber<T>), T? is a nullable annotation only, so the existing form remains correct. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picks up the parts of the BoundedInt feature that didn't exist when the
original PR was opened (the OpenApi.*, Api.IntegrationTests, and AspNetCore
projects all landed on main since), and fills the EF Core gap.
- OpenAPI: BoundedInt renders as `{ type: integer, format: int32, minimum,
maximum }` on both the Microsoft and Swashbuckle pipelines. The witness
bounds are read off the closed type's static Min/Max at schema time (the
range can't be derived from the generic definition alone). Schema tests
cover value, nullable-value, and a dedicated nullables endpoint across all
three document variants.
- Api integration: BoundedIntEntity round-trips through the request pipeline
and EF Core (both providers) via the shared EntityTests suite, which also
pins the 400 ValidationProblemDetails shape and the `$.value` /
`$.nullableValue` error keys.
- EF Core: BoundedIntExtensions added to the Unwrap method-call translator so
`.Unwrap()` translates to a bare int column, matching the convention that
already recognizes BoundedInt<>.
- Unit tests: full-range witness (int.MinValue..int.MaxValue) pins the
offset-storage modular round-trip; the error-message test now asserts the
witness bounds are interpolated, not left as a literal placeholder.
- Docs: Skill numeric/openapi references and the OpenApi package readmes now
cover BoundedInt and the custom-wrapper recipe.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
b0bd53c to
98a8b86
Compare
WPF: BoundedInt<TBounds> is a scalar IParsable value, so it belongs in the WPF TypeConverter wiring alongside the sign wrappers. Adds it to the type description provider, a PageSize property + PageSizeBounds witness on the test view-model, and a BoundedIntBindingTests class (OneWay display, TwoWay valid, out-of-range and non-numeric validation errors). WPF readme + Skill WPF reference updated. Docs: the original BoundedInt PR missed WPF, the EF Core Unwrap translator, and OpenAPI because the guidance only covered *test* projects, never the production wiring. Adds an "Adding a new strong type — integration checklist" to CLAUDE.md enumerating every package a type must be wired through (core, EF Core convention + Unwrap translator, both OpenAPI pipelines, WPF, analyzers), with pointers to testing.md and CONTRIBUTING for tests and docs. testing.md gains the missing WPF row and a WPF binding-test section; CONTRIBUTING points at the checklist. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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
Closes #59. Adds
BoundedInt<TBounds>— anintconstrained to a closed[Min, Max]range carried by a witness type implementingIBounds<int>, so e.g.BoundedInt<PageSizeBounds>makes the 1..100 invariant part of the signature.While I was here I also opened up the JSON converter factory to recognize any struct carrying
[NumericWrapper], not just the four built-ins. Combined with the existing source generator, defining a brand-new validated numeric wrapper is now just an attribute and aValue/TryCreatepair — the rest (Create, equality, comparison, operators, JSON round-trip,Min/Max/Unwrapextensions) is generated. Readme has a new "Defining your own validated wrapper" section showing the recipe.What's in the diff
src/StrongTypes/Numbers/IBounds.cs— the witness interface withstatic abstract Min/Max.src/StrongTypes/Numbers/BoundedInt.cs— the struct itself. Stores an offset relative toTBounds.Minsodefault(BoundedInt<TBounds>)wrapsMinand satisfies the invariant. The dynamic "must be between … and …" error message is achieved by passing an interpolatedInvariantDescriptionthrough to the source generator, which emits it inside the partial type body whereTBoundsis in scope.NumericStrongTypeJsonConverterFactory—CanConvertnow matches by[NumericWrapper]attribute presence; underlying type is read offValuesoBoundedInt<TBounds>works without a special case.StrongTypesConvention(EF Core) +MissingEfCorePackageAnalyzer— both now recognizeBoundedInt<>.NumberExtensions—AsBounded<TBounds>/ToBounded<TBounds>onint.BoundedIntTests.cs(FsCheck property tests for in/out-of-range, boundary fixed-cases, default-invariant, equality/comparison, JSON round-trip, error-message content, extensions); analyzer test theory gets a newDetects_bounded_int_wrappercase.Per the comment on #59 about the readme number-line diagram, the actual SVG isn't included — let me know if you'd like me to follow up with one.
Test plan
dotnet build(could not run locally — no SDK in this sandbox; CI will tell us)dotnet test src/StrongTypes.Tests—BoundedIntTestscovers the issue's three test requirements (in-range round-trip, out-of-range rejection by bothTryCreateandCreate, boundary acceptance) plus generated-member branch coverage and a JSON round-trip.dotnet test src/StrongTypes.Analyzers.Tests— newDetects_bounded_int_wrapperfact.BoundedInt<PageSizeBounds>.Create(0)shows the dynamic"between 1 and 100 (inclusive)"message at runtime.https://claude.ai/code/session_01TsE6jPbPttZztM6h2g7KqR
Generated by Claude Code