foundries 0.1.0 → 0.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a0ecd2813e2ccbe57b1889f98748c0590d208e83005b32fb0302004b8c31b708
4
- data.tar.gz: 82dd0fe2b8f3fee39e3eb6821fb2fd3e2c222cebd599c4d6a0cd898b3af780f6
3
+ metadata.gz: d9bda37a762c9a055ca42c5faa685e30a6201fbeccaa5bbb7adffa0ccaef3386
4
+ data.tar.gz: 4fe49113055d53bb2e61df790fa037c849cff8d7b380011e6fd298fab0ec68af
5
5
  SHA512:
6
- metadata.gz: 8585abf3336113d8ba508a67b9b83c207f7e3de3deeeb251f34cd1ba2449e9f62928e73b8e0f350205438f4284f0e07df59ac15456305fb8088d66febff6d738
7
- data.tar.gz: 02db8e30a432fddd54df80e32aa801bea689660bd252746ab3b79bc3a04e6bccf9cc75eed9c0977641ff37be25bce1237325bed770c457ca78225ec073d692bb
6
+ metadata.gz: 6e2b1f8a7fbc9107f684883ae9435c8c6cfc004698f7a5316ec47fb3531b9d100be9b12b55a4c097cefbb48e871c653717dbdf4ed5733dcbaf1032f5fed6a8e0
7
+ data.tar.gz: f25ae1307f8f675d4b6d617b13b9102639e49f29bd6f80f90145cc68023725a36eb3169207107c9af6cb1078188f8a0d104c6e591959fc1ee48d992563b9e827
data/CHANGELOG.md CHANGED
@@ -5,4 +5,34 @@ 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.0] - 2026-02-25
8
+ ## [0.1.2] - 2026-03-12
9
+
10
+ ### Added
11
+
12
+ - aliases DSL on Base for shorthand method names (377632f)
13
+ - lookup_order DSL on Blueprint for ancestor traversal (377632f)
14
+ - find_or_create pattern on Blueprint (377632f)
15
+ - Tests for inherited registries, parent-scoped find, collection_find_by kwargs, find_or_create, ascending_find, and parent_present? (2d92945)
16
+
17
+ ### Changed
18
+
19
+ - Collections initialize before blueprints (377632f)
20
+ - Fix find parent scoping and collection_find_by kwargs handling (3ebfc3e)
21
+
22
+ ## [0.1.1] - 2026-03-06
23
+
24
+ ### Added
25
+
26
+ - Blueprint base class with collection tracking, parent scoping, and factory_bot integration (0002336)
27
+ - Snapshot caching system with Postgres and SQLite adapters for test suite acceleration (0002336)
28
+ - Source file content hashing in snapshot fingerprint for automatic cache invalidation (cfb348d)
29
+
30
+ ### Changed
31
+
32
+ - Snapshot caching is opt-in via ARMATURE_CACHE=1 environment variable (0002336)
33
+ - Similarity detection only warns on identical structure, not containment (a5bdf25)
34
+
35
+ ### Fixed
36
+
37
+ - Parentless blueprints no longer raise NameError when find calls same_parent? (cfb348d)
38
+ - Snapshot-cached presets no longer produce false similarity warnings (a5bdf25)
@@ -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? }
@@ -77,20 +89,22 @@ module Foundries
77
89
  #
78
90
  def preset(name, &block)
79
91
  define_singleton_method(name) do
80
- with_similarity_recording(name, Similarity.enabled?) do
81
- if defined?(Foundries::Snapshot) && Foundries::Snapshot.enabled?
82
- store = Foundries::Snapshot::Store.new(name)
83
-
84
- if store.cached?
85
- store.restore
86
- next new # hollow — no block, data already in DB
87
- end
88
-
89
- store.record_empty_tables
90
- foundry = new(&block)
91
- store.capture
92
- foundry
93
- else
92
+ if defined?(Foundries::Snapshot) && Foundries::Snapshot.enabled?
93
+ store = Foundries::Snapshot::Store.new(name)
94
+
95
+ if store.cached?
96
+ store.restore
97
+ next new # hollow — no block, data already in DB
98
+ end
99
+
100
+ store.record_empty_tables
101
+ foundry = with_similarity_recording(name, Similarity.enabled?) do
102
+ new(&block)
103
+ end
104
+ store.capture
105
+ foundry
106
+ else
107
+ with_similarity_recording(name, Similarity.enabled?) do
94
108
  new(&block)
95
109
  end
96
110
  end
@@ -119,16 +133,17 @@ module Foundries
119
133
 
120
134
  def inherited(subclass)
121
135
  super
122
- # Ensure subclasses get their own registries
123
- subclass.instance_variable_set(:@blueprint_registry, {})
124
- 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)
125
140
  end
126
141
  end
127
142
 
128
143
  def initialize(&block)
129
144
  @_similarity_recorder = self.class._active_similarity_recorder
130
- instantiate_blueprints
131
145
  initialize_collections
146
+ instantiate_blueprints
132
147
  @current = OpenStruct.new(resource: self)
133
148
  setup
134
149
  instance_exec(&block) if block
@@ -211,6 +226,13 @@ module Foundries
211
226
  end
212
227
  end
213
228
  end
229
+
230
+ # Set up alias methods that delegate to existing blueprint methods
231
+ self.class.aliases.each do |alias_name, target_name|
232
+ define_singleton_method(alias_name) do |*args, **kwargs, &block|
233
+ send(target_name, *args, **kwargs, &block)
234
+ end
235
+ end
214
236
  end
215
237
 
216
238
  def initialize_collections
@@ -103,6 +103,19 @@ 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
+
106
119
  # Declare which attributes are allowed through to factory_bot.
107
120
  def permitted_attrs(attr_list)
108
121
  define_method(:permitted_attrs) do |attrs|
@@ -195,6 +208,47 @@ module Foundries
195
208
  end
196
209
  end
197
210
 
211
+ # Find or create: when no parent is present, walks ancestors
212
+ # via lookup_order; otherwise finds from parent or creates.
213
+ def find_or_create(name, attrs = {})
214
+ return ascending_find(name) unless parent_present?
215
+
216
+ find_from_parent(name) || create_object(name, attrs)
217
+ end
218
+
219
+ # Walk ancestor types declared in lookup_order, checking
220
+ # collection_name on each ancestor found in current state.
221
+ # Falls back to collection find.
222
+ def ascending_find(name)
223
+ object = nil
224
+ self.class.lookup_order.each do |ancestor_type|
225
+ ancestor = current.send(ancestor_type)
226
+ next unless ancestor
227
+
228
+ col = self.class.collection_name
229
+ object = ancestor.send(col).find_by(name:)
230
+ break if object
231
+ end
232
+
233
+ object || find(name)
234
+ end
235
+
236
+ # Whether a parent is available in the current context.
237
+ def parent_present?
238
+ parent_method = self.class.parent
239
+ return true if parent_method.in?(%i[self none])
240
+
241
+ parent
242
+ end
243
+
244
+ # Find from the parent's association, falling back to
245
+ # collection find.
246
+ def find_from_parent(name, col_name: "name")
247
+ col = self.class.collection_name
248
+ parent.send(col).find_by(col_name => name) ||
249
+ find(name, col_name:)
250
+ end
251
+
198
252
  # Find a record in the collection by name, falling back to the database.
199
253
  def find(name, col_name: "name")
200
254
  raise "#find called with nil :name, for col_name: #{col_name}." unless name
@@ -202,9 +256,13 @@ module Foundries
202
256
  found_record = collection.detect do |object|
203
257
  object.send(col_name).casecmp?(name) && same_parent?(object)
204
258
  end
259
+ return found_record if found_record
205
260
 
206
- found_record ||
207
- record_class.find_by(col_name => name)&.tap { |rec| collection << rec }
261
+ scope = record_class.where(col_name => name)
262
+ if parent_key && parent_id
263
+ scope = scope.where(parent_key => parent_id)
264
+ end
265
+ scope.first&.tap { |rec| collection << rec }
208
266
  end
209
267
 
210
268
  # Find a record in the collection by arbitrary criteria, falling back to the database.
@@ -254,7 +312,8 @@ module Foundries
254
312
  def method_missing(name, *args, **kwargs, &block)
255
313
  if (match = missing_find_by_request?(name))
256
314
  klass_name = match.named_captures["klass"]
257
- return collection_find_by(klass_name, args)
315
+ attrs = kwargs.any? ? kwargs : args.first
316
+ return collection_find_by(klass_name, attrs)
258
317
  end
259
318
 
260
319
  if foundry.respond_to?(name)
@@ -274,8 +333,7 @@ module Foundries
274
333
  method_name.match(/^find_(?<klass>.*)_by$/)
275
334
  end
276
335
 
277
- def collection_find_by(klass_name, args)
278
- attrs = args.first
336
+ def collection_find_by(klass_name, attrs)
279
337
  target_collection_name = "#{klass_name.pluralize}_collection"
280
338
  objects = foundry.send(target_collection_name)
281
339
  objects.detect do |object|
@@ -17,18 +17,6 @@ module Foundries
17
17
  ":#{preset_name(existing_key)} have identical structure " \
18
18
  "(#{display_tree(new_tree)})"
19
19
  }
20
- elsif existing_tree.contains?(new_tree)
21
- warnings << {
22
- pair: pair_key,
23
- message: "[Foundries] Preset :#{preset_name(new_key)} is " \
24
- "structurally contained within :#{preset_name(existing_key)}"
25
- }
26
- elsif new_tree.contains?(existing_tree)
27
- warnings << {
28
- pair: pair_key,
29
- message: "[Foundries] Preset :#{preset_name(existing_key)} is " \
30
- "structurally contained within :#{preset_name(new_key)}"
31
- }
32
20
  end
33
21
  end
34
22
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Foundries
4
- VERSION = "0.1.0"
5
- RELEASE_DATE = "2026-02-25"
4
+ VERSION = "0.1.2"
5
+ RELEASE_DATE = "2026-03-12"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foundries
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Dowd
@@ -87,7 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
87
87
  - !ruby/object:Gem::Version
88
88
  version: '0'
89
89
  requirements: []
90
- rubygems_version: 3.7.2
90
+ rubygems_version: 4.0.3
91
91
  specification_version: 4
92
92
  summary: Declarative trees of related data using factory_bot
93
93
  test_files: []