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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d40b5366532ded5404a82499db609a8e30e4edd70df68befef93c6c227cfb461
4
- data.tar.gz: bd03b7643aa9589044fcf10fa2bf63b35b3856f370198af3663c2beb6343280d
3
+ metadata.gz: 34c6761c416a974a8be62a22b642f9546b62cdc4f880ebe423dc37d3dcab3260
4
+ data.tar.gz: ead8f3521d7b6e70a781113c530fc6d894bee5d1cda504289ead2ac94a8e9701
5
5
  SHA512:
6
- metadata.gz: 8195ac0b45dfc6ee758e0c25b8e46195f4768b07ebde12debf8629377c10e97536687dcb2bf0848c94d34921803534dbb1611b3a3ace46eb6f0f850d280c0be2
7
- data.tar.gz: 42b7343562247873c5c01b96a58477e0775d19274a7ce94c740c71e190bb639a5f476809446d48043f44599d8e80f855f918cf6447e229c450c6787459d081ff
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
 
@@ -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, and no security-sensitive identifier
11
- # contains whitespace, a control or a formatting character.
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|
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mailschema
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mailschema
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - MailSchema contributors