Class: Textus::Manifest

Inherits:
Object
  • Object
show all
Defined in:
lib/textus/manifest.rb,
lib/textus/manifest/entry.rb,
lib/textus/manifest/rules.rb,
lib/textus/manifest/schema.rb

Defined Under Namespace

Modules: Schema Classes: Entry, Rules

Constant Summary collapse

EXT_TO_FORMAT =
{
  ".md" => "markdown",
  ".json" => "json",
  ".yaml" => "yaml",
  ".yml" => "yaml",
  ".txt" => "text",
}.freeze
TEXTUS_2_HINT =
"Install textus 0.11.x to run the migrator, then upgrade to this version. " \
"See https://github.com/patrick204nqh/textus/blob/main/CHANGELOG.md#0110".freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root, raw) ⇒ Manifest

Returns a new instance of Manifest.

Raises:



77
78
79
80
81
82
83
84
85
86
# File 'lib/textus/manifest.rb', line 77

def initialize(root, raw)
  @root = root
  @raw = raw
  raise BadFrontmatter.new(File.join(root, "manifest.yaml"), "manifest must declare zones:") if Array(raw["zones"]).empty?

  Schema.validate!(raw)

  @entries = Array(raw["entries"]).map { |e| Manifest::Entry.new(self, e) }
  validate_declared_keys!
end

Instance Attribute Details

#entriesObject (readonly)

Returns the value of attribute entries.



23
24
25
# File 'lib/textus/manifest.rb', line 23

def entries
  @entries
end

#rawObject (readonly)

Returns the value of attribute raw.



23
24
25
# File 'lib/textus/manifest.rb', line 23

def raw
  @raw
end

#rootObject (readonly)

Returns the value of attribute root.



23
24
25
# File 'lib/textus/manifest.rb', line 23

def root
  @root
end

Class Method Details

.load(root) ⇒ Object

Raises:



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

def self.load(root)
  manifest_path = File.join(root, "manifest.yaml")
  raise IoError.new("manifest not found: #{manifest_path}") unless File.exist?(manifest_path)

  raw = YAML.safe_load_file(manifest_path, aliases: false)
  unless raw["version"] == PROTOCOL
    raise BadFrontmatter.new(
      manifest_path,
      "unsupported manifest version #{raw["version"].inspect}; expected #{PROTOCOL.inspect}",
      hint: version_hint_for(raw["version"]),
    )
  end

  new(root, raw)
end

.parse(yaml_text, root: ".") ⇒ Object



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

def self.parse(yaml_text, root: ".")
  raw = YAML.safe_load(yaml_text, aliases: false)
  unless raw["version"] == PROTOCOL
    raise BadFrontmatter.new(
      "<string>",
      "unsupported manifest version #{raw["version"].inspect}; expected #{PROTOCOL.inspect}",
      hint: version_hint_for(raw["version"]),
    )
  end

  new(root, raw)
end

Instance Method Details

#enumerate(prefix: nil) ⇒ Object

Enumerate all entry files reachable through the manifest. Returns

{ key:, path:, manifest_entry: }, …


139
140
141
142
143
# File 'lib/textus/manifest.rb', line 139

def enumerate(prefix: nil)
  out = @entries.flat_map { |entry| entry.nested ? enumerate_nested(entry) : enumerate_leaf(entry) }
  out.select! { |row| row[:key] == prefix || row[:key].start_with?("#{prefix}.") } if prefix
  out.sort_by { |row| row[:key] }
end

#permission_for(zone_name) ⇒ Object



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

def permission_for(zone_name)
  Textus::Domain::Permission.new(
    zone: zone_name,
    write_policy: zone_writers(zone_name),
    read_policy: zone_readers[zone_name] || :all,
  )
end

#resolve(key) ⇒ Object

Returns [Manifest::Entry, resolved_path, remaining_segments]

Raises:



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/textus/manifest.rb', line 97

def resolve(key)
  validate_key!(key)
  segments = key.split(".")
  # longest-prefix match
  candidates = @entries
               .map { |e| [e, e.key.split(".")] }
               .select { |(_, esegs)| esegs == segments[0, esegs.length] }
               .sort_by { |(_, esegs)| -esegs.length }
  raise UnknownKey.new(key, suggestions: suggestions_for(key)) if candidates.empty?

  entry, esegs = candidates.first
  remaining = segments[esegs.length..]
  if remaining.empty?
    path = resolve_leaf_path(entry)
    [entry, path, []]
  else
    raise UnknownKey.new(key, suggestions: suggestions_for(key)) unless entry.nested

    path = if entry.index_filename
             File.join(@root, "zones", entry.path, *remaining, entry.index_filename)
           else
             primary_ext = Textus::Entry.for_format(entry.format).extensions.first
             File.join(@root, "zones", entry.path, *remaining) + primary_ext
           end
    [entry, path, remaining]
  end
end

#rulesObject



88
89
90
# File 'lib/textus/manifest.rb', line 88

def rules
  @rules ||= Textus::Manifest::Rules.parse(@raw["rules"] || [])
end

#rules_for(key) ⇒ Object



92
93
94
# File 'lib/textus/manifest.rb', line 92

def rules_for(key)
  rules.for(key)
end

#suggestions_for(key) ⇒ Object

Returns up to 5 dotted keys from the manifest that look similar to the requested key, ranked by shared-prefix length then Levenshtein distance.



127
128
129
130
131
132
133
134
135
# File 'lib/textus/manifest.rb', line 127

def suggestions_for(key)
  candidates = enumerate.map { |r| r[:key] }
  # Include declared (non-nested) entry keys even if file is missing.
  candidates.concat(@entries.reject(&:nested).map(&:key))
  candidates.uniq!
  Key::Distance.suggest(key, candidates, limit: 5)
rescue StandardError
  []
end

#validate_key!(key) ⇒ Object

Raises:



145
146
147
148
149
# File 'lib/textus/manifest.rb', line 145

def validate_key!(key)
  raise UsageError.new("empty key") if key.nil? || key.empty?

  Key::Grammar.validate!(key)
end

#zone_readersObject



29
30
31
32
33
34
# File 'lib/textus/manifest.rb', line 29

def zone_readers
  @zone_readers ||= Array(@raw["zones"]).to_h do |z|
    rp = z["read_policy"]
    [z["name"], rp.nil? ? :all : Array(rp)]
  end
end

#zone_writers(zone_name) ⇒ Object



36
37
38
# File 'lib/textus/manifest.rb', line 36

def zone_writers(zone_name)
  zones[zone_name] or raise UsageError.new("undeclared zone '#{zone_name}'")
end

#zonesObject



25
26
27
# File 'lib/textus/manifest.rb', line 25

def zones
  @zones ||= Array(@raw["zones"]).to_h { |z| [z["name"], Array(z["write_policy"])] }
end