resourcerer 1.0.0 → 2.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +191 -180
- data/lib/resourcerer.rb +12 -3
- data/lib/resourcerer/configuration.rb +166 -0
- data/lib/resourcerer/controller.rb +49 -0
- data/lib/resourcerer/resource.rb +195 -10
- data/lib/resourcerer/version.rb +5 -0
- data/spec/features/guitars_controller_spec.rb +51 -0
- data/spec/resourcerer/controller_spec.rb +394 -0
- data/spec/resourcerer/param_key_spec.rb +48 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/support/rails_app.rb +60 -0
- metadata +99 -29
- data/lib/resourcerer/configuration/strong_parameters.rb +0 -12
- data/lib/resourcerer/inflector.rb +0 -33
- data/lib/resourcerer/resource_configuration.rb +0 -49
- data/lib/resourcerer/resourceable.rb +0 -44
- data/lib/resourcerer/strategies/assign_attributes.rb +0 -34
- data/lib/resourcerer/strategies/assign_from_method.rb +0 -23
- data/lib/resourcerer/strategies/assign_from_params.rb +0 -13
- data/lib/resourcerer/strategies/default_strategy.rb +0 -40
- data/lib/resourcerer/strategies/eager_attributes_strategy.rb +0 -10
- data/lib/resourcerer/strategies/optional_strategy.rb +0 -24
- data/lib/resourcerer/strategies/strong_parameters_strategy.rb +0 -31
- data/lib/resourcerer/strategy.rb +0 -61
data/lib/resourcerer.rb
CHANGED
@@ -1,5 +1,14 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
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
|
data/lib/resourcerer/resource.rb
CHANGED
@@ -1,21 +1,206 @@
|
|
1
|
-
|
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, :
|
7
|
+
attr_reader :name, :options, :controller
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
14
|
-
|
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
|
-
|
18
|
-
|
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
|