hexx 5.4.0 → 6.0.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 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