foundries 0.1.0
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 +7 -0
- data/CHANGELOG.md +8 -0
- data/LICENSE +21 -0
- data/README.md +249 -0
- data/Rakefile +17 -0
- data/lib/foundries/base.rb +235 -0
- data/lib/foundries/blueprint.rb +286 -0
- data/lib/foundries/similarity/comparator.rb +49 -0
- data/lib/foundries/similarity/recorder.rb +36 -0
- data/lib/foundries/similarity/structure_tree.rb +74 -0
- data/lib/foundries/similarity.rb +32 -0
- data/lib/foundries/snapshot/adapter.rb +18 -0
- data/lib/foundries/snapshot/adapters/postgres_adapter.rb +68 -0
- data/lib/foundries/snapshot/adapters/sqlite_adapter.rb +53 -0
- data/lib/foundries/snapshot/fingerprint.rb +29 -0
- data/lib/foundries/snapshot/store.rb +72 -0
- data/lib/foundries/snapshot.rb +42 -0
- data/lib/foundries/version.rb +6 -0
- data/lib/foundries.rb +12 -0
- metadata +93 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a0ecd2813e2ccbe57b1889f98748c0590d208e83005b32fb0302004b8c31b708
|
|
4
|
+
data.tar.gz: 82dd0fe2b8f3fee39e3eb6821fb2fd3e2c222cebd599c4d6a0cd898b3af780f6
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8585abf3336113d8ba508a67b9b83c207f7e3de3deeeb251f34cd1ba2449e9f62928e73b8e0f350205438f4284f0e07df59ac15456305fb8088d66febff6d738
|
|
7
|
+
data.tar.gz: 02db8e30a432fddd54df80e32aa801bea689660bd252746ab3b79bc3a04e6bccf9cc75eed9c0977641ff37be25bce1237325bed770c457ca78225ec073d692bb
|
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SOFware LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# Foundries
|
|
2
|
+
|
|
3
|
+
Declarative trees of related records using factory_bot.
|
|
4
|
+
|
|
5
|
+
Foundries composes factory_bot factories into **blueprints** that know how to create, find, and relate records. You register blueprints with a **base** class, then build entire object graphs with a nested DSL:
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
TestFoundry.new do
|
|
9
|
+
team "Engineering" do
|
|
10
|
+
user "Alice"
|
|
11
|
+
admin "Bob"
|
|
12
|
+
|
|
13
|
+
project "API" do
|
|
14
|
+
task "Auth", priority: "high"
|
|
15
|
+
task "Caching"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Each method call creates a record (or finds an existing one), and nesting establishes parent-child context automatically. No manual foreign key wiring.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
gem "foundries"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
### Blueprints
|
|
32
|
+
|
|
33
|
+
A blueprint wraps a single factory_bot factory and declares how it participates in the tree:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
class TeamBlueprint < Foundries::Blueprint
|
|
37
|
+
handles :team
|
|
38
|
+
factory :team
|
|
39
|
+
collection :teams
|
|
40
|
+
parent :none
|
|
41
|
+
permitted_attrs %i[name]
|
|
42
|
+
|
|
43
|
+
def team(name, attrs = {}, &block)
|
|
44
|
+
@attrs = attrs.merge(name: name)
|
|
45
|
+
object = find(name) || create_object
|
|
46
|
+
update_state_for_block(object, &block) if block
|
|
47
|
+
object
|
|
48
|
+
ensure
|
|
49
|
+
reset_attrs
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def create_object
|
|
55
|
+
create(:team, attrs).tap { |record| collection << record }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def attrs
|
|
59
|
+
permitted_attrs @attrs
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
#### Blueprint DSL
|
|
65
|
+
|
|
66
|
+
| Method | Purpose |
|
|
67
|
+
|--------|---------|
|
|
68
|
+
| `handles :method_name` | Methods this blueprint exposes on the foundry |
|
|
69
|
+
| `factory :name` | Which factory_bot factory to use (inferred from class name if omitted) |
|
|
70
|
+
| `collection :name` | Collection name for tracking created records |
|
|
71
|
+
| `parent :name` | How to find the parent record (`:none`, `:self`, or a method on `current`) |
|
|
72
|
+
| `parent_key :foreign_key` | Foreign key column linking to the parent |
|
|
73
|
+
| `permitted_attrs %i[...]` | Attributes allowed through to factory_bot |
|
|
74
|
+
| `nested_attrs key => [...]` | For `accepts_nested_attributes_for` |
|
|
75
|
+
|
|
76
|
+
#### Finding records
|
|
77
|
+
|
|
78
|
+
Blueprints automatically prevent duplicates. `find(name)` checks the in-memory collection first, then falls back to the database. `find_by(criteria)` works with arbitrary attributes.
|
|
79
|
+
|
|
80
|
+
#### Parent context
|
|
81
|
+
|
|
82
|
+
When a block is passed to a blueprint method, `update_state_for_block` saves the current context, sets the new record as `current.resource`, executes the block, then restores the previous context. Child blueprints read their parent from `current`:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
class UserBlueprint < Foundries::Blueprint
|
|
86
|
+
handles :user
|
|
87
|
+
parent :team # reads current.team
|
|
88
|
+
parent_key :team_id # sets team_id on created records
|
|
89
|
+
# ...
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Base (the foundry)
|
|
94
|
+
|
|
95
|
+
Register blueprints and optional extra collections:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
class TestFoundry < Foundries::Base
|
|
99
|
+
blueprint TeamBlueprint
|
|
100
|
+
blueprint UserBlueprint
|
|
101
|
+
blueprint ProjectBlueprint
|
|
102
|
+
blueprint TaskBlueprint
|
|
103
|
+
|
|
104
|
+
collection :tags # extra collection not from a blueprint
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The base class:
|
|
109
|
+
|
|
110
|
+
- Instantiates each blueprint and delegates its `handles` methods
|
|
111
|
+
- Initializes a `Set` for each collection (e.g. `teams_collection`)
|
|
112
|
+
- Tracks `current` state so nested blocks know their parent context
|
|
113
|
+
- Deduplicates records via each blueprint's `find` logic
|
|
114
|
+
|
|
115
|
+
### Presets
|
|
116
|
+
|
|
117
|
+
Presets are named class methods that build a preconfigured foundry:
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
class TestFoundry < Foundries::Base
|
|
121
|
+
# ...
|
|
122
|
+
|
|
123
|
+
preset :dev_team do
|
|
124
|
+
team "Engineering" do
|
|
125
|
+
user "Alice"
|
|
126
|
+
project "Main" do
|
|
127
|
+
task "Setup"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# In a test:
|
|
134
|
+
let(:foundry) { TestFoundry.dev_team }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Reopening
|
|
138
|
+
|
|
139
|
+
Add more records to an existing foundry:
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
foundry = TestFoundry.dev_team
|
|
143
|
+
foundry.reopen do
|
|
144
|
+
team "Design" do
|
|
145
|
+
user "Carol"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Building from existing objects
|
|
151
|
+
|
|
152
|
+
Start from records already in the database:
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
foundry = TestFoundry.new
|
|
156
|
+
foundry.from(existing_team) do
|
|
157
|
+
user "New hire"
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Lifecycle hooks
|
|
162
|
+
|
|
163
|
+
Override `setup` and `teardown` in your base subclass for pre/post processing:
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
class TestFoundry < Foundries::Base
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
def setup
|
|
170
|
+
@pending_rules = []
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def teardown
|
|
174
|
+
process_pending_rules
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Snapshot Caching
|
|
180
|
+
|
|
181
|
+
When using ActiveRecord, Foundries can snapshot preset data to disk and restore it instead of re-running factories. This is useful for speeding up test suites where the same preset is called many times.
|
|
182
|
+
|
|
183
|
+
Enable with an environment variable:
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
FOUNDRIES_CACHE=1 bundle exec rspec
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Or configure directly:
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
Foundries::Snapshot.enabled = true
|
|
193
|
+
Foundries::Snapshot.storage_path = "tmp/foundries" # default
|
|
194
|
+
Foundries::Snapshot.source_paths = [
|
|
195
|
+
"lib/blueprints/**/*.rb",
|
|
196
|
+
"lib/test_foundry.rb"
|
|
197
|
+
]
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Snapshots are invalidated automatically when the schema version changes or when source files listed in `source_paths` are modified. Data is captured using database-native copy operations (PostgreSQL `COPY`, SQLite `INSERT`) and restored with referential integrity checks temporarily disabled.
|
|
201
|
+
|
|
202
|
+
## Similarity Detection
|
|
203
|
+
|
|
204
|
+
Foundries can detect when presets have overlapping structure, highlighting consolidation opportunities. When enabled, it records the normalized blueprint call tree of each preset and compares against previously seen presets.
|
|
205
|
+
|
|
206
|
+
Enable with an environment variable:
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
FOUNDRIES_SIMILARITY=1 bundle exec rspec
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Or configure directly:
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
Foundries::Similarity.enabled = true
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
When two presets share identical structure or one is structurally contained within another, a warning is printed to stderr:
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
[Foundries] Preset :basic and :extended have identical structure (team > [project > [task], user])
|
|
222
|
+
[Foundries] Preset :simple is structurally contained within :complex
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Each unique pair is warned once per process. The detection normalizes trees by deduplicating sibling nodes (keeping the richest subtree), collapsing pass-through chains, and sorting alphabetically. This means presets that build the same *shape* of data are detected regardless of the specific names or attribute values used.
|
|
226
|
+
|
|
227
|
+
## Requirements
|
|
228
|
+
|
|
229
|
+
- Ruby >= 4.0
|
|
230
|
+
- factory_bot >= 6.0
|
|
231
|
+
- ActiveRecord (optional, for snapshot caching)
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
MIT
|
|
236
|
+
|
|
237
|
+
## Development
|
|
238
|
+
|
|
239
|
+
After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rake` to run the tests.
|
|
240
|
+
|
|
241
|
+
To install this gem onto your local machine, run bundle exec rake install.
|
|
242
|
+
|
|
243
|
+
This project is managed with [Reissue](https://github.com/SOFware/reissue).
|
|
244
|
+
|
|
245
|
+
Releases are automated via the [shared release workflow](https://github.com/SOFware/reissue/blob/main/.github/workflows/SHARED_WORKFLOW_README.md). Trigger a release by running the "Release gem to RubyGems.org" workflow from the Actions tab.
|
|
246
|
+
|
|
247
|
+
## Contributing
|
|
248
|
+
|
|
249
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/SOFware/foundries.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
|
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
7
|
+
|
|
8
|
+
require "standard/rake"
|
|
9
|
+
|
|
10
|
+
task default: %i[spec standard]
|
|
11
|
+
|
|
12
|
+
require "reissue/gem"
|
|
13
|
+
|
|
14
|
+
Reissue::Task.create :reissue do |task|
|
|
15
|
+
task.version_file = "lib/foundries/version.rb"
|
|
16
|
+
task.fragment = :git
|
|
17
|
+
end
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ostruct"
|
|
4
|
+
|
|
5
|
+
module Foundries
|
|
6
|
+
# Base is the orchestrator that composes multiple Blueprints into a single
|
|
7
|
+
# declarative builder for trees of related records.
|
|
8
|
+
#
|
|
9
|
+
# Subclass Base and declare which blueprints it uses:
|
|
10
|
+
#
|
|
11
|
+
# class MyFoundry < Foundries::Base
|
|
12
|
+
# blueprint UserBlueprint
|
|
13
|
+
# blueprint ProjectBlueprint
|
|
14
|
+
#
|
|
15
|
+
# # Optional: additional collections beyond what blueprints declare
|
|
16
|
+
# collection :tags
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# MyFoundry.new do
|
|
20
|
+
# user "Alice" do
|
|
21
|
+
# project "Widget" do
|
|
22
|
+
# # ...
|
|
23
|
+
# end
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
class Base
|
|
28
|
+
include FactoryBot::Syntax::Methods
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
attr_accessor :_active_similarity_recorder
|
|
32
|
+
|
|
33
|
+
# Register a blueprint class with this foundry.
|
|
34
|
+
def blueprint(klass)
|
|
35
|
+
blueprint_registry[klass] = klass.handled_methods
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# All registered blueprint classes and their handled methods.
|
|
39
|
+
def blueprint_registry
|
|
40
|
+
@blueprint_registry ||= {}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Declare additional collection names beyond those from blueprints.
|
|
44
|
+
def collection(*names)
|
|
45
|
+
extra_collections.concat(names.map(&:to_s))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def extra_collections
|
|
49
|
+
@extra_collections ||= []
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# All collection accessor names (e.g. "users_collection").
|
|
53
|
+
def collection_accessors
|
|
54
|
+
(blueprint_collection_names + extra_collections).map { |name| "#{name}_collection" }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Collection names derived from blueprint declarations.
|
|
58
|
+
def blueprint_collection_names
|
|
59
|
+
blueprint_registry.keys.filter_map { |klass| klass.collection_name&.to_s }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Methods delegated from this foundry to its blueprint instances.
|
|
63
|
+
def delegations
|
|
64
|
+
blueprint_registry.select { |_, methods| methods.any? }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Define presets — named class methods that build a preconfigured foundry.
|
|
68
|
+
#
|
|
69
|
+
# class MyFoundry < Foundries::Base
|
|
70
|
+
# preset :full_team do
|
|
71
|
+
# user "Alice"
|
|
72
|
+
# user "Bob"
|
|
73
|
+
# end
|
|
74
|
+
# end
|
|
75
|
+
#
|
|
76
|
+
# MyFoundry.full_team # => configured foundry instance
|
|
77
|
+
#
|
|
78
|
+
def preset(name, &block)
|
|
79
|
+
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
|
|
94
|
+
new(&block)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def with_similarity_recording(preset_name, recording)
|
|
101
|
+
if recording
|
|
102
|
+
self._active_similarity_recorder = Similarity::Recorder.new
|
|
103
|
+
foundry = yield
|
|
104
|
+
tree = _active_similarity_recorder.normalized_tree
|
|
105
|
+
self._active_similarity_recorder = nil
|
|
106
|
+
|
|
107
|
+
key = "#{name}.#{preset_name}"
|
|
108
|
+
warnings = Similarity::Comparator.compare(key, tree, Similarity.registry)
|
|
109
|
+
warnings.each do |w|
|
|
110
|
+
next unless Similarity.warned_pairs.add?(w[:pair])
|
|
111
|
+
warn w[:message]
|
|
112
|
+
end
|
|
113
|
+
Similarity.registry[key] = tree
|
|
114
|
+
foundry
|
|
115
|
+
else
|
|
116
|
+
yield
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def inherited(subclass)
|
|
121
|
+
super
|
|
122
|
+
# Ensure subclasses get their own registries
|
|
123
|
+
subclass.instance_variable_set(:@blueprint_registry, {})
|
|
124
|
+
subclass.instance_variable_set(:@extra_collections, [])
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def initialize(&block)
|
|
129
|
+
@_similarity_recorder = self.class._active_similarity_recorder
|
|
130
|
+
instantiate_blueprints
|
|
131
|
+
initialize_collections
|
|
132
|
+
@current = OpenStruct.new(resource: self)
|
|
133
|
+
setup
|
|
134
|
+
instance_exec(&block) if block
|
|
135
|
+
teardown
|
|
136
|
+
@current.resource = nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
attr_accessor :current
|
|
140
|
+
|
|
141
|
+
# Reopen the foundry to add more records.
|
|
142
|
+
def reopen(&block)
|
|
143
|
+
@current = OpenStruct.new(resource: self)
|
|
144
|
+
instance_exec(&block) if block
|
|
145
|
+
teardown
|
|
146
|
+
@current.resource = nil
|
|
147
|
+
self
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Build within the context of existing objects.
|
|
151
|
+
def from(objects, &block)
|
|
152
|
+
execute_and_restore_state do
|
|
153
|
+
load_existing_objects(objects)
|
|
154
|
+
instance_exec(&block) if block
|
|
155
|
+
teardown
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def load_existing_objects(objects)
|
|
160
|
+
return if objects.nil? || (objects.respond_to?(:empty?) && objects.empty?)
|
|
161
|
+
|
|
162
|
+
Array(objects).each do |object|
|
|
163
|
+
load_state(object)
|
|
164
|
+
|
|
165
|
+
klass_name = object.class.name
|
|
166
|
+
blueprint_class = find_blueprint_class_for(klass_name)
|
|
167
|
+
next unless blueprint_class
|
|
168
|
+
|
|
169
|
+
blueprint_class.load_state_from(object, self)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def execute_and_restore_state
|
|
174
|
+
initial_state = @current.dup
|
|
175
|
+
yield.tap { @current = initial_state }
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def load_state(object)
|
|
179
|
+
klass_name = object.class.name.underscore.tr("/", "_")
|
|
180
|
+
current.send(:"#{klass_name}=", object)
|
|
181
|
+
collection_name = "#{klass_name.pluralize}_collection"
|
|
182
|
+
return unless respond_to?(collection_name)
|
|
183
|
+
|
|
184
|
+
send(collection_name) << object
|
|
185
|
+
end
|
|
186
|
+
alias_method :update_current, :load_state
|
|
187
|
+
|
|
188
|
+
private
|
|
189
|
+
|
|
190
|
+
# Override in subclasses for post-initialize hooks (e.g. pending phase rules).
|
|
191
|
+
def setup
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Override in subclasses for post-block hooks (e.g. processing pending items).
|
|
195
|
+
def teardown
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def instantiate_blueprints
|
|
199
|
+
self.class.blueprint_registry.each_key do |klass|
|
|
200
|
+
ivar = :"@#{ivar_name_for(klass)}"
|
|
201
|
+
instance_variable_set(ivar, klass.new(self))
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Set up delegation from foundry methods to blueprint instances
|
|
205
|
+
self.class.delegations.each do |klass, methods|
|
|
206
|
+
ivar = :"@#{ivar_name_for(klass)}"
|
|
207
|
+
blueprint_instance = instance_variable_get(ivar)
|
|
208
|
+
methods.each do |method_name|
|
|
209
|
+
define_singleton_method(method_name) do |*args, **kwargs, &block|
|
|
210
|
+
blueprint_instance.send(method_name, *args, **kwargs, &block)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def initialize_collections
|
|
217
|
+
self.class.collection_accessors.each do |col|
|
|
218
|
+
instance_variable_set(:"@#{col}", Set.new)
|
|
219
|
+
# Define accessor if not already defined
|
|
220
|
+
define_singleton_method(col) { instance_variable_get(:"@#{col}") }
|
|
221
|
+
define_singleton_method(:"#{col}=") { |val| instance_variable_set(:"@#{col}", val) }
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def ivar_name_for(klass)
|
|
226
|
+
klass.name.demodulize.underscore
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def find_blueprint_class_for(model_class_name)
|
|
230
|
+
self.class.blueprint_registry.keys.detect do |klass|
|
|
231
|
+
klass.name.demodulize.delete_suffix("Blueprint") == model_class_name
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|