hexx 0.0.1

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.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +45 -0
  3. data/CHANGELOG.rdoc +1 -0
  4. data/LICENSE.rdoc +21 -0
  5. data/README.rdoc +235 -0
  6. data/Rakefile +31 -0
  7. data/bin/hexx +54 -0
  8. data/lib/generators/base.rb +59 -0
  9. data/lib/generators/controller/controller.rb +87 -0
  10. data/lib/generators/controller/templates/controller.erb +18 -0
  11. data/lib/generators/controller/templates/controller_action.erb +8 -0
  12. data/lib/generators/controller/templates/controller_action_spec.erb +28 -0
  13. data/lib/generators/controller/templates/controller_spec.erb +52 -0
  14. data/lib/generators/controller/templates/routing_action_spec.erb +10 -0
  15. data/lib/generators/controller/templates/routing_spec.erb +10 -0
  16. data/lib/generators/dependency/dependency.rb +34 -0
  17. data/lib/generators/dependency/templates/dependency_setting.erb +4 -0
  18. data/lib/generators/dependency/templates/dependency_setting_spec.erb +34 -0
  19. data/lib/generators/dependency/templates/module_spec.erb +22 -0
  20. data/lib/generators/domain/domain.rb +24 -0
  21. data/lib/generators/domain/templates/spec.erb +84 -0
  22. data/lib/generators/install/install.rb +115 -0
  23. data/lib/generators/install/templates/CHANGELOG.erb +1 -0
  24. data/lib/generators/install/templates/Gemfile.erb +5 -0
  25. data/lib/generators/install/templates/LICENSE.erb +21 -0
  26. data/lib/generators/install/templates/README.erb +55 -0
  27. data/lib/generators/install/templates/Rakefile.erb +34 -0
  28. data/lib/generators/install/templates/bin/rails.erb +11 -0
  29. data/lib/generators/install/templates/config/routes.erb +6 -0
  30. data/lib/generators/install/templates/gemspec.erb +29 -0
  31. data/lib/generators/install/templates/lib/engine.erb +12 -0
  32. data/lib/generators/install/templates/lib/lib.erb +10 -0
  33. data/lib/generators/install/templates/lib/version.erb +4 -0
  34. data/lib/generators/install/templates/spec/coveralls.erb +4 -0
  35. data/lib/generators/install/templates/spec/database_cleaner.erb +27 -0
  36. data/lib/generators/install/templates/spec/factory_girl.erb +6 -0
  37. data/lib/generators/install/templates/spec/factory_girl_rails.erb +1 -0
  38. data/lib/generators/install/templates/spec/focus.erb +5 -0
  39. data/lib/generators/install/templates/spec/garbage_collection.erb +11 -0
  40. data/lib/generators/install/templates/spec/i18n.erb +1 -0
  41. data/lib/generators/install/templates/spec/migrations.erb +3 -0
  42. data/lib/generators/install/templates/spec/rails.erb +6 -0
  43. data/lib/generators/install/templates/spec/random_order.erb +4 -0
  44. data/lib/generators/install/templates/spec/rspec.erb +5 -0
  45. data/lib/generators/install/templates/spec/spec_helper.erb +12 -0
  46. data/lib/generators/install/templates/spec/timecop.erb +1 -0
  47. data/lib/generators/request/request.rb +52 -0
  48. data/lib/generators/request/templates/request_spec.erb +73 -0
  49. data/lib/generators/use_case/templates/use_case.erb +29 -0
  50. data/lib/generators/use_case/templates/use_case_spec.erb +77 -0
  51. data/lib/generators/use_case/use_case.rb +31 -0
  52. data/lib/hexx.rb +2 -0
  53. data/lib/hexx/exceptions/not_found_error.rb +12 -0
  54. data/lib/hexx/exceptions/record_invalid.rb +12 -0
  55. data/lib/hexx/exceptions/runtime_error.rb +24 -0
  56. data/lib/hexx/exceptions/use_case_invalid.rb +12 -0
  57. data/lib/hexx/models.rb +82 -0
  58. data/lib/hexx/settings.rb +47 -0
  59. data/lib/hexx/use_case.rb +228 -0
  60. data/lib/hexx/version.rb +4 -0
  61. data/spec/hexx/exceptions/not_found_error_spec.rb +27 -0
  62. data/spec/hexx/exceptions/record_invalid_spec.rb +27 -0
  63. data/spec/hexx/exceptions/runtime_error_spec.rb +61 -0
  64. data/spec/hexx/exceptions/use_case_invalid_spec.rb +27 -0
  65. data/spec/hexx/models_spec.rb +64 -0
  66. data/spec/hexx/settings_spec.rb +51 -0
  67. data/spec/hexx/use_case_spec.rb +262 -0
  68. data/spec/spec_helper.rb +3 -0
  69. data/spec/support/initializers/coveralls.rb +3 -0
  70. data/spec/support/initializers/focus.rb +5 -0
  71. data/spec/support/initializers/garbage_collection.rb +11 -0
  72. data/spec/support/initializers/i18n.rb +1 -0
  73. data/spec/support/initializers/random_order.rb +4 -0
  74. data/spec/support/initializers/rspec.rb +5 -0
  75. metadata +236 -0
@@ -0,0 +1,77 @@
1
+ require "spec_helper"
2
+
3
+ module <%= module_name %>
4
+ describe <%= class_name %> do
5
+
6
+ # ==========================================================================
7
+ # Prepare environment
8
+ # ==========================================================================
9
+
10
+ # before { Timecop.freeze }
11
+ # after { Timecop.return }
12
+
13
+ # ==========================================================================
14
+ # Prepare variables
15
+ # ==========================================================================
16
+
17
+ # let!(:params) { { key: value } }
18
+ # let!(:expected_result) { { something.to_struct.inspect } }
19
+ # let!(:listener) { double "listener" }
20
+
21
+ # def prepare_case(params)
22
+ # use_case = <%= class_name %>.new params
23
+ # use_case.subscribe(listener)
24
+ # use_case
25
+ # end
26
+
27
+ # ==========================================================================
28
+ # Run tests
29
+ # ==========================================================================
30
+
31
+ describe "#run" do
32
+
33
+ # context "with proper params" do
34
+
35
+ # let!(:use_case) { prepare_case params }
36
+
37
+ # it "does something" do
38
+ # expect { use_case.run }.to change { something }
39
+ # .from(something).to(something)
40
+ # end
41
+
42
+ # it "returns something" do
43
+ # expect(use_case.run.inspect).to eq expected_result
44
+ # end
45
+
46
+ # it "sends 'something' to subscribers" do
47
+ # expect(listener).to receive :something do |result|
48
+ # expect(result.inspect).to eq expected_result
49
+ # end
50
+ # use_case.run
51
+ # end
52
+ # end
53
+
54
+ # context "with some improper params" do
55
+
56
+ # before { params[:something] = something }
57
+ # let!(:use_case) { prepare_case params }
58
+
59
+ # it "doesn't do something" do
60
+ # expect { use_case.run }.not_to change { something }
61
+ # .from something
62
+ # end
63
+
64
+ # it "returns nil" do
65
+ # expect(use_case.run).to be_nil
66
+ # end
67
+
68
+ # it "sends 'error' to subscribers" do
69
+ # expect(listener).to receive :error do |messages|
70
+ # expect(messages).not_to be_blank
71
+ # end
72
+ # use_case.run
73
+ # end
74
+ # end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,31 @@
1
+ require_relative "../base"
2
+
3
+ module Hexx
4
+ module Generators
5
+
6
+ # Use case scaffolder.
7
+ class UseCase < Base
8
+
9
+ def self.source_root
10
+ super __FILE__
11
+ end
12
+
13
+ def add_use_case
14
+ template "use_case.erb", "app/#{ use_cases_path }/#{ file_name }.rb"
15
+ end
16
+
17
+ def add_use_case_spec
18
+ template(
19
+ "use_case_spec.erb",
20
+ "spec/#{ use_cases_path }/#{ file_name }_spec.rb"
21
+ )
22
+ end
23
+
24
+ private
25
+
26
+ def use_cases_path
27
+ "use_cases/#{ gem_name }"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,2 @@
1
+ lib = File.dirname(__FILE__)
2
+ Dir[File.join(lib, "hexx/**/*.rb")].each { |file| require file }
@@ -0,0 +1,12 @@
1
+ require_relative "runtime_error"
2
+
3
+ module Hexx
4
+
5
+ # An exception to be raised in case some record not found
6
+ class NotFoundError < RuntimeError
7
+
8
+ def message
9
+ "Not found: #{ errors.inspect }"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ require_relative "runtime_error"
2
+
3
+ module Hexx
4
+
5
+ # An exception to be raised by the <tt>Hexx::Models#validate!</tt> method.
6
+ class RecordInvalid < RuntimeError
7
+
8
+ def message
9
+ "Invalid use case: #{ errors.inspect }"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,24 @@
1
+ module Hexx
2
+
3
+ # An exception to be raised by some method with given object.
4
+ #
5
+ # It is expected, that the object stores error messages in its <tt>errors</tt>
6
+ # collection.
7
+ #
8
+ class RuntimeError < ::RuntimeError
9
+
10
+ attr_reader :object
11
+
12
+ def initialize(object)
13
+ @object = object
14
+ end
15
+
16
+ def errors
17
+ object.errors if object
18
+ end
19
+
20
+ def message
21
+ "Runtime error: #{ errors.inspect }"
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,12 @@
1
+ require_relative "runtime_error"
2
+
3
+ module Hexx
4
+
5
+ # An exception to be raised by the <tt>UseCase#validate!</tt> method.
6
+ class UseCaseInvalid < RuntimeError
7
+
8
+ def message
9
+ "Invalid use case: #{ errors.inspect }"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,82 @@
1
+ require "active_support"
2
+
3
+ module Hexx
4
+
5
+ # Module defines:
6
+ #
7
+ # * <tt>validate!</tt> public instance method
8
+ # * +attr_coerced+ public class methods.
9
+ #
10
+ # Include the module into the Rails model:
11
+ #
12
+ # require "hexx"
13
+ # require_relative "attributes/string"
14
+ #
15
+ # class User < ActiveRecord::Base
16
+ # include Hexx::Models
17
+ #
18
+ # # coerces attributes
19
+ # attr_coerced :name, login, type: Attributes::String
20
+ # end
21
+ #
22
+ module Models
23
+ extend ActiveSupport::Concern
24
+
25
+ def validate!
26
+ return true if valid?
27
+ fail RecordInvalid.new self
28
+ end
29
+
30
+ # Model class helpers for attributes coercion.
31
+ module ClassMethods
32
+
33
+ def attr_coerced(*names, type:)
34
+ names.each { |name| _attr_coerced(name, type) }
35
+ end
36
+
37
+ private
38
+
39
+ def _attr_coerced(name, type)
40
+ if ancestors.map(&:name).include? "ActiveRecord::Base"
41
+ coerce_activerecord_reader name, type
42
+ coerce_activerecord_writer name, type
43
+ else
44
+ coerce_simple_reader name, type
45
+ coerce_simple_writer name, type
46
+ end
47
+ end
48
+
49
+ def coerce_simple_reader(name, type)
50
+ class_eval(
51
+ "def #{ name };
52
+ #{ type.name }.new(@#{ name });
53
+ end"
54
+ )
55
+ end
56
+
57
+ def coerce_simple_writer(name, type)
58
+ class_eval(
59
+ "def #{ name }=(value);
60
+ @#{ name } = #{ type.name }.new(value);
61
+ end"
62
+ )
63
+ end
64
+
65
+ def coerce_activerecord_reader(name, type)
66
+ class_eval(
67
+ "def #{ name };
68
+ #{ type.name }.new read_attribute(:#{ name });
69
+ end"
70
+ )
71
+ end
72
+
73
+ def coerce_activerecord_writer(name, type)
74
+ class_eval(
75
+ "def #{ name }=(value);
76
+ write_attribute :#{ name }, #{ type.name }.new(value);
77
+ end"
78
+ )
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,47 @@
1
+ require "active_support"
2
+
3
+ module Hexx
4
+
5
+ # Storage for dependencies.
6
+ #
7
+ # Include it to your domain module and declare necessary dependencies.
8
+ #
9
+ # module MyProject
10
+ # include Hexx::Settigns
11
+ # class << self
12
+ #
13
+ # depends_on :some_class
14
+ # end
15
+ # end
16
+ #
17
+ # Then you can add a setting:
18
+ #
19
+ # # config/initializers/my_project.rb
20
+ # MyProject.configure do |c|
21
+ # c.some_class_name = "ExternalModule::SomeClass"
22
+ # end
23
+ #
24
+ # And use it in a code:
25
+ #
26
+ # MyProject.some_class # => ExternalModule::SomeClass
27
+ #
28
+ module Settings
29
+ extend ActiveSupport::Concern
30
+
31
+ # Settings helpers
32
+ module ClassMethods
33
+
34
+ def configure(&block)
35
+ block.call(self) if block_given?
36
+ end
37
+
38
+ def depends_on(name)
39
+ cattr_accessor "#{ name }_name"
40
+ define_singleton_method name do
41
+ const = send "#{ name }_name"
42
+ const ? Kernel.const_get(const) : nil
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,228 @@
1
+ require "active_model"
2
+ require "wisper"
3
+
4
+ module Hexx
5
+
6
+ # = About
7
+ #
8
+ # Use cases are a core part of a domain. They implement case-specific business
9
+ # rules (unlike Entities) and named as an imperative (_Add_Doc_, _Get_Doc_).
10
+ #
11
+ # Typical use case provides 5 methods:
12
+ #
13
+ # +new+:: class method that initializes use case instance.
14
+ # +subscribe+:: subscribes listeners for the use case notifications.
15
+ # +run+:: implements the use case.
16
+ # +run!+:: raises exceptions in case of errors.
17
+ # +errors+:: collects use case errors.
18
+ #
19
+ # The +run+ method returns a corresponding Value object, and notifies
20
+ # subscribers about the results (following The Observer Pattern).
21
+ #
22
+ # = Usage
23
+ #
24
+ # Inherit a use case from the <tt>Hexx::UseCase</tt> class:
25
+ #
26
+ # # app/my_domain/use_cases/do_something.rb
27
+ # require "hexx"
28
+ #
29
+ # module MyDomain
30
+ # class DoSomething < Hexx::UseCase
31
+ # end
32
+ # end
33
+ #
34
+ # Then add a <tt>run!</tt> instance method to the Use Case.
35
+ #
36
+ # class DoSomething < Hexx::UseCase
37
+ #
38
+ # def run!
39
+ # validate!
40
+ # # do something
41
+ # end
42
+ # end
43
+ #
44
+ # The +run+ (without a bang) method is defined by default (see below). If
45
+ # you need catching some exceptions specifically, do it in your <tt>run!</tt>
46
+ # method.
47
+ #
48
+ # Unless the <tt>run!</tt> method defined, calling the +run+ raises
49
+ # the <tt>NotImplementedError</tt>.
50
+ #
51
+ # == Allow params
52
+ #
53
+ # Use case constructor takes one argument with a parameters hash.
54
+ #
55
+ # use_case = DoSomething.new id: 1, name: "name"
56
+ #
57
+ # This sets the private argument +params+ to be blank hash.
58
+ #
59
+ # use_case.send :params # => {}
60
+ #
61
+ # For options to be assigned to +params+, their keys should be whitelisted:
62
+ #
63
+ # class DoSomething < Hexx::UseCase
64
+ #
65
+ # allow_params :id, :name
66
+ # end
67
+ #
68
+ # This will allow assigning values. Note that all the keys are stringified:
69
+ #
70
+ # use_case = DoSomething.new id: 1, name: "name", wrong_key: :value
71
+ # use_case.send :params # => { "id" => 1, "name" => "name" }
72
+ #
73
+ # == Validations
74
+ #
75
+ # You can use ActiveRecord validations.
76
+ #
77
+ # Be careful! Both the <tt>valid?</tt>, and <tt>invalid?</tt> are private.
78
+ # It is expected validations to be used implicitly in a course of use case
79
+ # running.
80
+ #
81
+ # To do this a private method <tt>validate!</tt> is available. It raises the
82
+ # <tt>Hexx::UseCaseInvalid</tt> exception in case of validation fails.
83
+ #
84
+ # Note the <tt>validate!</tt> private method call from the <tt>run!</tt>
85
+ # method in the example above.
86
+ #
87
+ # == Running a use case
88
+ #
89
+ # The <tt>run</tt> method is defined in a base class. This method
90
+ # catches some exceptions and publishes corresponding notifications:
91
+ #
92
+ # <tt>Hexx::NotFoundError</tt>:: publishes the <tt>not_found(messages)</tt>;
93
+ # <tt>Hexx::UseCaseInvalid</tt>:: publishes the <tt>error(messages)</tt>;
94
+ # <tt>Hexx::EntityInvalid</tt>:: publishes the <tt>error(messages)</tt>;
95
+ # <tt>StandardError</tt>:: any other runtume exception.
96
+ #
97
+ # == Notifications publishing
98
+ #
99
+ # A use case is expected to publish notifications for its subscribers.
100
+ #
101
+ # class DoSomething < Hexx::UseCase
102
+ #
103
+ # def run!
104
+ # validate!
105
+ # # do something (raise in case of any error)
106
+ # publish :done, result
107
+ # return result
108
+ # end
109
+ # end
110
+ #
111
+ # This will call a +done+ method of any subscriber. For details see the
112
+ # {wisper gem documentation}[https://github.com/krisleech/wisper].
113
+ #
114
+ # == Calling a use case
115
+ #
116
+ # Use cases can be called in two styles:
117
+ #
118
+ # === Observer Pattern style (main usage)
119
+ #
120
+ # From a controller you can call a use case:
121
+ #
122
+ # # app/controllers/my_controller.rb
123
+ # class MyController < ActionController::Base
124
+ #
125
+ # def my_action
126
+ # # initialize a use case
127
+ # use_case = DoSomething.new params
128
+ # # subscribe both controller (in a presenter role) and other services
129
+ # # such as mailers etc. to receive notifications.
130
+ # use_case.subscribe self
131
+ # use_case.subscribe MyMailer.new
132
+ # # run a use_case
133
+ # use_case.run
134
+ # end
135
+ #
136
+ # # the method will be called by <tt>use_case.run</tt> in case of
137
+ # # success (see the Notification publishing example above).
138
+ # def done(result)
139
+ # # return a response 200 to the user
140
+ # end
141
+ #
142
+ # # the method will be called by <tt>use_case.run</tt> in case of
143
+ # # NotFoundError raised.
144
+ # def not_found(options = {})
145
+ # # return a response 404 to the user
146
+ # end
147
+ #
148
+ # # this method will be called by use_case in case of any error
149
+ # def error(messages = [])
150
+ # # return a response 422 to the user
151
+ # end
152
+ # end
153
+ #
154
+ # === Procedural style
155
+ #
156
+ # When you use a case from another case it can be useful to get value directly
157
+ # without a subscription.
158
+ #
159
+ # use_case = DoSomething.new params
160
+ # result = use_case.run
161
+ #
162
+ # The +run+ method returns nil in case of any error.
163
+ #
164
+ # = Dependencies notes
165
+ #
166
+ # Use cases depends on:
167
+ #
168
+ # * *Entities* and their *Repositories*;
169
+ # * *Values* as an interfaces to external services (controllers, mailers etc.)
170
+ #
171
+ # Use cases should not depend from external services outside of the domain
172
+ # model: controllers, mailers, databases etc.
173
+ #
174
+ class UseCase
175
+ include ActiveModel::Validations, Wisper::Publisher
176
+
177
+ class << self
178
+
179
+ def allow_params(*keys)
180
+ @params = keys.map(&:to_s)
181
+ end
182
+
183
+ private
184
+
185
+ def params
186
+ @params ||= []
187
+ end
188
+ end
189
+
190
+ def initialize(options = {})
191
+ if options.is_a? Hash
192
+ @params = options.stringify_keys.slice(*self.class.send(:params))
193
+ else
194
+ @params = {}
195
+ end
196
+ end
197
+
198
+ def run!
199
+ fail(NotImplementedError.new "#{ self.class.name }#run! not implemented")
200
+ end
201
+
202
+ def run
203
+ run!
204
+ rescue Hexx::NotFoundError => error
205
+ finish_with :not_found, error.errors.values
206
+ rescue Hexx::RuntimeError => error
207
+ finish_with :error, error.errors.values
208
+ rescue StandardError => error
209
+ finish_with :error, [error.message]
210
+ end
211
+
212
+ private :valid?, :invalid?
213
+
214
+ private
215
+
216
+ attr_reader :params
217
+
218
+ def validate!
219
+ return if valid?
220
+ fail UseCaseInvalid.new(self)
221
+ end
222
+
223
+ def finish_with(name, arg)
224
+ publish name, arg
225
+ nil
226
+ end
227
+ end
228
+ end