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.
- 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,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,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,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
|