granite 0.7.0

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 (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