foundries 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -11
- data/lib/foundries/base.rb +49 -4
- data/lib/foundries/blueprint.rb +100 -5
- data/lib/foundries/version.rb +2 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c8245b09fe2227ef4a25d5751c30f4a94a1d2d80519ef5fb97b8ba2b9dc527ef
|
|
4
|
+
data.tar.gz: 24162154115e9f413117fb88d71aa0cbc19374cbbf9b6fa059f23e36535511e5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f0b22d20d6edc795ef1c7a2f3403b1a7e5cbbbc0d3ca4bd19fc82c25992bcea6d460175df202d8cd0465dd328a8bc189672171423ed283aa7879390450261318
|
|
7
|
+
data.tar.gz: 1ece80e18cdc964013db346ec665e2919f146bda411ddaf4254abb4c714c712a4a692cb30241a051da98029c164fbbc77b1e4fc44c2dac0cadda396c0fbccadf
|
data/CHANGELOG.md
CHANGED
|
@@ -5,22 +5,22 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
|
6
6
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
|
7
7
|
|
|
8
|
-
## [0.1.
|
|
8
|
+
## [0.1.3] - 2026-03-13
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
11
|
|
|
12
|
-
-
|
|
13
|
-
- Snapshot caching system with Postgres and SQLite adapters for test suite acceleration (0002336)
|
|
14
|
-
- Source file content hashing in snapshot fingerprint for automatic cache invalidation (cfb348d)
|
|
12
|
+
- ancestor DSL and ancestors_for for path-based hierarchy building (7027e06)
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
## [0.1.2] - 2026-03-12
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
- Similarity detection only warns on identical structure, not containment (a5bdf25)
|
|
16
|
+
### Added
|
|
20
17
|
|
|
21
|
-
|
|
18
|
+
- aliases DSL on Base for shorthand method names (377632f)
|
|
19
|
+
- lookup_order DSL on Blueprint for ancestor traversal (377632f)
|
|
20
|
+
- find_or_create pattern on Blueprint (377632f)
|
|
21
|
+
- Tests for inherited registries, parent-scoped find, collection_find_by kwargs, find_or_create, ascending_find, and parent_present? (2d92945)
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
- Snapshot-cached presets no longer produce false similarity warnings (a5bdf25)
|
|
23
|
+
### Changed
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
- Collections initialize before blueprints (377632f)
|
|
26
|
+
- Fix find parent scoping and collection_find_by kwargs handling (3ebfc3e)
|
data/lib/foundries/base.rb
CHANGED
|
@@ -59,6 +59,18 @@ module Foundries
|
|
|
59
59
|
blueprint_registry.keys.filter_map { |klass| klass.collection_name&.to_s }
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
+
# Declare shorthand aliases for blueprint methods.
|
|
63
|
+
#
|
|
64
|
+
# aliases member: :enrollment, hva_mod: :skilled_mod
|
|
65
|
+
#
|
|
66
|
+
# Resolves aliases during method_missing before blueprint
|
|
67
|
+
# delegation.
|
|
68
|
+
def aliases(mapping = nil)
|
|
69
|
+
return @aliases || {} unless mapping
|
|
70
|
+
|
|
71
|
+
@aliases = (@aliases || {}).merge(mapping)
|
|
72
|
+
end
|
|
73
|
+
|
|
62
74
|
# Methods delegated from this foundry to its blueprint instances.
|
|
63
75
|
def delegations
|
|
64
76
|
blueprint_registry.select { |_, methods| methods.any? }
|
|
@@ -121,16 +133,17 @@ module Foundries
|
|
|
121
133
|
|
|
122
134
|
def inherited(subclass)
|
|
123
135
|
super
|
|
124
|
-
#
|
|
125
|
-
subclass.instance_variable_set(:@blueprint_registry,
|
|
126
|
-
subclass.instance_variable_set(:@extra_collections,
|
|
136
|
+
# Copy parent registries so subclasses inherit blueprints
|
|
137
|
+
subclass.instance_variable_set(:@blueprint_registry, blueprint_registry.dup)
|
|
138
|
+
subclass.instance_variable_set(:@extra_collections, extra_collections.dup)
|
|
139
|
+
subclass.instance_variable_set(:@aliases, aliases.dup)
|
|
127
140
|
end
|
|
128
141
|
end
|
|
129
142
|
|
|
130
143
|
def initialize(&block)
|
|
131
144
|
@_similarity_recorder = self.class._active_similarity_recorder
|
|
132
|
-
instantiate_blueprints
|
|
133
145
|
initialize_collections
|
|
146
|
+
instantiate_blueprints
|
|
134
147
|
@current = OpenStruct.new(resource: self)
|
|
135
148
|
setup
|
|
136
149
|
instance_exec(&block) if block
|
|
@@ -187,6 +200,31 @@ module Foundries
|
|
|
187
200
|
end
|
|
188
201
|
alias_method :update_current, :load_state
|
|
189
202
|
|
|
203
|
+
# Build a hierarchy from a path string or array.
|
|
204
|
+
# Finds the blueprint that handles `type` and calls
|
|
205
|
+
# `ancestors` on it to recursively create the chain.
|
|
206
|
+
#
|
|
207
|
+
# ancestors_for :task, "org/block/template/phase/mod/event" do
|
|
208
|
+
# task "my_task"
|
|
209
|
+
# end
|
|
210
|
+
#
|
|
211
|
+
def ancestors_for(type, path = nil, path_arr: nil, &block)
|
|
212
|
+
path_arr ||= path.split("/")
|
|
213
|
+
blueprint_for(type).ancestors(path_arr, &block)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Find the blueprint instance that handles a given method.
|
|
217
|
+
def blueprint_for(method_name)
|
|
218
|
+
resolved = self.class.aliases[method_name] || method_name
|
|
219
|
+
self.class.delegations.each do |klass, methods|
|
|
220
|
+
if methods.include?(resolved)
|
|
221
|
+
ivar = :"@#{ivar_name_for(klass)}"
|
|
222
|
+
return instance_variable_get(ivar)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
raise "No blueprint handles :#{method_name}"
|
|
226
|
+
end
|
|
227
|
+
|
|
190
228
|
private
|
|
191
229
|
|
|
192
230
|
# Override in subclasses for post-initialize hooks (e.g. pending phase rules).
|
|
@@ -213,6 +251,13 @@ module Foundries
|
|
|
213
251
|
end
|
|
214
252
|
end
|
|
215
253
|
end
|
|
254
|
+
|
|
255
|
+
# Set up alias methods that delegate to existing blueprint methods
|
|
256
|
+
self.class.aliases.each do |alias_name, target_name|
|
|
257
|
+
define_singleton_method(alias_name) do |*args, **kwargs, &block|
|
|
258
|
+
send(target_name, *args, **kwargs, &block)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
216
261
|
end
|
|
217
262
|
|
|
218
263
|
def initialize_collections
|
data/lib/foundries/blueprint.rb
CHANGED
|
@@ -103,6 +103,36 @@ module Foundries
|
|
|
103
103
|
end
|
|
104
104
|
end
|
|
105
105
|
|
|
106
|
+
# Declare ancestor traversal order for ascending_find.
|
|
107
|
+
#
|
|
108
|
+
# lookup_order %i[evented_mod phase cohort]
|
|
109
|
+
#
|
|
110
|
+
# When no parent is present, ascending_find walks these
|
|
111
|
+
# ancestor types on `current`, checking collection_name
|
|
112
|
+
# on each.
|
|
113
|
+
def lookup_order(ancestors = nil)
|
|
114
|
+
return @lookup_order || [] unless ancestors
|
|
115
|
+
|
|
116
|
+
@lookup_order = ancestors
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Declare the ancestor type for path-based hierarchy
|
|
120
|
+
# creation via `ancestors_for`.
|
|
121
|
+
#
|
|
122
|
+
# ancestor :event
|
|
123
|
+
#
|
|
124
|
+
# Generates an `ancestors(path, &block)` method that
|
|
125
|
+
# pops the last path segment and either:
|
|
126
|
+
# - calls `foundry.send(type, name, &block)` if the
|
|
127
|
+
# path is empty (terminal)
|
|
128
|
+
# - calls `foundry.ancestors_for(type, ...)` to
|
|
129
|
+
# continue recursion otherwise
|
|
130
|
+
def ancestor(type = nil)
|
|
131
|
+
return @ancestor_type unless type
|
|
132
|
+
|
|
133
|
+
@ancestor_type = type
|
|
134
|
+
end
|
|
135
|
+
|
|
106
136
|
# Declare which attributes are allowed through to factory_bot.
|
|
107
137
|
def permitted_attrs(attr_list)
|
|
108
138
|
define_method(:permitted_attrs) do |attrs|
|
|
@@ -195,6 +225,67 @@ module Foundries
|
|
|
195
225
|
end
|
|
196
226
|
end
|
|
197
227
|
|
|
228
|
+
# Walk a path array to build ancestor hierarchy, then
|
|
229
|
+
# yield the block in the innermost context. Requires
|
|
230
|
+
# the `ancestor` class DSL to be declared.
|
|
231
|
+
#
|
|
232
|
+
# ancestors(["org", "block", "template"], &block)
|
|
233
|
+
#
|
|
234
|
+
def ancestors(path, &block)
|
|
235
|
+
type = self.class.ancestor
|
|
236
|
+
raise "No ancestor declared for #{self.class}" unless type
|
|
237
|
+
|
|
238
|
+
name = path.pop
|
|
239
|
+
if path.empty?
|
|
240
|
+
foundry.send(type, name, &block)
|
|
241
|
+
else
|
|
242
|
+
foundry.ancestors_for(type, path_arr: path) do
|
|
243
|
+
foundry.send(type, name, &block)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Find or create: when no parent is present, walks ancestors
|
|
249
|
+
# via lookup_order; otherwise finds from parent or creates.
|
|
250
|
+
def find_or_create(name, attrs = {})
|
|
251
|
+
return ascending_find(name) unless parent_present?
|
|
252
|
+
|
|
253
|
+
find_from_parent(name) || create_object(name, attrs)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Walk ancestor types declared in lookup_order, checking
|
|
257
|
+
# collection_name on each ancestor found in current state.
|
|
258
|
+
# Falls back to collection find.
|
|
259
|
+
def ascending_find(name)
|
|
260
|
+
object = nil
|
|
261
|
+
self.class.lookup_order.each do |ancestor_type|
|
|
262
|
+
ancestor = current.send(ancestor_type)
|
|
263
|
+
next unless ancestor
|
|
264
|
+
|
|
265
|
+
col = self.class.collection_name
|
|
266
|
+
object = ancestor.send(col).find_by(name:)
|
|
267
|
+
break if object
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
object || find(name)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Whether a parent is available in the current context.
|
|
274
|
+
def parent_present?
|
|
275
|
+
parent_method = self.class.parent
|
|
276
|
+
return true if parent_method.in?(%i[self none])
|
|
277
|
+
|
|
278
|
+
parent
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Find from the parent's association, falling back to
|
|
282
|
+
# collection find.
|
|
283
|
+
def find_from_parent(name, col_name: "name")
|
|
284
|
+
col = self.class.collection_name
|
|
285
|
+
parent.send(col).find_by(col_name => name) ||
|
|
286
|
+
find(name, col_name:)
|
|
287
|
+
end
|
|
288
|
+
|
|
198
289
|
# Find a record in the collection by name, falling back to the database.
|
|
199
290
|
def find(name, col_name: "name")
|
|
200
291
|
raise "#find called with nil :name, for col_name: #{col_name}." unless name
|
|
@@ -202,9 +293,13 @@ module Foundries
|
|
|
202
293
|
found_record = collection.detect do |object|
|
|
203
294
|
object.send(col_name).casecmp?(name) && same_parent?(object)
|
|
204
295
|
end
|
|
296
|
+
return found_record if found_record
|
|
205
297
|
|
|
206
|
-
|
|
207
|
-
|
|
298
|
+
scope = record_class.where(col_name => name)
|
|
299
|
+
if parent_key && parent_id
|
|
300
|
+
scope = scope.where(parent_key => parent_id)
|
|
301
|
+
end
|
|
302
|
+
scope.first&.tap { |rec| collection << rec }
|
|
208
303
|
end
|
|
209
304
|
|
|
210
305
|
# Find a record in the collection by arbitrary criteria, falling back to the database.
|
|
@@ -254,7 +349,8 @@ module Foundries
|
|
|
254
349
|
def method_missing(name, *args, **kwargs, &block)
|
|
255
350
|
if (match = missing_find_by_request?(name))
|
|
256
351
|
klass_name = match.named_captures["klass"]
|
|
257
|
-
|
|
352
|
+
attrs = kwargs.any? ? kwargs : args.first
|
|
353
|
+
return collection_find_by(klass_name, attrs)
|
|
258
354
|
end
|
|
259
355
|
|
|
260
356
|
if foundry.respond_to?(name)
|
|
@@ -274,8 +370,7 @@ module Foundries
|
|
|
274
370
|
method_name.match(/^find_(?<klass>.*)_by$/)
|
|
275
371
|
end
|
|
276
372
|
|
|
277
|
-
def collection_find_by(klass_name,
|
|
278
|
-
attrs = args.first
|
|
373
|
+
def collection_find_by(klass_name, attrs)
|
|
279
374
|
target_collection_name = "#{klass_name.pluralize}_collection"
|
|
280
375
|
objects = foundry.send(target_collection_name)
|
|
281
376
|
objects.detect do |object|
|
data/lib/foundries/version.rb
CHANGED