resourcerer 1.0.0 → 2.0.3
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 +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
|