granite 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/app/controllers/granite/controller.rb +44 -0
- data/lib/generators/USAGE +25 -0
- data/lib/generators/granite/install_controller_generator.rb +15 -0
- data/lib/generators/granite_generator.rb +32 -0
- data/lib/generators/templates/granite_action.rb.erb +22 -0
- data/lib/generators/templates/granite_action_spec.rb.erb +45 -0
- data/lib/generators/templates/granite_base_action.rb.erb +2 -0
- data/lib/generators/templates/granite_business_action.rb.erb +3 -0
- data/lib/granite.rb +24 -0
- data/lib/granite/action.rb +106 -0
- data/lib/granite/action/error.rb +14 -0
- data/lib/granite/action/performer.rb +23 -0
- data/lib/granite/action/performing.rb +132 -0
- data/lib/granite/action/policies.rb +92 -0
- data/lib/granite/action/policies/always_allow_strategy.rb +13 -0
- data/lib/granite/action/policies/any_strategy.rb +12 -0
- data/lib/granite/action/policies/required_performer_strategy.rb +14 -0
- data/lib/granite/action/preconditions.rb +107 -0
- data/lib/granite/action/preconditions/base_precondition.rb +25 -0
- data/lib/granite/action/preconditions/embedded_precondition.rb +42 -0
- data/lib/granite/action/projectors.rb +100 -0
- data/lib/granite/action/represents.rb +26 -0
- data/lib/granite/action/represents/attribute.rb +90 -0
- data/lib/granite/action/represents/reflection.rb +15 -0
- data/lib/granite/action/subject.rb +73 -0
- data/lib/granite/action/transaction.rb +40 -0
- data/lib/granite/action/translations.rb +39 -0
- data/lib/granite/action/types.rb +1 -0
- data/lib/granite/action/types/collection.rb +13 -0
- data/lib/granite/config.rb +23 -0
- data/lib/granite/context.rb +28 -0
- data/lib/granite/dispatcher.rb +64 -0
- data/lib/granite/error.rb +4 -0
- data/lib/granite/performer_proxy.rb +34 -0
- data/lib/granite/performer_proxy/proxy.rb +31 -0
- data/lib/granite/projector.rb +48 -0
- data/lib/granite/projector/controller_actions.rb +47 -0
- data/lib/granite/projector/error.rb +14 -0
- data/lib/granite/projector/helpers.rb +59 -0
- data/lib/granite/projector/translations.rb +52 -0
- data/lib/granite/projector/translations/helper.rb +22 -0
- data/lib/granite/projector/translations/view_helper.rb +12 -0
- data/lib/granite/rails.rb +11 -0
- data/lib/granite/routing.rb +4 -0
- data/lib/granite/routing/cache.rb +24 -0
- data/lib/granite/routing/caching.rb +18 -0
- data/lib/granite/routing/declarer.rb +25 -0
- data/lib/granite/routing/mapper.rb +15 -0
- data/lib/granite/routing/mapping.rb +23 -0
- data/lib/granite/routing/route.rb +29 -0
- data/lib/granite/rspec.rb +5 -0
- data/lib/granite/rspec/action_helpers.rb +8 -0
- data/lib/granite/rspec/have_projector.rb +24 -0
- data/lib/granite/rspec/projector_helpers.rb +54 -0
- data/lib/granite/rspec/raise_validation_error.rb +52 -0
- data/lib/granite/rspec/satisfy_preconditions.rb +96 -0
- metadata +338 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'granite/projector/error'
|
2
|
+
|
3
|
+
module Granite
|
4
|
+
class Projector
|
5
|
+
class ActionNotMountedError < Error
|
6
|
+
def initialize(projector)
|
7
|
+
super("Seems like #{projector.class} was not mounted. \
|
8
|
+
Do you have #{projector.action_class.name.underscore}##{projector.projector_name} declared in routes?", projector)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module Helpers
|
13
|
+
extend ActiveSupport::Concern
|
14
|
+
|
15
|
+
def view_context
|
16
|
+
Granite.view_context
|
17
|
+
end
|
18
|
+
alias h view_context
|
19
|
+
|
20
|
+
def action_url(action, **options)
|
21
|
+
action_path = controller_actions[action.to_sym].fetch(:as, action)
|
22
|
+
params = required_params.merge(projector_action: action_path)
|
23
|
+
|
24
|
+
Rails.application.routes.url_for(
|
25
|
+
options.reverse_merge(url_options).merge!(params),
|
26
|
+
corresponding_route.name
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
def action_path(action, **options)
|
31
|
+
action_url(action, **options, only_path: true)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def required_params
|
37
|
+
corresponding_route.required_parts
|
38
|
+
.map { |name| [name, action.public_send(name)] }
|
39
|
+
.to_h
|
40
|
+
end
|
41
|
+
|
42
|
+
def corresponding_route
|
43
|
+
@corresponding_route ||= fetch_corresponding_route
|
44
|
+
end
|
45
|
+
|
46
|
+
def route_id
|
47
|
+
[action_class.name.underscore, projector_name]
|
48
|
+
end
|
49
|
+
|
50
|
+
def url_options
|
51
|
+
h&.url_options || {}
|
52
|
+
end
|
53
|
+
|
54
|
+
def fetch_corresponding_route
|
55
|
+
Rails.application.routes.routes.granite_cache[*route_id] || fail(ActionNotMountedError, self)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Granite
|
2
|
+
class Projector
|
3
|
+
module Translations
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
class TranslationsWrapper
|
7
|
+
include ActionView::Helpers::TranslationHelper
|
8
|
+
end
|
9
|
+
|
10
|
+
def translate(*args)
|
11
|
+
TranslationsWrapper.new.translate(*self.class.scope_translation_args_by_projector(args))
|
12
|
+
end
|
13
|
+
alias t translate
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
def scope_translation_args_by_projector(args, action_name: nil)
|
17
|
+
options = args.extract_options!
|
18
|
+
|
19
|
+
lookups = expand_relative_key(args.first, action_name).map(&:to_sym)
|
20
|
+
lookups += [options[:default]]
|
21
|
+
lookups = lookups.flatten.compact
|
22
|
+
|
23
|
+
key = lookups.shift
|
24
|
+
options[:default] = lookups
|
25
|
+
|
26
|
+
[key, options]
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def expand_relative_key(key, action_name = nil)
|
32
|
+
return [key] unless key.is_a?(String) && key.start_with?('.')
|
33
|
+
|
34
|
+
base_keys = extract_base_keys(key, action_name)
|
35
|
+
|
36
|
+
action_class.lookup_ancestors.map do |klass|
|
37
|
+
base_keys.map do |base_key|
|
38
|
+
:"#{klass.i18n_scope}.#{klass.model_name.i18n_key}.#{base_key}"
|
39
|
+
end
|
40
|
+
end.flatten + base_keys
|
41
|
+
end
|
42
|
+
|
43
|
+
def extract_base_keys(key, action_name)
|
44
|
+
undotted_key = key.sub(/^\./, '')
|
45
|
+
base_keys = [:"#{projector_name}.#{undotted_key}"]
|
46
|
+
base_keys.unshift :"#{projector_name}.#{action_name}.#{undotted_key}" if action_name
|
47
|
+
base_keys
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'granite/projector/translations/view_helper'
|
2
|
+
|
3
|
+
module Granite
|
4
|
+
class Projector
|
5
|
+
module Translations
|
6
|
+
module Helper
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
delegate :scope_translation_args_by_projector, to: :projector_class
|
11
|
+
helper_method :scope_translation_args_by_projector
|
12
|
+
helper ViewHelper
|
13
|
+
end
|
14
|
+
|
15
|
+
def translate(*args)
|
16
|
+
super(*scope_translation_args_by_projector(args, action_name: action_name))
|
17
|
+
end
|
18
|
+
alias t translate
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Granite
|
2
|
+
# Core module for Rails extension. Implements some framework initialization,
|
3
|
+
# like setting up proper load paths.
|
4
|
+
class Railtie < ::Rails::Engine
|
5
|
+
isolate_namespace Granite
|
6
|
+
|
7
|
+
initializer 'granite.business_actions_paths', before: :set_autoload_paths do |app|
|
8
|
+
app.config.paths.add 'apq', eager_load: true, glob: '{actions,projectors}{,/concerns}'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Granite
|
2
|
+
module Routing
|
3
|
+
class Cache
|
4
|
+
attr_reader :routes
|
5
|
+
|
6
|
+
def initialize(routes)
|
7
|
+
@routes = routes
|
8
|
+
end
|
9
|
+
|
10
|
+
def [](action, projector)
|
11
|
+
projector = projector.to_s
|
12
|
+
Array(grouped_routes[action.to_s]).detect do |route|
|
13
|
+
route.required_defaults[:granite_projector] == projector
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def grouped_routes
|
20
|
+
@grouped_routes ||= routes.group_by { |r| r.required_defaults[:granite_action] }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'granite/routing/cache'
|
2
|
+
|
3
|
+
module Granite
|
4
|
+
module Routing
|
5
|
+
module Caching
|
6
|
+
def granite_cache
|
7
|
+
@granite_cache ||= Cache.new(self)
|
8
|
+
end
|
9
|
+
|
10
|
+
def clear_cache!
|
11
|
+
@granite_cache = nil
|
12
|
+
super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
ActionDispatch::Journey::Routes.prepend Granite::Routing::Caching
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Granite
|
2
|
+
module Routing
|
3
|
+
module Declarer
|
4
|
+
class << self
|
5
|
+
def declare(routing, route, **options)
|
6
|
+
routing.match route.path,
|
7
|
+
via: :all,
|
8
|
+
**options,
|
9
|
+
to: dispatcher,
|
10
|
+
as: route.as,
|
11
|
+
granite_action: route.action_path,
|
12
|
+
granite_projector: route.projector_name
|
13
|
+
end
|
14
|
+
|
15
|
+
def dispatcher
|
16
|
+
@dispatcher ||= Dispatcher.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def reset_dispatcher
|
20
|
+
dispatcher.reset!
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'granite/routing/declarer'
|
2
|
+
require 'granite/routing/route'
|
3
|
+
|
4
|
+
module Granite
|
5
|
+
module Routing
|
6
|
+
module Mapper
|
7
|
+
def granite(projector_path, **options)
|
8
|
+
route = Route.new(projector_path, options.extract!(:path, :as, :projector_prefix))
|
9
|
+
Declarer.declare(self, route, options)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
ActionDispatch::Routing::Mapper.include Granite::Routing::Mapper
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Granite
|
2
|
+
module Routing
|
3
|
+
module Mapping
|
4
|
+
# Override the `ActionDispatch::Routing::Mapper::Mapping#app` method to
|
5
|
+
# be able to mount custom Dispatcher objects. Otherwise, the only way to
|
6
|
+
# point a dispatcher to business actions is to mount it as a Rack app
|
7
|
+
# but we want to use regular Rails flow.
|
8
|
+
def app(*)
|
9
|
+
if to.is_a?(Granite::Dispatcher)
|
10
|
+
ActionDispatch::Routing::Mapper::Constraints.new(
|
11
|
+
to,
|
12
|
+
to.constraints,
|
13
|
+
ActionDispatch::Routing::Mapper::Constraints::SERVE
|
14
|
+
)
|
15
|
+
else
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
ActionDispatch::Routing::Mapper::Mapping.prepend Granite::Routing::Mapping
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Granite
|
2
|
+
module Routing
|
3
|
+
class Route
|
4
|
+
attr_reader :projector_path, :action_path, :projector_name
|
5
|
+
|
6
|
+
def initialize(projector_path, path: nil, as: nil, projector_prefix: false)
|
7
|
+
@projector_path = projector_path
|
8
|
+
@action_path, @projector_name = projector_path.split('#')
|
9
|
+
@path = path
|
10
|
+
@as = as
|
11
|
+
|
12
|
+
@action_name = @action_path.split('/').last
|
13
|
+
@action_name = "#{@projector_name}_#{@action_name}" if projector_prefix
|
14
|
+
end
|
15
|
+
|
16
|
+
def path
|
17
|
+
"#{@path || action_name}(/:projector_action)"
|
18
|
+
end
|
19
|
+
|
20
|
+
def as
|
21
|
+
@as || action_name
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
attr_reader :action_name
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
module Granite::ActionHelpers
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
delegate :perform!, to: :subject
|
5
|
+
end
|
6
|
+
|
7
|
+
RSpec.configuration.define_derived_metadata(file_path: %r{spec/apq/actions/}) { |metadata| metadata[:type] ||= :granite_action }
|
8
|
+
RSpec.configuration.include Granite::ActionHelpers, type: :granite_action
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# @scope Business Actions
|
2
|
+
#
|
3
|
+
# Checks if the business action has the expected projector
|
4
|
+
#
|
5
|
+
# Example:
|
6
|
+
#
|
7
|
+
# ```ruby
|
8
|
+
# is_expected.to have_projector(:simple)
|
9
|
+
# ```
|
10
|
+
RSpec::Matchers.define :have_projector do |expected_projector|
|
11
|
+
match do |action|
|
12
|
+
@expected_projector = expected_projector
|
13
|
+
@action_class = action.class
|
14
|
+
@action_class._projectors.names.include?(expected_projector)
|
15
|
+
end
|
16
|
+
|
17
|
+
failure_message do
|
18
|
+
"expected #{@action_class.name} to have a projector named #{@expected_projector}"
|
19
|
+
end
|
20
|
+
|
21
|
+
failure_message_when_negated do
|
22
|
+
"expected #{@action_class.name} not to have a projector named #{@expected_projector}"
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Granite::ProjectorHelpers
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
include RSpec::Rails::ControllerExampleGroup
|
6
|
+
include RSpec::Rails::RequestExampleGroup
|
7
|
+
before { Granite::Routing::Declarer.dispatcher.unmemoize_all }
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def draw_routes(&block)
|
12
|
+
before(:all) do
|
13
|
+
routes = Rails.application.routes
|
14
|
+
routes.disable_clear_and_finalize = true
|
15
|
+
routes.draw(&block)
|
16
|
+
end
|
17
|
+
|
18
|
+
after(:all) do
|
19
|
+
Rails.application.routes.disable_clear_and_finalize = false
|
20
|
+
Rails.application.reload_routes!
|
21
|
+
Rails.application.routes.routes.clear_cache!
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def projector(&block)
|
26
|
+
setup_controller(&block)
|
27
|
+
setup_view_context
|
28
|
+
let(:projector) { controller.projector }
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def setup_controller
|
34
|
+
singleton_class.class_eval do
|
35
|
+
def controller_class
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
before do
|
40
|
+
@controller = yield.controller_class.new
|
41
|
+
@controller.class.instance_variable_set(:@controller_path, yield.projector_path)
|
42
|
+
@controller.request = @request
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def setup_view_context
|
47
|
+
before { Granite.view_context = controller.view_context }
|
48
|
+
after { Granite.view_context = nil }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
RSpec.configuration.define_derived_metadata(file_path: %r{spec/apq/projectors/}) { |metadata| metadata[:type] ||= :granite_projector }
|
54
|
+
RSpec.configuration.include Granite::ProjectorHelpers, type: :granite_projector
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# @scope BusinessActions
|
2
|
+
#
|
3
|
+
# Checks if code in block raises `Granite::Action::ValidationError`.
|
4
|
+
#
|
5
|
+
# Modifiers:
|
6
|
+
# * `on_attribute(attribute)` -- error relates to attribute specified;
|
7
|
+
# * `of_type` -- checks the error has a message with the specified symbol;
|
8
|
+
#
|
9
|
+
# Examples:
|
10
|
+
#
|
11
|
+
# ```ruby
|
12
|
+
# expect { code }.to raise_validation_error.of_type(:some_error_key)
|
13
|
+
# expect { code }.to raise_validation_error.on_attribute(:skill_sets).of_type(:some_other_key)
|
14
|
+
# ```
|
15
|
+
#
|
16
|
+
RSpec::Matchers.define :raise_validation_error do
|
17
|
+
chain :on_attribute do |attribute|
|
18
|
+
@attribute = attribute
|
19
|
+
end
|
20
|
+
|
21
|
+
chain :of_type do |error_type|
|
22
|
+
@error_type = error_type
|
23
|
+
end
|
24
|
+
|
25
|
+
match do |block|
|
26
|
+
begin
|
27
|
+
block.call
|
28
|
+
false
|
29
|
+
rescue Granite::Action::ValidationError => e
|
30
|
+
@details = e.errors.details
|
31
|
+
@details_being_checked = @details[@attribute || :base]
|
32
|
+
@result = @details_being_checked&.any? { |x| x[:error] == @error_type }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
description do
|
37
|
+
expected = "raise validation error on attribute :#{@attribute || :base}"
|
38
|
+
expected << " of type #{@error_type.inspect}" if @error_type
|
39
|
+
expected << ", but raised #{@details.inspect}" unless @result
|
40
|
+
expected
|
41
|
+
end
|
42
|
+
|
43
|
+
failure_message do
|
44
|
+
"expected to #{description}"
|
45
|
+
end
|
46
|
+
|
47
|
+
failure_message_when_negated do
|
48
|
+
"expected not to #{description}"
|
49
|
+
end
|
50
|
+
|
51
|
+
supports_block_expectations
|
52
|
+
end
|