Class: Textus::Format::Yaml

Inherits:
Base
  • Object
show all
Defined in:
lib/textus/format/yaml.rb

Constant Summary collapse

RAW_REQUIRED =
%w[ingested_at content_hash].freeze
RAW_SOURCE_KINDS =
%w[url file asset].freeze

Class Method Summary collapse

Class Method Details

.data_to_payload(data) ⇒ Object



119
120
121
122
# File 'lib/textus/format/yaml.rb', line 119

def self.data_to_payload(data)
  data = data.transform_keys(&:to_s) if data.is_a?(Hash)
  { meta: data["_meta"] || {}, body: nil, content: data["content"] || data }
end

.enforce_name_match!(path, meta) ⇒ Object

Raises:



96
97
98
99
100
101
102
103
104
# File 'lib/textus/format/yaml.rb', line 96

def self.enforce_name_match!(path, meta)
  return unless meta.is_a?(Hash) && meta["name"]

  ext = extensions.first
  basename = File.basename(path, ext)
  return if meta["name"] == basename

  raise BadFrontmatter.new(path, "name '#{meta["name"]}' does not match basename '#{basename}'")
end

.extensionsObject



65
# File 'lib/textus/format/yaml.rb', line 65

def self.extensions = [".yaml", ".yml"]

.nested_globObject



67
# File 'lib/textus/format/yaml.rb', line 67

def self.nested_glob = "**/*.{yaml,yml}"

.parse(raw, path: nil) ⇒ Object

Raises:



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# File 'lib/textus/format/yaml.rb', line 6

def self.parse(raw, path: nil)
  raw = raw.dup.force_encoding(Encoding::UTF_8)
  raise BadFrontmatter.new(path, "entry is not valid UTF-8") unless raw.valid_encoding?

  begin
    parsed = ::YAML.safe_load(raw, permitted_classes: [Date, Time], aliases: false)
  rescue Psych::SyntaxError, Psych::AliasesNotEnabled, Psych::DisallowedClass => e
    raise BadFrontmatter.new(path, "YAML parse failed: #{e.message}")
  end
  raise BadFrontmatter.new(path, "YAML top-level must be a mapping") unless parsed.is_a?(Hash)

  meta = parsed["_meta"]
  fm = meta.is_a?(Hash) ? meta : {}
  content_without_meta = parsed.except("_meta")
  { "_meta" => fm, "body" => raw, "content" => content_without_meta }
end

.rewrite_name(path, basename) ⇒ Object

rubocop:disable Naming/PredicateMethod



85
86
87
88
89
90
91
92
93
94
# File 'lib/textus/format/yaml.rb', line 85

def self.rewrite_name(path, basename) # rubocop:disable Naming/PredicateMethod
  raw = File.binread(path)
  parsed = parse(raw, path: path)
  meta = parsed["_meta"]
  return false unless meta.is_a?(Hash) && meta["name"].is_a?(String) && meta["name"] != basename

  new_meta = meta.merge("name" => basename)
  File.binwrite(path, serialize(meta: new_meta, body: "", content: parsed["content"]))
  true
end

.serialize(meta:, body:, content: nil) ⇒ Object



23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/textus/format/yaml.rb', line 23

def self.serialize(meta:, body:, content: nil)
  if content.is_a?(Hash)
    on_disk = meta && !meta.empty? ? { "_meta" => meta }.merge(content) : content
    ::YAML.dump(on_disk).sub(/\A---\n/, "")
  elsif body && !body.to_s.empty?
    b = body.to_s
    b += "\n" unless b.end_with?("\n")
    b
  else
    raise UsageError.new("yaml serialize requires :content or :body")
  end
end

.serialize_for_put(meta:, body:, content:, path:) ⇒ Object

Raises:



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/textus/format/yaml.rb', line 69

def self.serialize_for_put(meta:, body:, content:, path:)
  raise UsageError.new("put for yaml requires content: or body:") if content.nil? && (body.nil? || body.to_s.empty?)

  if content.nil?
    begin
      parsed = parse(body.to_s, path: path)
    rescue BadFrontmatter => e
      raise BadContent.new(path, "bad_content: #{e.message}")
    end
    [body.to_s, parsed["_meta"], body.to_s, parsed["content"]]
  else
    bytes = serialize(meta: meta, body: "", content: content)
    [bytes, meta, bytes, content]
  end
end

.validate_against(schema, parsed) ⇒ Object



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

def self.validate_against(schema, parsed)
  schema.validate!(parsed["content"] || {})
end

.validate_path_extension(path, nested) ⇒ Object

Raises:



106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/textus/format/yaml.rb', line 106

def self.validate_path_extension(path, nested)
  ext = File.extname(path)
  if nested
    return if ext == ""

    raise UsageError.new("nested yaml path must not have an extension")
  end

  return if [".yaml", ".yml"].include?(ext)

  raise UsageError.new("yaml format requires '.yaml' or '.yml' path (got #{ext.inspect})")
end

.validate_raw_entry!(parsed, lane) ⇒ Object

Raises:



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/textus/format/yaml.rb', line 43

def self.validate_raw_entry!(parsed, lane)
  return unless lane == "raw"

  content = parsed["content"] || {}
  return if content["superseded_by"]

  missing = RAW_REQUIRED.reject { |f| content[f] }
  raise Textus::BadContent.new(nil, "raw entry missing required field(s): #{missing.join(", ")}") if missing.any?

  source = content["source"] || {}
  kind = source["kind"]
  unless RAW_SOURCE_KINDS.include?(kind)
    raise Textus::BadContent.new(
      nil, "raw entry source.kind must be #{RAW_SOURCE_KINDS.join("|")}, got #{kind.inspect}"
    )
  end

  return unless kind == "url" && !source["url"]

  raise Textus::BadContent.new(nil, "raw entry with source.kind=url must have source.url")
end