decent_exposure 2.3.3 → 3.0.0.beta1
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/.gitignore +22 -0
- data/.travis.yml +3 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +200 -363
- data/Rakefile +6 -0
- data/decent_exposure.gemspec +30 -0
- data/decent_exposure.png +0 -0
- data/hashrocket_logo.png +0 -0
- data/lib/decent_exposure.rb +13 -4
- data/lib/decent_exposure/attribute.rb +55 -0
- data/lib/decent_exposure/behavior.rb +100 -0
- data/lib/decent_exposure/context.rb +61 -0
- data/lib/decent_exposure/controller.rb +53 -0
- data/lib/decent_exposure/exposure.rb +199 -9
- data/lib/decent_exposure/flow.rb +89 -0
- data/lib/decent_exposure/version.rb +2 -2
- data/spec/controller_spec.rb +374 -0
- data/spec/integration_spec.rb +26 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/support/rails_app.rb +37 -0
- metadata +63 -41
- data/lib/decent_exposure/active_record_strategy.rb +0 -84
- data/lib/decent_exposure/active_record_with_eager_attributes_strategy.rb +0 -8
- data/lib/decent_exposure/configuration.rb +0 -19
- data/lib/decent_exposure/constant_resolver.rb +0 -34
- data/lib/decent_exposure/error.rb +0 -4
- data/lib/decent_exposure/expose.rb +0 -62
- data/lib/decent_exposure/inflector.rb +0 -39
- data/lib/decent_exposure/strategies/assign_from_method.rb +0 -20
- data/lib/decent_exposure/strategies/assign_from_params.rb +0 -24
- data/lib/decent_exposure/strategizer.rb +0 -54
- data/lib/decent_exposure/strategy.rb +0 -47
- data/lib/decent_exposure/strong_parameters_strategy.rb +0 -8
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require File.expand_path("../lib/decent_exposure/version", __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "decent_exposure"
|
5
|
+
spec.version = DecentExposure::VERSION
|
6
|
+
spec.authors = ["Pavel Pravosud", "Stephen Caudill"]
|
7
|
+
spec.email = ["info@hashrocket.com"]
|
8
|
+
spec.summary = "A helper for creating declarative interfaces in controllers"
|
9
|
+
spec.description = %q{
|
10
|
+
DecentExposure helps you program to an interface, rather than an
|
11
|
+
implementation in your Rails controllers. The fact of the matter is that
|
12
|
+
sharing state via instance variables in controllers promotes close coupling
|
13
|
+
with views. DecentExposure gives you a declarative manner of exposing an
|
14
|
+
interface to the state that controllers contain and thereby decreasing
|
15
|
+
coupling and improving your testability and overall design.
|
16
|
+
}
|
17
|
+
spec.homepage = "https://github.com/hashrocket/decent_exposure"
|
18
|
+
spec.license = "MIT"
|
19
|
+
spec.files = `git ls-files -z`.split("\x0")
|
20
|
+
spec.test_files = spec.files.grep(/\Aspec\//)
|
21
|
+
spec.require_path = "lib"
|
22
|
+
|
23
|
+
spec.required_ruby_version = "~> 2.0"
|
24
|
+
|
25
|
+
spec.add_dependency "railties", "~> 5.x"
|
26
|
+
spec.add_dependency "activesupport", "~> 5.x"
|
27
|
+
spec.add_development_dependency "rspec-rails", "~> 3.0"
|
28
|
+
spec.add_development_dependency "rake", "~> 10.3"
|
29
|
+
spec.add_development_dependency "pry"
|
30
|
+
end
|
data/decent_exposure.png
ADDED
Binary file
|
data/hashrocket_logo.png
ADDED
Binary file
|
data/lib/decent_exposure.rb
CHANGED
@@ -1,6 +1,15 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require "decent_exposure/version"
|
2
|
+
require "active_support/all"
|
3
3
|
|
4
|
-
|
5
|
-
|
4
|
+
module DecentExposure
|
5
|
+
autoload :Controller, "decent_exposure/controller"
|
6
|
+
autoload :Exposure, "decent_exposure/exposure"
|
7
|
+
autoload :Attribute, "decent_exposure/attribute"
|
8
|
+
autoload :Context, "decent_exposure/context"
|
9
|
+
autoload :Behavior, "decent_exposure/behavior"
|
10
|
+
autoload :Flow, "decent_exposure/flow"
|
11
|
+
|
12
|
+
ActiveSupport.on_load :action_controller do
|
13
|
+
include Controller
|
14
|
+
end
|
6
15
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module DecentExposure
|
2
|
+
class Attribute
|
3
|
+
attr_reader :name, :fetch, :ivar_name
|
4
|
+
|
5
|
+
# Public: Initialize an Attribute
|
6
|
+
#
|
7
|
+
# options - Hash of options for the Attribute
|
8
|
+
# :name - The String name of the Attribute instance
|
9
|
+
# :fetch - The Proc fetch to calculate
|
10
|
+
# the value of the Attribute instance.
|
11
|
+
# This is only called if the attribute's
|
12
|
+
# instance variable is not defined.
|
13
|
+
# :ivar_name - The String instance variable name that
|
14
|
+
# is associated with the attribute.
|
15
|
+
def initialize(options)
|
16
|
+
@name = options.fetch(:name)
|
17
|
+
@fetch = options.fetch(:fetch)
|
18
|
+
@ivar_name = options.fetch(:ivar_name)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Public: The getter method for the Attribute.
|
22
|
+
#
|
23
|
+
# Returns the name of the Attribute as a Symbol.
|
24
|
+
def getter_method_name
|
25
|
+
name.to_sym
|
26
|
+
end
|
27
|
+
|
28
|
+
# Public: The setter method for the Attribute.
|
29
|
+
#
|
30
|
+
# Returns the name of the attribute as a Symbol with an appended '='.
|
31
|
+
def setter_method_name
|
32
|
+
"#{name}=".to_sym
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
# Public: Expose a getter and setter method for the Attribute
|
37
|
+
# on the passed in Controller class.
|
38
|
+
#
|
39
|
+
# klass - The Controller class where the Attribute getter and setter
|
40
|
+
# methods will be exposed.
|
41
|
+
def expose!(klass)
|
42
|
+
attribute = self
|
43
|
+
|
44
|
+
klass.instance_eval do
|
45
|
+
define_method attribute.getter_method_name do
|
46
|
+
Context.new(self, attribute).get
|
47
|
+
end
|
48
|
+
|
49
|
+
define_method attribute.setter_method_name do |value|
|
50
|
+
Context.new(self, attribute).set(value)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module DecentExposure
|
2
|
+
module Behavior
|
3
|
+
# Public: Fetches a scope.
|
4
|
+
#
|
5
|
+
# Finds an object. If it isn't found, the object gets instantiated.
|
6
|
+
#
|
7
|
+
# Returns the decorated object.
|
8
|
+
def fetch
|
9
|
+
instance = id ? find(id, computed_scope) : build(build_params, computed_scope)
|
10
|
+
decorate(instance)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Public: Checks a params hash for an id attribute.
|
14
|
+
#
|
15
|
+
# Checks a hash of parameters for keys that represent an object's id.
|
16
|
+
#
|
17
|
+
# Returns the value of the id parameter, if it exists. Otherwise nil.
|
18
|
+
def id
|
19
|
+
params_id_key_candidates.each do |key|
|
20
|
+
value = params[key]
|
21
|
+
return value if value.present?
|
22
|
+
end
|
23
|
+
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
|
27
|
+
# Public: An object query. Essentially, this method is designed to be
|
28
|
+
# overridden.
|
29
|
+
#
|
30
|
+
# model - The Class to be scoped or queried.
|
31
|
+
#
|
32
|
+
# Returns the object scope.
|
33
|
+
def scope(model)
|
34
|
+
model
|
35
|
+
end
|
36
|
+
|
37
|
+
# Public: Converts a name into a standard Class name.
|
38
|
+
#
|
39
|
+
# Examples
|
40
|
+
# 'egg_and_hams'.model # => EggAndHam
|
41
|
+
#
|
42
|
+
# Returns a standard Class name.
|
43
|
+
def model
|
44
|
+
name.to_s.classify.constantize
|
45
|
+
end
|
46
|
+
|
47
|
+
# Public: Find an object on the supplied scope.
|
48
|
+
#
|
49
|
+
# id - The Integer id attribute of the desired object
|
50
|
+
# scope - The collection that will be searched.
|
51
|
+
#
|
52
|
+
# Returns the found object.
|
53
|
+
def find(id, scope)
|
54
|
+
scope.find(id)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Public: Builds a new object on the passed-in scope.
|
58
|
+
#
|
59
|
+
# params - A Hash of attributes for the object to-be built.
|
60
|
+
# scope - The collection that will be searched.
|
61
|
+
#
|
62
|
+
# Returns the new object.
|
63
|
+
def build(params, scope)
|
64
|
+
scope.new(params)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Public: Returns a decorated object. This method is designed to be
|
68
|
+
# overridden.
|
69
|
+
#
|
70
|
+
# Returns the decorated object.
|
71
|
+
def decorate(instance)
|
72
|
+
instance
|
73
|
+
end
|
74
|
+
|
75
|
+
# Public: Get all the parameters of the current request.
|
76
|
+
#
|
77
|
+
# Returns the controller's parameters for the current request.
|
78
|
+
def build_params
|
79
|
+
if controller.respond_to?(params_method_name, true) && !get_request?
|
80
|
+
controller.send(params_method_name)
|
81
|
+
else
|
82
|
+
{}
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
protected
|
87
|
+
|
88
|
+
def params_id_key_candidates
|
89
|
+
[ "#{model_param_key}_id", "#{name}_id", "id" ].uniq
|
90
|
+
end
|
91
|
+
|
92
|
+
def model_param_key
|
93
|
+
model.name.underscore
|
94
|
+
end
|
95
|
+
|
96
|
+
def computed_scope
|
97
|
+
scope(model)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module DecentExposure
|
2
|
+
class Context
|
3
|
+
attr_reader :context, :attribute
|
4
|
+
|
5
|
+
# Public: Initialize a context.
|
6
|
+
#
|
7
|
+
# context - The Class where the attribute is defined.
|
8
|
+
# attribute - The attribute that will be accessed by a getter
|
9
|
+
# and setter.
|
10
|
+
def initialize(context, attribute)
|
11
|
+
@context, @attribute = context, attribute
|
12
|
+
end
|
13
|
+
|
14
|
+
# Public: Read an attribute on the context Class.
|
15
|
+
#
|
16
|
+
# Get an attribute's value. If the attribute's instance
|
17
|
+
# variable is not defined, it will create one,
|
18
|
+
# execute attribute#fetch, and assign the result
|
19
|
+
# to the instance variable.
|
20
|
+
#
|
21
|
+
# Returns the attribute's value.
|
22
|
+
def get
|
23
|
+
ivar_defined?? ivar_get : set(fetch_value)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Public: Write to an attribute on the context Class.
|
27
|
+
#
|
28
|
+
# value - The value that will be set to the attribute's
|
29
|
+
# instance variable.
|
30
|
+
#
|
31
|
+
# Returns the attribute's value.
|
32
|
+
def set(value)
|
33
|
+
ivar_set(value)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
delegate :instance_variable_set, :instance_variable_get,
|
39
|
+
:instance_variable_defined?, to: :context
|
40
|
+
|
41
|
+
def ivar_defined?
|
42
|
+
instance_variable_defined?(ivar_name)
|
43
|
+
end
|
44
|
+
|
45
|
+
def ivar_get
|
46
|
+
instance_variable_get(ivar_name)
|
47
|
+
end
|
48
|
+
|
49
|
+
def ivar_set(value)
|
50
|
+
instance_variable_set(ivar_name, value)
|
51
|
+
end
|
52
|
+
|
53
|
+
def ivar_name
|
54
|
+
"@#{attribute.ivar_name}"
|
55
|
+
end
|
56
|
+
|
57
|
+
def fetch_value
|
58
|
+
context.instance_exec(&attribute.fetch)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module DecentExposure
|
2
|
+
module Controller
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
class_attribute :exposure_configuration,
|
7
|
+
instance_accessor: false, instance_predicate: false
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
# Public: Exposes an attribute to a controller Class.
|
12
|
+
#
|
13
|
+
# *args - An Array of attributes for the new exposure. See
|
14
|
+
# Exposure#initialize for attribute details.
|
15
|
+
# block - If supplied, the exposed attribute method executes
|
16
|
+
# the Proc when accessed.
|
17
|
+
#
|
18
|
+
# Returns the helper methods that are now defined on the class
|
19
|
+
# where this method is included.
|
20
|
+
def expose(*args, &block)
|
21
|
+
Exposure.expose! self, *args, &block
|
22
|
+
end
|
23
|
+
|
24
|
+
# Public: Exposes an attribute to a controller Class.
|
25
|
+
# The exposed methods are then set to a before_action
|
26
|
+
# callback.
|
27
|
+
#
|
28
|
+
# name - The String name of the Exposure instance.
|
29
|
+
# *args - An Array of attributes for the new exposure. See
|
30
|
+
# Exposure#initialize for attribute details.
|
31
|
+
# block - If supplied, the exposed attribute method executes
|
32
|
+
# the Proc when accessed.
|
33
|
+
#
|
34
|
+
# Sets the exposed attribute to a before_action callback in the
|
35
|
+
# controller.
|
36
|
+
def expose!(name, *args, &block)
|
37
|
+
expose name, *args, &block
|
38
|
+
before_action name
|
39
|
+
end
|
40
|
+
|
41
|
+
# Public: Configures an Exposure instance for a controller Class.
|
42
|
+
#
|
43
|
+
# name - The String name of the Exposure instance.
|
44
|
+
# options - The Hash of options to configure the Exposure instance.
|
45
|
+
#
|
46
|
+
# Returns the exposure configuration Hash.
|
47
|
+
def exposure_config(name, options)
|
48
|
+
store = self.exposure_configuration ||= {}
|
49
|
+
self.exposure_configuration = store.merge(name => options)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -1,17 +1,207 @@
|
|
1
|
-
require 'decent_exposure/inflector'
|
2
|
-
|
3
1
|
module DecentExposure
|
4
2
|
class Exposure
|
5
|
-
|
3
|
+
attr_reader :controller, :options
|
4
|
+
|
5
|
+
# Public: Initializes an Exposure and makes it accessible to a controller.
|
6
|
+
# For each Exposure, a getter and setter is defined.
|
7
|
+
# Those getters and setters are made available to
|
8
|
+
# the controller as helper methods.
|
9
|
+
#
|
10
|
+
# *args - An Array of all parameters for the new Exposure. See
|
11
|
+
# #initialize.
|
12
|
+
# block - If supplied, the exposed attribute method executes
|
13
|
+
# the Proc when called.
|
14
|
+
#
|
15
|
+
# Returns a collection of exposed helper methods.
|
16
|
+
def self.expose!(*args, &block)
|
17
|
+
new(*args, &block).expose!
|
18
|
+
end
|
19
|
+
|
20
|
+
# Public: Initalize an Exposure with a hash of options.
|
21
|
+
#
|
22
|
+
# If a block is given, the Proc is assigned to value
|
23
|
+
# of options[name].
|
24
|
+
#
|
25
|
+
# The `asserts_*` section raise errors if the controller
|
26
|
+
# was initialized with an unacceptable options Hash.
|
27
|
+
#
|
28
|
+
# controller - The Controller class where methods will be exposed.
|
29
|
+
# name - The String name of the Exposure instance.
|
30
|
+
# fetch_block - Proc that will be executed if the exposed
|
31
|
+
# attribute has no value (default: nil).
|
32
|
+
# options - Hash of options for the Behavior of the exposed methods.
|
33
|
+
# block - If supplied, the exposed attribute method executes
|
34
|
+
# the Proc.
|
35
|
+
#
|
36
|
+
# Returns a normalized options Hash.
|
37
|
+
def initialize(controller, name, fetch_block=nil, **options, &block)
|
38
|
+
@controller = controller
|
39
|
+
@options = options.with_indifferent_access.merge(name: name)
|
40
|
+
|
41
|
+
merge_lambda_option :fetch, fetch_block if fetch_block
|
42
|
+
merge_lambda_option :fetch, block if block_given?
|
43
|
+
|
44
|
+
assert_singleton_option :fetch
|
45
|
+
assert_singleton_option :from
|
46
|
+
assert_incompatible_options_pair :parent, :model
|
47
|
+
assert_incompatible_options_pair :parent, :scope
|
48
|
+
assert_incompatible_options_pair :find_by, :find
|
49
|
+
|
50
|
+
normalize_options
|
51
|
+
end
|
52
|
+
|
53
|
+
# Public: Creates a getter and setter methods for the attribute.
|
54
|
+
# Those methods are made avaiable to the controller as
|
55
|
+
# helper methods.
|
56
|
+
#
|
57
|
+
# Returns a collection of exposed helper methods.
|
58
|
+
def expose!
|
59
|
+
expose_attribute!
|
60
|
+
expose_helper_methods!
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def expose_attribute!
|
66
|
+
attribute.expose! controller
|
67
|
+
end
|
68
|
+
|
69
|
+
def expose_helper_methods!
|
70
|
+
helper_methods = [ attribute.getter_method_name, attribute.setter_method_name ]
|
71
|
+
controller.helper_method *helper_methods
|
72
|
+
end
|
73
|
+
|
74
|
+
def normalize_options
|
75
|
+
normalize_fetch_option
|
76
|
+
normalize_with_option
|
77
|
+
normalize_id_option
|
78
|
+
normalize_model_option
|
79
|
+
normalize_build_params_option
|
80
|
+
normalize_scope_options
|
81
|
+
normalize_parent_option
|
82
|
+
normalize_from_option
|
83
|
+
normalize_find_by_option
|
84
|
+
end
|
85
|
+
|
86
|
+
def normalize_fetch_option
|
87
|
+
normalize_non_proc_option :fetch do |method_name|
|
88
|
+
->{ send(method_name) }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def normalize_find_by_option
|
93
|
+
if find_by = options.delete(:find_by)
|
94
|
+
merge_lambda_option :find, ->(id, scope){ scope.find_by!(find_by => id) }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def normalize_parent_option
|
99
|
+
exposure_name = options.fetch(:name)
|
100
|
+
|
101
|
+
if parent = options.delete(:parent)
|
102
|
+
merge_lambda_option :scope, ->{ send(parent).send(exposure_name.to_s.pluralize) }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def normalize_from_option
|
107
|
+
exposure_name = options.fetch(:name)
|
108
|
+
|
109
|
+
if from = options.delete(:from)
|
110
|
+
merge_lambda_option :build, ->{ send(from).send(exposure_name) }
|
111
|
+
merge_lambda_option :model, ->{ nil }
|
112
|
+
merge_lambda_option :id, ->{ nil }
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def normalize_with_option
|
117
|
+
if configs = options.delete(:with)
|
118
|
+
Array.wrap(configs).each{ |config| reverse_merge_config! config }
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def normalize_id_option
|
123
|
+
normalize_non_proc_option :id do |ids|
|
124
|
+
->{ Array.wrap(ids).map{ |id| params[id] }.find(&:present?) }
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def normalize_model_option
|
129
|
+
normalize_non_proc_option :model do |value|
|
130
|
+
model = if [String, Symbol].include?(value.class)
|
131
|
+
value.to_s.classify.constantize
|
132
|
+
else
|
133
|
+
value
|
134
|
+
end
|
135
|
+
|
136
|
+
->{ model }
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def normalize_build_params_option
|
141
|
+
normalize_non_proc_option :build_params do |value|
|
142
|
+
options[:build_params_method] = value
|
143
|
+
nil
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def normalize_scope_options
|
148
|
+
normalize_non_proc_option :scope do |custom_scope|
|
149
|
+
->(model){ model.send(custom_scope) }
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def normalize_non_proc_option(name)
|
154
|
+
option_value = options[name]
|
155
|
+
return if Proc === option_value
|
156
|
+
if option_value.present?
|
157
|
+
normalized_value = yield(option_value)
|
158
|
+
if normalized_value
|
159
|
+
merge_lambda_option name, normalized_value
|
160
|
+
else
|
161
|
+
options.delete name
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def merge_lambda_option(name, body)
|
167
|
+
if previous_value = options[name] and Proc === previous_value
|
168
|
+
fail ArgumentError, "#{name.to_s.titleize} block is already defined"
|
169
|
+
end
|
170
|
+
|
171
|
+
options[name] = body
|
172
|
+
end
|
173
|
+
|
174
|
+
def attribute
|
175
|
+
@attribute ||= begin
|
176
|
+
local_options = options
|
177
|
+
|
178
|
+
name = options.fetch(:name)
|
179
|
+
ivar_name = "exposed_#{name}"
|
180
|
+
fetch = ->{ Flow.new(self, local_options).fetch }
|
181
|
+
|
182
|
+
Attribute.new(
|
183
|
+
name: name,
|
184
|
+
ivar_name: ivar_name,
|
185
|
+
fetch: fetch
|
186
|
+
)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def assert_incompatible_options_pair(key1, key2)
|
191
|
+
if options.key?(key1) && options.key?(key2)
|
192
|
+
fail ArgumentError, "Using #{key1.inspect} option with #{key2.inspect} doesn't make sense"
|
193
|
+
end
|
194
|
+
end
|
6
195
|
|
7
|
-
def
|
8
|
-
|
9
|
-
|
10
|
-
|
196
|
+
def assert_singleton_option(name)
|
197
|
+
if options.except(name, :name, :decorate).any? && options.key?(name)
|
198
|
+
fail ArgumentError, "Using #{name.inspect} option with other options doesn't make sense"
|
199
|
+
end
|
11
200
|
end
|
12
201
|
|
13
|
-
def
|
14
|
-
|
202
|
+
def reverse_merge_config!(name)
|
203
|
+
config = controller.exposure_configuration.fetch(name)
|
204
|
+
options.reverse_merge! config
|
15
205
|
end
|
16
206
|
end
|
17
207
|
end
|