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 kind].freeze
ROLE_KINDS =
%w[accept_authority generator proposer runner].freeze
ZONE_KEYS =
%w[name write_policy read_policy].freeze
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 refresh intake_handler_allowlist promotion retention].freeze
REFRESH_KEYS =
%w[ttl on_stale sync_budget_ms fetch_timeout_seconds].freeze
FETCH_TIMEOUT_SECONDS_CEILING =
3600
PROMOTION_KEYS =
%w[requires].freeze
AUDIT_KEYS =
%w[max_size keep].freeze

Class Method Summary collapse

Class Method Details

.validate!(raw) ⇒ Object

Raises:



21
22
23
24
25
26
27
28
29
30
31
# File 'lib/textus/manifest/schema.rb', line 21

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_zone_writers_declared!(raw)
end

.validate_entries!(entries) ⇒ Object



39
40
41
42
43
44
45
46
# File 'lib/textus/manifest/schema.rb', line 39

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

Raises:



100
101
102
103
104
105
106
107
# File 'lib/textus/manifest/schema.rb', line 100

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

Raises:



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/textus/manifest/schema.rb', line 76

def self.validate_roles!(roles)
  return if roles.nil?
  raise BadManifest.new("roles: must be a list") unless roles.is_a?(Array)

  accept_authority_count = 0
  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")
    kind = r["kind"] or raise BadManifest.new("role '#{name}' at '#{path}' missing kind")
    unless ROLE_KINDS.include?(kind)
      raise BadManifest.new("unknown role kind '#{kind}' at '#{path}' (known: #{ROLE_KINDS.join(", ")})")
    end

    accept_authority_count += 1 if kind == "accept_authority"
  end
  return unless accept_authority_count > 1

  raise BadManifest.new(
    "manifest declares #{accept_authority_count} accept_authority roles; " \
    "at most one accept_authority role is allowed",
  )
end

.validate_rules!(rules) ⇒ Object



48
49
50
51
52
53
54
55
56
57
58
# File 'lib/textus/manifest/schema.rb', line 48

def self.validate_rules!(rules)
  Array(rules).each_with_index do |r, i|
    path = "$.rules[#{i}]"
    walk(r, RULE_KEYS, path)
    if r["refresh"].is_a?(Hash)
      walk(r["refresh"], REFRESH_KEYS, "#{path}.refresh")
      validate_fetch_timeout!(r["refresh"]["fetch_timeout_seconds"], "#{path}.refresh.fetch_timeout_seconds")
    end
    walk(r["promotion"], PROMOTION_KEYS, "#{path}.promotion") if r["promotion"].is_a?(Hash)
  end
end

.validate_zone_writers_declared!(raw) ⇒ Object



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/textus/manifest/schema.rb', line 60

def self.validate_zone_writers_declared!(raw)
  return if raw["roles"].nil? # default mapping is permissive

  declared = Array(raw["roles"]).map { |r| r["name"] }.compact.to_set
  Array(raw["zones"]).each do |z|
    Array(z["write_policy"]).each_with_index do |w, j|
      next if declared.include?(w)

      raise BadManifest.new(
        "zone '#{z["name"]}' write_policy[#{j}] references undeclared role '#{w}' " \
        "(declared roles: #{declared.to_a.join(", ")})",
      )
    end
  end
end

.validate_zones!(zones) ⇒ Object



33
34
35
36
37
# File 'lib/textus/manifest/schema.rb', line 33

def self.validate_zones!(zones)
  Array(zones).each_with_index do |z, i|
    walk(z, ZONE_KEYS, "$.zones[#{i}]")
  end
end

.walk(hash, allowed, path) ⇒ Object



109
110
111
112
113
114
115
116
117
# File 'lib/textus/manifest/schema.rb', line 109

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