Class: Foundries::Blueprint
- Inherits:
-
Object
- Object
- Foundries::Blueprint
- Includes:
- FactoryBot::Syntax::Methods
- Defined in:
- lib/foundries/blueprint.rb
Overview
Blueprint is the base class for individual factory wrappers within a Foundry.
Each Blueprint wraps one or more factory_bot factories and knows how to:
- Create records using factory_bot
- Track created records in a collection
- Navigate parent-child relationships
- Find existing records before creating duplicates
Subclass Blueprint and use the class-level DSL to declare behavior:
class UserBlueprint < Foundries::Blueprint
handles :user, :admin
factory :user
collection :users
parent :none
permitted_attrs %i[name email]
end
Class Attribute Summary collapse
-
.collection_name ⇒ Object
readonly
Returns the value of attribute collection_name.
Instance Attribute Summary collapse
-
#foundry ⇒ Object
readonly
Returns the value of attribute foundry.
Class Method Summary collapse
-
.ancestor(type = nil) ⇒ Object
Declare the ancestor type for path-based hierarchy creation via
ancestors_for. -
.collection(method_name = nil) ⇒ Object
Declare the collection name for tracking created records.
-
.factory(name = nil) ⇒ Object
Declare which factory_bot factory this blueprint uses.
- .factory_name ⇒ Object
- .handled_methods ⇒ Object
- .handles(*methods) ⇒ Object
-
.load_state_from(object, foundry) ⇒ Object
Load state from an existing object back into a foundry.
-
.lookup_order(ancestors = nil) ⇒ Object
Declare ancestor traversal order for ascending_find.
-
.nested_attrs(hash) ⇒ Object
Declare nested attributes (for accepts_nested_attributes_for).
- .new ⇒ Object
-
.parent(method_name = nil) ⇒ Object
Declare how to find the parent record from
currentstate. -
.parent_key(key = nil) ⇒ Object
Declare the foreign key used to link to the parent.
-
.permitted_attrs(attr_list) ⇒ Object
Declare which attributes are allowed through to factory_bot.
Instance Method Summary collapse
- #_wrap_for_similarity_recording!(recorder) ⇒ Object
-
#ancestors(path, &block) ⇒ Object
Walk a path array to build ancestor hierarchy, then yield the block in the innermost context.
-
#ascending_find(name) ⇒ Object
Walk ancestor types declared in lookup_order, checking collection_name on each ancestor found in current state.
- #assume_trait?(val) ⇒ Boolean
- #current_accessor ⇒ Object
-
#find(name, col_name: "name") ⇒ Object
Find a record in the collection by name, falling back to the database.
-
#find_by(criteria = {}) ⇒ Object
Find a record in the collection by arbitrary criteria, falling back to the database.
-
#find_from_parent(name, col_name: "name") ⇒ Object
Find from the parent's association, falling back to collection find.
-
#find_or_create(name, attrs = {}) ⇒ Object
Find or create: when no parent is present, walks ancestors via lookup_order; otherwise finds from parent or creates.
- #find_or_create_object ⇒ Object
-
#initialize(foundry) ⇒ Blueprint
constructor
A new instance of Blueprint.
- #inspect ⇒ Object
-
#method_missing(name, *args, **kwargs, &block) ⇒ Object
Delegate unknown methods to the foundry so that all blueprint methods are available in nested blocks.
- #mode ⇒ Object
- #parent ⇒ Object
- #parent_id ⇒ Object
- #parent_key ⇒ Object
-
#parent_present? ⇒ Boolean
Whether a parent is available in the current context.
- #reset_attrs ⇒ Object
- #reset_attrs_and_type ⇒ Object
- #respond_to_missing?(name, include_private = false) ⇒ Boolean
- #same_parent?(object) ⇒ Boolean
-
#update_state_for_block(object, &block) ⇒ Object
Saves current state, yields, then restores state.
Constructor Details
#initialize(foundry) ⇒ Blueprint
Returns a new instance of Blueprint.
179 180 181 182 |
# File 'lib/foundries/blueprint.rb', line 179 def initialize(foundry) @foundry = foundry @attrs = {} end |
Dynamic Method Handling
This class handles dynamic methods through the method_missing method
#method_missing(name, *args, **kwargs, &block) ⇒ Object
Delegate unknown methods to the foundry so that all blueprint methods
are available in nested blocks. Also supports dynamic find_
349 350 351 352 353 354 355 356 357 358 359 360 361 |
# File 'lib/foundries/blueprint.rb', line 349 def method_missing(name, *args, **kwargs, &block) if (match = missing_find_by_request?(name)) klass_name = match.named_captures["klass"] attrs = kwargs.any? ? kwargs : args.first return collection_find_by(klass_name, attrs) end if foundry.respond_to?(name) return foundry.send(name, *args, **kwargs, &block) end super end |
Class Attribute Details
.collection_name ⇒ Object (readonly)
Returns the value of attribute collection_name.
67 68 69 |
# File 'lib/foundries/blueprint.rb', line 67 def collection_name @collection_name end |
Instance Attribute Details
#foundry ⇒ Object (readonly)
Returns the value of attribute foundry.
25 26 27 |
# File 'lib/foundries/blueprint.rb', line 25 def foundry @foundry end |
Class Method Details
.ancestor(type = nil) ⇒ Object
Declare the ancestor type for path-based hierarchy
creation via ancestors_for.
ancestor :event
Generates an ancestors(path, &block) method that
pops the last path segment and either:
- calls
foundry.send(type, name, &block)if the path is empty (terminal) - calls
foundry.ancestors_for(type, ...)to continue recursion otherwise
130 131 132 133 134 |
# File 'lib/foundries/blueprint.rb', line 130 def ancestor(type = nil) return @ancestor_type unless type @ancestor_type = type end |
.collection(method_name = nil) ⇒ Object
Declare the collection name for tracking created records.
This also defines #collection and #record_class instance methods.
53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/foundries/blueprint.rb', line 53 def collection(method_name = nil) return @collection_name unless method_name @collection_name = method_name define_method(:collection) do foundry.send(:"#{method_name}_collection") end define_method(:record_class) do method_name.to_s.classify.constantize end end |
.factory(name = nil) ⇒ Object
Declare which factory_bot factory this blueprint uses. If not set, inferred from the class name.
39 40 41 42 43 44 45 |
# File 'lib/foundries/blueprint.rb', line 39 def factory(name = nil) if name @factory_name = name else @factory_name || inferred_factory_name end end |
.factory_name ⇒ Object
47 48 49 |
# File 'lib/foundries/blueprint.rb', line 47 def factory_name factory end |
.handled_methods ⇒ Object
33 34 35 |
# File 'lib/foundries/blueprint.rb', line 33 def handled_methods @handled_methods || [] end |
.handles(*methods) ⇒ Object
28 29 30 31 |
# File 'lib/foundries/blueprint.rb', line 28 def handles(*methods) @handled_methods ||= [] @handled_methods.concat(methods) end |
.load_state_from(object, foundry) ⇒ Object
Load state from an existing object back into a foundry.
157 158 159 160 161 162 |
# File 'lib/foundries/blueprint.rb', line 157 def load_state_from(object, foundry) return unless respond_to?(:parent_accessor) parent_object = object.send(parent_accessor) foundry.load_existing_objects(parent_object) end |
.lookup_order(ancestors = nil) ⇒ Object
Declare ancestor traversal order for ascending_find.
lookup_order %i[evented_mod phase cohort]
When no parent is present, ascending_find walks these
ancestor types on current, checking collection_name
on each.
113 114 115 116 117 |
# File 'lib/foundries/blueprint.rb', line 113 def lookup_order(ancestors = nil) return @lookup_order || [] unless ancestors @lookup_order = ancestors end |
.nested_attrs(hash) ⇒ Object
Declare nested attributes (for accepts_nested_attributes_for).
146 147 148 149 150 151 152 153 154 |
# File 'lib/foundries/blueprint.rb', line 146 def nested_attrs(hash) nested_object_name, attr_names = hash.shift define_method(:nested_attrs) do |attrs| attrs_to_nest = attrs.slice(*attr_names) key = :"#{nested_object_name}_attributes" {key => attrs_to_nest} end end |
.new ⇒ Object
171 172 173 174 175 176 177 |
# File 'lib/foundries/blueprint.rb', line 171 def self.new(...) instance = super foundry = instance.foundry recorder = foundry&.instance_variable_get(:@_similarity_recorder) instance._wrap_for_similarity_recording!(recorder) if recorder instance end |
.parent(method_name = nil) ⇒ Object
Declare how to find the parent record from current state.
parent :none - no parent relationship
parent :self - self-referential (e.g. nested categories)
parent :competency - reads current.competency
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/foundries/blueprint.rb', line 83 def parent(method_name = nil) return @parent_method unless method_name @parent_method = method_name if method_name == :none define_method(:same_parent?) { |_| true } elsif method_name == :self define_method(:same_parent?) { |_| true } define_method(:parent) do current.send(current_accessor) end else define_method(:parent) do current.send(method_name) end define_singleton_method(:parent_accessor) do method_name end end end |
.parent_key(key = nil) ⇒ Object
Declare the foreign key used to link to the parent.
70 71 72 73 74 75 |
# File 'lib/foundries/blueprint.rb', line 70 def parent_key(key = nil) return @parent_key_name unless key @parent_key_name = key define_method(:parent_key) { key } end |
.permitted_attrs(attr_list) ⇒ Object
Declare which attributes are allowed through to factory_bot.
137 138 139 140 141 142 143 |
# File 'lib/foundries/blueprint.rb', line 137 def permitted_attrs(attr_list) define_method(:permitted_attrs) do |attrs| keys = attr_list.dup keys << parent_key if parent_key attrs.slice(*keys) end end |
Instance Method Details
#_wrap_for_similarity_recording!(recorder) ⇒ Object
184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/foundries/blueprint.rb', line 184 def _wrap_for_similarity_recording!(recorder) methods_to_wrap = self.class.public_instance_methods(false).select do |m| self.class.instance_method(m).parameters.any? { |type, _| type == :block } end methods_to_wrap.each do |method_name| original = method(method_name) define_singleton_method(method_name) do |*args, **kwargs, &block| recorder.record(method_name.to_s, has_block: !block.nil?) do original.call(*args, **kwargs, &block) end end end end |
#ancestors(path, &block) ⇒ Object
Walk a path array to build ancestor hierarchy, then
yield the block in the innermost context. Requires
the ancestor class DSL to be declared.
ancestors(["org", "block", "template"], &block)
234 235 236 237 238 239 240 241 242 243 244 245 246 |
# File 'lib/foundries/blueprint.rb', line 234 def ancestors(path, &block) type = self.class.ancestor raise "No ancestor declared for #{self.class}" unless type name = path.pop if path.empty? foundry.send(type, name, &block) else foundry.ancestors_for(type, path_arr: path) do foundry.send(type, name, &block) end end end |
#ascending_find(name) ⇒ Object
Walk ancestor types declared in lookup_order, checking collection_name on each ancestor found in current state. Falls back to collection find.
259 260 261 262 263 264 265 266 267 268 269 270 271 |
# File 'lib/foundries/blueprint.rb', line 259 def ascending_find(name) object = nil self.class.lookup_order.each do |ancestor_type| ancestor = current.send(ancestor_type) next unless ancestor col = self.class.collection_name object = ancestor.send(col).find_by(name:) break if object end object || find(name) end |
#assume_trait?(val) ⇒ Boolean
202 203 204 |
# File 'lib/foundries/blueprint.rb', line 202 def assume_trait?(val) val.is_a?(Symbol) || val.is_a?(Array) end |
#current_accessor ⇒ Object
326 327 328 |
# File 'lib/foundries/blueprint.rb', line 326 def current_accessor self.class.name.demodulize.underscore.delete_suffix("_blueprint") end |
#find(name, col_name: "name") ⇒ Object
Find a record in the collection by name, falling back to the database.
290 291 292 293 294 295 296 297 298 299 300 301 302 303 |
# File 'lib/foundries/blueprint.rb', line 290 def find(name, col_name: "name") raise "#find called with nil :name, for col_name: #{col_name}." unless name found_record = collection.detect do |object| object.send(col_name).casecmp?(name) && same_parent?(object) end return found_record if found_record scope = record_class.where(col_name => name) if parent_key && parent_id scope = scope.where(parent_key => parent_id) end scope.first&.tap { |rec| collection << rec } end |
#find_by(criteria = {}) ⇒ Object
Find a record in the collection by arbitrary criteria, falling back to the database.
306 307 308 309 310 311 312 313 314 |
# File 'lib/foundries/blueprint.rb', line 306 def find_by(criteria = {}) found_record = collection.detect do |object| criteria.all? { |attr, value| object.send(attr) == value } end return found_record if found_record record_class.find_by(criteria)&.tap { |record| collection << record } end |
#find_from_parent(name, col_name: "name") ⇒ Object
Find from the parent's association, falling back to collection find.
283 284 285 286 287 |
# File 'lib/foundries/blueprint.rb', line 283 def find_from_parent(name, col_name: "name") col = self.class.collection_name parent.send(col).find_by(col_name => name) || find(name, col_name:) end |
#find_or_create(name, attrs = {}) ⇒ Object
Find or create: when no parent is present, walks ancestors via lookup_order; otherwise finds from parent or creates.
250 251 252 253 254 |
# File 'lib/foundries/blueprint.rb', line 250 def find_or_create(name, attrs = {}) return ascending_find(name) unless parent_present? find_from_parent(name) || create_object(name, attrs) end |
#find_or_create_object ⇒ Object
330 331 332 |
# File 'lib/foundries/blueprint.rb', line 330 def find_or_create_object send(:"#{mode}_object") end |
#inspect ⇒ Object
206 207 208 |
# File 'lib/foundries/blueprint.rb', line 206 def inspect self.class.name end |
#mode ⇒ Object
334 335 336 |
# File 'lib/foundries/blueprint.rb', line 334 def mode current.resource.nil? ? :find : :create end |
#parent ⇒ Object
214 215 216 |
# File 'lib/foundries/blueprint.rb', line 214 def parent nil end |
#parent_id ⇒ Object
322 323 324 |
# File 'lib/foundries/blueprint.rb', line 322 def parent_id parent&.id end |
#parent_key ⇒ Object
210 211 212 |
# File 'lib/foundries/blueprint.rb', line 210 def parent_key nil end |
#parent_present? ⇒ Boolean
Whether a parent is available in the current context.
274 275 276 277 278 279 |
# File 'lib/foundries/blueprint.rb', line 274 def parent_present? parent_method = self.class.parent return true if parent_method.in?(%i[self none]) parent end |
#reset_attrs ⇒ Object
343 344 345 |
# File 'lib/foundries/blueprint.rb', line 343 def reset_attrs @attrs = {} end |
#reset_attrs_and_type ⇒ Object
338 339 340 341 |
# File 'lib/foundries/blueprint.rb', line 338 def reset_attrs_and_type @type = nil reset_attrs end |
#respond_to_missing?(name, include_private = false) ⇒ Boolean
363 364 365 |
# File 'lib/foundries/blueprint.rb', line 363 def respond_to_missing?(name, include_private = false) missing_find_by_request?(name) || foundry.respond_to?(name) || super end |
#same_parent?(object) ⇒ Boolean
316 317 318 319 320 |
# File 'lib/foundries/blueprint.rb', line 316 def same_parent?(object) return true unless parent object.send(parent_key) == parent_id end |
#update_state_for_block(object, &block) ⇒ Object
Saves current state, yields, then restores state. Use this when entering a nested block to scope context.
220 221 222 223 224 225 226 |
# File 'lib/foundries/blueprint.rb', line 220 def update_state_for_block(object, &block) execute_and_restore_state do update_current(object) current.resource = object instance_exec(&block) end end |