Class: Textus::Produce::Acquire::Projection

Inherits:
Object
  • Object
show all
Defined in:
lib/textus/produce/acquire/projection.rb

Overview

Builds an entry’s DATA artifact (ADR 0094) by running the projection pipeline; rendering is a publish concern. External entries are NOT built here — they are generated by an out-of-band runner; Derived#publish_via filters them out before reaching this point.

Merges the former Write::DataBuilder wrapper and Builder::Pipeline module into one class (ADR 0100 produce/ topology refactor).

Defined Under Namespace

Modules: InjectMeta Classes: Deps

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(container:, call:) ⇒ Projection

Returns a new instance of Projection.



47
48
49
50
51
52
53
54
# File 'lib/textus/produce/acquire/projection.rb', line 47

def initialize(container:, call:)
  @container  = container
  @call       = call
  @manifest   = container.manifest
  @file_store = container.file_store
  @rpc        = container.rpc
  @root       = container.root
end

Class Method Details

.pipeline_run(mentry:, deps:) ⇒ Object



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
111
112
113
114
# File 'lib/textus/produce/acquire/projection.rb', line 81

def self.pipeline_run(mentry:, deps:)
  # 1. Load sources + project + reduce. Only projection-derived entries are
  # buildable in-process; External entries are generated out-of-band and are
  # filtered out upstream (Derived#publish_via), so reaching here with a
  # non-projection source is a wiring bug — fail loudly rather than emit an
  # empty payload (and never re-stamp the volatile generated_at, ADR 0070).
  unless mentry.projection?
    raise UsageError.new(
      "builder: '#{mentry.key}' is not a projection-derived entry; only projections are buildable",
    )
  end

  data =
    Textus::Projection.new(
      reader: deps.reader,
      spec: mentry.source.projection_spec,
      lister: deps.lister,
      rpc: deps.rpc,
      transform_context: deps.transform_context,
    ).run

  # 2. Serialize as DATA. Rendering through a template is a publish concern
  # (ADR 0094) — the build never consults a template.
  klass = renderers[mentry.format] or
    raise UsageError.new("builder: unsupported data format #{mentry.format.inspect} for '#{mentry.key}'")
  bytes = klass.new.call(mentry: mentry, data: data)

  # 3. Write (idempotent: skip if only generated_at would differ)
  target_path = Key::Path.resolve(deps.manifest.data, mentry)
  FileUtils.mkdir_p(File.dirname(target_path))
  write_if_changed(target_path, bytes, mentry.format)

  target_path
end

.renderersObject



39
40
41
42
43
44
45
# File 'lib/textus/produce/acquire/projection.rb', line 39

def self.renderers
  @renderers ||= {
    "text" => Produce::Acquire::Serializer::Text,
    "json" => Produce::Acquire::Serializer::Json,
    "yaml" => Produce::Acquire::Serializer::Yaml,
  }
end

.write_if_changed(target_path, bytes, _format) ⇒ Object

Built artifacts are content-addressed (no volatile timestamp, ADR 0070), so identity is plain byte-equality: skip the write when nothing changed. ‘format` is retained for signature stability across renderers.



119
120
121
122
123
# File 'lib/textus/produce/acquire/projection.rb', line 119

def self.write_if_changed(target_path, bytes, _format)
  return if File.exist?(target_path) && File.binread(target_path) == bytes

  File.binwrite(target_path, bytes)
end

Instance Method Details

#run(mentry) ⇒ Object

Runs the projection pipeline for ‘mentry` and returns the on-disk target_path string.



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/textus/produce/acquire/projection.rb', line 58

def run(mentry)
  reader   = Textus::Read::Get.new(container: @container, call: @call)
  # Projections must be able to read source data from any nested entry,
  # including keyless (publish_tree) ones like knowledge.decisions.
  # The `include_keyless: true` option makes the resolver walk those dirs
  # without exposing them on the public `list` / CLI surface (ADR 0047).
  resolver = @manifest.resolver
  lister = lambda do |prefix:|
    resolver.enumerate(prefix: prefix, include_keyless: true)
            .map { |row| { "key" => row[:key], "zone" => row[:manifest_entry].zone, "path" => row[:path] } }
  end
  self.class.pipeline_run(
    mentry: mentry,
    deps: Deps.new(
      manifest: @manifest,
      reader: reader.method(:call),
      lister: lister,
      rpc: @rpc,
      transform_context: @container,
    ),
  )
end