Module: Textus::Manifest::Schema
- Defined in:
- lib/textus/manifest/schema.rb
Constant Summary collapse
- ROOT_KEYS =
%w[version roles zones entries rules audit].freeze
- ROLE_KEYS =
%w[name can].freeze
- ZONE_KEYS =
%w[name kind owner desc].freeze
- LANES =
The closed coordination vocabulary (ADR 0028; completed at five in ADR 0033; unified here in ADR 0034). Each lane pairs a zone-kind with the single capability that authorizes originating bytes in it — a total bijection. This table is the ONE source of truth; the three legacy constants below are derived from it so a zone-kind and its required capability cannot drift. Key order is canon-first so the unknown-kind error message reads canon, workspace, quarantine, queue, derived.
{ "canon" => "author", "workspace" => "keep", "quarantine" => "fetch", "queue" => "propose", "derived" => "build", }.freeze
- ZONE_KINDS =
LANES.keys.freeze
- CAPABILITIES =
LANES.values.freeze
- KIND_REQUIRES_VERB =
LANES- ENTRY_KEYS =
%w[ key path zone kind schema owner nested format compute template publish_to publish_each intake events inject_boot index_filename ].freeze
- COMPUTE_KEYS =
%w[kind select pluck sort_by limit transform command sources].freeze
- INTAKE_KEYS =
%w[handler config].freeze
- RULE_KEYS =
%w[match fetch intake_handler_allowlist guard retention].freeze
- FETCH_KEYS =
%w[ttl on_stale sync_budget_ms fetch_timeout_seconds].freeze
- FETCH_TIMEOUT_SECONDS_CEILING =
3600- RETENTION_KEYS =
%w[expire_after archive_after].freeze
- AUDIT_KEYS =
%w[max_size keep].freeze
Class Method Summary collapse
- .validate!(raw) ⇒ Object
- .validate_entries!(entries) ⇒ Object
- .validate_fetch_timeout!(value, path) ⇒ Object
- .validate_roles!(roles) ⇒ Object
- .validate_rules!(rules) ⇒ Object
- .validate_single_queue!(raw) ⇒ Object
-
.validate_zone_kind_consistency!(raw) ⇒ Object
Write authority is derived from capabilities (ADR 0030): a zone of a given kind can only be written by a role that holds the kind’s required verb.
- .validate_zones!(zones) ⇒ Object
- .walk(hash, allowed, path) ⇒ Object
Class Method Details
.validate!(raw) ⇒ Object
38 39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/textus/manifest/schema.rb', line 38 def self.validate!(raw) raise BadManifest.new("manifest must be a hash") unless raw.is_a?(Hash) walk(raw, ROOT_KEYS, "$") validate_roles!(raw["roles"]) validate_zones!(raw["zones"]) validate_entries!(raw["entries"]) validate_rules!(raw["rules"]) walk(raw["audit"], AUDIT_KEYS, "$.audit") if raw["audit"].is_a?(Hash) validate_single_queue!(raw) validate_zone_kind_consistency!(raw) end |
.validate_entries!(entries) ⇒ Object
65 66 67 68 69 70 71 72 |
# File 'lib/textus/manifest/schema.rb', line 65 def self.validate_entries!(entries) Array(entries).each_with_index do |e, i| path = "$.entries[#{i}]" walk(e, ENTRY_KEYS, path) walk(e["compute"], COMPUTE_KEYS, "#{path}.compute") if e["compute"].is_a?(Hash) walk(e["intake"], INTAKE_KEYS, "#{path}.intake") if e["intake"].is_a?(Hash) end end |
.validate_fetch_timeout!(value, path) ⇒ Object
112 113 114 115 116 117 118 119 |
# File 'lib/textus/manifest/schema.rb', line 112 def self.validate_fetch_timeout!(value, path) return if value.nil? return if value.is_a?(Integer) && value.positive? && value <= FETCH_TIMEOUT_SECONDS_CEILING raise BadManifest.new( "fetch_timeout_seconds at '#{path}' must be a positive integer ≤ #{FETCH_TIMEOUT_SECONDS_CEILING} (got #{value.inspect})", ) end |
.validate_roles!(roles) ⇒ Object
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/textus/manifest/schema.rb', line 86 def self.validate_roles!(roles) return if roles.nil? raise BadManifest.new("roles: must be a list") unless roles.is_a?(Array) roles.each_with_index do |r, i| path = "$.roles[#{i}]" walk(r, ROLE_KEYS, path) name = r["name"] or raise BadManifest.new("role at '#{path}' missing name") Array(r["can"]).each do |verb| next if CAPABILITIES.include?(verb) raise BadManifest.new( "unknown capability '#{verb}' for role '#{name}' at '#{path}' " \ "(known: #{CAPABILITIES.join(", ")})", ) end end = roles.count { |r| Array(r["can"]).include?("author") } return if <= 1 raise BadManifest.new( "manifest declares #{} roles with the author capability; at most one is allowed", ) end |
.validate_rules!(rules) ⇒ Object
74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/textus/manifest/schema.rb', line 74 def self.validate_rules!(rules) Array(rules).each_with_index do |r, i| path = "$.rules[#{i}]" walk(r, RULE_KEYS, path) if r["fetch"].is_a?(Hash) walk(r["fetch"], FETCH_KEYS, "#{path}.fetch") validate_fetch_timeout!(r["fetch"]["fetch_timeout_seconds"], "#{path}.fetch.fetch_timeout_seconds") end walk(r["retention"], RETENTION_KEYS, "#{path}.retention") if r["retention"].is_a?(Hash) end end |
.validate_single_queue!(raw) ⇒ Object
131 132 133 134 135 136 137 138 |
# File 'lib/textus/manifest/schema.rb', line 131 def self.validate_single_queue!(raw) queues = Array(raw["zones"]).select { |z| z["kind"] == "queue" }.map { |z| z["name"] } return if queues.size <= 1 raise BadManifest.new( "at most one zone may declare kind: queue (found: #{queues.join(", ")})", ) end |
.validate_zone_kind_consistency!(raw) ⇒ Object
Write authority is derived from capabilities (ADR 0030): a zone of a given kind can only be written by a role that holds the kind’s required verb. Reject a manifest declaring a zone whose required verb is held by no role. Capabilities.resolve returns the defaults when ‘roles:` is nil, so the capability union is all four verbs and every kind is satisfied.
145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/textus/manifest/schema.rb', line 145 def self.validate_zone_kind_consistency!(raw) held = Capabilities.resolve(raw["roles"]).values.flatten.uniq Array(raw["zones"]).each_with_index do |z, i| verb = KIND_REQUIRES_VERB[z["kind"]] next if verb.nil? || held.include?(verb) raise BadManifest.new( "zone '#{z["name"]}' (#{z["kind"]}) at '$.zones[#{i}]' " \ "needs a role with capability '#{verb}'; none declared", ) end end |
.validate_zones!(zones) ⇒ Object
51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/textus/manifest/schema.rb', line 51 def self.validate_zones!(zones) Array(zones).each_with_index do |z, i| walk(z, ZONE_KEYS, "$.zones[#{i}]") if z["kind"].nil? raise BadManifest.new("zone '#{z["name"]}' at '$.zones[#{i}]' must declare a kind (one of: #{ZONE_KINDS.join(", ")})") end next if ZONE_KINDS.include?(z["kind"]) raise BadManifest.new( "unknown zone kind '#{z["kind"]}' at '$.zones[#{i}]' (known: #{ZONE_KINDS.join(", ")})", ) end end |
.walk(hash, allowed, path) ⇒ Object
121 122 123 124 125 126 127 128 129 |
# File 'lib/textus/manifest/schema.rb', line 121 def self.walk(hash, allowed, path) return unless hash.is_a?(Hash) hash.each_key do |k| next if allowed.include?(k) raise BadManifest.new("unknown key '#{k}' at '#{path}'") end end |