Skip to content

Commit 69ef08c

Browse files
committed
docs: Add design for Wave 2 item 8 (numeric format widths)
1 parent e6e5002 commit 69ef08c

1 file changed

Lines changed: 94 additions & 0 deletions

File tree

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Numeric format width validation (Wave 2 item 8)
2+
3+
**Status:** design approved 2026-05-08
4+
**Source inventory:** `docs/superpowers/specs/2026-05-07-openapi-refactor-design.md` §9, Wave 2 item 8
5+
6+
## Goal
7+
8+
Honor `format` on `IntegerSchema` and `NumberSchema` for the four OpenAPI-defined numeric widths:
9+
10+
- `int32` — value must fit in 32-bit signed (`[Integer.MIN_VALUE, Integer.MAX_VALUE]`).
11+
- `int64` — recognized, always passes (already enforced by the validator's internal `long` coercion).
12+
- `float` — value's magnitude must not exceed `Float.MAX_VALUE` (cast to `float` would otherwise yield ±Infinity). NaN / Infinity inputs also fail.
13+
- `double` — recognized, always passes (already enforced by the validator's internal `double` coercion).
14+
15+
Today `validateStringFormat` exists, but `validateInteger` / `validateNumber` ignore the `format` field entirely.
16+
17+
## Non-goals
18+
19+
- Decimal-precision validation for `float` (option B from brainstorming, rejected). A strict `(float)n != n` check would reject nearly all legitimate non-integer JSON values (`0.1`, `1.1`, …). Industry validators (AJV, jsonschema-validator) check overflow only.
20+
- BigInteger / BigDecimal inputs larger than `long` / `double`. Those already fail upstream with `"type" expected integer/number` and never reach the format check.
21+
- Consumer-defined numeric formats / SPI. Deferred, non-breaking to add later (mirroring the decision made for string formats).
22+
- Toggling assertion vs. annotation behavior — we always assert.
23+
- Changes to `IntegerSchema` / `NumberSchema` record shapes or `Spec` parsing.
24+
25+
## Decisions
26+
27+
- **Overflow only for `float`.** Matches widespread validator behavior.
28+
- **`int64` and `double` are recognized no-ops.** Documents that they're known formats rather than unknown-and-ignored. Same pattern Wave 2 #5 used for `binary` / `password`.
29+
- **Unknown numeric formats remain silently ignored.** Consistent with the string-format contract.
30+
31+
## Per-format strategy
32+
33+
| Format | Schema | Predicate | Failure message |
34+
|---------|--------------|------------------------------------------------------------------------|------------------------------|
35+
| `int32` | `IntegerSchema` | `n >= Integer.MIN_VALUE && n <= Integer.MAX_VALUE` | `"value does not fit in int32"` |
36+
| `int64` | `IntegerSchema` | `n -> true` | `"value does not fit in int64"` (unreachable) |
37+
| `float` | `NumberSchema` | `!Double.isNaN(n) && !Double.isInfinite(n) && Math.abs(n) <= Float.MAX_VALUE` | `"value does not fit in float"` |
38+
| `double`| `NumberSchema` | `n -> true` | `"value does not fit in double"` (unreachable) |
39+
40+
## Code organization
41+
42+
Two new dispatch maps inside `DefaultValidator`, mirroring the `FORMAT_CHECKS` pattern used for strings:
43+
44+
```java
45+
private record IntegerFormatCheck(LongPredicate isValid, String message) {}
46+
private record NumberFormatCheck(DoublePredicate isValid, String message) {}
47+
48+
private static final Map<String, IntegerFormatCheck> INTEGER_FORMAT_CHECKS = Map.of(
49+
"int32", new IntegerFormatCheck(
50+
n -> n >= Integer.MIN_VALUE && n <= Integer.MAX_VALUE,
51+
"value does not fit in int32"),
52+
"int64", new IntegerFormatCheck(n -> true, "value does not fit in int64"));
53+
54+
private static final Map<String, NumberFormatCheck> NUMBER_FORMAT_CHECKS = Map.of(
55+
"float", new NumberFormatCheck(
56+
n -> !Double.isNaN(n) && !Double.isInfinite(n) && Math.abs(n) <= Float.MAX_VALUE,
57+
"value does not fit in float"),
58+
"double", new NumberFormatCheck(n -> true, "value does not fit in double"));
59+
```
60+
61+
Two new private methods:
62+
63+
```java
64+
private void validateIntegerFormat(long n, String format, String pointer);
65+
private void validateNumberFormat(double n, String format, String pointer);
66+
```
67+
68+
Each is a single map lookup; missing key → no-op (preserves the "unknown format silently ignored" contract).
69+
70+
Called from the existing `validateInteger` / `validateNumber` at the end, guarded by `s.format() != null`.
71+
72+
Failure renders via the existing `fail(pointer, FORMAT_KEYWORD, message, n)` path — same RFC 7807 400 response shape as string-format failures.
73+
74+
## Tests
75+
76+
Add to `src/test/java/com/retailsvc/http/validate/StringIntegerNumberTest.java` (despite the name, this file already covers integer and number formats):
77+
78+
- `integerFormatInt32``Integer.MAX_VALUE` passes; `Integer.MAX_VALUE + 1L` and `Integer.MIN_VALUE - 1L` fail with keyword `format`.
79+
- `integerFormatInt64NoOp``Long.MAX_VALUE`, `Long.MIN_VALUE`, and arbitrary mid-range values pass.
80+
- `numberFormatFloat``1.5` passes; `1e40` fails with keyword `format`. Negative overflow (`-1e40`) also fails.
81+
- `numberFormatDoubleNoOp``Double.MAX_VALUE`, `-Double.MAX_VALUE`, small values pass.
82+
- `integerFormatUnknownIsIgnored` / `numberFormatUnknownIsIgnored` — lock in the silent-ignore contract for unknown formats.
83+
84+
Integration coverage: one IT case in `src/test/java/com/retailsvc/http/OpenApiServerIT.java` exercising `format: int32` via a query parameter, asserting 400 + `application/problem+json` on overflow input and 200 on a valid value. Test fixtures: add the corresponding operation to `src/test/resources/openapi.json` and mirror it in `src/test/resources/openapi.yaml` (project rule).
85+
86+
## Acceptance criteria
87+
88+
- `int32` values outside the 32-bit signed range produce a 400 with `format` in the violation pointer.
89+
- `int64` / `double` formats are recognized but never produce failures from format checks alone (type/range checks elsewhere are unchanged).
90+
- `float` values whose magnitude exceeds `Float.MAX_VALUE`, plus NaN/Infinity inputs, produce a 400.
91+
- Unknown numeric `format` values are still silently ignored.
92+
- String format behavior (Wave 2 item 5) is unchanged byte-for-byte.
93+
- No new runtime dependencies.
94+
- `mvn verify` passes.

0 commit comments

Comments
 (0)