resourcerer 1.0.0 → 2.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/lib/resourcerer.rb CHANGED
@@ -1,5 +1,14 @@
1
- require 'resourcerer/resourceable'
1
+ # frozen_string_literal: true
2
2
 
3
- ActiveSupport.on_load(:action_controller) do
4
- include Resourcerer::Resourceable
3
+ require 'resourcerer/version'
4
+ require 'active_support/all'
5
+
6
+ module Resourcerer
7
+ autoload :Configuration, 'resourcerer/configuration'
8
+ autoload :Controller, 'resourcerer/controller'
9
+ autoload :Resource, 'resourcerer/resource'
10
+
11
+ ActiveSupport.on_load :action_controller do
12
+ include Controller
13
+ end
5
14
  end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Resourcerer
4
+ # Internal: Normalizes configuration options by providing common shortcuts for
5
+ # certain options. These shortcuts make the library easier to use.
6
+ #
7
+ # Examples:
8
+ # find_by: :name ->(name, collection) { collection.find_by(name: name)}
9
+ # assign?: :update -> { action_name == 'update' }
10
+ # id: :person_id -> { params[:person_id] }
11
+ class Configuration
12
+ # Public: Available configuration options for a Resource.
13
+ OPTIONS = [
14
+ :assign,
15
+ :assign?,
16
+ :attrs,
17
+ :build,
18
+ :collection,
19
+ :find,
20
+ :find_by,
21
+ :id,
22
+ :model,
23
+ :permit,
24
+ ].freeze
25
+
26
+ attr_reader :options
27
+
28
+ # Public: Normalizes configuration options for a Resource, ensuring every
29
+ # relevant option is assigned a Proc.
30
+ #
31
+ # options - Config Hash for the new Resource. See OPTIONS.
32
+ # block - If supplied, the block is executed to provide options.
33
+ #
34
+ # Returns a Hash where every value is a Proc.
35
+ def initialize(options, &block)
36
+ @options = options
37
+ instance_eval(&block) if block_given?
38
+
39
+ assert_incompatible_options_pair :find_by, :find
40
+ assert_incompatible_options_pair :permit, :attrs
41
+
42
+ normalize_assign_option
43
+ normalize_attrs_option
44
+ normalize_find_by_option
45
+ normalize_id_option
46
+ normalize_model_option
47
+ normalize_permit_option
48
+
49
+ assert_proc_options *OPTIONS
50
+ end
51
+
52
+ # Public: Applies the configuration to the specified options.
53
+ # Does not override an option if it had previously been specified.
54
+ #
55
+ # Returns the updated configuration options.
56
+ def apply(other_options)
57
+ other_options.reverse_merge!(options)
58
+ end
59
+
60
+ # Internal: Every option can also be specified in the block, DSL-style.
61
+ #
62
+ # Each generated method captures the value of an option.
63
+ OPTIONS.each do |name|
64
+ define_method(name) do |arg = nil, &block|
65
+ options[name] = arg || block
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+
72
+ # Internal: Normalizes the `find_by` option to be a `find` Proc.
73
+ #
74
+ # Example:
75
+ # find_by: :name ->(name, collection) { collection.find_by(name: name)}
76
+ def normalize_find_by_option
77
+ if find_by = options.delete(:find_by)
78
+ options[:find] = ->(id, scope) { scope.find_by!(find_by => id) }
79
+ end
80
+ end
81
+
82
+ # Internal: Normalizes the `permit` option to be a Proc.
83
+ #
84
+ # Example:
85
+ # permit: [:name] -> { [:name] }
86
+ def normalize_permit_option
87
+ option_to_proc :permit do |*fields|
88
+ -> { fields }
89
+ end
90
+ end
91
+
92
+ # Internal: Normalizes the `assign?` option to be a Proc.
93
+ #
94
+ # Example:
95
+ # assign?: false -> { false }
96
+ # assign?: :update -> { action_name == 'update' }
97
+ # assign?: [:edit, :update] -> { action_name.in?(['edit', 'update']) }
98
+ def normalize_assign_option
99
+ bool = options[:assign?]
100
+ options[:assign?] = -> { bool } if bool == !!bool
101
+
102
+ option_to_proc :assign? do |*actions|
103
+ actions = Set.new(actions.map(&:to_s))
104
+ -> { actions.member?(action_name) }
105
+ end
106
+ end
107
+
108
+ # Internal: Normalizes the `attrs` option to be a Proc.
109
+ #
110
+ # Example:
111
+ # attrs: :person_params -> { person_params }
112
+ def normalize_attrs_option
113
+ option_to_proc :attrs do |params_method|
114
+ -> { send(params_method) }
115
+ end
116
+ end
117
+
118
+ # Internal: Normalizes the `id` option to be a Proc.
119
+ #
120
+ # Example:
121
+ # id: :person_id -> { params[:person_id] }
122
+ def normalize_id_option
123
+ option_to_proc :id do |*ids|
124
+ -> { ids.map { |id| params[id] }.find(&:present?) }
125
+ end
126
+ end
127
+
128
+ # Internal: Normalizes the `model` option to be a Proc.
129
+ #
130
+ # Example:
131
+ # model: :electric_guitar -> { ElectricGuitar }
132
+ def normalize_model_option
133
+ option_to_proc :model do |value|
134
+ model = case value
135
+ when String, Symbol then value.to_s.classify.constantize
136
+ else value
137
+ end
138
+
139
+ -> { model }
140
+ end
141
+ end
142
+
143
+ # Internal: Helper to normalize a non-proc value passed as an option.
144
+ def option_to_proc(name)
145
+ return unless option = options[name]
146
+ options[name] = yield(*option) unless option.is_a?(Proc)
147
+ end
148
+
149
+ # Internal: Asserts that the specified options are a Proc, if present.
150
+ def assert_proc_options(*names)
151
+ names.each do |name|
152
+ if options.key?(name) && !options[name].is_a?(Proc)
153
+ raise ArgumentError, "Can't handle #{name.inspect} => #{options[name].inspect} option"
154
+ end
155
+ end
156
+ end
157
+
158
+ # Internal: Performs a basic assertion to fail early if the specified
159
+ # options would result in undetermined behavior.
160
+ def assert_incompatible_options_pair(key1, key2)
161
+ if options.key?(key1) && options.key?(key2)
162
+ raise ArgumentError, "Using #{key1.inspect} option with #{key2.inspect} does not make sense"
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Resourcerer
4
+ # Public: Provides two `resource` helper methods to simplify the definition
5
+ # and usage of Resources.
6
+ #
7
+ # It's also possible to define presets, which can then be reused by providing
8
+ # the :using option when using or defining a Resource.
9
+ module Controller
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ # Public: Available configuration presets that can be used when defining
14
+ # a resource.
15
+ class_attribute :resourcerer_configuration,
16
+ instance_accessor: false, instance_predicate: false
17
+ end
18
+
19
+ def resource(*args)
20
+ Resource.new(self, *args).get(self)
21
+ end
22
+
23
+ module ClassMethods
24
+ # Public: Defines a Resource in a controller Class.
25
+ #
26
+ # *args - See Resource#initialize for details.
27
+ # block - If supplied, the block is executed to provide options.
28
+ #
29
+ # Returns the name of the defined resource.
30
+ def resource(*args, &block)
31
+ Resource.define(self, *args, &block)
32
+ end
33
+
34
+ # Public: Defines a Configuration preset that can be reused in different
35
+ # Resources by providing the :using option.
36
+ #
37
+ # name - The Symbol name of the configuration preset.
38
+ # options - The Hash of options to define the preset.
39
+ # block - If supplied, the block is executed to provide options.
40
+ #
41
+ # Returns a Hash with all the resource configurations.
42
+ def resourcerer_config(name, **options, &block)
43
+ self.resourcerer_configuration = (resourcerer_configuration || {}).merge(
44
+ name => Configuration.new(options, &block)
45
+ )
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,21 +1,206 @@
1
- require 'resourcerer/inflector'
2
- require 'resourcerer/strategies/strong_parameters_strategy'
1
+ # frozen_string_literal: true
3
2
 
4
3
  module Resourcerer
4
+ # Public: Representation of a model that can be found, built, and assigned
5
+ # attributes.
5
6
  class Resource
6
- attr_reader :name, :strategy, :options, :config_proc
7
+ attr_reader :name, :options, :controller
7
8
 
8
- def self.for(name, options={}, config_proc=nil)
9
- strategy_class = options.delete(:strategy) || Strategies::StrongParametersStrategy
10
- new strategy_class, name, options, config_proc
9
+ # Public: Defines a Resource and makes it accessible to a controller.
10
+ # For each Resource, a getter and setter is defined in the controller.
11
+ #
12
+ # klass - The Controller class where the Resource getter will be defined.
13
+ # name - The name of the generated Resource.
14
+ # options - Config Hash for the new Resource. See Configuration::OPTIONS.
15
+ # block - If supplied, the block is executed to provide options.
16
+ #
17
+ # Returns nothing.
18
+ def self.define(klass, name, **options, &block)
19
+ resource = new(klass, name, **options, &block)
20
+
21
+ klass.instance_eval do
22
+ ivar = "@resourcerer_#{ name.to_s.gsub('?', '_question_mark') }"
23
+
24
+ private define_method(name) {
25
+ if instance_variable_defined?(ivar)
26
+ instance_variable_get(ivar)
27
+ else
28
+ instance_variable_set(ivar, resource.clone.get(self))
29
+ end
30
+ }
31
+
32
+ private define_method("#{ name }=") { |value|
33
+ instance_variable_set(ivar, value)
34
+ }
35
+ end
36
+ end
37
+
38
+ # Public: Initalize a Resource with configuration options.
39
+ #
40
+ # klass - The Controller class where the Resource is executed.
41
+ # name - The Symbol name of the Resource instance.
42
+ # options - Hash of options for the Configuration of the methods.
43
+ # block - If supplied, the block is executed to provide options.
44
+ #
45
+ # Returns a normalized options Hash.
46
+ def initialize(klass, name, using: [], **options, &block)
47
+ @name = name
48
+ @options = Configuration.new(options, &block).options
49
+
50
+ Array.wrap(using).each do |preset|
51
+ klass.resourcerer_configuration.fetch(preset).apply(options)
52
+ end
53
+ end
54
+
55
+ # Public: Returns an object using the specified Resource configuration.
56
+ # The object will be built or found, and might be assigned attributes.
57
+ #
58
+ # controller - The instance of the controller where the resource is fetched.
59
+ #
60
+ # Returns the resource object.
61
+ def get(controller)
62
+ @controller = controller
63
+ collection = call(:collection, call(:model))
64
+
65
+ if id = call(:id)
66
+ call(:find, id, collection)
67
+ else
68
+ call(:build, safe_attrs, collection)
69
+ end.tap do |object|
70
+ call(:assign, object, safe_attrs) if object && call(:assign?, object)
71
+ end
72
+ end
73
+
74
+ protected
75
+
76
+ # Strategy: A query or table. Designed to be overridden.
77
+ #
78
+ # model - The Class to be scoped or queried.
79
+ #
80
+ # Returns the object collection.
81
+ def collection(model = name.to_s.classify.constantize)
82
+ model
83
+ end
84
+
85
+ # Strategy: Converts a name into a standard Class name.
86
+ #
87
+ # Examples
88
+ # 'egg_and_hams'.model # => EggAndHam
89
+ #
90
+ # Returns a standard Class name.
91
+ def model
92
+ options.key?(:collection) ? call(:collection).klass : collection
93
+ end
94
+
95
+ # Strategy: Checks controller params to retrieve an id value.
96
+ #
97
+ # Returns the id parameter, if any, or nil.
98
+ def id
99
+ ["#{name}_id", "#{model_name}_id", 'id'].uniq.
100
+ map { |id| controller.params[id] }.find(&:present?)
101
+ end
102
+
103
+ # Strategy: Find an object on the supplied scope.
104
+ #
105
+ # id - The Integer id attribute of the desired object
106
+ # scope - The collection that will be searched.
107
+ #
108
+ # Returns the found object.
109
+ def find(id, collection)
110
+ collection.find(id)
111
+ end
112
+
113
+ # Strategy: Builds a new object on the passed-in scope.
114
+ #
115
+ # params - A Hash of attributes for the object to-be built.
116
+ # scope - The collection where the object will be built from.
117
+ #
118
+ # Returns the new object.
119
+ def build(attrs, collection)
120
+ collection.new(attrs)
121
+ end
122
+
123
+ # Strategy: Assigns attributes to the found or built object.
124
+ #
125
+ # attrs - A Hash of attributes to be assigned.
126
+ # object - The Resource object.
127
+ #
128
+ # Returns nothing.
129
+ def assign(object, attrs)
130
+ object.assign_attributes(attrs)
11
131
  end
12
132
 
13
- def initialize(strategy, name, options, config_proc=nil)
14
- @strategy, @name, @options, @config_proc = strategy, name, options, config_proc
133
+ # Strategy: Whether the attributes should be assigned.
134
+ #
135
+ # object - The Resource object.
136
+ #
137
+ # Returns true if attributes should be assigned, or false otherwise.
138
+ def assign?(object)
139
+ controller.action_name == 'update'
15
140
  end
16
141
 
17
- def call(controller)
18
- strategy.new(controller, name, options, config_proc).resource
142
+ # Strategy: Get all the parameters of the current request.
143
+ #
144
+ # Returns the controller's parameters for the current request.
145
+ def attrs
146
+ if options[:permit]
147
+ controller.params.require(model_name).permit(*call(:permit))
148
+ else
149
+ params_method = "#{name}_params"
150
+ if controller.respond_to?(params_method, true)
151
+ controller.send(params_method)
152
+ else
153
+ {}
154
+ end
155
+ end
156
+ end
157
+
158
+ private
159
+
160
+ # Internal: Avoids assigning attributes when the request is a GET request.
161
+ #
162
+ # Returns the controller's parameters for the current request.
163
+ def safe_attrs
164
+ controller.request.get? ? {} : call(:attrs)
165
+ end
166
+
167
+ # Internal: Returns a Symbol name that follows the parameter convention.
168
+ def model_name
169
+ @model_name ||= if defined?(ActiveModel::Name)
170
+ ActiveModel::Name.new(call(:model)).param_key
171
+ else
172
+ call(:model).name.underscore
173
+ end.to_sym
174
+ end
175
+
176
+ # Internal: Invokes a Proc that was passed as an option, or the default
177
+ # strategy for that function.
178
+ def call(name, *args)
179
+ memoize(name) {
180
+ if options.key?(name)
181
+ execute_option_function(options[name], *args)
182
+ else
183
+ send(name, *args)
184
+ end
185
+ }
186
+ end
187
+
188
+ # Internal: Invokes a Proc that was passed as an option. The Proc executes
189
+ # within the context of the controller.
190
+ def execute_option_function(function, *args)
191
+ args = args.first(function.parameters.length)
192
+ controller.instance_exec(*args, &function)
193
+ end
194
+
195
+ # Internal: Helper method to perform simple memoization.
196
+ def memoize(name)
197
+ ivar = "@#{ name.to_s.gsub('?', '_question_mark') }"
198
+
199
+ if instance_variable_defined?(ivar)
200
+ instance_variable_get(ivar)
201
+ else
202
+ instance_variable_set(ivar, yield)
203
+ end
19
204
  end
20
205
  end
21
206
  end