model_orchestration 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +0 -0
- data/README.md +69 -0
- data/lib/model_orchestration.rb +43 -0
- data/lib/model_orchestration/base.rb +201 -0
- data/lib/model_orchestration/persistence.rb +51 -0
- metadata +65 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: de10668d300579c46514a77f11173ae448475a17
|
4
|
+
data.tar.gz: 14efbe148901ed70aae1e6d1f21acc70c39fa374
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6073f623db3865ce5cba153d84a6e22ac337e4ff4001b6f335c889e855d33b7c14580002cc0ff26d3b34c8d27f52e9bd30607582495739ee7b2e4c1f3bb2dd33
|
7
|
+
data.tar.gz: 6121df9b444912aa9e3ddb6f27e3cadb06bd161c2f176976cb8ba7e7c8627b327096bd78b26e174ed440b7cfac00ccd1138b0f6fc470db35b7f6be143b6c635b
|
data/MIT-LICENSE
ADDED
File without changes
|
data/README.md
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
# Model Orchestration - orchestrating actions on related models
|
2
|
+
|
3
|
+
`ModelOrchestration` is a toolkit to help with orchestrating handling of multiple models that are related to each other.
|
4
|
+
|
5
|
+
The basic idea is to create a new model in which you can nest other models. You can declare relations between the nested models. The new orchestration model then provides interfaces for persistence actions, validations etc. Under the hood, the model orchestrates the save, validate etc. actions on all the nested models.
|
6
|
+
|
7
|
+
## Example
|
8
|
+
|
9
|
+
Consider you're developing a B2B SaaS application with a signup form. When a new client signs up, you will probably need to create multiple models and save them to the database, for example a user object, representing the person signing up and an organization object which attaches the user to a legal entity which represents your customer, billing data etc.
|
10
|
+
|
11
|
+
+ModelOrchestration+ allows you to nest the user model and the organization model into a meta model, on which all the actions necessary can be performed (validation, persistence). Let's call this "meta model" simply `Signup`.
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
class User < ActiveRecord::Base
|
15
|
+
belongs_to :organization
|
16
|
+
validates :name, presence: true
|
17
|
+
validates :email, presence: true
|
18
|
+
end
|
19
|
+
|
20
|
+
class Organization < ActiveRecord::Base
|
21
|
+
has_many :users
|
22
|
+
validates :name, presence: true
|
23
|
+
end
|
24
|
+
|
25
|
+
class Signup
|
26
|
+
include ModelOrchestration::Base
|
27
|
+
include ModelOrchestration::Persistence
|
28
|
+
|
29
|
+
nested_model :organization
|
30
|
+
nested_model :user
|
31
|
+
nested_model_dependency from: :user, to: :organization
|
32
|
+
end
|
33
|
+
|
34
|
+
signup = Signup.new(user: {name: "Nils", email: "nils@example.org"}, organization: {name: "Nils' Webdesign Agency"})
|
35
|
+
|
36
|
+
signup.valid? # => true
|
37
|
+
|
38
|
+
signup.user.name # => "Nils"
|
39
|
+
signup.user # => #<User:0x007f81f498d078>
|
40
|
+
signup[:user][:email] # => "nils@example.org"
|
41
|
+
|
42
|
+
signup.save # => true
|
43
|
+
```
|
44
|
+
|
45
|
+
`ModelOrchestration::Persistence` only needs to be included if you need
|
46
|
+
`save` and `create` method variations. Leaving it you can still use `ModelOrchestration::Base` alone with `ActiveModel` models.
|
47
|
+
|
48
|
+
## Download and installation
|
49
|
+
|
50
|
+
The latest version of Orchestration Model can be installed with RubyGems:
|
51
|
+
|
52
|
+
```bash
|
53
|
+
gem install model_orchestration
|
54
|
+
```
|
55
|
+
|
56
|
+
The source code can be downloaded on GitHub
|
57
|
+
|
58
|
+
* https://github.com/nsommer/orchestration_model
|
59
|
+
|
60
|
+
## Development & Contributions
|
61
|
+
|
62
|
+
Contributions are welcome. Please use GitHub's issue tracker and pull requests to provide patches.
|
63
|
+
|
64
|
+
## License
|
65
|
+
|
66
|
+
Model Orchestration is released under the MIT license:
|
67
|
+
|
68
|
+
* http://www.opensource.org/licenses/MIT
|
69
|
+
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'active_support/all'
|
2
|
+
|
3
|
+
##
|
4
|
+
# +ModelOrchestration+ is a toolkit to help with orchestrating handling
|
5
|
+
# of multiple models that are related to each other.
|
6
|
+
#
|
7
|
+
# The basic idea is to create a new model in which you can nest other models.
|
8
|
+
# You can declare relations between the nested models. The new orchestration
|
9
|
+
# model then provides interfaces for persistence actions, validations etc.
|
10
|
+
# Under the hood, the model orchestrates the save, validate etc. actions on
|
11
|
+
# all the nested models.
|
12
|
+
#
|
13
|
+
# Maybe it's easier to explain by using an example. Consider a web application
|
14
|
+
# where users can sign up and create an account. But instead of just creating
|
15
|
+
# a user account, you also want to attach them to an organization, e.g. to
|
16
|
+
# connect billing information which is shared between multiple users. Therefore
|
17
|
+
# you might want to create an employee model and a company model at the same
|
18
|
+
# time.
|
19
|
+
#
|
20
|
+
# class Signup
|
21
|
+
# include ModelOrchestration::Base
|
22
|
+
# include ModelOrchestration::Persistence
|
23
|
+
#
|
24
|
+
# nested_model :company
|
25
|
+
# nested_model :employee
|
26
|
+
#
|
27
|
+
# nested_model_dependency from: :employee, to: :company
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# The Signup class prodives you with a lot of automatically generated methods
|
31
|
+
# that feel familiar from an ActiveModel/ActiveRecord perspective.
|
32
|
+
#
|
33
|
+
# For example, you automaticall get a constructor, that accepts an attributes
|
34
|
+
# hash which feeds the nested models.
|
35
|
+
#
|
36
|
+
# signup = Signup.new(company: {name: "Foo Inc"}, employee: {name: "Bob"})
|
37
|
+
#
|
38
|
+
module ModelOrchestration
|
39
|
+
extend ActiveSupport::Autoload
|
40
|
+
|
41
|
+
autoload :Base
|
42
|
+
autoload :Persistence
|
43
|
+
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
module ModelOrchestration
|
2
|
+
class DependencyCycleError < StandardError
|
3
|
+
end
|
4
|
+
|
5
|
+
##
|
6
|
+
# The module containing the base functionality. This includes class methods
|
7
|
+
# to specify nested models and dependencies, as well as ActiveModel-like
|
8
|
+
# functionality, such as attribute accessors for the nested models and
|
9
|
+
# validation methods.
|
10
|
+
module Base
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
|
13
|
+
included do
|
14
|
+
class_attribute :nested_models, :dependencies
|
15
|
+
self.nested_models = []
|
16
|
+
self.dependencies = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
##
|
21
|
+
# Class method to declare a nested model. This invokes instantiation
|
22
|
+
# of the model when the holding model is instantiated. The nested model
|
23
|
+
# can be declared by symbol, type or string.
|
24
|
+
#
|
25
|
+
# class HoldingClass
|
26
|
+
# include ModelOrchestration::Base
|
27
|
+
#
|
28
|
+
# nested_model :user
|
29
|
+
# end
|
30
|
+
def nested_model(model)
|
31
|
+
model_key = symbolize(model)
|
32
|
+
|
33
|
+
self.nested_models << model_key
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# Class method to declare a dependency from one nested model to another.
|
38
|
+
#
|
39
|
+
# class Signup
|
40
|
+
# include ModelOrchestration::Base
|
41
|
+
#
|
42
|
+
# nested_model :company
|
43
|
+
# nested_model :employee
|
44
|
+
#
|
45
|
+
# nested_model_dependency from: :employee, to: :company
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# The example obove might be used to orchestrate the signup process of
|
49
|
+
# a user who creates an account representative for his company. In the
|
50
|
+
# database schema, company and employee have a 1:n relation, which is
|
51
|
+
# represented by +nested_model_dependency+.
|
52
|
+
def nested_model_dependency(args = {})
|
53
|
+
unless args.include?(:from) && args.include?(:to)
|
54
|
+
raise ArgumentError, ":from and :to hash keys must be included."
|
55
|
+
end
|
56
|
+
|
57
|
+
from = symbolize(args[:from])
|
58
|
+
to = symbolize(args[:to])
|
59
|
+
|
60
|
+
if dependency_introduces_cycle?(from, to)
|
61
|
+
raise ModelOrchestration::DependencyCycleError, "#{from} is already a dependency of #{to}"
|
62
|
+
end
|
63
|
+
|
64
|
+
self.dependencies[from] = to
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def symbolize(arg)
|
70
|
+
if arg.is_a? Symbol
|
71
|
+
arg
|
72
|
+
elsif arg.is_a? String
|
73
|
+
arg.underscore.to_sym
|
74
|
+
elsif arg.is_a? Class
|
75
|
+
arg.name.underscore.to_sym
|
76
|
+
else
|
77
|
+
arg.to_s.underscore.to_sym
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def dependency_introduces_cycle?(from, to)
|
82
|
+
self.dependencies.include?(to) && (self.dependencies[to] == from)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
##
|
87
|
+
# Instantiate the model and all nested models. Attributes can be submitted
|
88
|
+
# as a hash and will be handed over to the nested models.
|
89
|
+
def initialize(attrs = {})
|
90
|
+
@nested_model_instances = {}
|
91
|
+
|
92
|
+
self.nested_models.each do |model|
|
93
|
+
klass = model.to_s.classify.constantize
|
94
|
+
|
95
|
+
if attrs.include?(model)
|
96
|
+
@nested_model_instances[model] = klass.new(attrs[model])
|
97
|
+
else
|
98
|
+
@nested_model_instances[model] = klass.new
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
self.dependencies.each do |from, to|
|
103
|
+
@nested_model_instances[from].public_send("#{to.to_s}=", @nested_model_instances[to])
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
##
|
108
|
+
# Get a nested model by name (symbol).
|
109
|
+
#
|
110
|
+
# class Signup
|
111
|
+
# include ModelOrchestration::Base
|
112
|
+
#
|
113
|
+
# nested_model :company
|
114
|
+
# nested_model :employee
|
115
|
+
#
|
116
|
+
# nested_model_dependency from: :employee, to: :company
|
117
|
+
# end
|
118
|
+
#
|
119
|
+
# signup = Signup.new
|
120
|
+
# company = signup[:company]
|
121
|
+
#
|
122
|
+
# Because ActiveRecord classes also support the +[]+ method, it can
|
123
|
+
# be chained to get attributes of nested models.
|
124
|
+
#
|
125
|
+
# company_name = signup[:company][:name]
|
126
|
+
#
|
127
|
+
def [](key)
|
128
|
+
send key
|
129
|
+
end
|
130
|
+
|
131
|
+
##
|
132
|
+
# Set a nested model by name (symbol).
|
133
|
+
#
|
134
|
+
# class Signup
|
135
|
+
# include ModelOrchestration::Base
|
136
|
+
#
|
137
|
+
# nested_model :company
|
138
|
+
# nested_model :employee
|
139
|
+
#
|
140
|
+
# nested_model_dependency from: :employee, to: :company
|
141
|
+
# end
|
142
|
+
#
|
143
|
+
# signup = Signup.new
|
144
|
+
# signup[:company] = Company.new
|
145
|
+
def []=(key, value)
|
146
|
+
send "#{key}=", value
|
147
|
+
end
|
148
|
+
|
149
|
+
##
|
150
|
+
# Implements attribute accessor methods for nested models.
|
151
|
+
def method_missing(message, *args, &block)
|
152
|
+
if @nested_model_instances.include?(message)
|
153
|
+
# Get nested model accessor
|
154
|
+
@nested_model_instances[message]
|
155
|
+
elsif message =~ /^([^=]+)=$/
|
156
|
+
# Set nested model accessor
|
157
|
+
attr = message.to_s.chop.to_sym
|
158
|
+
if @nested_model_instances.include?(attr)
|
159
|
+
@nested_model_instances[attr] = args.first
|
160
|
+
else
|
161
|
+
super
|
162
|
+
end
|
163
|
+
else
|
164
|
+
super
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
##
|
169
|
+
# See: http://api.rubyonrails.org/classes/ActiveModel/Validations.html#method-i-valid-3F
|
170
|
+
def valid?(context = nil)
|
171
|
+
valid = true
|
172
|
+
|
173
|
+
@nested_model_instances.each do |key, instance|
|
174
|
+
valid = false unless instance.valid?(context)
|
175
|
+
end
|
176
|
+
|
177
|
+
valid
|
178
|
+
end
|
179
|
+
|
180
|
+
##
|
181
|
+
# See: http://api.rubyonrails.org/classes/ActiveModel/Validations.html#method-i-validate
|
182
|
+
alias_method :validate, :valid?
|
183
|
+
|
184
|
+
# See: http://api.rubyonrails.org/classes/ActiveModel/Validations.html#method-i-invalid-3F
|
185
|
+
def invalid?(context = nil)
|
186
|
+
!valid?(context)
|
187
|
+
end
|
188
|
+
|
189
|
+
##
|
190
|
+
# See: http://api.rubyonrails.org/classes/ActiveModel/Validations.html#method-i-validate-21
|
191
|
+
def validate!(context = nil)
|
192
|
+
valid?(context) || raise_validation_error
|
193
|
+
end
|
194
|
+
|
195
|
+
protected
|
196
|
+
|
197
|
+
def raise_validation_error
|
198
|
+
raise(ValidationError.new(self))
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module ModelOrchestration
|
2
|
+
##
|
3
|
+
# Including this module will give an OrchestrationModel::Base ActiveRecord-like
|
4
|
+
# methods for perstistence. The methods available are restricted to
|
5
|
+
# variations of save and create because updating/deleting/finding are
|
6
|
+
# usually actions performed on a single record, not an orchestrated meta
|
7
|
+
# model.
|
8
|
+
module Persistence
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
included do
|
12
|
+
include ModelOrchestration::Base
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
##
|
17
|
+
# See: http://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-create
|
18
|
+
def create(attrs = {}, &block)
|
19
|
+
object = new(attrs, &block)
|
20
|
+
object.save
|
21
|
+
object
|
22
|
+
end
|
23
|
+
|
24
|
+
##
|
25
|
+
# http://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-create-21
|
26
|
+
def create!(attrs = {}, &block)
|
27
|
+
object = new(attrs, &block)
|
28
|
+
object.save!
|
29
|
+
object
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# See: http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-save
|
35
|
+
def save(*args)
|
36
|
+
@nested_model_instances.each do |key, instance|
|
37
|
+
return false unless instance.save(args)
|
38
|
+
end
|
39
|
+
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# See: http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-save-21
|
45
|
+
def save!(*args)
|
46
|
+
@nested_model_instances.each do |key, instance|
|
47
|
+
instance.save(args)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: model_orchestration
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nils Sommer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-02-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5'
|
27
|
+
description: In more complex workflows, multiple models that are related to each other
|
28
|
+
need to be created at the same time. This toolkit allows to specifiy models that
|
29
|
+
orchestrate persistence and validation actions of multiple models by nesting them
|
30
|
+
into a so called orchestration model.
|
31
|
+
email: mail@nilssommer.de
|
32
|
+
executables: []
|
33
|
+
extensions: []
|
34
|
+
extra_rdoc_files: []
|
35
|
+
files:
|
36
|
+
- MIT-LICENSE
|
37
|
+
- README.md
|
38
|
+
- lib/model_orchestration.rb
|
39
|
+
- lib/model_orchestration/base.rb
|
40
|
+
- lib/model_orchestration/persistence.rb
|
41
|
+
homepage: https://github.com/nsommer/model_orchestration
|
42
|
+
licenses:
|
43
|
+
- MIT
|
44
|
+
metadata: {}
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options: []
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 2.2.2
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
requirements: []
|
60
|
+
rubyforge_project:
|
61
|
+
rubygems_version: 2.5.1
|
62
|
+
signing_key:
|
63
|
+
specification_version: 4
|
64
|
+
summary: A toolkit for orchestrating actions on related models.
|
65
|
+
test_files: []
|