activeshepherd 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/Guardfile +16 -0
- data/LICENSE.txt +22 -0
- data/README.md +138 -0
- data/Rakefile +24 -0
- data/activeshepherd.gemspec +34 -0
- data/lib/active_shepherd.rb +24 -0
- data/lib/active_shepherd/active_record_shim.rb +15 -0
- data/lib/active_shepherd/aggregate.rb +69 -0
- data/lib/active_shepherd/aggregate_root.rb +152 -0
- data/lib/active_shepherd/changes_validator.rb +11 -0
- data/lib/active_shepherd/class_validator.rb +11 -0
- data/lib/active_shepherd/deep_reverse_changes.rb +22 -0
- data/lib/active_shepherd/method.rb +81 -0
- data/lib/active_shepherd/methods/apply_changes.rb +53 -0
- data/lib/active_shepherd/methods/apply_state.rb +58 -0
- data/lib/active_shepherd/methods/query_changes.rb +53 -0
- data/lib/active_shepherd/methods/query_state.rb +38 -0
- data/lib/active_shepherd/traversal.rb +34 -0
- data/lib/active_shepherd/version.rb +3 -0
- data/lib/activeshepherd.rb +1 -0
- data/tags +123 -0
- data/test/integration/.gitkeep +0 -0
- data/test/integration/project_todo_scenario_test.rb +334 -0
- data/test/setup_test_models.rb +115 -0
- data/test/test_helper.rb +21 -0
- data/test/unit/.gitkeep +0 -0
- data/test/unit/aggregate_test.rb +4 -0
- data/test/unit/apply_changes_test.rb +17 -0
- data/test/unit/changes_validator_test.rb +19 -0
- data/test/unit/class_validator_test.rb +39 -0
- metadata +257 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6aedd38764552813b40c075d7215d87b588d37cf
|
4
|
+
data.tar.gz: 53fa8aa99d06d683c778a4b045a9f051e0c47c10
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d2c9ed3ee3217b7d9ebe2c0e153742e1b6f721bab5c929a19b4936809e49d13fbe1bfa6f60111f48a3c3bc883e8d964fda86313ac1006dc8c4d02815113c0c97
|
7
|
+
data.tar.gz: 43dbfc71a82519448d42a9d1abaf8ec1647d804498ff5bc65ff3f61a322d38e8bd70d34284db7537f392336f4e3af8c07c916fe5b4c2b19ab57e64d9ca1fc83e
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.0.0-p0
|
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'minitest' do
|
5
|
+
# with Minitest::Unit
|
6
|
+
watch(%r|^test/(.*)\/?(.*)_test\.rb|)
|
7
|
+
watch(%r{^lib/active_shepherd/(.*/)?([^/]+)\.rb$}) { |m| "test/unit/#{m[1]}#{m[2]}_test.rb" }
|
8
|
+
watch(%r{^lib/active_shepherd.rb}) { "test" }
|
9
|
+
watch(%r|^test/test_helper\.rb|) { "test" }
|
10
|
+
|
11
|
+
watch(%r|^test/integration/project_todo_scenario/(.*)\.rb$|) do
|
12
|
+
"test/integration/project_todo_scenario_test.rb"
|
13
|
+
end
|
14
|
+
|
15
|
+
watch(%r{^lib/.*\.rb$}) { |m| "test/integration/project_todo_scenario_test.rb" }
|
16
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 ntl
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
# WARNING: DANGER AHEAD!
|
2
|
+
|
3
|
+
*This repo is titled "eat-my-babies" for a reason. It's essentially scratch code at this point.*
|
4
|
+
|
5
|
+
# ActiveShepherd
|
6
|
+
|
7
|
+
Is your app/models directory growing unweildy? Do you find yourself desiring the notion of aggregates to help corral your less important models under the umbrella of more important "business entities?" That's the problem I had that led me to write this gem. I wanted to be able to reason about an entire namespace of models as one thing; or an "aggregate" in enterprisey development parlance.
|
8
|
+
|
9
|
+
My main goal was to be able to keep using ActiveRecord and intrude on it as little as possible. The result was an approach that requires you to wire up your models a bit more strictly -- you need to be setting options like `dependent: 'destroy'`, `autosave: true`, and `inverse_of` on all associations to the sub objects. The benefit you get from this gem is to be able to both query and manipulate the state of the entire aggregate all at once.
|
10
|
+
|
11
|
+
There are more requirements that are outlined by Eric Evans in his brilliant Domain Driven Design book, whose self titled concept is still very new to me.
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
gem 'activeshepherd'
|
18
|
+
|
19
|
+
And then execute:
|
20
|
+
|
21
|
+
$ bundle
|
22
|
+
|
23
|
+
Or install it yourself as:
|
24
|
+
|
25
|
+
$ gem install activeshepherd
|
26
|
+
|
27
|
+
In your `config/initializers` directory, add a tiny shim into ActiveRecord::Base:
|
28
|
+
|
29
|
+
ActiveShepherd.enable!(ActiveRecord::Base)
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
1. Pick a model you'd like to make into an aggregate root
|
34
|
+
2. Add `act_as_aggregate_root!` to the model, e.g.:
|
35
|
+
end
|
36
|
+
3. Make sure it follows the rules (e.g. [see this blog post](http://lostechies.com/jimmybogard/2008/05/21/entities-value-objects-aggregates-and-roots/))
|
37
|
+
4. ??
|
38
|
+
5. Profit!
|
39
|
+
|
40
|
+
## Examples:
|
41
|
+
|
42
|
+
See the test suite for more fleshed out examples. For now, say you have two models:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
# app/models/my_model.rb
|
46
|
+
class MyModel < ActiveRecord::Base
|
47
|
+
act_as_aggregate_root!
|
48
|
+
|
49
|
+
has_many :bunnies, autosave: true, dependent: :destroy, inverse_of: :my_model,
|
50
|
+
validate: true
|
51
|
+
end
|
52
|
+
|
53
|
+
# app/models/my_model/bunny.rb
|
54
|
+
class MyModel::Bunny < ActiveRecord::Base
|
55
|
+
belongs_to :my_model, inverse_of: :bunnies, touch: true
|
56
|
+
end
|
57
|
+
```
|
58
|
+
<!-- ` -->
|
59
|
+
|
60
|
+
Now add a test to make sure your models always meet the requirements for being an aggregate root:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
# spec/models/my_model_spec.rb
|
64
|
+
describe MyModel do
|
65
|
+
it "is an aggregate root" do
|
66
|
+
MyModel.should be_able_to_act_as_aggregate_root
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# test/unit/my_model_test.rb
|
71
|
+
class MyModel::TestCase < Minitest::Unit::TestCase
|
72
|
+
def test_should_be_aggregate_root
|
73
|
+
assert MyModel.able_to_act_as_aggregate_root?
|
74
|
+
end
|
75
|
+
end
|
76
|
+
```
|
77
|
+
<!-- ` -->
|
78
|
+
|
79
|
+
You now get some new behavior on MyModel that will let you deal with the entire aggregate nicely:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
>> @my_model = MyModel.new
|
83
|
+
>> @my_model.bunnies.build({ name: "Roger"})
|
84
|
+
>> @my_model.save
|
85
|
+
|
86
|
+
# Nothing new, right? wrong.
|
87
|
+
|
88
|
+
>> @my_model.aggregate_state
|
89
|
+
=> {
|
90
|
+
bunnies: [
|
91
|
+
{ name: "Roger" }
|
92
|
+
]
|
93
|
+
}
|
94
|
+
|
95
|
+
# Sweet, what about changes?
|
96
|
+
|
97
|
+
>> @my_model.bunnies.first.name = "Roger Rabbit"
|
98
|
+
>> @my_model.bunnies.build({ name: "Energizer" })
|
99
|
+
|
100
|
+
# BAM!
|
101
|
+
|
102
|
+
>> @my_model.aggregate_changes
|
103
|
+
=> {
|
104
|
+
bunnies: {
|
105
|
+
0 => { name: ["Roger", "Roger Rabbit"] },
|
106
|
+
1 => { name: [nil, "Energizer"] }
|
107
|
+
}
|
108
|
+
}
|
109
|
+
```
|
110
|
+
<!-- ` -->
|
111
|
+
|
112
|
+
So `#aggregate_changes` is just like ActiveRecord's `#changes`, except it includes all of the nested changes within the aggregate.
|
113
|
+
|
114
|
+
That's a brief description of what this gem does. Here are the main methods that `acts_as_aggregate_root!` brings to your ActiveRecord models:
|
115
|
+
|
116
|
+
| Method name | Description |
|
117
|
+
|:---------------------:|:----------------------------------------------------------------------------------:|
|
118
|
+
| `#aggregate_state` | Serializes the entire state of the aggregate |
|
119
|
+
| `#aggregate_state=` | Takes a serialized blob and uses it to set the entire state of the aggregate |
|
120
|
+
| `#aggregate_changes` | Analagous to `#changes`; it tells you what all has changes in the entire aggregate |
|
121
|
+
| `#aggregate_changes=` | Takes an existing set of changes and applies it to the aggregate |
|
122
|
+
|
123
|
+
## Todo
|
124
|
+
|
125
|
+
This project is way alpha right now, hence the "eat-my-babies" project name.
|
126
|
+
|
127
|
+
1. Implement `ClassValidator` which will correctly tell you if a class can be an aggregate root (e.g. are your associations wired up correctly?)
|
128
|
+
2. Implement `ChangeValidator` that adds a little more niceness around `#aggregate_changes=`
|
129
|
+
|
130
|
+
My main goal right now is to use the code as it exists for a while and deal with problems as they arise. Consider the entire gem incomplete for right now.
|
131
|
+
|
132
|
+
## Contributing
|
133
|
+
|
134
|
+
1. Fork it
|
135
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
136
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
137
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
138
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
require "rake/testtask"
|
4
|
+
|
5
|
+
desc "Update ctags"
|
6
|
+
task :ctags do
|
7
|
+
`ctags -R lib test`
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "Jump into a console with the test environment loaded"
|
11
|
+
task :console do
|
12
|
+
$:.push File.expand_path("../test", __FILE__)
|
13
|
+
require "test_helper"
|
14
|
+
require "irb"
|
15
|
+
|
16
|
+
binding.pry
|
17
|
+
end
|
18
|
+
|
19
|
+
Rake::TestTask.new do |t|
|
20
|
+
t.pattern = "test/**/*_test.rb"
|
21
|
+
t.libs << "test"
|
22
|
+
end
|
23
|
+
|
24
|
+
task default: "test"
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'active_shepherd/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "activeshepherd"
|
8
|
+
gem.version = ActiveShepherd::VERSION
|
9
|
+
gem.authors = ["ntl"]
|
10
|
+
gem.email = ["nathanladd+github@gmail.com"]
|
11
|
+
gem.description = %q{Wrangle unweildy app/models directories by unobtrusively adding the aggregate pattern into ActiveRecord}
|
12
|
+
gem.summary = %q{Wrangle unweildy app/models directories with aggregates}
|
13
|
+
gem.homepage = "http://github.com/ntl/activeshepherd"
|
14
|
+
gem.license = "MIT"
|
15
|
+
|
16
|
+
gem.files = `git ls-files`.split($/)
|
17
|
+
gem.executables = gem.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
gem.test_files = gem.files.grep(%r{^(test|features)/})
|
19
|
+
gem.require_paths = ["lib"]
|
20
|
+
|
21
|
+
gem.add_dependency "activerecord", ">= 2.3.17"
|
22
|
+
gem.add_dependency "activesupport", ">= 2.3.17"
|
23
|
+
|
24
|
+
gem.add_development_dependency "activerecord", "4.0.0.beta1"
|
25
|
+
gem.add_development_dependency "guard"
|
26
|
+
gem.add_development_dependency "guard-minitest"
|
27
|
+
gem.add_development_dependency "hashie"
|
28
|
+
gem.add_development_dependency "minitest-reporters"
|
29
|
+
gem.add_development_dependency "pry"
|
30
|
+
gem.add_development_dependency "pry-debugger"
|
31
|
+
gem.add_development_dependency "rake"
|
32
|
+
gem.add_development_dependency "rb-fsevent"
|
33
|
+
gem.add_development_dependency "sqlite3"
|
34
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ActiveShepherd ; end
|
2
|
+
|
3
|
+
require 'active_shepherd/active_record_shim'
|
4
|
+
require 'active_shepherd/aggregate'
|
5
|
+
require 'active_shepherd/aggregate_root'
|
6
|
+
require 'active_shepherd/changes_validator'
|
7
|
+
require 'active_shepherd/deep_reverse_changes'
|
8
|
+
require 'active_shepherd/method'
|
9
|
+
require 'active_shepherd/methods/apply_changes'
|
10
|
+
require 'active_shepherd/methods/apply_state'
|
11
|
+
require 'active_shepherd/methods/query_changes'
|
12
|
+
require 'active_shepherd/methods/query_state'
|
13
|
+
require 'active_shepherd/traversal'
|
14
|
+
require 'active_shepherd/version'
|
15
|
+
|
16
|
+
module ActiveShepherd
|
17
|
+
AggregateMismatchError = Class.new(StandardError)
|
18
|
+
BadChangeError = Class.new(StandardError)
|
19
|
+
InvalidChangesError = Class.new(StandardError)
|
20
|
+
|
21
|
+
def self.deep_reverse_changes(changes)
|
22
|
+
DeepReverseChanges.new(changes).reverse
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module ActiveShepherd
|
2
|
+
def self.enable!(activerecord_base)
|
3
|
+
class << activerecord_base
|
4
|
+
# FIXME: make this actually check the model to meet the criteria for being
|
5
|
+
# an Aggregate Root
|
6
|
+
def able_to_act_as_aggregate_root?
|
7
|
+
true
|
8
|
+
end
|
9
|
+
|
10
|
+
def act_as_aggregate_root!
|
11
|
+
include ::ActiveShepherd::AggregateRoot
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
class ActiveShepherd::Aggregate
|
2
|
+
attr_reader :excluded_attributes
|
3
|
+
attr_reader :model
|
4
|
+
|
5
|
+
def initialize(model, excluded_attributes = [])
|
6
|
+
@model = model
|
7
|
+
|
8
|
+
@excluded_attributes = ["id", "created_at", "updated_at"]
|
9
|
+
@excluded_attributes.concat(Array.wrap(excluded_attributes).map(&:to_s))
|
10
|
+
end
|
11
|
+
|
12
|
+
def default_attributes
|
13
|
+
model.class.new.attributes
|
14
|
+
end
|
15
|
+
|
16
|
+
def raw_attributes
|
17
|
+
model.attributes_before_type_cast
|
18
|
+
end
|
19
|
+
|
20
|
+
def traversable_associations
|
21
|
+
associations.traversable
|
22
|
+
end
|
23
|
+
|
24
|
+
def untraversable_association_names
|
25
|
+
associations.untraversable.keys
|
26
|
+
end
|
27
|
+
|
28
|
+
def serialize_value(attribute_name, value)
|
29
|
+
run_through_serializer(attribute_name, value, :dump)
|
30
|
+
end
|
31
|
+
|
32
|
+
def deserialize_value(attribute_name, value)
|
33
|
+
run_through_serializer(attribute_name, value, :load)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def associations
|
39
|
+
@associations ||= begin
|
40
|
+
all_associations = model.class.reflect_on_all_associations
|
41
|
+
ostruct = OpenStruct.new untraversable: {}, traversable: {}
|
42
|
+
all_associations.each_with_object(ostruct) do |association_reflection, ostruct|
|
43
|
+
if traverse_association?(association_reflection)
|
44
|
+
key = :traversable
|
45
|
+
else
|
46
|
+
key = :untraversable
|
47
|
+
end
|
48
|
+
ostruct.send(key)[association_reflection.name] = association_reflection
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def run_through_serializer(attribute_name, value, method)
|
54
|
+
serializer = model.class.serialized_attributes[attribute_name.to_s]
|
55
|
+
if serializer
|
56
|
+
serializer.send(method, value)
|
57
|
+
else
|
58
|
+
value
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def traverse_association?(association)
|
63
|
+
return false if association.options[:readonly]
|
64
|
+
return false if association.macro == :belongs_to
|
65
|
+
|
66
|
+
true
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
module ActiveShepherd::AggregateRoot
|
2
|
+
def self.included(base)
|
3
|
+
base.extend(ClassMethods)
|
4
|
+
end
|
5
|
+
|
6
|
+
# Private: returns the behind the scenes object that does all the work
|
7
|
+
def aggregate
|
8
|
+
@aggregate ||= ActiveShepherd::Aggregate.new(self)
|
9
|
+
end
|
10
|
+
private :aggregate
|
11
|
+
|
12
|
+
# Public: Given a serializable blob of changes (Hash, Array, and String)
|
13
|
+
# objects, apply those changes to
|
14
|
+
#
|
15
|
+
# Examples:
|
16
|
+
#
|
17
|
+
# @project.aggregate_changes = { name: ["Clean House", "Clean My House"] }
|
18
|
+
#
|
19
|
+
# Returns nothing.
|
20
|
+
# Raises ActiveShepherd::InvalidChangesError if the changes supplied do not
|
21
|
+
# pass #valid_aggregate_changes? (see below)
|
22
|
+
# Raises ActiveShepherd::BadChangeError if a particular attribute change
|
23
|
+
# cannot be applied.
|
24
|
+
# Raises an ActiveShepherd::AggregateMismatchError if any objects in the
|
25
|
+
# aggregate are being asked to change attributes that do not exist.
|
26
|
+
def aggregate_changes=(changes)
|
27
|
+
changes_errors = ActiveShepherd::ChangesValidator.new(self).validate changes
|
28
|
+
unless changes_errors.empty?
|
29
|
+
raise ActiveShepherd::InvalidChangesError, "changes hash is invalid: "\
|
30
|
+
"#{changes_errors.join(', ')}"
|
31
|
+
end
|
32
|
+
# The validation process actually runs the changes internally. This means
|
33
|
+
# we don't have to explicitly invoke # #apply_changes here.
|
34
|
+
# XXX: remove when ChangesValidator does this
|
35
|
+
ActiveShepherd::Methods::ApplyChanges.apply_changes aggregate, changes
|
36
|
+
end
|
37
|
+
|
38
|
+
# Public: Reverses the effect of #aggregate_changes=
|
39
|
+
def reverse_aggregate_changes=(changes)
|
40
|
+
self.aggregate_changes = ActiveShepherd::DeepReverseChanges.new(changes).reverse
|
41
|
+
end
|
42
|
+
|
43
|
+
# Public: Returns the list of changes to the aggregate that would persist if
|
44
|
+
# #save were called on the aggregate root.
|
45
|
+
#
|
46
|
+
# Examples
|
47
|
+
#
|
48
|
+
# @project.aggregate_changes
|
49
|
+
# # => { name: ["Clean House", "Clean My House"], todos: [{ text: ["Take out trash", "Take out the trash" ...
|
50
|
+
#
|
51
|
+
# Returns all changes in the aggregate
|
52
|
+
def aggregate_changes
|
53
|
+
ActiveShepherd::Methods::QueryChanges.query_changes aggregate
|
54
|
+
end
|
55
|
+
|
56
|
+
# Public: Injects the entire state of the aggregate from a serializable blob.
|
57
|
+
#
|
58
|
+
# Examples:
|
59
|
+
#
|
60
|
+
# @project.aggregate_state = { name: "Clean House", todos: [{ text: "Take out trash" ...
|
61
|
+
#
|
62
|
+
# Returns nothing.
|
63
|
+
# Raises an AggregateMismatchError if the blob contains references to objects
|
64
|
+
# or attributes that do not exist in this aggregate.
|
65
|
+
def aggregate_state=(blob)
|
66
|
+
ActiveShepherd::Methods::ApplyState.apply_state aggregate, blob
|
67
|
+
end
|
68
|
+
|
69
|
+
# Public: Returns the entire state of the aggregate as a serializable blob.
|
70
|
+
# All id values (primary keys) are extracted.
|
71
|
+
#
|
72
|
+
# Examples
|
73
|
+
#
|
74
|
+
# @project.aggregate_state
|
75
|
+
# # => { name: "Clean House", todos: [{ text: "Take out trash" ...
|
76
|
+
#
|
77
|
+
# Returns serializable blob.
|
78
|
+
def aggregate_state
|
79
|
+
ActiveShepherd::Methods::QueryState.query_state aggregate
|
80
|
+
end
|
81
|
+
|
82
|
+
# Public: Reloads the entire aggregate by invoking #reload on each of the
|
83
|
+
# records in the aggregate.
|
84
|
+
def reload_aggregate
|
85
|
+
reload
|
86
|
+
raise "NYI"
|
87
|
+
end
|
88
|
+
|
89
|
+
# Public: Validates a set of changes for the aggregate root.
|
90
|
+
#
|
91
|
+
# * Does deep_reverse(deep_reverse(changes)) == changes?
|
92
|
+
# * Assuming the model is currently valid, if the changes were applied,
|
93
|
+
# would the aggregate be valid?
|
94
|
+
# * If I apply the changes, and then apply deep_reverse(changes), does
|
95
|
+
# #aggregate_state change?
|
96
|
+
#
|
97
|
+
# See ActiveShepherd.deep_reverse
|
98
|
+
#
|
99
|
+
# Examples:
|
100
|
+
#
|
101
|
+
# @project.valid_aggregate_changes?(@project.aggregate_changes)
|
102
|
+
# # => true
|
103
|
+
#
|
104
|
+
# Returns true if and only if the supplied changes pass muster.
|
105
|
+
def valid_aggregate_changes?(changes, emit_boolean = true)
|
106
|
+
validator = ActiveShepherd::ChangesValidator.new(self)
|
107
|
+
errors = validator.validate changes
|
108
|
+
emit_boolean ? errors.blank? : errors
|
109
|
+
ensure
|
110
|
+
reload_aggregate
|
111
|
+
end
|
112
|
+
|
113
|
+
module ClassMethods
|
114
|
+
# Public: Determines whether or not the including class can behave like an
|
115
|
+
# aggregate. Designed to be used by tests that want to make sure that any
|
116
|
+
# of the models that make up the aggregate never change in a way that would
|
117
|
+
# break the functionality of Aggregate::Root.
|
118
|
+
#
|
119
|
+
# In order for this method to return true, this model and its associated
|
120
|
+
# models are each checked rigorously to ensure they are wired up in a way
|
121
|
+
# that meets the requirements of ActiveShepherd. These requirements are:
|
122
|
+
#
|
123
|
+
# * All models in the namespace defined by the root itself are visible to
|
124
|
+
# associations that meet this criteria:
|
125
|
+
# * The root model autosaves all associated models in the aggregate.
|
126
|
+
# (:autosave is true on the association)
|
127
|
+
# * The root model validates all associated models in the aggregate.
|
128
|
+
# (:validate is true on the association)
|
129
|
+
# * Associated objects touch the root model when they are updated
|
130
|
+
# (:touch is true on the association)
|
131
|
+
# * When any root model is destroyed, all associated models in the
|
132
|
+
# aggregate boundary are also destroyed, or else their references are
|
133
|
+
# nullified. (:dependent => :destroy/:nullify)
|
134
|
+
# * The entire object constellation within the boundary can be traversed
|
135
|
+
# without accessing the persistence layer, providing they have all been
|
136
|
+
# eager loaded. (:inverse_of is set on the associations)
|
137
|
+
# * If the association references any external aggregate root, then
|
138
|
+
# when that root is deleted, then either the associations reference
|
139
|
+
# must be nullified, or else the associated model itself must be
|
140
|
+
# deleted.
|
141
|
+
# * All models within the aggregate are only referenced inside this
|
142
|
+
# aggregate boundary, with the exception of the root itself.
|
143
|
+
# * Any model in the namespace of the root that has its own sub namespace
|
144
|
+
# under it is recursively checked.
|
145
|
+
#
|
146
|
+
# Returns true if and only if this model is an aggregate root.
|
147
|
+
def behave_like_an_aggregate?(emit_boolean = true)
|
148
|
+
errors = ActiveShepherd::ClassValidator.new(self).validate
|
149
|
+
emit_boolean ? errors.blank? : errors
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|