model_orchestration 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
File without changes
@@ -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: []