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.
- checksums.yaml +7 -0
- data/.rubocop.yml +45 -0
- data/CHANGELOG.rdoc +1 -0
- data/LICENSE.rdoc +21 -0
- data/README.rdoc +235 -0
- data/Rakefile +31 -0
- data/bin/hexx +54 -0
- data/lib/generators/base.rb +59 -0
- data/lib/generators/controller/controller.rb +87 -0
- data/lib/generators/controller/templates/controller.erb +18 -0
- data/lib/generators/controller/templates/controller_action.erb +8 -0
- data/lib/generators/controller/templates/controller_action_spec.erb +28 -0
- data/lib/generators/controller/templates/controller_spec.erb +52 -0
- data/lib/generators/controller/templates/routing_action_spec.erb +10 -0
- data/lib/generators/controller/templates/routing_spec.erb +10 -0
- data/lib/generators/dependency/dependency.rb +34 -0
- data/lib/generators/dependency/templates/dependency_setting.erb +4 -0
- data/lib/generators/dependency/templates/dependency_setting_spec.erb +34 -0
- data/lib/generators/dependency/templates/module_spec.erb +22 -0
- data/lib/generators/domain/domain.rb +24 -0
- data/lib/generators/domain/templates/spec.erb +84 -0
- data/lib/generators/install/install.rb +115 -0
- data/lib/generators/install/templates/CHANGELOG.erb +1 -0
- data/lib/generators/install/templates/Gemfile.erb +5 -0
- data/lib/generators/install/templates/LICENSE.erb +21 -0
- data/lib/generators/install/templates/README.erb +55 -0
- data/lib/generators/install/templates/Rakefile.erb +34 -0
- data/lib/generators/install/templates/bin/rails.erb +11 -0
- data/lib/generators/install/templates/config/routes.erb +6 -0
- data/lib/generators/install/templates/gemspec.erb +29 -0
- data/lib/generators/install/templates/lib/engine.erb +12 -0
- data/lib/generators/install/templates/lib/lib.erb +10 -0
- data/lib/generators/install/templates/lib/version.erb +4 -0
- data/lib/generators/install/templates/spec/coveralls.erb +4 -0
- data/lib/generators/install/templates/spec/database_cleaner.erb +27 -0
- data/lib/generators/install/templates/spec/factory_girl.erb +6 -0
- data/lib/generators/install/templates/spec/factory_girl_rails.erb +1 -0
- data/lib/generators/install/templates/spec/focus.erb +5 -0
- data/lib/generators/install/templates/spec/garbage_collection.erb +11 -0
- data/lib/generators/install/templates/spec/i18n.erb +1 -0
- data/lib/generators/install/templates/spec/migrations.erb +3 -0
- data/lib/generators/install/templates/spec/rails.erb +6 -0
- data/lib/generators/install/templates/spec/random_order.erb +4 -0
- data/lib/generators/install/templates/spec/rspec.erb +5 -0
- data/lib/generators/install/templates/spec/spec_helper.erb +12 -0
- data/lib/generators/install/templates/spec/timecop.erb +1 -0
- data/lib/generators/request/request.rb +52 -0
- data/lib/generators/request/templates/request_spec.erb +73 -0
- data/lib/generators/use_case/templates/use_case.erb +29 -0
- data/lib/generators/use_case/templates/use_case_spec.erb +77 -0
- data/lib/generators/use_case/use_case.rb +31 -0
- data/lib/hexx.rb +2 -0
- data/lib/hexx/exceptions/not_found_error.rb +12 -0
- data/lib/hexx/exceptions/record_invalid.rb +12 -0
- data/lib/hexx/exceptions/runtime_error.rb +24 -0
- data/lib/hexx/exceptions/use_case_invalid.rb +12 -0
- data/lib/hexx/models.rb +82 -0
- data/lib/hexx/settings.rb +47 -0
- data/lib/hexx/use_case.rb +228 -0
- data/lib/hexx/version.rb +4 -0
- data/spec/hexx/exceptions/not_found_error_spec.rb +27 -0
- data/spec/hexx/exceptions/record_invalid_spec.rb +27 -0
- data/spec/hexx/exceptions/runtime_error_spec.rb +61 -0
- data/spec/hexx/exceptions/use_case_invalid_spec.rb +27 -0
- data/spec/hexx/models_spec.rb +64 -0
- data/spec/hexx/settings_spec.rb +51 -0
- data/spec/hexx/use_case_spec.rb +262 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/support/initializers/coveralls.rb +3 -0
- data/spec/support/initializers/focus.rb +5 -0
- data/spec/support/initializers/garbage_collection.rb +11 -0
- data/spec/support/initializers/i18n.rb +1 -0
- data/spec/support/initializers/random_order.rb +4 -0
- data/spec/support/initializers/rspec.rb +5 -0
- 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
|
data/lib/hexx.rb
ADDED
@@ -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
|
data/lib/hexx/models.rb
ADDED
@@ -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
|