chain_options 0.1.0
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 +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +138 -0
- data/.travis.yml +21 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +72 -0
- data/LICENSE.txt +21 -0
- data/README.md +319 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/chain_options.gemspec +32 -0
- data/lib/chain_options/builder.rb +27 -0
- data/lib/chain_options/integration.rb +96 -0
- data/lib/chain_options/option.rb +195 -0
- data/lib/chain_options/option_set.rb +123 -0
- data/lib/chain_options/test_integration/rspec.rb +131 -0
- data/lib/chain_options/util.rb +99 -0
- data/lib/chain_options/version.rb +5 -0
- data/lib/chain_options.rb +12 -0
- metadata +165 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChainOptions
|
4
|
+
#
|
5
|
+
# A simple helper class to set multiple chain options after another
|
6
|
+
# by using a `set NAME, VALUE` syntax instead of having to
|
7
|
+
# use constructs like
|
8
|
+
# instance = instance.NAME(VALUE)
|
9
|
+
# instance = instance.NAME2(VALUE2) if XY
|
10
|
+
# ...
|
11
|
+
#
|
12
|
+
class Builder
|
13
|
+
def initialize(initial_instance, &block)
|
14
|
+
@instance = initial_instance
|
15
|
+
ChainOptions::Util.instance_eval_or_call(self, &block)
|
16
|
+
result
|
17
|
+
end
|
18
|
+
|
19
|
+
def result
|
20
|
+
@instance
|
21
|
+
end
|
22
|
+
|
23
|
+
def set(option_name, *value, &block)
|
24
|
+
@instance = @instance.send(option_name, *value, &block)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChainOptions
|
4
|
+
module Integration
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
base.extend(ClassMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
#
|
12
|
+
# Generates a combined getter and setter method for the
|
13
|
+
# option with the given name.
|
14
|
+
#
|
15
|
+
# @param [String, Symbol] name
|
16
|
+
#
|
17
|
+
# @param [Hash] options
|
18
|
+
# @option options [Object, Proc] :default (nil)
|
19
|
+
# Sets the value which should be used whenever no custom value was set for this option
|
20
|
+
#
|
21
|
+
# @option options [Symbol] :invalid (:raise)
|
22
|
+
# Sets the behaviour when an invalid value is given.
|
23
|
+
# If set to `:raise`, an ArgumentError is raised if an option validation fails,
|
24
|
+
# if set to `:default`, the default value is used instead of the invalid value
|
25
|
+
#
|
26
|
+
# @option options [Proc] :validate (nil)
|
27
|
+
# Sets up a validation proc for the option value.
|
28
|
+
# See :invalid for information about what happens when a validation fails
|
29
|
+
#
|
30
|
+
# @option options [Proc, Symbol] :filter (nil)
|
31
|
+
# An optional filter method to reject certain values.
|
32
|
+
# See ChainOptions::OptionSet#filter_value for more information
|
33
|
+
#
|
34
|
+
# @option options [Proc, Symbol] :transform (nil)
|
35
|
+
# An optional transformation that transforms the given value.
|
36
|
+
# See ChainOptions::OptionSet#transform_value for more information.
|
37
|
+
#
|
38
|
+
# @option options [Boolean] :incremental (false)
|
39
|
+
# If set to +true+, overriding an option value will no longer be possible.
|
40
|
+
# Instead, the whole option value is treated as an Array with each new value simply being
|
41
|
+
# appended to it.
|
42
|
+
# user.favourite_books('Momo', 'Neverending Story').favourite_books('Lord of the Rings', 'The Hobbit')
|
43
|
+
# => [["Momo", "Neverending Story"], ["Lord of the Rings", "The Hobbit"]]
|
44
|
+
#
|
45
|
+
# @option options [Boolean] :allow_block (false)
|
46
|
+
# Sets whether the option value may be a proc object given through a block.
|
47
|
+
# If set to +true+, the following statement would result in `block` being saved as option value:
|
48
|
+
# instance.my_option(&block)
|
49
|
+
#
|
50
|
+
def chain_option(name, **options)
|
51
|
+
available_chain_options[name.to_sym] = options
|
52
|
+
|
53
|
+
ChainOptions::OptionSet.handle_warnings(name, **options)
|
54
|
+
|
55
|
+
define_method(name) do |*args, &block|
|
56
|
+
chain_option_set.handle_option_call(name, *args, &block)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def available_chain_options
|
61
|
+
@available_chain_options ||= {}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def initialize(**options)
|
66
|
+
@chain_option_values = options
|
67
|
+
end
|
68
|
+
|
69
|
+
#
|
70
|
+
# Allows setting multiple options in a block. This makes long option chains easier to read.
|
71
|
+
#
|
72
|
+
# @example The following expressions are equivalent
|
73
|
+
# instance.option1(value).option2(value).option3 { value3 }
|
74
|
+
# instance.build_options do
|
75
|
+
# set :option1, value
|
76
|
+
# set :option2, value2
|
77
|
+
# set(:option3) { value3 }
|
78
|
+
#
|
79
|
+
def build_options(&block)
|
80
|
+
ChainOptions::Builder.new(self, &block).result
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def chain_option_set
|
86
|
+
@chain_option_set ||= OptionSet.new(self, self.class.available_chain_options, chain_option_values)
|
87
|
+
end
|
88
|
+
|
89
|
+
#
|
90
|
+
# @return [Hash] the currently set options for the current instance.
|
91
|
+
#
|
92
|
+
def chain_option_values
|
93
|
+
@chain_option_values ||= {}
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChainOptions
|
4
|
+
#
|
5
|
+
# This class represents an Option from a ChainOptions::OptionSet and is
|
6
|
+
# mainly responsible for handling option state.
|
7
|
+
#
|
8
|
+
class Option
|
9
|
+
# The Parameters that need to be turned into instance methods, if they are symbols.
|
10
|
+
METHOD_SYMBOLS = %i[filter validate].freeze
|
11
|
+
PARAMETERS = %i[incremental default transform filter validate invalid allow_block].freeze
|
12
|
+
|
13
|
+
(PARAMETERS - [:allow_block]).each do |param|
|
14
|
+
define_method(param) { options[param] }
|
15
|
+
private param
|
16
|
+
end
|
17
|
+
|
18
|
+
def allow_block
|
19
|
+
options[:allow_block]
|
20
|
+
end
|
21
|
+
|
22
|
+
#
|
23
|
+
# Extracts options and sets all the parameters.
|
24
|
+
#
|
25
|
+
def initialize(options)
|
26
|
+
self.options = options
|
27
|
+
@dirty = false
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# Builds a new value for the option.
|
32
|
+
# It automatically applies transformations and filters and validates the
|
33
|
+
# resulting value, raising an exception if the value is not valid.
|
34
|
+
#
|
35
|
+
def new_value(*args, &block)
|
36
|
+
value = value_from_args(args, &block)
|
37
|
+
|
38
|
+
value = if incremental
|
39
|
+
incremental_value(value)
|
40
|
+
else
|
41
|
+
filter_value(transformed_value(value))
|
42
|
+
end
|
43
|
+
|
44
|
+
if value_valid?(value)
|
45
|
+
self.custom_value = value
|
46
|
+
elsif invalid.to_s == 'default' && !incremental
|
47
|
+
default_value
|
48
|
+
else
|
49
|
+
fail ArgumentError, "The value #{value.inspect} is not valid."
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# The current value if there is one. Otherwise returns the default value.
|
55
|
+
#
|
56
|
+
def current_value
|
57
|
+
return custom_value if dirty?
|
58
|
+
|
59
|
+
default_value
|
60
|
+
end
|
61
|
+
|
62
|
+
#
|
63
|
+
# Circumvents the normal value process to set an initial_value. Only works on a
|
64
|
+
# clean option.
|
65
|
+
#
|
66
|
+
def initial_value(value)
|
67
|
+
raise ArgumentError, "The initial_value was already set to #{custom_value.inspect}." if dirty?
|
68
|
+
|
69
|
+
self.custom_value = value
|
70
|
+
end
|
71
|
+
|
72
|
+
#
|
73
|
+
# Looks through the parameters and returns the non-nil values as a hash
|
74
|
+
#
|
75
|
+
def to_h
|
76
|
+
PARAMETERS.each_with_object({}) do |param, hash|
|
77
|
+
next if send(param).nil?
|
78
|
+
|
79
|
+
hash[param] = send(param)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
attr_accessor :options
|
86
|
+
|
87
|
+
# A value that has been set by a user. Overrides default_value.
|
88
|
+
attr_reader :custom_value
|
89
|
+
|
90
|
+
#
|
91
|
+
# Sets the current value and marks the option as dirty.
|
92
|
+
#
|
93
|
+
def custom_value=(value)
|
94
|
+
@dirty = true
|
95
|
+
@custom_value = value
|
96
|
+
end
|
97
|
+
|
98
|
+
#
|
99
|
+
# Returns the block if nothing else if given and blocks are allowed to be
|
100
|
+
# values.
|
101
|
+
#
|
102
|
+
def value_from_args(args, &block)
|
103
|
+
return block if ChainOptions::Util.blank?(args) && block && allow_block
|
104
|
+
|
105
|
+
flat_value(args)
|
106
|
+
end
|
107
|
+
|
108
|
+
#
|
109
|
+
# Reverses the auto-cast to Array that is applied at `new_value`.
|
110
|
+
#
|
111
|
+
def flat_value(args)
|
112
|
+
return args.first if args.is_a?(Enumerable) && args.count == 1
|
113
|
+
|
114
|
+
args
|
115
|
+
end
|
116
|
+
|
117
|
+
#
|
118
|
+
# Incremental values assume 2d arrays. Here the current_value is only
|
119
|
+
# prepended if the option is dirty.
|
120
|
+
#
|
121
|
+
def incremental_value(value)
|
122
|
+
dirty? ? [*current_value, Array(value)] : [Array(value)]
|
123
|
+
end
|
124
|
+
|
125
|
+
#
|
126
|
+
# Describes whether the default value has already been replaced.
|
127
|
+
#
|
128
|
+
def dirty?
|
129
|
+
!!@dirty
|
130
|
+
end
|
131
|
+
|
132
|
+
#
|
133
|
+
# Checks whether a new chain option value is valid or not using the `validate` values
|
134
|
+
# used when setting up `chain_option`s.
|
135
|
+
#
|
136
|
+
# Please note that the value passed to this function already went through
|
137
|
+
# the chain option's filters - if any. This means that it will always be a collection
|
138
|
+
# if a filter was defined.
|
139
|
+
#
|
140
|
+
# If no validation was set up, the new value will always be accepted
|
141
|
+
#
|
142
|
+
# @return [Boolean] +true+ if the new value is valid.
|
143
|
+
#
|
144
|
+
def value_valid?(value)
|
145
|
+
return true unless validate
|
146
|
+
|
147
|
+
validate.call(value)
|
148
|
+
end
|
149
|
+
|
150
|
+
#
|
151
|
+
# Applies a transformation to the given value.
|
152
|
+
#
|
153
|
+
# @param [Object] value
|
154
|
+
# The new value to be transformed
|
155
|
+
#
|
156
|
+
# If a `transform` was set up for the given option, it is used
|
157
|
+
# as `to_proc` target when iterating over the value.
|
158
|
+
# The value is always treated as a collection during this phase.
|
159
|
+
#
|
160
|
+
def transformed_value(value)
|
161
|
+
return value unless transform
|
162
|
+
|
163
|
+
transformed = Array(value).map(&transform)
|
164
|
+
value.is_a?(Enumerable) ? transformed : transformed.first
|
165
|
+
end
|
166
|
+
|
167
|
+
#
|
168
|
+
# Applies a filter to the given value.
|
169
|
+
# Expects the value to be some kind of collection. If it isn't, it is treated
|
170
|
+
# as an array with one element.
|
171
|
+
#
|
172
|
+
# @param [Object] value
|
173
|
+
# The new value to be filtered
|
174
|
+
#
|
175
|
+
# @example using a filter proc
|
176
|
+
# filter_value [1, 2, 3, 4, 5] # with filter: ->(entry) { entry.even? }
|
177
|
+
# #=> [2, 4]
|
178
|
+
#
|
179
|
+
def filter_value(value)
|
180
|
+
return value unless filter
|
181
|
+
|
182
|
+
Array(value).select(&filter)
|
183
|
+
end
|
184
|
+
|
185
|
+
#
|
186
|
+
# @return [Object] the default value which was set for the given option name.
|
187
|
+
# If a proc is given, the result of `proc.call` will be returned.
|
188
|
+
#
|
189
|
+
def default_value
|
190
|
+
return default unless default.respond_to?(:call)
|
191
|
+
|
192
|
+
default.call
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChainOptions
|
4
|
+
class OptionSet
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
#
|
9
|
+
# Warns of incompatible options for a chain_option.
|
10
|
+
# This does not necessarily mean that an error will be raised.
|
11
|
+
#
|
12
|
+
def warn_incompatible_options(option_name, *options)
|
13
|
+
STDERR.puts "The options #{options.join(', ')} are incompatible for the chain_option #{option_name}."
|
14
|
+
end
|
15
|
+
|
16
|
+
#
|
17
|
+
# Prints warnings for incompatible options which were used as arguments in `chain_option`
|
18
|
+
#
|
19
|
+
def handle_warnings(name, incremental: false, invalid: :raise, filter: nil, transform: nil, **)
|
20
|
+
if incremental
|
21
|
+
warn_incompatible_options(name, 'invalid: :default', 'incremental: true') if invalid.to_s == 'default'
|
22
|
+
warn_incompatible_options(name, 'incremental: true', 'filter:') if filter
|
23
|
+
warn_incompatible_options(name, 'incremental: true', 'transform:') if transform
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# @param [Object] instance the object that uses the chain_option set.
|
30
|
+
# @param [Hash] chain_options a hash of `{name: config_hash}` to initialize
|
31
|
+
# options from a config hash.
|
32
|
+
# @param [Hash] values a hash of `{name: value}` with initial values for the
|
33
|
+
# named options.
|
34
|
+
#
|
35
|
+
def initialize(instance, chain_options = {}, values = {})
|
36
|
+
@instance = instance
|
37
|
+
@values = values
|
38
|
+
@chain_options = chain_options.inject({}) do |options, (name, config)|
|
39
|
+
options.merge(name => config.merge(instance_method_hash(config)))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
attr_reader :instance
|
44
|
+
|
45
|
+
#
|
46
|
+
# Checks the given option-parameters for incompatibilities and registers a
|
47
|
+
# new option.
|
48
|
+
#
|
49
|
+
def add_option(name, parameters)
|
50
|
+
self.class.handle_warnings(name, **parameters.dup)
|
51
|
+
chain_options.merge(name => parameters.merge(method_hash(parameters)))
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# Returns the current_value of an option.
|
56
|
+
#
|
57
|
+
def current_value(name)
|
58
|
+
option(name).current_value
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# Returns an option registered under `name`.
|
63
|
+
#
|
64
|
+
def option(name)
|
65
|
+
config = chain_options[name] || raise_no_option_error(name)
|
66
|
+
Option.new(config).tap { |o| o.initial_value(values[name]) if values.key?(name) }
|
67
|
+
end
|
68
|
+
|
69
|
+
#
|
70
|
+
# Builds a new value for the given chain option.
|
71
|
+
# It automatically applies transformations and filters and validates the
|
72
|
+
# resulting value, raising an exception if the value is not valid.
|
73
|
+
#
|
74
|
+
def new_value(name, *args, &block)
|
75
|
+
option(name).new_value(*args, &block)
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# Handles a call of #option_name.
|
80
|
+
# Determines whether the call was meant to be a setter or a getter and
|
81
|
+
# acts accordingly.
|
82
|
+
#
|
83
|
+
def handle_option_call(option_name, *args, &block)
|
84
|
+
if getter?(option_name, *args, &block)
|
85
|
+
current_value(option_name)
|
86
|
+
else
|
87
|
+
new_value = new_value(option_name, *args, &block)
|
88
|
+
instance.class.new(@values.merge(option_name.to_sym => new_value))
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
attr_reader :values, :chain_options
|
95
|
+
|
96
|
+
#
|
97
|
+
# @return [Boolean] +true+ if a call to the corresponding option method with the given args / block
|
98
|
+
# can be handled as a getter (no args / no block usage)
|
99
|
+
#
|
100
|
+
def getter?(option_name, *args, &block)
|
101
|
+
args.empty? && (block.nil? || !option(option_name).allow_block)
|
102
|
+
end
|
103
|
+
|
104
|
+
# no-doc
|
105
|
+
def raise_no_option_error(name)
|
106
|
+
fail ArgumentError, "There is no option registered called #{name}."
|
107
|
+
end
|
108
|
+
|
109
|
+
#
|
110
|
+
# Checks the given options and transforms certain options into closures,
|
111
|
+
# if they are symbols before.
|
112
|
+
# The keys that are being transformed can be seen at `Option::METHOD_SYMBOLS`.
|
113
|
+
# @return [Hash] a set of parameters and closures.
|
114
|
+
#
|
115
|
+
def instance_method_hash(options)
|
116
|
+
ChainOptions::Util.slice(options, Option::METHOD_SYMBOLS).each_with_object({}) do |(meth, value), h|
|
117
|
+
next h[meth] = value if value.respond_to?(:call)
|
118
|
+
|
119
|
+
h[meth] = instance.public_method(value)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rspec/expectations'
|
4
|
+
|
5
|
+
#
|
6
|
+
# Custom matcher to test for chain option behaviour.
|
7
|
+
#
|
8
|
+
# Every matcher call starts with `have_chain_option` which ensures the the given
|
9
|
+
# object actually has access to a chain option with the given name.
|
10
|
+
#
|
11
|
+
# ## Value Acceptance
|
12
|
+
#
|
13
|
+
# To test for values which should raise an exception when being set as a chain option value,
|
14
|
+
# continue the matcher as follows:
|
15
|
+
# it { is_expected.to have_chain_option(:my_option).which_takes(42).and_raises_an_exception }
|
16
|
+
#
|
17
|
+
# ## Value Filters / Transformations
|
18
|
+
#
|
19
|
+
# To test whether the option is actually set to the correct value after passing an object to it,
|
20
|
+
# continue the matcher as follows:
|
21
|
+
# it { is_expected.to have_chain_option(:my_option).which_takes(42).and_sets_it_as_value }
|
22
|
+
#
|
23
|
+
# If you expect the option to perform a filtering and/or transformation, you can also
|
24
|
+
# specify the actual value you expect to be set:
|
25
|
+
# it { is_expected.to have_chain_option(:my_option).which_takes(42).and_sets("42").as_value }
|
26
|
+
#
|
27
|
+
# ## Default Value
|
28
|
+
#
|
29
|
+
# To test whether the option has a certain default value, continue the matcher as follows:
|
30
|
+
# it { is_expected.to have_chain_option(:my_option).with_the_default_value(21) }
|
31
|
+
#
|
32
|
+
module ChainOptions
|
33
|
+
module TestIntegration
|
34
|
+
module Rspec
|
35
|
+
extend ::RSpec::Matchers::DSL
|
36
|
+
|
37
|
+
matcher :have_chain_option do |option_name|
|
38
|
+
match do |instance|
|
39
|
+
unless chain_option?(instance)
|
40
|
+
error_lines "Expected the class `#{instance.class}`",
|
41
|
+
"to define the chain option `:#{option_name}`,",
|
42
|
+
"but it didn't."
|
43
|
+
next false
|
44
|
+
end
|
45
|
+
|
46
|
+
if instance_variable_defined?('@expected_default_value')
|
47
|
+
next false unless correct_default_value?(instance)
|
48
|
+
end
|
49
|
+
|
50
|
+
if instance_variable_defined?('@given_value')
|
51
|
+
if @exception_expected
|
52
|
+
check_for_exception(instance)
|
53
|
+
elsif instance_variable_defined?('@expected_value')
|
54
|
+
check_for_expected_value(instance)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
@error.nil?
|
59
|
+
end
|
60
|
+
|
61
|
+
def error_lines(*lines)
|
62
|
+
@error = lines[1..-1].reduce(lines.first) do |error_string, line|
|
63
|
+
error_string + "\n #{line}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
define_method :chain_option? do |instance|
|
68
|
+
instance.class.available_chain_options.key?(option_name.to_sym)
|
69
|
+
end
|
70
|
+
|
71
|
+
define_method :correct_default_value? do |instance|
|
72
|
+
actual_default_value = instance.send(option_name)
|
73
|
+
next true if actual_default_value == @expected_default_value
|
74
|
+
|
75
|
+
error_lines "Expected the chain option `:#{option_name}`",
|
76
|
+
"of the class `#{instance.class}`",
|
77
|
+
"to have the default value `#{@expected_default_value.inspect}`",
|
78
|
+
"but the actual default value is `#{actual_default_value.inspect}`"
|
79
|
+
end
|
80
|
+
|
81
|
+
define_method :check_for_exception do |instance|
|
82
|
+
begin
|
83
|
+
instance.send(option_name, @given_value)
|
84
|
+
error_lines "Expected the chain option `:#{option_name}`",
|
85
|
+
"not to accept the value `#{@given_value.inspect}`,",
|
86
|
+
'but it did.'
|
87
|
+
rescue ArgumentError => e
|
88
|
+
raise unless e.message.include?('not valid')
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
define_method :check_for_expected_value do |instance|
|
93
|
+
actual_value = instance.send(option_name, @given_value).send(option_name)
|
94
|
+
if actual_value != @expected_value
|
95
|
+
error_lines "Expected the chain option `:#{option_name}`",
|
96
|
+
"of the class `#{instance.class}`",
|
97
|
+
"to accept the value `#{@given_value.inspect}`",
|
98
|
+
"and set the option value to `#{@expected_value.inspect}`,",
|
99
|
+
"but it was set to `#{actual_value.inspect}`"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
chain :with_the_default_value do |expected_default_value|
|
104
|
+
@expected_default_value = expected_default_value
|
105
|
+
end
|
106
|
+
|
107
|
+
chain :which_takes do |value|
|
108
|
+
@given_value = value
|
109
|
+
end
|
110
|
+
|
111
|
+
chain :and_sets do |expected_value|
|
112
|
+
@expected_value = expected_value
|
113
|
+
end
|
114
|
+
|
115
|
+
chain :and_sets_it_as_value do
|
116
|
+
@expected_value = @given_value
|
117
|
+
end
|
118
|
+
|
119
|
+
chain :and_raises_an_exception do
|
120
|
+
@exception_expected = true
|
121
|
+
end
|
122
|
+
|
123
|
+
chain(:as_value) {}
|
124
|
+
|
125
|
+
failure_message do
|
126
|
+
@error.to_s
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChainOptions
|
4
|
+
module Util
|
5
|
+
|
6
|
+
def self.blank?(obj)
|
7
|
+
(obj.is_a?(Enumerable) && obj.empty?) || obj.nil? || obj == ''
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.slice(hash, keys)
|
11
|
+
keys.each_with_object({}) do |key, h|
|
12
|
+
h[key] = hash[key] if hash[key]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
#
|
17
|
+
# Evaluate the given proc in the context of the given object if the
|
18
|
+
# block's arity is non-positive, or by passing the given object as an
|
19
|
+
# argument if it is negative.
|
20
|
+
#
|
21
|
+
# ==== Parameters
|
22
|
+
#
|
23
|
+
# object<Object>:: Object to pass to the proc
|
24
|
+
#
|
25
|
+
def self.instance_eval_or_call(object, &block)
|
26
|
+
if block.arity.positive?
|
27
|
+
block.call(object)
|
28
|
+
else
|
29
|
+
ContextBoundDelegate.instance_eval_with_context(object, &block)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
# Shamelessly copied from sunspot/sunspot with some alterations
|
35
|
+
#
|
36
|
+
class ContextBoundDelegate
|
37
|
+
class << self
|
38
|
+
def instance_eval_with_context(receiver, &block)
|
39
|
+
calling_context = eval('self', block.binding, __FILE__, __LINE__)
|
40
|
+
|
41
|
+
parent_calling_context = calling_context.instance_eval do
|
42
|
+
@__calling_context__ if defined?(@__calling_context__)
|
43
|
+
end
|
44
|
+
|
45
|
+
calling_context = parent_calling_context if parent_calling_context
|
46
|
+
new(receiver, calling_context).instance_eval(&block)
|
47
|
+
end
|
48
|
+
|
49
|
+
private :new
|
50
|
+
end
|
51
|
+
|
52
|
+
BASIC_METHODS = Set[:==, :equal?, :"!", :"!=", :instance_eval,
|
53
|
+
:object_id, :__send__, :__id__]
|
54
|
+
|
55
|
+
instance_methods.each do |method|
|
56
|
+
unless BASIC_METHODS.include?(method.to_sym)
|
57
|
+
undef_method(method)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def initialize(receiver, calling_context)
|
62
|
+
@__receiver__, @__calling_context__ = receiver, calling_context
|
63
|
+
end
|
64
|
+
|
65
|
+
def id
|
66
|
+
@__calling_context__.__send__(:id)
|
67
|
+
rescue ::NoMethodError => e
|
68
|
+
begin
|
69
|
+
@__receiver__.__send__(:id)
|
70
|
+
rescue ::NoMethodError
|
71
|
+
raise(e)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Special case due to `Kernel#sub`'s existence
|
76
|
+
def sub(*args, &block)
|
77
|
+
__proxy_method__(:sub, *args, &block)
|
78
|
+
end
|
79
|
+
|
80
|
+
def method_missing(method, *args, &block)
|
81
|
+
__proxy_method__(method, *args, &block)
|
82
|
+
end
|
83
|
+
|
84
|
+
def respond_to_missing?(meth)
|
85
|
+
@__receiver__.respond_to?(meth) || @__calling_context__.respond_to?(meth)
|
86
|
+
end
|
87
|
+
|
88
|
+
def __proxy_method__(method, *args, &block)
|
89
|
+
@__receiver__.__send__(method.to_sym, *args, &block)
|
90
|
+
rescue ::NoMethodError => e
|
91
|
+
begin
|
92
|
+
@__calling_context__.__send__(method.to_sym, *args, &block)
|
93
|
+
rescue ::NoMethodError
|
94
|
+
raise(e)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'chain_options/version'
|
4
|
+
|
5
|
+
require 'chain_options/util'
|
6
|
+
require 'chain_options/option_set'
|
7
|
+
require 'chain_options/option'
|
8
|
+
require 'chain_options/integration'
|
9
|
+
require 'chain_options/builder'
|
10
|
+
|
11
|
+
module ChainOptions
|
12
|
+
end
|