diff --git a/UPSTREAM.md b/UPSTREAM.md index 37f3183..a7e2010 100644 --- a/UPSTREAM.md +++ b/UPSTREAM.md @@ -3,3 +3,5 @@ Version `0.1.2` was prepared from [`mailschema/mailschema@eddc745`](https://github.com/mailschema/mailschema/commit/eddc7456910bf6a3c33de51c46ebf50f61ee7342). The included contribution schema has SHA-256 digest `a8a241b4837971a6251bcc135c704283536f4fb825fbbaedaf1598c6e688d564`. The canonical schemas live in the main MailSchema repository; this repository owns the Python package surface and its release history. + +The unreleased MAP contract projection is derived from [`mailschema/mailschema@ab91b37`](https://github.com/mailschema/mailschema/commit/ab91b37d29dbf95a2e208a34a144bf5f7c26d0a6). Its MAP schema digest is `d908a960f53fade9ab47876bf69b1b3d037adeb7096091e22ca71fbc3909429c`; its Content Review schema digest is `dba2a2f1e57f843f43080f5a40ced2d9c84e7c7036ec306c371a82d39f6d6c9f`. These files are projections of that source commit and do not independently define MAP. diff --git a/src/mailschema/content-review-0.1.schema.json b/src/mailschema/content-review-0.1.schema.json index 1d67ac5..61c1f22 100644 --- a/src/mailschema/content-review-0.1.schema.json +++ b/src/mailschema/content-review-0.1.schema.json @@ -29,7 +29,12 @@ "type": "object", "additionalProperties": false, "properties": { - "feedback": { "type": "string", "minLength": 1, "maxLength": 12000 } + "feedback": { + "type": "string", + "minLength": 1, + "maxLength": 12000, + "pattern": "\\S" + } }, "required": ["feedback"] } diff --git a/src/mailschema/map-0.1.schema.json b/src/mailschema/map-0.1.schema.json index 2b08ace..2cc1dfe 100644 --- a/src/mailschema/map-0.1.schema.json +++ b/src/mailschema/map-0.1.schema.json @@ -18,6 +18,12 @@ "format": "uri", "maxLength": 2048 }, + "httpsIdentifier": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "maxLength": 2048 + }, "uuidUrn": { "type": "string", "pattern": "^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" @@ -93,7 +99,7 @@ "type": "object", "additionalProperties": false, "properties": { - "url": { "$ref": "#/$defs/identifier" }, + "url": { "$ref": "#/$defs/httpsIdentifier" }, "method": { "const": "POST" }, "requestMediaType": { "const": "application/json" }, "resultMediaType": { "const": "application/json" }, @@ -118,7 +124,7 @@ "resultRetentionSeconds" ] }, - "humanUrl": { "$ref": "#/$defs/identifier" }, + "humanUrl": { "$ref": "#/$defs/httpsIdentifier" }, "authorization": { "type": "object", "additionalProperties": false, @@ -196,15 +202,18 @@ "profile": { "$ref": "#/$defs/profile" }, "requestId": { "$ref": "#/$defs/uuidUrn" }, "interactionId": { "$ref": "#/$defs/uuidUrn" }, + "type": { "$ref": "#/$defs/typeReference" }, "operation": { "type": "string", "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", "maxLength": 100 }, - "state": { "enum": ["accepted", "completed", "pending", "approval-required"] }, + "state": { + "enum": ["accepted", "completed", "failed", "pending", "approval-required"] + }, "target": { "$ref": "#/$defs/target" }, "recordedAt": { "type": "string", "format": "date-time" }, - "resultUrl": { "$ref": "#/$defs/identifier" }, + "resultUrl": { "$ref": "#/$defs/httpsIdentifier" }, "output": { "type": "object", "maxProperties": 100 } }, "required": [ @@ -212,6 +221,7 @@ "profile", "requestId", "interactionId", + "type", "operation", "state", "target", @@ -227,6 +237,8 @@ "type": { "enum": [ "https://mailschema.org/problems/refused", + "https://mailschema.org/problems/authentication-required", + "https://mailschema.org/problems/result-not-found", "https://mailschema.org/problems/stale-target", "https://mailschema.org/problems/expired-interaction", "https://mailschema.org/problems/invalid-request", @@ -247,6 +259,8 @@ "code": { "enum": [ "refused", + "authentication-required", + "result-not-found", "stale-target", "expired-interaction", "invalid-request", @@ -269,6 +283,52 @@ "requestId", "interactionId", "code" + ], + "allOf": [ + { + "if": { "properties": { "code": { "const": "invalid-request" } }, "required": ["code"] }, + "then": { "properties": { "type": { "const": "https://mailschema.org/problems/invalid-request" }, "status": { "const": 400 } } } + }, + { + "if": { "properties": { "code": { "const": "authentication-required" } }, "required": ["code"] }, + "then": { "properties": { "type": { "const": "https://mailschema.org/problems/authentication-required" }, "status": { "const": 401 } } } + }, + { + "if": { "properties": { "code": { "const": "refused" } }, "required": ["code"] }, + "then": { "properties": { "type": { "const": "https://mailschema.org/problems/refused" }, "status": { "const": 403 } } } + }, + { + "if": { "properties": { "code": { "const": "result-not-found" } }, "required": ["code"] }, + "then": { "properties": { "type": { "const": "https://mailschema.org/problems/result-not-found" }, "status": { "const": 404 } } } + }, + { + "if": { "properties": { "code": { "const": "stale-target" } }, "required": ["code"] }, + "then": { "properties": { "type": { "const": "https://mailschema.org/problems/stale-target" }, "status": { "const": 409 } }, "required": ["target"] } + }, + { + "if": { "properties": { "code": { "const": "idempotency-conflict" } }, "required": ["code"] }, + "then": { "properties": { "type": { "const": "https://mailschema.org/problems/idempotency-conflict" }, "status": { "const": 409 } } } + }, + { + "if": { "properties": { "code": { "const": "request-in-progress" } }, "required": ["code"] }, + "then": { "properties": { "type": { "const": "https://mailschema.org/problems/request-in-progress" }, "status": { "const": 409 } } } + }, + { + "if": { "properties": { "code": { "const": "expired-interaction" } }, "required": ["code"] }, + "then": { "properties": { "type": { "const": "https://mailschema.org/problems/expired-interaction" }, "status": { "const": 410 } } } + }, + { + "if": { "properties": { "code": { "const": "unsupported-profile" } }, "required": ["code"] }, + "then": { "properties": { "type": { "const": "https://mailschema.org/problems/unsupported-profile" }, "status": { "const": 422 } } } + }, + { + "if": { "properties": { "code": { "const": "unsupported-type" } }, "required": ["code"] }, + "then": { "properties": { "type": { "const": "https://mailschema.org/problems/unsupported-type" }, "status": { "const": 422 } } } + }, + { + "if": { "properties": { "code": { "const": "unsupported-operation" } }, "required": ["code"] }, + "then": { "properties": { "type": { "const": "https://mailschema.org/problems/unsupported-operation" }, "status": { "const": 422 } } } + } ] } } diff --git a/tests/map-result.json b/tests/map-result.json new file mode 100644 index 0000000..27fa0c4 --- /dev/null +++ b/tests/map-result.json @@ -0,0 +1,23 @@ +{ + "kind": "MapResult", + "profile": "https://mailschema.org/profiles/map/0.1", + "requestId": "urn:uuid:018f47a2-b4d3-7c02-b491-7bdf2eaac67c", + "interactionId": "urn:uuid:018f47a2-5d7c-7b11-9a3d-4d2160b85b10", + "type": { + "id": "https://mailschema.org/types/content-review", + "version": "0.1", + "recordDigest": "sha-256:0e6365df1bf904f2475f972ec66a5561edc0bfaae69fd7a1b41f53b40bf8ac1c" + }, + "operation": "approve", + "state": "completed", + "target": { + "id": "https://reviews.example/content/campaign-42", + "revision": "4", + "digest": "sha-256:9096ce5e9226696d3bdc80f7d62a8e1ca4995c4fd8c4b9f0155287529aefe8fd" + }, + "recordedAt": "2026-09-23T01:06:01Z", + "resultUrl": "https://reviews.example/map/results/urn%3Auuid%3A018f47a2-b4d3-7c02-b491-7bdf2eaac67c", + "output": { + "decision": "approved" + } +} diff --git a/tests/test_package.py b/tests/test_package.py index 6e701e4..ef98135 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -22,6 +22,7 @@ def setUp(self): self.record = json.loads(Path("tests/content-review.json").read_text()) self.description = json.loads(Path("tests/map-description.json").read_text()) self.request = json.loads(Path("tests/map-request.json").read_text()) + self.result = json.loads(Path("tests/map-result.json").read_text()) def test_valid_contribution_and_record(self): validate_contribution(self.fixture) @@ -47,6 +48,8 @@ def test_schema_copies_and_record_reference(self): def test_map_documents(self): validate_map_document(self.description) validate_map_document(self.request) + validate_map_document(self.result) + self.assertEqual(self.result["type"], self.request["type"]) validate_content_review_request(self.request) invalid = copy.deepcopy(self.request) invalid["operation"] = "publish"