Class: Textus::Store::Entry::Writer

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

Overview

Owns the write pipeline (validate, serialize, etag-check, write, audit). Talks to ports (FileStore, Schemas, AuditLog, Manifest) and an Reader for the existing-uid lookup.

Invariant: every public method's final action is @audit_log.append(...).

No permission check, no event firing — those belong to the caller (Write::Put / ::Delete / ::Mv).

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(file_store:, manifest:, schemas:, audit_log:, call:, reader:, layout:) ⇒ Writer

Returns a new instance of Writer.



28
29
30
31
32
33
34
35
36
# File 'lib/textus/store/entry/writer.rb', line 28

def initialize(file_store:, manifest:, schemas:, audit_log:, call:, reader:, layout:)
  @file_store = file_store
  @manifest   = manifest
  @schemas    = schemas
  @audit_log  = audit_log
  @call       = call
  @reader     = reader
  @layout     = layout
end

Class Method Details

.from(container:, call:) ⇒ Object



15
16
17
18
19
20
21
22
23
24
25
26
# File 'lib/textus/store/entry/writer.rb', line 15

def self.from(container:, call:)
  # If the container exposes a writer factory (in tests we set this),
  # use it. Otherwise, construct a fresh Writer.
  return container.writer.call(call) if container.respond_to?(:writer) && container.writer

  new(
    file_store: container.file_store, manifest: container.manifest,
    schemas: container.schemas, audit_log: container.audit_log,
    call: call, reader: Reader.from(container: container),
    layout: container.layout
  )
end

Instance Method Details

#delete(key, mentry: nil, if_etag: nil) ⇒ Object

rubocop:disable Lint/UnusedMethodArgument

Raises:



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/textus/store/entry/writer.rb', line 61

def delete(key, mentry: nil, if_etag: nil) # rubocop:disable Lint/UnusedMethodArgument
  # `mentry:` is accepted for symmetry with `put` / `move` and to
  # leave room for future format-specific delete hooks; no field
  # on it is needed today.
  path = @manifest.resolver.resolve(key).path
  raise UnknownKey.new(key, suggestions: @manifest.resolver.suggestions_for(key)) unless @file_store.exists?(path)

  etag_before = @file_store.etag(path)
  raise EtagMismatch.new(key, if_etag, etag_before) if if_etag && if_etag != etag_before

  @file_store.delete(path)
  prune_empty_parents(path)
  @audit_log.append(
    role: @call.role, verb: "key_delete", key: key,
    etag_before: etag_before, etag_after: nil,
    extras: @call.correlation_id ? { "correlation_id" => @call.correlation_id } : nil
  )
end

#move(from_key:, to_key:, new_mentry:, if_etag: nil) ⇒ Object

Raises:



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/textus/store/entry/writer.rb', line 80

def move(from_key:, to_key:, new_mentry:, if_etag: nil)
  from_path = @manifest.resolver.resolve(from_key).path
  to_path   = @manifest.resolver.resolve(to_key).path
  raise UnknownKey.new(from_key, suggestions: @manifest.resolver.suggestions_for(from_key)) unless @file_store.exists?(from_path)

  etag_before = @file_store.etag(from_path)
  raise EtagMismatch.new(from_key, if_etag, etag_before) if if_etag && if_etag != etag_before

  @file_store.mv(from_path, to_path)
  prune_empty_parents(from_path)
  basename = to_key.split(".").last
  Format.for(new_mentry.format).rewrite_name(to_path, basename)
  etag_after = Value::Etag.for_file(to_path)

  envelope = @reader.read(to_key)

  extras = {
    "from_key" => from_key, "to_key" => to_key,
    "from_path" => from_path, "to_path" => to_path,
    "uid" => envelope.uid
  }
  extras["correlation_id"] = @call.correlation_id if @call.correlation_id

  @audit_log.append(
    role: @call.role, verb: "key_mv", key: to_key,
    etag_before: etag_before, etag_after: etag_after,
    extras: extras
  )

  envelope
end

#put(key, mentry:, payload:, if_etag: nil) ⇒ Object



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/textus/store/entry/writer.rb', line 38

def put(key, mentry:, payload:, if_etag: nil)
  path  = resolve_path(key)
  meta  = payload.meta || {}
  content = payload.content

  existing_env   = read_existing(key)
  existing_meta  = existing_env ? existing_env.meta : {}
  meta, content  = inject_meta(meta, content, existing_meta, mentry.format)

  bytes, eff_meta, eff_body, eff_content = serialize_entry(mentry, path, meta, payload, content)

  enforce_name_match!(path, eff_meta, mentry.format)
  validate_schema(mentry, eff_meta, eff_content)
  validate_raw(eff_meta, eff_content, mentry.lane, mentry.format)

  etag_before = check_etag!(path, key, if_etag)
  write_bytes(path, bytes)

  envelope = build_envelope(key, mentry, path, eff_meta, eff_body, eff_content, bytes)
  audit_put(key, etag_before, envelope.etag)
  envelope
end