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,15 @@
1
+ require 'granite/action/represents/attribute'
2
+
3
+ module Granite
4
+ class Action
5
+ module Represents
6
+ class Reflection < ActiveData::Model::Attributes::Reflections::Represents
7
+ class << self
8
+ def attribute_class
9
+ @attribute_class ||= Granite::Action::Represents::Attribute
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,73 @@
1
+ module Granite
2
+ class Action
3
+ class SubjectNotFoundError < ArgumentError
4
+ def initialize(action_class)
5
+ super "Unable to initialize #{action_class} without subject provided"
6
+ end
7
+ end
8
+
9
+ class SubjectTypeMismatchError < ArgumentError
10
+ def initialize(action_class, candidate, expected)
11
+ super "Unable to initialize #{action_class} with #{candidate} as subject, expecting instance of #{expected}"
12
+ end
13
+ end
14
+
15
+ module Subject
16
+ extend ActiveSupport::Concern
17
+
18
+ included do
19
+ class_attribute :_subject
20
+ end
21
+
22
+ module ClassMethods
23
+ def subject(name, *args, &block)
24
+ reflection = reflect_on_association(name)
25
+ reflection ||= references_one name, *args, &block
26
+
27
+ alias_association :subject, reflection.name
28
+ alias_attribute :id, reflection.reference_key
29
+
30
+ self._subject = name
31
+ end
32
+ end
33
+
34
+ def initialize(*args)
35
+ if self.class._subject.blank?
36
+ super
37
+ return
38
+ end
39
+
40
+ reflection = self.class.reflect_on_association(self.class._subject)
41
+ attributes = extract_initialize_attributes(args)
42
+
43
+ subject_attributes = extract_subject_attributes!(attributes, reflection)
44
+ assign_subject(args, subject_attributes, reflection)
45
+
46
+ super attributes
47
+ end
48
+
49
+ private
50
+
51
+ def extract_initialize_attributes(args)
52
+ if args.last.respond_to?(:to_unsafe_hash)
53
+ args.pop.to_unsafe_hash
54
+ else
55
+ args.extract_options!
56
+ end.symbolize_keys
57
+ end
58
+
59
+ def assign_subject(args, attributes, reflection)
60
+ assign_attributes(attributes)
61
+
62
+ self.subject = args.first unless args.empty?
63
+ fail SubjectNotFoundError, self.class unless subject
64
+ rescue ActiveData::AssociationTypeMismatch
65
+ raise SubjectTypeMismatchError.new(self.class, args.first.class.name, reflection.klass)
66
+ end
67
+
68
+ def extract_subject_attributes!(attributes, reflection)
69
+ attributes.extract!(:subject, :id, reflection.name, reflection.reference_key)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,40 @@
1
+ module Granite
2
+ class Action
3
+ class Rollback < defined?(ActiveRecord) ? ActiveRecord::Rollback : StandardError
4
+ end
5
+
6
+ module Transaction
7
+ extend ActiveSupport::Concern
8
+
9
+ private
10
+
11
+ def transactional(&block)
12
+ if transactional?
13
+ yield
14
+ else
15
+ @_transactional = true
16
+ result = transaction(&block) || false
17
+ @_transactional = nil
18
+ result
19
+ end
20
+ end
21
+
22
+ def transactional?
23
+ # Fuck the police!
24
+ !(!@_transactional)
25
+ end
26
+
27
+ def transaction(&block)
28
+ if defined?(ActiveRecord::Base)
29
+ ActiveRecord::Base.transaction(&block)
30
+ else
31
+ begin
32
+ yield
33
+ rescue Granite::Action::Rollback
34
+ false
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,39 @@
1
+ module Granite
2
+ class Action
3
+ module Translations
4
+ extend ActiveSupport::Concern
5
+
6
+ def translate(*args)
7
+ I18n.translate(*self.class.scope_translation_args(args))
8
+ end
9
+ alias t translate
10
+
11
+ module ClassMethods
12
+ def scope_translation_args(args)
13
+ options = args.extract_options!
14
+
15
+ lookups = expand_relative_key(args.first).map(&:to_sym)
16
+ lookups += [options[:default]]
17
+ lookups = lookups.flatten.compact
18
+
19
+ key = lookups.shift
20
+ options[:default] = lookups
21
+
22
+ [key, options]
23
+ end
24
+
25
+ private
26
+
27
+ def expand_relative_key(key)
28
+ return [key] unless key.is_a?(String) && key.start_with?('.')
29
+
30
+ base_key = key.sub(/^\./, '')
31
+
32
+ lookup_ancestors.map do |klass|
33
+ :"#{klass.i18n_scope}.#{klass.model_name.i18n_key}.#{base_key}"
34
+ end.flatten + [base_key]
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1 @@
1
+ require 'granite/action/types/collection'
@@ -0,0 +1,13 @@
1
+ module Granite
2
+ class Action
3
+ module Types
4
+ class Collection
5
+ attr_reader :subtype
6
+
7
+ def initialize(subtype)
8
+ @subtype = subtype
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,23 @@
1
+ require 'singleton'
2
+ require 'active_support/core_ext/object/try'
3
+
4
+ module Granite
5
+ class Config
6
+ include Singleton
7
+
8
+ attr_accessor :base_controller
9
+ attr_writer :precondition_namespaces
10
+
11
+ def base_controller_class
12
+ base_controller&.constantize || ActionController::Base
13
+ end
14
+
15
+ def precondition_namespaces
16
+ @precondition_namespaces ||= %w[Granite::Action::Preconditions]
17
+ end
18
+
19
+ def self.delegated
20
+ public_instance_methods - superclass.public_instance_methods - Singleton.public_instance_methods
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,28 @@
1
+ require 'singleton'
2
+
3
+ module Granite
4
+ class Context
5
+ include Singleton
6
+
7
+ def view_context
8
+ Thread.current[:granite_view_context]
9
+ end
10
+
11
+ def view_context=(context)
12
+ Thread.current[:granite_view_context] = context
13
+ end
14
+
15
+ def with_view_context(context)
16
+ old_view_context = view_context
17
+ self.view_context = context
18
+
19
+ yield
20
+ ensure
21
+ self.view_context = old_view_context
22
+ end
23
+
24
+ def self.delegated
25
+ public_instance_methods - superclass.public_instance_methods - Singleton.public_instance_methods
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,64 @@
1
+ require 'memoist'
2
+ require 'action_controller/metal/exceptions'
3
+
4
+ class Granite::Dispatcher
5
+ extend Memoist
6
+
7
+ # Make dispatcher object pristine, clean memoist cache.
8
+ def reset!
9
+ unmemoize_all
10
+ end
11
+
12
+ def call(*)
13
+ # Pretend to be a Rack app, however we are still dispatcher, so this method should never be called
14
+ # see lib/granite/routing/mapping.rb for more info.
15
+ fail 'Dispatcher can\'t be used as a Rack app.'
16
+ end
17
+
18
+ def serve(req)
19
+ controller, action = detect_controller_class_and_action_name(req)
20
+ controller.action(action).call(req.env)
21
+ end
22
+
23
+ def constraints
24
+ [->(req) { detect_controller_class_and_action_name(req).all?(&:present?) }]
25
+ end
26
+
27
+ def controller(params, *_args)
28
+ projector(params.slice(:granite_action, :granite_projector).symbolize_keys)&.controller_class
29
+ end
30
+
31
+ def prepare_params!(params, *_args)
32
+ params
33
+ end
34
+
35
+ private
36
+
37
+ def detect_controller_class_and_action_name(req)
38
+ [
39
+ controller(req.params),
40
+ action_name(
41
+ req.request_method_symbol,
42
+ req.params.slice(:granite_action, :granite_projector, :projector_action).symbolize_keys
43
+ )
44
+ ]
45
+ end
46
+
47
+ memoize def action_name(request_method_symbol, granite_action:, granite_projector:, projector_action: '')
48
+ projector = projector(granite_action: granite_action, granite_projector: granite_projector)
49
+ return unless projector
50
+
51
+ projector.action_for(request_method_symbol, projector_action)
52
+ end
53
+
54
+ memoize def projector(granite_action:, granite_projector:)
55
+ action = business_action(granite_action)
56
+
57
+ action.public_send(granite_projector) if action.respond_to?(granite_projector)
58
+ end
59
+
60
+ memoize def business_action(granite_action)
61
+ granite_action.camelize.safe_constantize ||
62
+ fail(ActionController::RoutingError, "Granite action '#{granite_action}' is mounted but class '#{granite_action.camelize}' can't be found")
63
+ end
64
+ end
@@ -0,0 +1,4 @@
1
+ module Granite
2
+ class Error < StandardError
3
+ end
4
+ end
@@ -0,0 +1,34 @@
1
+ require 'granite/performer_proxy/proxy'
2
+
3
+ module Granite
4
+ # This concern contains class methods used for actions and projectors
5
+ #
6
+ module PerformerProxy
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods
10
+ def as(performer)
11
+ Proxy.new(self, performer)
12
+ end
13
+
14
+ def with_proxy_performer(performer)
15
+ key = proxy_performer_key
16
+ old_performer = Thread.current[key]
17
+ Thread.current[key] = performer
18
+ yield
19
+ ensure
20
+ Thread.current[key] = old_performer
21
+ end
22
+
23
+ def proxy_performer
24
+ Thread.current[proxy_performer_key]
25
+ end
26
+
27
+ private
28
+
29
+ def proxy_performer_key
30
+ :"granite_proxy_performer_#{hash}"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,31 @@
1
+ module Granite
2
+ module PerformerProxy
3
+ # Proxy helps to wrap the following method call with
4
+ # performer-enabled context.
5
+ #
6
+ class Proxy
7
+ def initialize(klass, performer)
8
+ @klass = klass
9
+ @performer = performer
10
+ end
11
+
12
+ def inspect
13
+ "<#{@klass}PerformerProxy #{@performer}>"
14
+ end
15
+
16
+ def method_missing(method, *args, &block)
17
+ if @klass.respond_to?(method)
18
+ @klass.with_proxy_performer(@performer) do
19
+ @klass.public_send(method, *args, &block)
20
+ end
21
+ else
22
+ super
23
+ end
24
+ end
25
+
26
+ def respond_to_missing?(*args)
27
+ @klass.respond_to?(*args)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,48 @@
1
+ require 'granite/projector/controller_actions'
2
+ require 'granite/projector/error'
3
+ require 'granite/projector/helpers'
4
+ require 'granite/projector/translations'
5
+ require 'granite/performer_proxy'
6
+
7
+ module Granite
8
+ class Projector
9
+ include PerformerProxy
10
+ include ControllerActions
11
+ include Helpers
12
+ include Translations
13
+
14
+ singleton_class.__send__(:attr_accessor, :action_class)
15
+ delegate :action_class, :projector_name, to: 'self.class'
16
+ attr_reader :action
17
+
18
+ def self.controller_class
19
+ return Granite::Controller unless superclass.respond_to?(:controller_class)
20
+
21
+ @controller_class ||= Class.new(superclass.controller_class).tap do |klass|
22
+ klass.projector_class = self
23
+ end
24
+ end
25
+
26
+ def self.projector_path
27
+ @projector_path ||= name.remove(/Projector$/).underscore
28
+ end
29
+
30
+ def self.projector_name
31
+ @projector_name ||= name.demodulize.remove(/Projector$/).underscore
32
+ end
33
+
34
+ def initialize(*args)
35
+ @action = if args.first.is_a?(Granite::Action) # Temporary solutions for backwards compatibility.
36
+ args.first
37
+ else
38
+ build_action(*args)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def build_action(*args)
45
+ action_class.as(self.class.proxy_performer).new(*args)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,47 @@
1
+ require 'action_dispatch/routing'
2
+
3
+ module Granite
4
+ class Projector
5
+ module ControllerActions
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class_attribute :controller_actions
10
+ self.controller_actions = {}
11
+
12
+ ActionDispatch::Routing::HTTP_METHODS.each do |method|
13
+ define_singleton_method method do |name, options = {}, &block|
14
+ action(name, options.merge(method: method), &block)
15
+ end
16
+ end
17
+ end
18
+
19
+ module ClassMethods
20
+ def action(name, options = {}, &block)
21
+ if block
22
+ self.controller_actions = controller_actions.merge(name.to_sym => options)
23
+ controller_class.__send__(:define_method, name, &block)
24
+ class_eval <<-METHOD, __FILE__, __LINE__ + 1
25
+ def #{name}_url(options = {})
26
+ action_url(:#{name}, options)
27
+ end
28
+
29
+ def #{name}_path(options = {})
30
+ action_path(:#{name}, options)
31
+ end
32
+ METHOD
33
+ else
34
+ controller_actions[name.to_sym]
35
+ end
36
+ end
37
+
38
+ def action_for(http_method, action)
39
+ controller_actions.find do |controller_action, controller_action_options|
40
+ controller_action_options.fetch(:as, controller_action).to_s == action &&
41
+ Array(controller_action_options.fetch(:method)).include?(http_method)
42
+ end&.first
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end