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