Module: Textus::Manifest::Schema

Defined in:
lib/textus/manifest/schema.rb

Constant Summary collapse

ROOT_KEYS =
%w[version roles zones entries rules].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_intro 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

Class Method Summary collapse

Class Method Details

.validate!(raw) ⇒ Object

Raises:



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/textus/manifest/schema.rb', line 20

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"])
  Array(raw["zones"]).each_with_index do |z, i|
    walk(z, ZONE_KEYS, "$.zones[#{i}]")
  end
  Array(raw["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
  Array(raw["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
  validate_zone_writers_declared!(raw)
end

.validate_fetch_timeout!(value, path) ⇒ Object

Raises:



86
87
88
89
90
91
92
93
# File 'lib/textus/manifest/schema.rb', line 86

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:



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/textus/manifest/schema.rb', line 62

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



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

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

.walk(hash, allowed, path) ⇒ Object



95
96
97
98
99
100
101
102
103
# File 'lib/textus/manifest/schema.rb', line 95

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