mailschema 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +3 -1
- data/lib/mailschema/artifacts.rb +38 -2
- data/lib/mailschema/description.rb +15 -2
- data/lib/mailschema/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 34c6761c416a974a8be62a22b642f9546b62cdc4f880ebe423dc37d3dcab3260
|
|
4
|
+
data.tar.gz: ead8f3521d7b6e70a781113c530fc6d894bee5d1cda504289ead2ac94a8e9701
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6dee1f1ee55052fce0ea79cfe7d13c9b2446cd0ad8165f01b7979b2115fe1584262194cd00a09ef390ca709358d2c48378257e0035fb2b1c5ec8b6bc5625bc5a
|
|
7
|
+
data.tar.gz: 6f7c472a6a72a808ab8c0c493de8707ae19912935b62a55ce279231b4b918f383e1085a129b4bf073efd4bb033f6a124f683625fc9ef9654b97921daf2c3f59a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.1] - 2026-10-21
|
|
4
|
+
|
|
5
|
+
- `description_errors` refuses an `inLanguage` that is not a well-formed BCP 47 language tag, as MAP 0.3 Core requires.
|
|
6
|
+
- Where a schema offers alternatives told apart by a constant, such as a schedule's `mode`, errors report only the alternative the value chose, or the allowed values when it chose none.
|
|
7
|
+
|
|
3
8
|
## [0.3.0] - 2026-10-07
|
|
4
9
|
|
|
5
10
|
- Mail Action Protocol 0.3: strict MAP JSON parsing, RFC 8785 canonicalization and digests, descriptions, type contracts and implementation records, with the reference implementation's rules and messages.
|
data/README.md
CHANGED
|
@@ -47,7 +47,9 @@ All under `Mailschema`:
|
|
|
47
47
|
- `Message.part(description)`: the `Mail::Part` that carries a description, to place in a `multipart/related` entity after the readable content.
|
|
48
48
|
- `Authentication::Signature` and `Authentication.qualifying(raw_message:, signatures:, organizational_domain: nil)`: the first signature your DKIM verifier reported that qualifies the raw message, or nil. Alignment is strict unless you pass a callable that returns a domain's organizational domain.
|
|
49
49
|
|
|
50
|
-
Every `*_errors` method returns an array of strings, empty when the value is valid. Schemas are validated with `json_schemer` (JSON Schema 2020-12, ECMA-262 regular expressions) against the profile artifacts the gem bundles.
|
|
50
|
+
Every `*_errors` method returns an array of strings, empty when the value is valid. Schemas are validated with `json_schemer` (JSON Schema 2020-12, ECMA-262 regular expressions) against the profile artifacts the gem bundles. Where a schema offers alternatives told apart by a constant, such as a schedule's `mode`, only the alternative the value chose is reported.
|
|
51
|
+
|
|
52
|
+
A check covers the document: the core rules, and that `details` and the operations are what the contract allows. Whether the service, recipient, `details` and terms match a request recorded when the person started it, whether the current time is between `issuedAt` and `expiresAt`, and whether the message is authentic are for the implementation that acts on it, under the contract's requirements and the specification.
|
|
51
53
|
|
|
52
54
|
## Links
|
|
53
55
|
|
data/lib/mailschema/artifacts.rb
CHANGED
|
@@ -35,14 +35,50 @@ module Mailschema
|
|
|
35
35
|
def bundled(name) = compile(JSON.parse(File.read(File.join(ARTIFACTS, name))))
|
|
36
36
|
|
|
37
37
|
# Each validation error as its JSON Pointer and message, the pointer placed inside an
|
|
38
|
-
# enclosing document by `prefix`; an error at the document's root is at /.
|
|
38
|
+
# enclosing document by `prefix`; an error at the document's root is at /. A oneOf whose
|
|
39
|
+
# branches are told apart by a const reports only the branch the value chose.
|
|
39
40
|
def errors(schema, value, prefix = "")
|
|
40
|
-
schema.validate(value).map do |error|
|
|
41
|
+
chosen_branches(schema.validate(value).to_a).map do |error|
|
|
41
42
|
pointer = "#{prefix}#{error.fetch("data_pointer")}"
|
|
42
43
|
"#{pointer.empty? ? "/" : pointer}: #{error.fetch("error")}"
|
|
43
44
|
end
|
|
44
45
|
end
|
|
45
46
|
|
|
47
|
+
ONE_OF_BRANCH = %r{\A(.*/oneOf)/(\d+)(?:/|\z)}
|
|
48
|
+
|
|
49
|
+
def chosen_branches(errors)
|
|
50
|
+
errors.group_by { |error| error.fetch("schema_pointer")[ONE_OF_BRANCH, 1] }.flat_map do |one_of, group|
|
|
51
|
+
one_of ? one_of_errors(one_of, group) : group
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# A failed oneOf's errors: the chosen branch's, the allowed values when the value chose none,
|
|
56
|
+
# or every branch's when no const tells them apart.
|
|
57
|
+
def one_of_errors(one_of, group)
|
|
58
|
+
branches = group.group_by { |error| error.fetch("schema_pointer")[ONE_OF_BRANCH, 2] }
|
|
59
|
+
refused = branches.transform_values { |branch| discriminator(branch, one_of) }
|
|
60
|
+
chosen = refused.select { |_, error| error.nil? }.keys
|
|
61
|
+
return branches.fetch(chosen.first) if chosen.size == 1 && refused.size > 1
|
|
62
|
+
return group unless chosen.empty?
|
|
63
|
+
|
|
64
|
+
not_one_of(refused.values) || group
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def not_one_of(refusals)
|
|
68
|
+
pointers = refusals.map { |error| error.fetch("data_pointer") }.uniq
|
|
69
|
+
return unless pointers.size == 1
|
|
70
|
+
|
|
71
|
+
allowed = refusals.map { |error| error.dig("schema", "const").to_json }.join(", ")
|
|
72
|
+
[{ "data_pointer" => pointers.first, "error" => "value at `#{pointers.first}` is not one of: [#{allowed}]" }]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The const a oneOf branch refused on a member of the value it was choosing for.
|
|
76
|
+
def discriminator(branch, one_of)
|
|
77
|
+
branch.find do |error|
|
|
78
|
+
error.fetch("type") == "const" && error.fetch("schema_pointer").sub(one_of, "").count("/") == 3
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
46
82
|
CORE = bundled("core.schema.json")
|
|
47
83
|
CONTRACT = bundled("contract.schema.json")
|
|
48
84
|
IMPLEMENTATION = bundled("implementation.schema.json")
|
|
@@ -6,9 +6,21 @@ module Mailschema
|
|
|
6
6
|
UNSAFE = /\p{White_Space}|\p{Cc}|\p{Cf}/
|
|
7
7
|
private_constant :UNSAFE
|
|
8
8
|
|
|
9
|
+
# A well-formed BCP 47 language tag: RFC 5646's langtag, privateuse and grandfathered forms.
|
|
10
|
+
LANGUAGE_TAG = Regexp.new(
|
|
11
|
+
"\\A(?:(?:[a-z]{2,3}(?:-[a-z]{3}){0,3}|[a-z]{4}|[a-z]{5,8})(?:-[a-z]{4})?(?:-(?:[a-z]{2}|[0-9]{3}))?" \
|
|
12
|
+
"(?:-(?:[a-z0-9]{5,8}|[0-9][a-z0-9]{3}))*(?:-[0-9a-wy-z](?:-[a-z0-9]{2,8})+)*(?:-x(?:-[a-z0-9]{1,8})+)?" \
|
|
13
|
+
"|x(?:-[a-z0-9]{1,8})+" \
|
|
14
|
+
"|en-gb-oed|i-(?:ami|bnn|default|enochian|hak|klingon|lux|mingo|navajo|pwn|tao|tay|tsu)" \
|
|
15
|
+
"|sgn-(?:be-fr|be-nl|ch-de)|art-lojban|cel-gaulish|no-(?:bok|nyn)|zh-(?:guoyu|hakka|min|min-nan|xiang))\\z",
|
|
16
|
+
Regexp::IGNORECASE
|
|
17
|
+
)
|
|
18
|
+
private_constant :LANGUAGE_TAG
|
|
19
|
+
|
|
9
20
|
# Why the value is not a MAP 0.3 description, or no reasons. Beyond the core schema: expiry
|
|
10
|
-
# follows issuance, operation identifiers are unique,
|
|
11
|
-
# contains whitespace, a control or a formatting
|
|
21
|
+
# follows issuance, inLanguage is a well-formed BCP 47 tag, operation identifiers are unique,
|
|
22
|
+
# and no security-sensitive identifier contains whitespace, a control or a formatting
|
|
23
|
+
# character.
|
|
12
24
|
def self.description_errors(value)
|
|
13
25
|
errors = Schemas.errors(Schemas::CORE, value)
|
|
14
26
|
return errors if errors.any?
|
|
@@ -16,6 +28,7 @@ module Mailschema
|
|
|
16
28
|
if Time.iso8601(value["expiresAt"]) <= Time.iso8601(value["issuedAt"])
|
|
17
29
|
errors << "/expiresAt: must be later than issuedAt"
|
|
18
30
|
end
|
|
31
|
+
errors << "/inLanguage: must be a well-formed BCP 47 language tag" unless LANGUAGE_TAG.match?(value["inLanguage"])
|
|
19
32
|
ids = value["operations"].map { |operation| operation["id"] }
|
|
20
33
|
errors << "/operations: identifiers must be unique" if ids.uniq.size != ids.size
|
|
21
34
|
identifiers(value).each do |pointer, identifier|
|
data/lib/mailschema/version.rb
CHANGED