hexx 5.4.0 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fa67c0939cdf123932cc4357f1cdb143559287a7
4
- data.tar.gz: d31b064fcc971a2162bd995555d38971c29bf8d7
3
+ metadata.gz: 2e8768af29b2a06a1eec7ae917896144373edf4e
4
+ data.tar.gz: 33b14d58fda376b285b3674a69efc05fcf0ad3d9
5
5
  SHA512:
6
- metadata.gz: cc793326fe14ff8c7fe62a5d46c9912a3175773702c44de92dda5e1bff5f30913ff56e646852523995874f1cf281018097bd3181da6ce292058c59f24ce29d73
7
- data.tar.gz: bcfbc983fa5c1fdfd8eba166baaf47f4239707358eed3527ce7d2eee44617c6b2853a13b8c790c0c37a0ba6429cbc3ba0177c5b9ccf66f289acc2dff7fbf81f4
6
+ metadata.gz: 688a02c4e5b617233bd222451e44580e4974e0ba93a54ae6532ba160a01aa37c4bbd67d0db3ee12b22a19567502947a6b7cb29cf79883a23b476093a1e8f67c3
7
+ data.tar.gz: 32504f32691f79843f70416690f75c70f9679e8517659d3cec6ff62029365341673d017a1555e1de2137e1b80ca314a9e9fec6cd8917a2de1f9801100f089b31
data/README.rdoc CHANGED
@@ -7,16 +7,25 @@
7
7
  {<img src="http://img.shields.io/coveralls/nepalez/hexx.svg?style=flat" alt="Coverage Status" />}[https://coveralls.io/r/nepalez/hexx]
8
8
  {<img src="http://img.shields.io/badge/license-MIT-blue.svg?style=flat" alt="License" />}[https://github.com/nepalez/hexx/blob/master/LICENSE.rdoc]
9
9
 
10
- The module provides a base service objects class and some features for the
11
- domain models (entities).
10
+ The base library for domain models.
12
11
 
13
- Provides:
14
- * +Service+ base class for decoupling a domain business logics from a controller
15
- * +Models+ module for extending domain models.
12
+ == API
16
13
 
17
- The module is expected to be used in PORO domain. For usage in active record
18
- domains consider { hexx-active_record }[https://github.com/nepalez/hexx-active_record]
19
- gem instead.
14
+ Includes classes and modules as below:
15
+ <tt>Hexx::Service</tt>:: The base class for service objects.
16
+ <tt>Hexx::Service::Message</tt>:: The message provided by service objects.
17
+ <tt>Hexx::Null</tt>:: The Null object.
18
+ <tt>Hexx::Coercible</tt>:: The module that makes model attributes coercible.
19
+ <tt>Hexx::Configurable</tt>:: The module to convert a core domain module to the
20
+ dependency injection framework.
21
+ <tt>Hexx::Dependable</tt>:: The module provides +depends_on+ class helper methodto
22
+ to implement the setter dependency injection.
23
+
24
+ The module is expected to be used in PORO domains for Ruby MRI 2.1+.
25
+
26
+ For usage in active record bases domains consider the
27
+ { hexx-active_record }[https://github.com/nepalez/hexx-active_record]
28
+ gem extension.
20
29
 
21
30
  == Installation
22
31
 
@@ -32,47 +41,172 @@ Or install it yourself as:
32
41
 
33
42
  $ gem install hexx
34
43
 
35
- == Pattern
44
+ == Usage
45
+
46
+ === Hexx::Configurable
47
+
48
+ Adds the +configure+ and +depends_on+ helpers to the module to convert it
49
+ to the {dependency injection container}[http://en.m.wikipedia.org/wiki/Dependency_injection].
50
+
51
+ Extend the base class of the gem and declare the module dependencies from
52
+ outer classes and modules with the +depend_on+ helper:
53
+
54
+ # lib/my_gem.rb
55
+ module MyGem
56
+ extend Hexx::Configurable
57
+
58
+ depend_on :get_item, :add_item
59
+ end
60
+
61
+ Inject the dependencies in the gem config with the +configure+ wrapper:
62
+
63
+ # config/dependencies.rb
64
+ MyGem.configure do |c|
65
+ c.get_item = OuterModule::Services::Get
66
+ c.add_item = OuterModule::Services::Add
67
+ end
68
+
69
+ Use the dependencies somewhere inside the code of the gem:
70
+
71
+ MyGem.get_item # => OuterModule::Services::Get
72
+
73
+ === Hexx::Coercible
74
+
75
+ Adds the +attr_coerced+ class helper method to the PORO model.
76
+
77
+ Provide a value object that accepts <tt>0..1</tt> arguments.
78
+
79
+ # app/attributes/coercer.rb
80
+ class Coercer < MultiByte::Chars
81
+ def self.new(source = nil)
82
+ return unless source
83
+ end
84
+
85
+ def initialize(source)
86
+ # ...
87
+ end
88
+ end
89
+
90
+ Extend the model with a +Coercible+ module and declare its attributes
91
+ with the +attr_coerced+ helper.
92
+
93
+ # app/models/some_model.rb
94
+ class SomeModel
95
+ extend Hexx::Coercible
96
+
97
+ attr_coerced :name, type: Coercer
98
+ end
99
+
100
+ Both the getter and setter will return the coerced value, provided by
101
+ the +Coercer+ class.
102
+
103
+ object = SomeModel.new name: "Ivo"
104
+ object.name
105
+ # #<Coercer @wrapped_string="Ivo" >
106
+
107
+ Be careful when designing a coercer class. Its constructor should accept both
108
+ the raw value ("Ivo") and the coerced one (#<Coercer @wrapped_string = "Ivo">).
109
+ This is needed because the coercer works twofold - it coerces both the
110
+ setter and getter. The getter coercer will take the coerced value.
111
+
112
+ This feature is added for comparison with +ActiveRecord+ attributes coersion
113
+ where the getter is given with stored raw values (a string etc.)
36
114
 
37
- Service objects decouple business logics from:
115
+ *Note*: The coercer from +hexx+ gem itself won't work for +ActiveRecord+ models.
116
+ Use the +hexx-active_record+ gem instead. The gem extends the +Coercible+ model
117
+ so that the +attr_coerced+ reloads +ActiveRecord+ attributes properly.
38
118
 
39
- * Domain _models_ (entities).
40
- * Delivery mechanism _controllers_ (such as Rails framework).
119
+ === Hexx::Service
41
120
 
42
- To follow object oriented architecture Hexx services exploit the
43
- *Observer* (Listener) pattern via { Wisper }[https://github.com/krisleech/wisper]
44
- gem.
121
+ Inherit services from the <tt>Hexx::Service</tt> class.
45
122
 
46
- Some examples of this pattern implementation are available at the
47
- { wisper gem wiki }[https://github.com/krisleech/wisper/wiki].
123
+ The class implements a set of patterns:
48
124
 
49
- === Service
125
+ * The {Service object pattern}[http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/] used to decouple business logics from both the models and web delivery mechanism (such as +Rails+).
126
+ * The {Observer pattern}[http://reefpoints.dockyard.com/2013/08/20/design-patterns-observer-pattern.html] to follow the {Tell, don't ask}[http://martinfowler.com/bliki/TellDontAsk.html] design princible.
127
+ The pattern is implemented with the help of {wisper}[http://www.github.com/krisleech/wisper] gem by Kris Leech.
128
+ * The {Setter-based dependency injection}[http://brandonhilkert.com/blog/a-ruby-refactor-exploring-dependency-injection-options/] to decouple the service from another services it uses.
50
129
 
51
130
  A typical service object is shown below:
52
131
 
132
+ # app/services/add_item.rb
53
133
  require 'hexx'
54
-
55
134
  class AddItem < Hexx::Service
56
135
 
57
- # whitelists parameters and defines corresponding attributes.
136
+ # Injects the dependency from the service for getting an item.
137
+ # Provides default implementation for the dependency, that could be
138
+ # redefined later (in a test suite etc.).
139
+ depends_on :get_item, default: GetItem
140
+
141
+ # Whitelists parameters and defines corresponding attributes.
142
+ # For example, the #name attribute is avalable.
58
143
  allow_params :name
59
144
 
60
- # defines some validation using ActiveModel::Validations helpers.
145
+ # Defines some validation using ActiveModel::Validations helpers.
61
146
  validate :name, presence: true
62
147
 
63
- # runs a service
148
+ # Runs a service
64
149
  def run
65
- # re-raises StandardErrors as Hexx::Service::Invalid
66
- escape { MyModel.save! }
67
- rescue => err # Hexx::Service::Invalid only
150
+ run!
151
+ rescue Found
152
+ # Publishes notification in case the item exists.
153
+ publish :found, item
154
+ rescue => err
68
155
  publish :error, err.messages
69
156
  else
70
- publish :success
157
+ # The notification to be published if the #run! raises nothing.
158
+ publish :added, item
159
+ end
160
+
161
+ private
162
+
163
+ attr_accessor :item
164
+
165
+ # Errors to be raised by the #run! method call and captured in a #run.
166
+ class Found < StandardError; end
167
+
168
+ # The sequence of the service steps. Any step can raise error to
169
+ # be rescued in #run with publishing a corresponding notification.
170
+ def run!
171
+ find_item
172
+ add_item
71
173
  end
72
- end
73
174
 
74
- Usage of the service (in a Rails controller):
175
+ def find_item
176
+ # The method runs another service and listens to its notifications
177
+ # via private callback methods available to that service only.
178
+ # The callback names should start from given prefix (:on_item_).
179
+ run_service get_item, :on_item, name: name
180
+ end
181
+
182
+ # The callback to listen to :found notification of the 'get_item' service.
183
+ def on_item_found(item, *)
184
+ @item = item
185
+ # Adds the Hexx::Message object of type "error" to the +messages+ array.
186
+ # The :not_found key will be translated in context of current service:
187
+ # {locale}.activemodule.messages.models.add_item.not_found
188
+ add_message "error", :not_found
189
+ fail Found # goes to publishing a result
190
+ end
191
+
192
+ # The callback to listen to :error notification of the 'get_item' service.
193
+ # that is expected to publish a list of error messages.
194
+ def on_item_error(*, messages)
195
+ # The helper raises Hexx::Service::Invalid exception where the messages
196
+ # are added to. The exception will be rescued by the #run method.
197
+ on_error(messages)
198
+ end
199
+
200
+ def add_item
201
+ # The escape re-raises any error as the Hexx::Service::Invalid
202
+ # with the array of Hexx::Service::Message messages.
203
+ escape { @item = Item.create! name: name }
204
+ end
205
+ end
75
206
 
207
+ A typical usage of the service (in a Rails controller):
208
+
209
+ # app/controllers/items_controller.rb
76
210
  class ItemsController < ActionController::Base
77
211
 
78
212
  # Creates an item with given name
@@ -88,87 +222,83 @@ Usage of the service (in a Rails controller):
88
222
  render "created", status: 201
89
223
  end
90
224
 
225
+ # Responds with 304 (not changed)
226
+ def on_found(*)
227
+ render nothing: true, status: 304
228
+ end
229
+
91
230
  # Publishes an error messages
92
231
  def on_error(messages)
93
232
  @messages = messages
94
- render "error"
233
+ render "error", status: 422
95
234
  end
96
235
  end
97
236
 
98
237
  The controller knows nothing about the action itself. It only needs to
99
- sort out a request and send it to a corresponding service.
238
+ send the request to a corresponding service and sort out the notifications.
100
239
 
101
- Any controller action does one thing only.
240
+ === Hexx::Service::Message
102
241
 
103
- * +create+ action sorts out requests.
104
- * both +on_success+ and +on_created+ listens to services and report their
105
- results back to client.
242
+ The messages published by the service has two attributes: +type+ and +text+.
106
243
 
107
- === Models and Entities
244
+ message = Hexx::Service::Message.new type: :error, text: "some error message"
245
+ message.type # => "error"
246
+ message.text # => "some error message"
108
247
 
109
- The module also defines <tt>Hexx::Models</tt> module to extend domain models
110
- (entities). This allows coercion of attributes with +attr_coerced+ helper:
248
+ Inside a service use the +add_message+ to add message to the +messages+ array:
111
249
 
112
- class User
113
- extend Hexx::Models
114
- attr_coerced :name, type: ActiveSupport::Multibyte::Chars
115
- end
250
+ add_message "error", "text"
251
+ messages # => [#<Hexx::Service::Message @type="error", @text="text" >]
116
252
 
117
- user = User.new name: "Ivan"
118
-
119
- user.name
120
- # => "Ivan"
121
-
122
- user.name.class
123
- # => ActiveSupport::Multibyte::Chars
253
+ === Hexx::Null
124
254
 
125
- The method redefines both getter and setter and can be used for value
126
- preparation:
255
+ The class implements the {Null object}[http://robots.thoughtbot.com/rails-refactoring-example-introduce-null-object] pattern. The object:
127
256
 
128
- class StrippedString < String
129
- def initialize(value)
130
- super value.strip
131
- end
132
- end
257
+ * responds like +nil+ to +<=>+, +eq?+, +nil?+, +false?+, +true?+, +to_s+,
258
+ +to_i+, +to_f+, +to_c+, +to_r+, +to_nil+
259
+ * responds with +self+ to any other method call
133
260
 
134
- class User
135
- extend Hexx::Models
136
- attr_coerced :name, type: StrippedString
137
- end
261
+ Providing {this problem}[http://devblog.avdi.org/2011/05/30/null-objects-and-falsiness/], use double bang in logical expressions:
138
262
 
139
- user = User.name " Ivan "
140
- user.name # => "Ivan"
263
+ # Though:
264
+ Hexx::Null && true # => true
265
+
266
+ # But:
267
+ !!Hexx::Null && true # => false
141
268
 
142
- == Dependencies
269
+ === Hexx::Dependable
143
270
 
144
- The module provides methods +depends_on+ and +config+ to create
145
- dependency injection framework.
271
+ This module is a part of <tt>Hexx::Service</tt> that provides
272
+ setter dependency declaration +depends_on+.
146
273
 
147
- Declare gem dependencies (external classes and modules).
274
+ Extend the class and declare the dependency with optional default
275
+ implementation:
148
276
 
149
- # lib/my_gem.rb
150
- module MyGem
151
- extend Hexx::Dependencies
152
- depends_on :get_item, :add_item
277
+ class MyClass
278
+ extend Hexx::Dependable
279
+
280
+ depends_on :another_class, default: AhotherClass
281
+ depends_on :one_more_class
153
282
  end
154
283
 
155
- Inject gem dependencies:
284
+ Now the dependency can be injected afterwards:
156
285
 
157
- # config/my_gem.rb
158
- MyGem.configure do |config|
159
- config.get_item = AnotherGem::Services::Get # as a constant
160
- config.add_item = "AnotherGem::Services::Add" # as a name of a constant
161
- end
286
+ object = MyClass
162
287
 
163
- Use them somewhere in a code:
288
+ # The default implementation
289
+ object.another_class # => AnotherClass
164
290
 
165
- MyGem.get_item # => AnotherGem::Services::Get
166
- MyGem.add_item # => AnotherGem::Services::Add # the name is constantized
291
+ # Inject another implementation
292
+ object.another_class = NewClass
293
+ object.another_class # => NewClass
167
294
 
168
- == Relevant Links
295
+ # Reset it to default
296
+ object.another_class = nil
297
+ object.another_class # => AnotherClass
169
298
 
170
- * Matt Wynne's talk {Hexagonal Rails}[http://www.confreaks.com/videos/977-goruco2012-hexagonal-rails]
171
- * { wisper }[http://www.github.com/krisleech/wisper] gem by Kris Leech.
299
+ # Implementation is needed
300
+ object.one_more_class
301
+ # fails with a NotImplementedError
172
302
 
173
303
  == License
174
304
 
data/Rakefile CHANGED
@@ -19,6 +19,5 @@ task :check do
19
19
  system "coveralls report"
20
20
  system "rubocop"
21
21
  system "inch --pedantic"
22
- system "metric_fu --no-cane --no-churn --no-rails-best-practices " \
23
- "--no-hotspots --no-reek"
22
+ system "metric_fu --no-cane --no-saikuro --no-roodi --no-hotspots --no-stats"
24
23
  end
data/lib/hexx.rb CHANGED
@@ -5,6 +5,7 @@ require "wisper"
5
5
  # Loading gem code (the order is essential).
6
6
  root = File.expand_path "../hexx", __FILE__
7
7
  Dir[
8
+ "#{ root }/helpers/*.rb",
8
9
  "#{ root }/models/**/*.rb",
9
10
  "#{ root }/service/**/*.rb",
10
11
  "#{ root }/**/*.rb"
@@ -10,7 +10,7 @@ module Hexx
10
10
  # require_relative "attributes/string"
11
11
  #
12
12
  # class User
13
- # extend Hexx::Models
13
+ # extend Hexx::Coercible
14
14
  # attr_coerced :name, type: ActiveSupport::Multibyte::Chars
15
15
  # end
16
16
  #
@@ -18,26 +18,24 @@ module Hexx
18
18
  # user = User.new name: "Иоанн"
19
19
  # user.name
20
20
  # # => #<ActiveSupport::Multibyte::Chars @wrapped_string = "Иоанн">
21
- module Models
21
+ module Coercible
22
22
 
23
23
  private
24
24
 
25
+ # @api hide
26
+ def self.extended(klass)
27
+ klass.send :extend, Configurable
28
+ klass.send :depends_on, :coersion, default: Helpers::Coersion
29
+ end
30
+
25
31
  # @!method attr_coerced(*names, options)
26
32
  # Coerced the attribute(s) with given type.
27
- # @example (see Hexx::Models)
33
+ # @example (see Hexx::Coercible)
28
34
  # @param [Array<Symbol, String>] names The list of attributes to be coerced.
29
35
  # @param [Hash] options The coersion options.
30
36
  # @option options [Class] :type The class for coersion.
31
37
  def attr_coerced(*names, type:)
32
- names.each { |name| coercer.new(self, name, type).coerce }
33
- end
34
-
35
- # @api hide
36
- # Applied coercer for attributes.
37
- # @note The method is reloaded in the 'hexx-active_record' gem to allow
38
- # coersion of ActiveRecord models' attributes.
39
- def coercer
40
- @coercer ||= BaseCoercer
38
+ names.flatten.each { |name| coersion.add self, name, type }
41
39
  end
42
40
  end
43
41
  end
@@ -0,0 +1,101 @@
1
+ # encoding: utf-8
2
+
3
+ module Hexx
4
+
5
+ # Contains methods to declare and inject module dependencies.
6
+ #
7
+ # Allows to provide DJ framework for the gem.
8
+ #
9
+ # @example
10
+ # # Declare the dependencies for the gem root module
11
+ #
12
+ # # lib/my_gem.rb
13
+ # module MyGem
14
+ # extend Hexx::Configurable
15
+ # self.depends_on :get_item, :add_item
16
+ # self.depends_on :delete_item, default: DeleteItem
17
+ # end
18
+ #
19
+ # # Inject dependencies (as a constant or its name)
20
+ #
21
+ # # config/my_gem.rb
22
+ # MyGem.configure do |config|
23
+ # config.get_item = AnotherGem::Services::Get
24
+ # config.add_item
25
+ # end
26
+ #
27
+ # # Use dependency in the module code (models, services, etc.)
28
+ # # When the dependency hasn't been set, fails with +NotImplementedError+
29
+ #
30
+ # MyGem.get_item # => AnotherGem::Services::Get
31
+ # MyGem.get_item # => DeleteItem
32
+ # MyGem.add_item # fails with a NotImplementedError
33
+ module Configurable
34
+
35
+ # Yields a block and gives it self
36
+ #
37
+ # @example
38
+ # module MyGem
39
+ # extend Hexx::Configurable
40
+ # end
41
+ #
42
+ # MyGem.configure do |config|
43
+ # c # => MyGem
44
+ # end
45
+ #
46
+ # @yield The block.
47
+ # @yieldparam [Hexx::Configurable] +self+.
48
+ def configure
49
+ yield(self)
50
+ end
51
+
52
+ # @!method depends_on(*names, options)
53
+ # Declares a dependency getter and setter.
54
+ #
55
+ # @example
56
+ # module MyGem
57
+ # extend Hexx::Configurable
58
+ # self.depends_on :some_class
59
+ # end
60
+ #
61
+ # MyGem.some_class = String
62
+ # # => "String"
63
+ # MyGem.some_class # => String
64
+ #
65
+ # MyGem.some_class = "String"
66
+ # # => "String"
67
+ # MyGem.some_class # => String
68
+ #
69
+ # MyGem.some_class = :String
70
+ # # => "String"
71
+ # MyGem.some_class # => String
72
+ #
73
+ # @example Dependencies can be declared by a plain list of names
74
+ # module MyGem
75
+ # extend Hexx::Configurable
76
+ # self.depends_on :some_class, "another_class"
77
+ # end
78
+ #
79
+ # @example Dependencies can be declared by an array of names
80
+ # module MyGem
81
+ # extend Hexx::Configurable
82
+ # self.depends_on %w(some_class another_class)
83
+ # end
84
+ #
85
+ # @example A dependency can have default implementation
86
+ # module MyGem
87
+ # extend Hexx::Configurable
88
+ # self.depends_on :item, default: Item
89
+ # end
90
+ #
91
+ # @param [Array<String, Symbol>] names The list of names for dependencies.
92
+ # @param [Hash] options The list of options for dependency declaration.
93
+ # @option options [Module, nil] :default (nil) The default implementation
94
+ # for the dependency.
95
+ def depends_on(*names, default: nil)
96
+ names.flatten.each do |name|
97
+ Helpers::ModuleDependency.add self, name, default
98
+ end
99
+ end
100
+ end
101
+ end