Class: Textus::Store::Writer

Inherits:
Object
  • Object
show all
Defined in:
lib/textus/store/writer.rb

Overview

rubocop:disable Metrics/ParameterLists

Instance Method Summary collapse

Constructor Details

#initialize(store) ⇒ Writer

Returns a new instance of Writer.



7
8
9
10
11
# File 'lib/textus/store/writer.rb', line 7

def initialize(store)
  @store = store
  @manifest = store.manifest
  @reader = store.reader
end

Instance Method Details

#accept(key, as:) ⇒ Object



126
127
128
# File 'lib/textus/store/writer.rb', line 126

def accept(key, as:)
  Proposal.accept(@store, key, as: as)
end

#delete(key, if_etag: nil, as: Role::DEFAULT, suppress_events: false) ⇒ Object

Raises:



111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/textus/store/writer.rb', line 111

def delete(key, if_etag: nil, as: Role::DEFAULT, suppress_events: false)
  mentry, path, = @manifest.resolve(key)
  writers = @manifest.zone_writers(mentry.zone)
  raise WriteForbidden.new(key, mentry.zone, writers: writers) unless writers.include?(as)
  raise UnknownKey.new(key, suggestions: @manifest.suggestions_for(key)) unless File.exist?(path)

  etag_before = Etag.for_file(path)
  raise EtagMismatch.new(key, if_etag, etag_before) if if_etag && if_etag != etag_before

  File.delete(path)
  @store.audit_log.append(role: as, verb: "delete", key: key, etag_before: etag_before, etag_after: nil)
  @store.fire_event(:delete, key: key) unless suppress_events
  { "protocol" => PROTOCOL, "ok" => true, "key" => key, "deleted" => true }
end

#enforce_name_match!(path, meta, format) ⇒ Object

Raises:



76
77
78
79
80
81
82
83
84
85
# File 'lib/textus/store/writer.rb', line 76

def enforce_name_match!(path, meta, format)
  return unless %w[markdown json yaml].include?(format)
  return unless meta.is_a?(Hash) && meta["name"]

  ext = Entry.for_format(format).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

#ensure_uid(format, meta, content, existing_uid) ⇒ Object



65
66
67
68
69
70
71
72
73
74
# File 'lib/textus/store/writer.rb', line 65

def ensure_uid(format, meta, content, existing_uid)
  case format
  when "markdown", "json", "yaml"
    m = meta.is_a?(Hash) ? meta.dup : {}
    m["uid"] = existing_uid || Store.mint_uid unless m["uid"].is_a?(String) && !m["uid"].empty?
    [m, content]
  else
    [meta, content]
  end
end

#existing_uid_for(mentry, path) ⇒ Object



55
56
57
58
59
60
61
62
63
# File 'lib/textus/store/writer.rb', line 55

def existing_uid_for(mentry, path)
  return nil unless File.exist?(path)

  raw = File.binread(path)
  parsed = Entry.for_format(mentry.format).parse(raw, path: path)
  Envelope.extract_uid(parsed["_meta"])
rescue StandardError
  nil
end

#put(key, meta: nil, body: nil, content: nil, if_etag: nil, as: Role::DEFAULT, suppress_events: false) ⇒ Object

Raises:



13
14
15
16
17
18
19
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
45
46
47
48
49
50
51
52
53
# File 'lib/textus/store/writer.rb', line 13

def put(key, meta: nil, body: nil, content: nil, if_etag: nil, as: Role::DEFAULT, suppress_events: false)
  @manifest.validate_key!(key)
  mentry, path, = @manifest.resolve(key)
  writers = @manifest.zone_writers(mentry.zone)
  raise WriteForbidden.new(key, mentry.zone, writers: writers) unless writers.include?(as)

  meta ||= {}
  strategy = Entry.for_format(mentry.format)

  existing_uid = existing_uid_for(mentry, path)
  meta, content = ensure_uid(mentry.format, meta, content, existing_uid)

  bytes, eff_meta, eff_body, eff_content = serialize_for_put(
    mentry: mentry, path: path, strategy: strategy,
    meta: meta, body: body, content: content
  )

  enforce_name_match!(path, eff_meta, mentry.format)

  schema = @store.schema_for(mentry.schema)
  if schema
    Entry.for_format(mentry.format).validate_against(
      schema,
      { "_meta" => eff_meta, "content" => eff_content },
    )
  end

  etag_before = File.exist?(path) ? Etag.for_file(path) : nil
  raise EtagMismatch.new(key, if_etag, etag_before) if if_etag && (etag_before != if_etag)

  FileUtils.mkdir_p(File.dirname(path))
  File.binwrite(path, bytes)
  etag_after = Etag.for_bytes(bytes)
  @store.audit_log.append(role: as, verb: "put", key: key, etag_before: etag_before, etag_after: etag_after)
  envelope = Envelope.build(
    key: key, mentry: mentry, path: path,
    meta: eff_meta, body: eff_body, etag: etag_after, content: eff_content
  )
  @store.fire_event(:put, key: key, envelope: envelope) unless suppress_events
  envelope
end

#serialize_for_put(mentry:, path:, strategy:, meta:, body:, content:) ⇒ Object



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/textus/store/writer.rb', line 87

def serialize_for_put(mentry:, path:, strategy:, meta:, body:, content:)
  case mentry.format
  when "markdown", "text"
    bytes = strategy.serialize(meta: meta, body: body.to_s)
    [bytes, meta, body.to_s, nil]
  when "json", "yaml"
    raise UsageError.new("put for #{mentry.format} requires content: or body:") if content.nil? && (body.nil? || body.to_s.empty?)

    if content.nil?
      begin
        parsed = strategy.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 = strategy.serialize(meta: meta, body: "", content: content)
      [bytes, meta, bytes, content]
    end
  else
    raise UsageError.new("unknown format #{mentry.format.inspect}")
  end
end