61 lines
3.2 KiB
Markdown
61 lines
3.2 KiB
Markdown
# 8.3 — Request/response schema validation, KV+type DSL (GREEN)
|
|||
|
|
|
||
|
|
Phase: 8 — ServiceAdapter CRD rollout
|
||
|
|
Stage: RED
|
||
|
|
Depends on: 8.1 (CRD types carry `requestSchema`/`responseSchema`), 8.2 (dispatcher calls this per request)
|
||
|
|
|
||
|
|
Design contract: [API_ROUTING_HYBRID_DESIGN.md](../API_ROUTING_HYBRID_DESIGN.md) §1
|
||
|
|
"Request/response validation" subsection — the full DSL including the array/nullable
|
||
|
|
extension. Read that subsection in full before implementing; do not reinvent the
|
||
|
|
shape from the checklist below alone.
|
||
|
|
|
||
|
|
- [ ] `internal/serviceadapter/validate.go` (new, no external dependency —
|
||
|
|
not a JSON Schema library): validates a parsed body (`map[string]interface{}`
|
||
|
|
or `[]interface{}` for array-typed schemas) against a `FieldSchema`
|
||
|
|
- [ ] Object schema: `required` fields present, each present field in `fields`
|
||
|
|
matches its declared Go runtime type (`string`→string, `number`→float64,
|
||
|
|
`boolean`→bool, `array`→`[]interface{}`, `object`→`map[string]interface{}`)
|
||
|
|
- [ ] `nullable: true` on a field accepts JSON `null` regardless of declared type;
|
||
|
|
a `null` on a non-nullable field is a `type_mismatch`; bare `field: string`
|
||
|
|
shorthand means `{type: string, nullable: false}`
|
||
|
|
- [ ] `strict: true` rejects body keys not listed in `fields`; default `false` is permissive
|
||
|
|
- [ ] Array schema (`type: array`): `items: string` validates every element is that
|
||
|
|
scalar type; `items: { fields: {...} }` validates every element as an object
|
||
|
|
schema, one level deep inside each item — no further nesting
|
||
|
|
- [ ] Compiled once per CR add/update in 8.1's informer callback, not per-request —
|
||
|
|
the parsed `FieldSchema` stored in the same registry entry as the route
|
||
|
|
- [ ] Request-side violation → 400, RFC 9457 `problem+json`,
|
||
|
|
`{type, title, detail, errors: [{field, reason}]}`, `reason` one of
|
||
|
|
`missing`, `type_mismatch: want X got Y`, `unknown_field`
|
||
|
|
- [ ] Response-side violation does **not** block the response — forwarded unchanged,
|
||
|
|
emits `serviceadapter_response_schema_mismatch{service,resource}` metric + log line
|
||
|
|
- [ ] GET requests with no body skip request-schema validation entirely (query-param
|
||
|
|
validation is out of scope for this task — schemas in this repo's adapters only
|
||
|
|
apply to POST/PATCH bodies per the CRs in §1/§3/§6)
|
||
|
|
|
||
|
|
## Verify
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# missing required field
|
||
|
|
curl -s -X POST https://api.riotpiao.com/ \
|
||
|
|
-H 'Authorization: Bearer <jwt>' -H 'X-Service: postgres' -H 'X-Resource: query' \
|
||
|
|
-d '{"params": []}'
|
||
|
|
# expected: 400, errors: [{"field":"sql","reason":"missing"}]
|
||
|
|
|
||
|
|
# type mismatch
|
||
|
|
curl -s -X POST https://api.riotpiao.com/ \
|
||
|
|
-H 'Authorization: Bearer <jwt>' -H 'X-Service: postgres' -H 'X-Resource: query' \
|
||
|
|
-d '{"sql": 42}'
|
||
|
|
# expected: 400, errors: [{"field":"sql","reason":"type_mismatch: want string got number"}]
|
||
|
|
|
||
|
|
# nullable field accepted
|
||
|
|
curl -s -o /dev/null -w '%{http_code}\n' https://api.riotpiao.com/ \
|
||
|
|
-H 'Authorization: Bearer <jwt>' -H 'X-Service: memory' -H 'X-Resource: skill'
|
||
|
|
# expected: 200 even though generated_from is null in the response body — response
|
||
|
|
# validation must not reject/replace the body
|
||
|
|
|
||
|
|
# unit test, not curl:
|
||
|
|
go test ./internal/serviceadapter/... -run TestValidate -v
|
||
|
|
# expected: covers array-of-scalar, array-of-object, strict rejection, nullable
|
||
|
|
```
|