hexx 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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