Class: Textus::Entry::Yaml

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

Overview

YAML entry storage. Top-level must be a mapping so we can carry _meta.

Class Method Summary collapse

Class Method Details

.enforce_name_match!(path, meta) ⇒ Object

Raises:



74
75
76
77
78
79
80
81
82
# File 'lib/textus/entry/yaml.rb', line 74

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



42
# File 'lib/textus/entry/yaml.rb', line 42

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

.inject_uid(meta, content, existing_uid) ⇒ Object



84
85
86
87
88
# File 'lib/textus/entry/yaml.rb', line 84

def self.inject_uid(meta, content, existing_uid)
  m = meta.is_a?(Hash) ? meta.dup : {}
  m["uid"] = existing_uid || Textus::Store.mint_uid unless m["uid"].is_a?(String) && !m["uid"].empty?
  [m, content]
end

.nested_globObject



44
# File 'lib/textus/entry/yaml.rb', line 44

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

.parse(raw, path: nil) ⇒ Object

Raises:



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

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

Mutating filesystem op; returns true if a write happened.



63
64
65
66
67
68
69
70
71
72
# File 'lib/textus/entry/yaml.rb', line 63

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



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

def self.serialize(meta:, body:, content: nil)
  if content.is_a?(Hash)
    # Re-inject _meta as the first key so on-disk shape is stable.
    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:



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

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



38
39
40
# File 'lib/textus/entry/yaml.rb', line 38

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

.validate_path_extension(path, nested) ⇒ Object

Raises:



90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/textus/entry/yaml.rb', line 90

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