New contracts/cultnet/cultnet.raw-document-header.schema.json for
CultNetRawDocumentHeader - it has no payload/payloadEncoding members at all,
so headers must not $ref the record schema (which required both). The
response page's own schema now lists every property CultNetSnapshotResponseRawV1Message
declares as `required` (present on the wire, nil included) rather than only
the four that happened to be non-nullable - MessagePack-CSharp's map-mode
formatter writes every declared key regardless of null, so `headers`/`documents`
are always both present, one an array and the other an explicit null by
projection. The old `oneOf: [{required:[headers]},{required:[documents]}]`
matched both branches on every real page (both keys present) and so never
validated anything; it is now `oneOf` over each key's type (array vs null),
which is mutually exclusive. cultnet.raw-document-record.schema.json gained
schemaName/schemaVersion/schemaContentHash (present on every real record, not
previously in the schema at all - any real page would have failed
additionalProperties:false on them). fieldPredicate's two oneOf branches each
gained `not: {required: [the other]}`, so `values` and `number` cannot both
be present (previously only `required` was checked, which does not exclude
the sibling key).
Added MiniJsonSchemaValidator, a deliberately small draft-2020-12 subset
validator (type/required/additionalProperties/properties/items/const/enum/
pattern/minLength/minItems/oneOf/$ref, same-document and cross-document) -
no schema-validation dependency existed in this repo, and this only needs to
cover what contracts/cultnet/*.schema.json actually use. CultNetSchemaContractTests
decodes real wire bytes from a live CultNetDatabaseServer (header projection,
document projection, and a hop with edges) through MessagePackSerializer.ConvertToJson
and checks them against the committed schema files, plus three
fieldPredicate-shape tests pinning the values/number exclusivity.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
100 lines
3.4 KiB
JSON
100 lines
3.4 KiB
JSON
{
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.selection.schema.json",
|
|
"title": "CultNet Typed Selection",
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"schemas": {
|
|
"type": "array",
|
|
"items": { "type": "string", "minLength": 1 },
|
|
"minItems": 1,
|
|
"description": "Absent means every schema. Present and empty is refused at the door (selection_invalid), never treated as \"every schema\": omit the field instead."
|
|
},
|
|
"keys": {
|
|
"type": "array",
|
|
"items": { "type": "string", "minLength": 1 },
|
|
"minItems": 1,
|
|
"description": "Absent means every key. Present and empty is refused at the door (selection_invalid), never treated as \"every key\": omit the field instead."
|
|
},
|
|
"fields": {
|
|
"type": "array",
|
|
"items": { "$ref": "#/$defs/fieldPredicate" }
|
|
},
|
|
"cites": { "$ref": "#/$defs/citation" },
|
|
"cited": { "$ref": "#/$defs/incoming" },
|
|
"projection": {
|
|
"type": "string",
|
|
"enum": ["header", "document"],
|
|
"default": "header"
|
|
},
|
|
"descending": { "type": "boolean", "default": false },
|
|
"limit": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"maximum": 200,
|
|
"description": "Clamped to 1..=200 by the answering server, not refused outside that range: an absent value or one outside the bound is treated as the nearer bound (absent = 200)."
|
|
},
|
|
"cursor": { "type": "string" }
|
|
},
|
|
"$defs": {
|
|
"recordRef": {
|
|
"type": "object",
|
|
"required": ["schemaId", "recordKey"],
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"schemaId": { "type": "string", "minLength": 1 },
|
|
"recordKey": { "type": "string", "minLength": 1 }
|
|
}
|
|
},
|
|
"fieldPredicate": {
|
|
"type": "object",
|
|
"required": ["index", "op"],
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"index": { "type": "string", "minLength": 1 },
|
|
"op": { "type": "string", "enum": ["any_of", "lt", "le", "ge", "gt"] },
|
|
"values": {
|
|
"type": "array",
|
|
"items": { "type": "string" },
|
|
"minItems": 1
|
|
},
|
|
"number": {
|
|
"type": "string",
|
|
"description": "Q-J: a canonical decimal string, never a JSON number. Matches ^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$ and is not \"-0\"; any other spelling is refused typed at the door (selection_invalid), never normalised.",
|
|
"pattern": "^(?!-0$)-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$"
|
|
}
|
|
},
|
|
"oneOf": [
|
|
{
|
|
"required": ["values"],
|
|
"not": { "required": ["number"] },
|
|
"properties": { "op": { "const": "any_of" } }
|
|
},
|
|
{
|
|
"required": ["number"],
|
|
"not": { "required": ["values"] },
|
|
"properties": { "op": { "enum": ["lt", "le", "ge", "gt"] } }
|
|
}
|
|
]
|
|
},
|
|
"citation": {
|
|
"type": "object",
|
|
"required": ["target"],
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"target": { "$ref": "#/$defs/recordRef" },
|
|
"role": { "type": "string", "minLength": 1 }
|
|
}
|
|
},
|
|
"incoming": {
|
|
"type": "object",
|
|
"required": ["role", "exists"],
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"role": { "type": "string", "minLength": 1 },
|
|
"exists": { "type": "boolean" }
|
|
}
|
|
}
|
|
}
|
|
}
|