granite 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +22 -0
  3. data/app/controllers/granite/controller.rb +44 -0
  4. data/lib/generators/USAGE +25 -0
  5. data/lib/generators/granite/install_controller_generator.rb +15 -0
  6. data/lib/generators/granite_generator.rb +32 -0
  7. data/lib/generators/templates/granite_action.rb.erb +22 -0
  8. data/lib/generators/templates/granite_action_spec.rb.erb +45 -0
  9. data/lib/generators/templates/granite_base_action.rb.erb +2 -0
  10. data/lib/generators/templates/granite_business_action.rb.erb +3 -0
  11. data/lib/granite.rb +24 -0
  12. data/lib/granite/action.rb +106 -0
  13. data/lib/granite/action/error.rb +14 -0
  14. data/lib/granite/action/performer.rb +23 -0
  15. data/lib/granite/action/performing.rb +132 -0
  16. data/lib/granite/action/policies.rb +92 -0
  17. data/lib/granite/action/policies/always_allow_strategy.rb +13 -0
  18. data/lib/granite/action/policies/any_strategy.rb +12 -0
  19. data/lib/granite/action/policies/required_performer_strategy.rb +14 -0
  20. data/lib/granite/action/preconditions.rb +107 -0
  21. data/lib/granite/action/preconditions/base_precondition.rb +25 -0
  22. data/lib/granite/action/preconditions/embedded_precondition.rb +42 -0
  23. data/lib/granite/action/projectors.rb +100 -0
  24. data/lib/granite/action/represents.rb +26 -0
  25. data/lib/granite/action/represents/attribute.rb +90 -0
  26. data/lib/granite/action/represents/reflection.rb +15 -0
  27. data/lib/granite/action/subject.rb +73 -0
  28. data/lib/granite/action/transaction.rb +40 -0
  29. data/lib/granite/action/translations.rb +39 -0
  30. data/lib/granite/action/types.rb +1 -0
  31. data/lib/granite/action/types/collection.rb +13 -0
  32. data/lib/granite/config.rb +23 -0
  33. data/lib/granite/context.rb +28 -0
  34. data/lib/granite/dispatcher.rb +64 -0
  35. data/lib/granite/error.rb +4 -0
  36. data/lib/granite/performer_proxy.rb +34 -0
  37. data/lib/granite/performer_proxy/proxy.rb +31 -0
  38. data/lib/granite/projector.rb +48 -0
  39. data/lib/granite/projector/controller_actions.rb +47 -0
  40. data/lib/granite/projector/error.rb +14 -0
  41. data/lib/granite/projector/helpers.rb +59 -0
  42. data/lib/granite/projector/translations.rb +52 -0
  43. data/lib/granite/projector/translations/helper.rb +22 -0
  44. data/lib/granite/projector/translations/view_helper.rb +12 -0
  45. data/lib/granite/rails.rb +11 -0
  46. data/lib/granite/routing.rb +4 -0
  47. data/lib/granite/routing/cache.rb +24 -0
  48. data/lib/granite/routing/caching.rb +18 -0
  49. data/lib/granite/routing/declarer.rb +25 -0
  50. data/lib/granite/routing/mapper.rb +15 -0
  51. data/lib/granite/routing/mapping.rb +23 -0
  52. data/lib/granite/routing/route.rb +29 -0
  53. data/lib/granite/rspec.rb +5 -0
  54. data/lib/granite/rspec/action_helpers.rb +8 -0
  55. data/lib/granite/rspec/have_projector.rb +24 -0
  56. data/lib/granite/rspec/projector_helpers.rb +54 -0
  57. data/lib/granite/rspec/raise_validation_error.rb +52 -0
  58. data/lib/granite/rspec/satisfy_preconditions.rb +96 -0
  59. metadata +338 -0
@@ -0,0 +1,14 @@
1
+ require 'granite/error'
2
+
3
+ module Granite
4
+ class Projector
5
+ class Error < Granite::Error
6
+ attr_reader :projector
7
+
8
+ def initialize(message, projector = nil)
9
+ @projector = projector
10
+ super(message)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -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,12 @@
1
+ module Granite
2
+ class Projector
3
+ module Translations
4
+ module ViewHelper
5
+ def translate(*args)
6
+ super(*scope_translation_args_by_projector(args, action_name: action_name))
7
+ end
8
+ alias t translate
9
+ end
10
+ end
11
+ end
12
+ 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,4 @@
1
+ require 'action_dispatch'
2
+ require 'granite/routing/caching'
3
+ require 'granite/routing/mapping'
4
+ require 'granite/routing/mapper'
@@ -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,5 @@
1
+ require 'granite/rspec/action_helpers'
2
+ require 'granite/rspec/have_projector'
3
+ require 'granite/rspec/projector_helpers'
4
+ require 'granite/rspec/raise_validation_error'
5
+ require 'granite/rspec/satisfy_preconditions'
@@ -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