mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-23 20:29:06 -05:00
fix: the API schema holds each endpoint's fields
The data page promises that v1 fields are only ever added, and the envelope schema left items unconstrained, so a renamed field passed validation. Each kind now lists the fields its items carry and their types.
This commit is contained in:
1 parent
1bbb1d4749
commit
1ff2bfc3c0
2 files changed
+265
-6
No files matched your search
@@ -4,10 +4,21 @@
|
|||||||
"title": "RetroBIOS static API v1 envelope",
|
"title": "RetroBIOS static API v1 envelope",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["schema_version", "generated_at", "kind", "count", "items"],
|
"required": [
|
||||||
|
"schema_version",
|
||||||
|
"generated_at",
|
||||||
|
"kind",
|
||||||
|
"count",
|
||||||
|
"items"
|
||||||
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"schema_version": { "const": 1 },
|
"schema_version": {
|
||||||
"generated_at": { "type": "string", "format": "date-time" },
|
"const": 1
|
||||||
|
},
|
||||||
|
"generated_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
"kind": {
|
"kind": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
@@ -18,10 +29,238 @@
|
|||||||
"file-locations"
|
"file-locations"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"count": { "type": "integer", "minimum": 0 },
|
"count": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
"items": {
|
"items": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": { "type": "object" }
|
"items": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"description": "Every endpoint shares this envelope. Within v1 an item keeps the fields listed for its kind and their types: fields may be added, never renamed or removed. A breaking change takes a new prefix.",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"const": "catalog"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"bytes",
|
||||||
|
"media_type",
|
||||||
|
"sha256",
|
||||||
|
"title",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"media_type": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"bytes": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
"sha256": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9a-f]{64}$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"const": "emulators"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"profile"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"const": "platforms"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"contract",
|
||||||
|
"coverage",
|
||||||
|
"id",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"contract": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"coverage": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"const": "verification-and-coverage-gaps"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"destination",
|
||||||
|
"discrepancy",
|
||||||
|
"emulator",
|
||||||
|
"expected",
|
||||||
|
"in_repo",
|
||||||
|
"layer",
|
||||||
|
"name",
|
||||||
|
"platform",
|
||||||
|
"platform_id",
|
||||||
|
"reason",
|
||||||
|
"required",
|
||||||
|
"status",
|
||||||
|
"system"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"platform": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"platform_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"system": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"emulator": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"destination": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"layer": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"expected": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"discrepancy": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"in_repo": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"const": "file-locations"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"sha1",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"sha1": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9a-f]{40}$"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
@@ -5706,5 +5706,25 @@ class WhatOnlyAnInstallProvidesIsListed(unittest.TestCase):
|
|||||||
self.assertEqual([row["path"] for row in rows], [str(Path(tmp, "icon.png"))])
|
self.assertEqual([row["path"] for row in rows], [str(Path(tmp, "icon.png"))])
|
||||||
|
|
||||||
|
|
||||||
|
class TheApiSchemaHoldsTheFieldsItPromises(unittest.TestCase):
|
||||||
|
"""The data page promises fields are only added within v1, and the
|
||||||
|
published schema left items unconstrained: renaming url in
|
||||||
|
file-locations.json passed validation and broke consumers silently."""
|
||||||
|
|
||||||
|
def test_a_renamed_field_is_refused(self):
|
||||||
|
try:
|
||||||
|
import jsonschema
|
||||||
|
except ImportError:
|
||||||
|
self.skipTest("jsonschema is tooling only")
|
||||||
|
schema = json.loads((ROOT / "schemas" / "site-api-envelope.schema.json").read_text())
|
||||||
|
envelope = {"schema_version": 1, "generated_at": "2026-10-20T00:00:00Z",
|
||||||
|
"kind": "file-locations", "count": 1,
|
||||||
|
"items": [{"sha1": "a" * 40, "url": "https://x/y"}]}
|
||||||
|
jsonschema.validate(envelope, schema)
|
||||||
|
envelope["items"] = [{"sha1": "a" * 40, "href": "https://x/y"}]
|
||||||
|
with self.assertRaises(jsonschema.ValidationError):
|
||||||
|
jsonschema.validate(envelope, schema)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
Reference in new issue
Block a user