Class: Foundries::Base

Inherits:
Object
  • Object
show all
Includes:
FactoryBot::Syntax::Methods
Defined in:
lib/foundries/base.rb

Overview

Base is the orchestrator that composes multiple Blueprints into a single declarative builder for trees of related records.

Subclass Base and declare which blueprints it uses:

class MyFoundry < Foundries::Base
blueprint UserBlueprint
blueprint ProjectBlueprint

# Optional: additional collections beyond what blueprints declare
collection :tags
end

MyFoundry.new do
user "Alice" do
  project "Widget" do
    # ...
  end
end
end

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(&block) ⇒ Base

Returns a new instance of Base.



143
144
145
146
147
148
149
150
151
152
# File 'lib/foundries/base.rb', line 143

def initialize(&block)
  @_similarity_recorder = self.class._active_similarity_recorder
  initialize_collections
  instantiate_blueprints
  @current = OpenStruct.new(resource: self)
  setup
  instance_exec(&block) if block
  teardown
  @current.resource = nil
end

Class Attribute Details

._active_similarity_recorderObject

Returns the value of attribute _active_similarity_recorder.



31
32
33
# File 'lib/foundries/base.rb', line 31

def _active_similarity_recorder
  @_active_similarity_recorder
end

Instance Attribute Details

#currentObject

Returns the value of attribute current.



154
155
156
# File 'lib/foundries/base.rb', line 154

def current
  @current
end

Class Method Details

.aliases(mapping = nil) ⇒ Object

Declare shorthand aliases for blueprint methods.

aliases member: :enrollment, hva_mod: :skilled_mod

Resolves aliases during method_missing before blueprint delegation.



68
69
70
71
72
# File 'lib/foundries/base.rb', line 68

def aliases(mapping = nil)
  return @aliases || {} unless mapping

  @aliases = (@aliases || {}).merge(mapping)
end

.blueprint(klass) ⇒ Object

Register a blueprint class with this foundry.



34
35
36
# File 'lib/foundries/base.rb', line 34

def blueprint(klass)
  blueprint_registry[klass] = klass.handled_methods
end

.blueprint_collection_namesObject

Collection names derived from blueprint declarations.



58
59
60
# File 'lib/foundries/base.rb', line 58

def blueprint_collection_names
  blueprint_registry.keys.filter_map { |klass| klass.collection_name&.to_s }
end

.blueprint_registryObject

All registered blueprint classes and their handled methods.



39
40
41
# File 'lib/foundries/base.rb', line 39

def blueprint_registry
  @blueprint_registry ||= {}
end

.collection(*names) ⇒ Object

Declare additional collection names beyond those from blueprints.



44
45
46
# File 'lib/foundries/base.rb', line 44

def collection(*names)
  extra_collections.concat(names.map(&:to_s))
end

.collection_accessorsObject

All collection accessor names (e.g. "users_collection").



53
54
55
# File 'lib/foundries/base.rb', line 53

def collection_accessors
  (blueprint_collection_names + extra_collections).map { |name| "#{name}_collection" }
end

.delegationsObject

Methods delegated from this foundry to its blueprint instances.



75
76
77
# File 'lib/foundries/base.rb', line 75

def delegations
  blueprint_registry.select { |_, methods| methods.any? }
end

.extra_collectionsObject



48
49
50
# File 'lib/foundries/base.rb', line 48

def extra_collections
  @extra_collections ||= []
end

.inherited(subclass) ⇒ Object



134
135
136
137
138
139
140
# File 'lib/foundries/base.rb', line 134

def inherited(subclass)
  super
  # Copy parent registries so subclasses inherit blueprints
  subclass.instance_variable_set(:@blueprint_registry, blueprint_registry.dup)
  subclass.instance_variable_set(:@extra_collections, extra_collections.dup)
  subclass.instance_variable_set(:@aliases, aliases.dup)
end

.preset(name, &block) ⇒ Object

Define presets — named class methods that build a preconfigured foundry.

class MyFoundry < Foundries::Base
preset :full_team do
  user "Alice"
  user "Bob"
end
end

MyFoundry.full_team  # => configured foundry instance


90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/foundries/base.rb', line 90

def preset(name, &block)
  define_singleton_method(name) do
    if defined?(Foundries::Snapshot) && Foundries::Snapshot.enabled?
      store = Foundries::Snapshot::Store.new(name)

      if store.cached?
        store.restore
        next new # hollow — no block, data already in DB
      end

      store.record_empty_tables
      foundry = with_similarity_recording(name, Similarity.enabled?) do
        new(&block)
      end
      store.capture
      foundry
    else
      with_similarity_recording(name, Similarity.enabled?) do
        new(&block)
      end
    end
  end
end

.with_similarity_recording(preset_name, recording) ⇒ Object



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/foundries/base.rb', line 114

def with_similarity_recording(preset_name, recording)
  if recording
    self._active_similarity_recorder = Similarity::Recorder.new
    foundry = yield
    tree = _active_similarity_recorder.normalized_tree
    self._active_similarity_recorder = nil

    key = "#{name}.#{preset_name}"
    warnings = Similarity::Comparator.compare(key, tree, Similarity.registry)
    warnings.each do |w|
      next unless Similarity.warned_pairs.add?(w[:pair])
      warn w[:message]
    end
    Similarity.registry[key] = tree
    foundry
  else
    yield
  end
end

Instance Method Details

#ancestors_for(type, path = nil, path_arr: nil, &block) ⇒ Object

Build a hierarchy from a path string or array. Finds the blueprint that handles type and calls ancestors on it to recursively create the chain.

ancestors_for :task, "org/block/template/phase/mod/event" do
task "my_task"
end


211
212
213
214
# File 'lib/foundries/base.rb', line 211

def ancestors_for(type, path = nil, path_arr: nil, &block)
  path_arr ||= path.split("/")
  blueprint_for(type).ancestors(path_arr, &block)
end

#blueprint_for(method_name) ⇒ Object

Find the blueprint instance that handles a given method.



217
218
219
220
221
222
223
224
225
226
# File 'lib/foundries/base.rb', line 217

def blueprint_for(method_name)
  resolved = self.class.aliases[method_name] || method_name
  self.class.delegations.each do |klass, methods|
    if methods.include?(resolved)
      ivar = :"@#{ivar_name_for(klass)}"
      return instance_variable_get(ivar)
    end
  end
  raise "No blueprint handles :#{method_name}"
end

#execute_and_restore_stateObject



188
189
190
191
# File 'lib/foundries/base.rb', line 188

def execute_and_restore_state
  initial_state = @current.dup
  yield.tap { @current = initial_state }
end

#from(objects, &block) ⇒ Object

Build within the context of existing objects.



166
167
168
169
170
171
172
# File 'lib/foundries/base.rb', line 166

def from(objects, &block)
  execute_and_restore_state do
    load_existing_objects(objects)
    instance_exec(&block) if block
    teardown
  end
end

#load_existing_objects(objects) ⇒ Object



174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/foundries/base.rb', line 174

def load_existing_objects(objects)
  return if objects.nil? || (objects.respond_to?(:empty?) && objects.empty?)

  Array(objects).each do |object|
    load_state(object)

    klass_name = object.class.name
    blueprint_class = find_blueprint_class_for(klass_name)
    next unless blueprint_class

    blueprint_class.load_state_from(object, self)
  end
end

#load_state(object) ⇒ Object Also known as: update_current



193
194
195
196
197
198
199
200
# File 'lib/foundries/base.rb', line 193

def load_state(object)
  klass_name = object.class.name.underscore.tr("/", "_")
  current.send(:"#{klass_name}=", object)
  collection_name = "#{klass_name.pluralize}_collection"
  return unless respond_to?(collection_name)

  send(collection_name) << object
end

#reopen(&block) ⇒ Object

Reopen the foundry to add more records.



157
158
159
160
161
162
163
# File 'lib/foundries/base.rb', line 157

def reopen(&block)
  @current = OpenStruct.new(resource: self)
  instance_exec(&block) if block
  teardown
  @current.resource = nil
  self
end