class_composer 2.0.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/docs/basic_composer.md +15 -0
- data/docs/generating_initializer.md +1 -1
- data/lib/class_composer/generate_config.rb +21 -3
- data/lib/class_composer/generator/class_methods.rb +233 -0
- data/lib/class_composer/generator/instance_methods.rb +73 -0
- data/lib/class_composer/generator.rb +2 -252
- data/lib/class_composer/version.rb +1 -1
- data/lib/class_composer.rb +0 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ce164acfb44d923b971f227a236fef5388cbeef8295f8419a46f815ac8bdaa0f
|
4
|
+
data.tar.gz: a0adb222a5704fd56916621669c9cce3112e07a27e338ca2c5e8b51e4a91d42f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fee77c510173e27b695924ff5f5ddb050e00bfb3d26e4b8f5a124576fa72554f462ca06e0ecd31abbb150b6aca339cb22fef11d46724829e0bb0f11f937d4ac8
|
7
|
+
data.tar.gz: 5ce561d2dcc6b3e0e87efc4e55eec8ab243aff03ae758c069ce816674378a39c6f3e8e0c36e925c013e3d255b56d714647dd6ca4a831f3cd82dc0c3306ad6abd
|
data/CHANGELOG.md
CHANGED
@@ -10,6 +10,11 @@
|
|
10
10
|
|
11
11
|
# Release Notes
|
12
12
|
|
13
|
+
## v2.1.0 (Dec 2024)
|
14
|
+
- Default Dynamic Configuration -- Allow config options to be defaulted to other config variables. Ability to provide a Symbol of an existing `composer` or a Proc to custom define from anything in the application
|
15
|
+
- Update how default_shown works when generating the config
|
16
|
+
- Extract `class_methods` and `instance_methods` to their own files
|
17
|
+
|
13
18
|
## v2.0.0 (Nov 2024)
|
14
19
|
- Minimum Ruby Version bumped to v3.1
|
15
20
|
- Initializer File Generator
|
data/docs/basic_composer.md
CHANGED
@@ -39,6 +39,21 @@ add_composer :status, allowed: Symbol, default: :different_default
|
|
39
39
|
add_composer :status, allowed: Symbol, default: "This type does not match Symbol. This default value will raise error"
|
40
40
|
```
|
41
41
|
|
42
|
+
### Dynamic Default
|
43
|
+
- Required: False
|
44
|
+
- Description: This option allows you to set a dynamic default composed of other `add_composer` values. The `dynamic_default` is lazy loaded.
|
45
|
+
- Type: Expected value to be a Symbol mapping to another `add_composer` method or a Proc that takes an instance as an argument.
|
46
|
+
- Note: When using `dynamic_default`, validations will lazily get run on retrieval. This means that you could have a runtime error if the dynamic value is not of the correct `allowed` type.
|
47
|
+
|
48
|
+
Examples
|
49
|
+
```ruby
|
50
|
+
add_composer :app_name, allowed: Symbol, default: "My Cool App Name"
|
51
|
+
|
52
|
+
add_composer :app_name_comms, allowed: String, dynamic_default: :app_name
|
53
|
+
|
54
|
+
add_composer :app_name_website, allowed: String, dynamic_default: ->(instance) { "#{instance.app_name} + #{instance.app_name_comms}" }
|
55
|
+
```
|
56
|
+
|
42
57
|
### Description
|
43
58
|
- Required: False (Recommend)
|
44
59
|
- Description: This option provides a human readable description of what the current configuration option does. This value is useful when [Generating an Initializer](generating_initializer.md)
|
@@ -28,7 +28,7 @@ end
|
|
28
28
|
class AppConfiguration
|
29
29
|
include ClassComposer::Generator
|
30
30
|
|
31
|
-
|
31
|
+
add_composer :login, allowed: LoginStrategy, default: LoginStrategy.new, desc: "Login Strategy for my Application"
|
32
32
|
|
33
33
|
add_composer_blocking :lockable, composer_class: LockableStrategy, enable_attr: :enable, desc: "Lock Strategy for my Application. By default this is disabled"
|
34
34
|
end
|
@@ -57,7 +57,6 @@ module ClassComposer
|
|
57
57
|
children.each do |child|
|
58
58
|
children_config += generate(mapping: child, space_count:, demeters_deep: config_prepend)
|
59
59
|
end
|
60
|
-
# binding.pry
|
61
60
|
|
62
61
|
children_config.flatten(1)
|
63
62
|
else
|
@@ -100,10 +99,18 @@ module ClassComposer
|
|
100
99
|
def spec(key:, metadata:, space_count:, demeters_deep:)
|
101
100
|
config = concat_demeter_with_key(key, demeters_deep)
|
102
101
|
|
103
|
-
if metadata[:
|
102
|
+
if metadata[:default_shown]
|
103
|
+
default = metadata[:default_shown]
|
104
|
+
elsif metadata[:dynamic_default]
|
105
|
+
if Symbol === metadata[:dynamic_default]
|
106
|
+
default = concat_demeter_with_key(metadata[:dynamic_default], demeters_deep)
|
107
|
+
else
|
108
|
+
default = " # Proc provided for :dynamic_default parameter. :default_shown parameter not provided"
|
109
|
+
end
|
110
|
+
elsif metadata[:allowed].include?(String)
|
104
111
|
default = "\"#{metadata[:default]}\""
|
105
112
|
else
|
106
|
-
default = metadata[:default]
|
113
|
+
default = custom_case(metadata[:default])
|
107
114
|
end
|
108
115
|
arr = []
|
109
116
|
|
@@ -114,6 +121,17 @@ module ClassComposer
|
|
114
121
|
arr
|
115
122
|
end
|
116
123
|
|
124
|
+
def custom_case(default)
|
125
|
+
case default
|
126
|
+
when Symbol
|
127
|
+
default.inspect
|
128
|
+
when (ActiveSupport::Duration rescue NilClass)
|
129
|
+
default.inspect.gsub(" ", ".")
|
130
|
+
else
|
131
|
+
default
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
117
135
|
def prepending(space_count)
|
118
136
|
"#{" " * space_count}#"
|
119
137
|
end
|
@@ -0,0 +1,233 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClassComposer
|
4
|
+
module Generator
|
5
|
+
module ClassMethods
|
6
|
+
COMPOSER_VALIDATE_METHOD_NAME = ->(name) { :"__composer_#{name}_is_valid__?" }
|
7
|
+
COMPOSER_ASSIGNED_ATTR_NAME = ->(name) { :"@__composer_#{name}_value_assigned__" }
|
8
|
+
COMPOSER_ASSIGNED_ARRAY_METHODS = ->(name) { :"@__composer_#{name}_array_methods_set__" }
|
9
|
+
COMPOSER_ALLOWED_FROZEN_TYPE_ARGS = [:raise, :log]
|
10
|
+
|
11
|
+
def add_composer_blocking(name, composer_class:, desc: nil, block_prepend: "with", enable_attr: nil)
|
12
|
+
unless composer_class.include?(ClassComposer::Generator)
|
13
|
+
raise ClassComposer::Error, ".add_composer_blocking passed `composer_class:` that does not include ClassComposer::Generator. Passed argument must include ClassComposer::Generator"
|
14
|
+
end
|
15
|
+
|
16
|
+
blocking_name = "#{block_prepend}_#{name}"
|
17
|
+
blocking_attributes = { block_name: blocking_name, enable_attr: enable_attr }
|
18
|
+
add_composer(name, allowed: composer_class, default: composer_class.new, desc: desc, blocking_attributes: blocking_attributes)
|
19
|
+
|
20
|
+
define_method(blocking_name) do |&blk|
|
21
|
+
instance = public_send(:"#{name}")
|
22
|
+
instance.public_send(:"#{enable_attr}=", true) if enable_attr
|
23
|
+
|
24
|
+
blk.(instance) if blk
|
25
|
+
|
26
|
+
method(:"#{name}=").call(instance)
|
27
|
+
end
|
28
|
+
|
29
|
+
if enable_attr
|
30
|
+
define_method("#{name}?") do
|
31
|
+
public_send(:"#{name}").public_send(enable_attr)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_composer(name, allowed:, desc: nil, validator: ->(_) { true }, validation_error_klass: ::ClassComposer::ValidatorError, error_klass: ::ClassComposer::Error, blocking_attributes: nil, default_shown: nil, **params, &blk)
|
37
|
+
default =
|
38
|
+
if params.has_key?(:default)
|
39
|
+
params[:default]
|
40
|
+
else
|
41
|
+
ClassComposer::DefaultObject
|
42
|
+
end
|
43
|
+
|
44
|
+
if params[:default] && params[:dynamic_default]
|
45
|
+
raise Error, "Composer :#{name} had both the `:default` and `:dynamic_default` assigned. Only one allowed"
|
46
|
+
end
|
47
|
+
|
48
|
+
if dynamic_default = params[:dynamic_default]
|
49
|
+
if ![Proc, Symbol].include?(dynamic_default.class)
|
50
|
+
raise Error, "Composer :#{name} defined `:dynamic_default: #{dynamic_default}`. Expected value to be a Symbol mapped to a composer element or a Proc"
|
51
|
+
end
|
52
|
+
|
53
|
+
if Symbol === dynamic_default && composer_mapping[dynamic_default].nil?
|
54
|
+
raise Error, "Composer :#{name} defined `dynamic_default: #{dynamic_default}`. #{dynamic_default} is not defined. Please ensure that all dynamic_default's are defined before setting them"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
if allowed.is_a?(Array)
|
59
|
+
allowed << ClassComposer::DefaultObject
|
60
|
+
else
|
61
|
+
allowed = [allowed, ClassComposer::DefaultObject]
|
62
|
+
end
|
63
|
+
|
64
|
+
if allowed.select { _1.include?(ClassComposer::Generator) }.count > 1
|
65
|
+
raise Error, "Allowed arguments has multiple classes that include ClassComposer::Generator. Max 1 is allowed"
|
66
|
+
end
|
67
|
+
|
68
|
+
validate_proc = __composer_validator_proc__(validator: validator, allowed: allowed, name: name, error_klass: error_klass)
|
69
|
+
__composer_validate_options__!(name: name, validate_proc: validate_proc, default: default, validation_error_klass: validation_error_klass, error_klass: error_klass)
|
70
|
+
|
71
|
+
array_proc = __composer_array_proc__(name: name, validator: validator, allowed: allowed, params: params)
|
72
|
+
__composer_assignment__(name: name, allowed: allowed, params: params, validator: validate_proc, array_proc: array_proc, validation_error_klass: validation_error_klass, error_klass: error_klass, &blk)
|
73
|
+
__composer_retrieval__(name: name, allowed: allowed, default: default, array_proc: array_proc, params: params, validator: validate_proc, validation_error_klass: validation_error_klass)
|
74
|
+
|
75
|
+
# Add to mapping
|
76
|
+
__add_to_composer_mapping__(name: name, default: default, allowed: allowed, desc: desc, blocking_attributes: blocking_attributes, default_shown: default_shown, dynamic_default: params[:dynamic_default])
|
77
|
+
end
|
78
|
+
|
79
|
+
def composer_mapping
|
80
|
+
@composer_mapping ||= {}
|
81
|
+
end
|
82
|
+
|
83
|
+
def composer_generate_config(wrapping:, require_file: nil, space_count: 2)
|
84
|
+
@composer_generate_config ||= GenerateConfig.new(instance: self)
|
85
|
+
|
86
|
+
@composer_generate_config.execute(wrapping:, require_file:, space_count:)
|
87
|
+
end
|
88
|
+
|
89
|
+
def __add_to_composer_mapping__(name:, default:, allowed:, desc:, blocking_attributes:, default_shown: nil, dynamic_default: nil)
|
90
|
+
children = Array(allowed).select { _1.include?(ClassComposer::Generator) }.map do |allowed_class|
|
91
|
+
allowed_class.composer_mapping
|
92
|
+
end
|
93
|
+
|
94
|
+
composer_mapping[name] = {
|
95
|
+
desc: desc,
|
96
|
+
children: children.empty? ? nil : children,
|
97
|
+
dynamic_default: dynamic_default,
|
98
|
+
default_shown: default_shown,
|
99
|
+
default: (default.to_s.start_with?("#<") ? default.class : default),
|
100
|
+
blocking_attributes: blocking_attributes,
|
101
|
+
allowed: allowed,
|
102
|
+
}.compact
|
103
|
+
end
|
104
|
+
|
105
|
+
def __composer_validate_options__!(name:, validate_proc:, default:, params: {}, validation_error_klass:, error_klass:)
|
106
|
+
unless validate_proc.(default)
|
107
|
+
raise validation_error_klass, "Default value [#{default}] for #{self.class}.#{name} is not valid"
|
108
|
+
end
|
109
|
+
|
110
|
+
if instance_methods.include?(name.to_sym)
|
111
|
+
raise error_klass, "[#{name}] is already defined. Ensure composer names are all uniq and do not class with class instance methods"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def __composer_array_proc__(name:, validator:, allowed:, params:)
|
116
|
+
Proc.new do |value, _itself|
|
117
|
+
_itself.send(:"#{name}=", value)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# create assignment method for the incoming name
|
122
|
+
def __composer_assignment__(name:, params:, allowed:, validator:, array_proc:, validation_error_klass:, error_klass:, &blk)
|
123
|
+
define_method(:"#{name}=") do |value|
|
124
|
+
case class_composer_frozen!(name)
|
125
|
+
when false
|
126
|
+
# false is returned when the instance is frozen AND we do not allow the operation to proceed
|
127
|
+
return
|
128
|
+
when true
|
129
|
+
# true is returned when the instance is frozen AND we allow the operation to proceed
|
130
|
+
when nil
|
131
|
+
# nil is returned when the instance is not frozen
|
132
|
+
end
|
133
|
+
|
134
|
+
is_valid = self.class.__run_validation_item(name: name, validator: validator, allowed: allowed, value: value, params: params)
|
135
|
+
|
136
|
+
if is_valid[:valid]
|
137
|
+
instance_variable_set(COMPOSER_ASSIGNED_ATTR_NAME.(name), true)
|
138
|
+
instance_variable_set(:"@#{name}", value)
|
139
|
+
else
|
140
|
+
raise validation_error_klass, is_valid[:message].compact.join(" ")
|
141
|
+
end
|
142
|
+
|
143
|
+
if value.is_a?(Array) && !value.instance_variable_get(COMPOSER_ASSIGNED_ARRAY_METHODS.(name))
|
144
|
+
_itself = itself
|
145
|
+
value.define_singleton_method(:<<) do |val|
|
146
|
+
array_proc.(super(val), _itself)
|
147
|
+
end
|
148
|
+
value.instance_variable_set(COMPOSER_ASSIGNED_ARRAY_METHODS.(name), true)
|
149
|
+
end
|
150
|
+
|
151
|
+
if blk
|
152
|
+
yield(name, value)
|
153
|
+
end
|
154
|
+
|
155
|
+
value
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def __run_validation_item(validator:, name:, value:, allowed:, params:)
|
160
|
+
if validator.(value)
|
161
|
+
return { valid: true }
|
162
|
+
end
|
163
|
+
|
164
|
+
message = ["#{self.class}.#{name} failed validation. #{name} is expected to be #{allowed}. Received [#{value}](#{value.class})"]
|
165
|
+
message << (params[:invalid_message].is_a?(Proc) ? params[:invalid_message].(value) : params[:invalid_message].to_s)
|
166
|
+
|
167
|
+
{ valid: false, message: message }
|
168
|
+
end
|
169
|
+
|
170
|
+
# retrieve the value for the name -- Or return the default value
|
171
|
+
def __composer_retrieval__(name:, default:, array_proc:, allowed:, params:, validator:, validation_error_klass:)
|
172
|
+
define_method(:"#{name}") do
|
173
|
+
value = instance_variable_get(:"@#{name}")
|
174
|
+
return value if instance_variable_get(COMPOSER_ASSIGNED_ATTR_NAME.(name))
|
175
|
+
|
176
|
+
if dynamic_default = params[:dynamic_default]
|
177
|
+
if Proc === dynamic_default
|
178
|
+
value = dynamic_default.(self)
|
179
|
+
else
|
180
|
+
# We know the method exists because we already checked validity from within
|
181
|
+
# `compose_mapping` on add_composer creation
|
182
|
+
value = method(:"#{dynamic_default}").()
|
183
|
+
end
|
184
|
+
is_valid = self.class.__run_validation_item(name: name, validator: validator, allowed: allowed, value: value, params: params)
|
185
|
+
|
186
|
+
if is_valid[:valid]
|
187
|
+
instance_variable_set(COMPOSER_ASSIGNED_ATTR_NAME.(name), true)
|
188
|
+
instance_variable_set(:"@#{name}", value)
|
189
|
+
else
|
190
|
+
raise validation_error_klass, is_valid[:message].compact.join(" ")
|
191
|
+
end
|
192
|
+
|
193
|
+
return value
|
194
|
+
end
|
195
|
+
|
196
|
+
if default.is_a?(Array) && !default.instance_variable_get(COMPOSER_ASSIGNED_ARRAY_METHODS.(name))
|
197
|
+
_itself = itself
|
198
|
+
default.define_singleton_method(:<<) do |value|
|
199
|
+
array_proc.(super(value), _itself)
|
200
|
+
end
|
201
|
+
default.instance_variable_set(COMPOSER_ASSIGNED_ARRAY_METHODS.(name), true)
|
202
|
+
end
|
203
|
+
|
204
|
+
default == ClassComposer::DefaultObject ? ClassComposer::DefaultObject.value : default
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# create validator method for incoming name
|
209
|
+
def __composer_validator_proc__(validator:, allowed:, name:, error_klass:)
|
210
|
+
if validator && !validator.is_a?(Proc)
|
211
|
+
raise error_klass, "Expected validator to be a Proc. Received [#{validator.class}]"
|
212
|
+
end
|
213
|
+
|
214
|
+
# Proc will validate the entire attribute -- Full assignment must occur before validate is called
|
215
|
+
Proc.new do |value|
|
216
|
+
begin
|
217
|
+
allow =
|
218
|
+
if allowed.is_a?(Array)
|
219
|
+
allowed.include?(value.class)
|
220
|
+
else
|
221
|
+
allowed == value.class
|
222
|
+
end
|
223
|
+
# order is important -- Do not run validator if it is the default object
|
224
|
+
# Default object will likely raise an error if there is a custom validator
|
225
|
+
(allowed.include?(ClassComposer::DefaultObject) && value == ClassComposer::DefaultObject) || (allow && validator.(value))
|
226
|
+
rescue StandardError => e
|
227
|
+
raise error_klass, "#{e} occurred during validation for value [#{value}]. Check custom validator for #{name}"
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClassComposer
|
4
|
+
module Generator
|
5
|
+
module InstanceMethods
|
6
|
+
def class_composer_frozen!(key)
|
7
|
+
# when nil, we allow changes to the instance methods
|
8
|
+
return if @class_composer_frozen.nil?
|
9
|
+
|
10
|
+
# When frozen is a proc, we let the user decide how to handle
|
11
|
+
# The return value decides if the value can be changed or not
|
12
|
+
if Proc === @class_composer_frozen
|
13
|
+
return @class_composer_frozen.(self, key)
|
14
|
+
end
|
15
|
+
|
16
|
+
msg = "#{self.class} instance methods are frozen. Attempted to change variable [#{key}]."
|
17
|
+
case @class_composer_frozen
|
18
|
+
when FROZEN_LOG_AND_ALLOW
|
19
|
+
msg += " This operation will proceed."
|
20
|
+
Kernel.warn(msg)
|
21
|
+
return true
|
22
|
+
when FROZEN_LOG_AND_SKIP
|
23
|
+
msg += " This operation will NOT proceed."
|
24
|
+
Kernel.warn(msg)
|
25
|
+
return false
|
26
|
+
when FROZEN_RAISE
|
27
|
+
raise Error, msg
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def class_composer_assign_defaults!(children: false)
|
32
|
+
self.class.composer_mapping.each do |key, metadata|
|
33
|
+
assigned_value = method(:"#{key}").call
|
34
|
+
method(:"#{key}=").call(assigned_value)
|
35
|
+
|
36
|
+
if children && metadata[:children]
|
37
|
+
method(:"#{key}").call().class_composer_assign_defaults!(children: children)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def class_composer_freeze_objects!(behavior: nil, children: false, &block)
|
45
|
+
if behavior && block
|
46
|
+
raise ArgumentError, "`behavior` and `block` can not both be present. Choose one"
|
47
|
+
end
|
48
|
+
|
49
|
+
if behavior.nil? && block.nil?
|
50
|
+
raise ArgumentError, "`behavior` or `block` must be present."
|
51
|
+
end
|
52
|
+
|
53
|
+
if block
|
54
|
+
@class_composer_frozen = block
|
55
|
+
else
|
56
|
+
if !FROZEN_TYPES.include?(behavior)
|
57
|
+
raise Error, "Unknown behavior [#{behavior}]. Expected one of #{FROZEN_TYPES}."
|
58
|
+
end
|
59
|
+
@class_composer_frozen = behavior
|
60
|
+
end
|
61
|
+
|
62
|
+
# If children is set, iterate the children, otherwise exit early
|
63
|
+
return if children == false
|
64
|
+
|
65
|
+
self.class.composer_mapping.each do |key, metadata|
|
66
|
+
next unless metadata[:children]
|
67
|
+
|
68
|
+
method(:"#{key}").call().class_composer_freeze_objects!(behavior:, children:, &block)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
require "class_composer/default_object"
|
4
4
|
require "class_composer/generate_config"
|
5
|
+
require "class_composer/generator/instance_methods"
|
6
|
+
require "class_composer/generator/class_methods"
|
5
7
|
|
6
8
|
module ClassComposer
|
7
9
|
FROZEN_TYPES = [
|
@@ -14,257 +16,5 @@ module ClassComposer
|
|
14
16
|
base.extend(ClassMethods)
|
15
17
|
base.include(InstanceMethods)
|
16
18
|
end
|
17
|
-
|
18
|
-
module InstanceMethods
|
19
|
-
def class_composer_frozen!(key)
|
20
|
-
# when nil, we allow changes to the instance methods
|
21
|
-
return if @class_composer_frozen.nil?
|
22
|
-
|
23
|
-
# When frozen is a proc, we let the user decide how to handle
|
24
|
-
# The return value decides if the value can be changed or not
|
25
|
-
if Proc === @class_composer_frozen
|
26
|
-
return @class_composer_frozen.(self, key)
|
27
|
-
end
|
28
|
-
|
29
|
-
msg = "#{self.class} instance methods are frozen. Attempted to change variable [#{key}]."
|
30
|
-
case @class_composer_frozen
|
31
|
-
when FROZEN_LOG_AND_ALLOW
|
32
|
-
msg += " This operation will proceed."
|
33
|
-
Kernel.warn(msg)
|
34
|
-
return true
|
35
|
-
when FROZEN_LOG_AND_SKIP
|
36
|
-
msg += " This operation will NOT proceed."
|
37
|
-
Kernel.warn(msg)
|
38
|
-
return false
|
39
|
-
when FROZEN_RAISE
|
40
|
-
raise Error, msg
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
def class_composer_assign_defaults!(children: false)
|
45
|
-
self.class.composer_mapping.each do |key, metadata|
|
46
|
-
assigned_value = method(:"#{key}").call
|
47
|
-
method(:"#{key}=").call(assigned_value)
|
48
|
-
|
49
|
-
if children && metadata[:children]
|
50
|
-
method(:"#{key}").call().class_composer_assign_defaults!(children: children)
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
nil
|
55
|
-
end
|
56
|
-
|
57
|
-
def class_composer_freeze_objects!(behavior: nil, children: false, &block)
|
58
|
-
if behavior && block
|
59
|
-
raise ArgumentError, "`behavior` and `block` can not both be present. Choose one"
|
60
|
-
end
|
61
|
-
|
62
|
-
if behavior.nil? && block.nil?
|
63
|
-
raise ArgumentError, "`behavior` or `block` must be present."
|
64
|
-
end
|
65
|
-
|
66
|
-
if block
|
67
|
-
@class_composer_frozen = block
|
68
|
-
else
|
69
|
-
if !FROZEN_TYPES.include?(behavior)
|
70
|
-
raise Error, "Unknown behavior [#{behavior}]. Expected one of #{FROZEN_TYPES}."
|
71
|
-
end
|
72
|
-
@class_composer_frozen = behavior
|
73
|
-
end
|
74
|
-
|
75
|
-
# If children is set, iterate the children, otherwise exit early
|
76
|
-
return if children == false
|
77
|
-
|
78
|
-
self.class.composer_mapping.each do |key, metadata|
|
79
|
-
next unless metadata[:children]
|
80
|
-
|
81
|
-
method(:"#{key}").call().class_composer_freeze_objects!(behavior:, children:, &block)
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
module ClassMethods
|
87
|
-
COMPOSER_VALIDATE_METHOD_NAME = ->(name) { :"__composer_#{name}_is_valid__?" }
|
88
|
-
COMPOSER_ASSIGNED_ATTR_NAME = ->(name) { :"@__composer_#{name}_value_assigned__" }
|
89
|
-
COMPOSER_ASSIGNED_ARRAY_METHODS = ->(name) { :"@__composer_#{name}_array_methods_set__" }
|
90
|
-
COMPOSER_ALLOWED_FROZEN_TYPE_ARGS = [:raise, :log]
|
91
|
-
|
92
|
-
def add_composer_blocking(name, composer_class:, desc: nil, block_prepend: "with", enable_attr: nil)
|
93
|
-
unless composer_class.include?(ClassComposer::Generator)
|
94
|
-
raise ClassComposer::Error, ".add_composer_blocking passed `composer_class:` that does not include ClassComposer::Generator. Passed argument must include ClassComposer::Generator"
|
95
|
-
end
|
96
|
-
|
97
|
-
blocking_name = "#{block_prepend}_#{name}"
|
98
|
-
blocking_attributes = { block_name: blocking_name, enable_attr: enable_attr }
|
99
|
-
add_composer(name, allowed: composer_class, default: composer_class.new, desc: desc, blocking_attributes: blocking_attributes)
|
100
|
-
|
101
|
-
define_method(blocking_name) do |&blk|
|
102
|
-
instance = public_send(:"#{name}")
|
103
|
-
instance.public_send(:"#{enable_attr}=", true) if enable_attr
|
104
|
-
|
105
|
-
blk.(instance) if blk
|
106
|
-
|
107
|
-
method(:"#{name}=").call(instance)
|
108
|
-
end
|
109
|
-
|
110
|
-
if enable_attr
|
111
|
-
define_method("#{name}?") do
|
112
|
-
public_send(:"#{name}").public_send(enable_attr)
|
113
|
-
end
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
def add_composer(name, allowed:, desc: nil, validator: ->(_) { true }, validation_error_klass: ::ClassComposer::ValidatorError, error_klass: ::ClassComposer::Error, blocking_attributes: nil, default_shown: nil, **params, &blk)
|
118
|
-
default =
|
119
|
-
if params.has_key?(:default)
|
120
|
-
params[:default]
|
121
|
-
else
|
122
|
-
ClassComposer::DefaultObject
|
123
|
-
end
|
124
|
-
|
125
|
-
if allowed.is_a?(Array)
|
126
|
-
allowed << ClassComposer::DefaultObject
|
127
|
-
else
|
128
|
-
allowed = [allowed, ClassComposer::DefaultObject]
|
129
|
-
end
|
130
|
-
|
131
|
-
if allowed.select { _1.include?(ClassComposer::Generator) }.count > 1
|
132
|
-
raise Error, "Allowed arguments has multiple classes that include ClassComposer::Generator. Max 1 is allowed"
|
133
|
-
end
|
134
|
-
|
135
|
-
validate_proc = __composer_validator_proc__(validator: validator, allowed: allowed, name: name, error_klass: error_klass)
|
136
|
-
__composer_validate_options__!(name: name, validate_proc: validate_proc, default: default, validation_error_klass: validation_error_klass, error_klass: error_klass)
|
137
|
-
|
138
|
-
array_proc = __composer_array_proc__(name: name, validator: validator, allowed: allowed, params: params)
|
139
|
-
__composer_assignment__(name: name, allowed: allowed, params: params, validator: validate_proc, array_proc: array_proc, validation_error_klass: validation_error_klass, error_klass: error_klass, &blk)
|
140
|
-
__composer_retrieval__(name: name, default: default, array_proc: array_proc)
|
141
|
-
|
142
|
-
# Add to mapping
|
143
|
-
__add_to_composer_mapping__(name: name, default: default, allowed: allowed, desc: desc, blocking_attributes: blocking_attributes, default_shown: default_shown)
|
144
|
-
end
|
145
|
-
|
146
|
-
def composer_mapping
|
147
|
-
@composer_mapping ||= {}
|
148
|
-
end
|
149
|
-
|
150
|
-
def composer_generate_config(wrapping:, require_file: nil, space_count: 2)
|
151
|
-
@composer_generate_config ||= GenerateConfig.new(instance: self)
|
152
|
-
|
153
|
-
@composer_generate_config.execute(wrapping:, require_file:, space_count:)
|
154
|
-
end
|
155
|
-
|
156
|
-
def __add_to_composer_mapping__(name:, default:, allowed:, desc:, blocking_attributes:, default_shown: nil)
|
157
|
-
children = Array(allowed).select { _1.include?(ClassComposer::Generator) }.map do |allowed_class|
|
158
|
-
allowed_class.composer_mapping
|
159
|
-
end
|
160
|
-
|
161
|
-
composer_mapping[name] = {
|
162
|
-
desc: desc,
|
163
|
-
children: children.empty? ? nil : children,
|
164
|
-
default: default_shown || (default.to_s.start_with?("#<") ? default.class : default),
|
165
|
-
blocking_attributes: blocking_attributes,
|
166
|
-
allowed: allowed,
|
167
|
-
}.compact
|
168
|
-
end
|
169
|
-
|
170
|
-
def __composer_validate_options__!(name:, validate_proc:, default:, params: {}, validation_error_klass:, error_klass:)
|
171
|
-
unless validate_proc.(default)
|
172
|
-
raise validation_error_klass, "Default value [#{default}] for #{self.class}.#{name} is not valid"
|
173
|
-
end
|
174
|
-
|
175
|
-
if instance_methods.include?(name.to_sym)
|
176
|
-
raise error_klass, "[#{name}] is already defined. Ensure composer names are all uniq and do not class with class instance methods"
|
177
|
-
end
|
178
|
-
end
|
179
|
-
|
180
|
-
def __composer_array_proc__(name:, validator:, allowed:, params:)
|
181
|
-
Proc.new do |value, _itself|
|
182
|
-
_itself.send(:"#{name}=", value)
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
186
|
-
# create assignment method for the incoming name
|
187
|
-
def __composer_assignment__(name:, params:, allowed:, validator:, array_proc:, validation_error_klass:, error_klass:, &blk)
|
188
|
-
define_method(:"#{name}=") do |value|
|
189
|
-
case class_composer_frozen!(name)
|
190
|
-
when false
|
191
|
-
# false is returned when the instance is frozen AND we do not allow the operation to proceed
|
192
|
-
return
|
193
|
-
when true
|
194
|
-
# true is returned when the instance is frozen AND we allow the operation to proceed
|
195
|
-
when nil
|
196
|
-
# nil is returned when the instance is not frozen
|
197
|
-
end
|
198
|
-
|
199
|
-
is_valid = validator.(value)
|
200
|
-
|
201
|
-
if is_valid
|
202
|
-
instance_variable_set(COMPOSER_ASSIGNED_ATTR_NAME.(name), true)
|
203
|
-
instance_variable_set(:"@#{name}", value)
|
204
|
-
else
|
205
|
-
message = ["#{self.class}.#{name} failed validation. #{name} is expected to be #{allowed}. Received [#{value}](#{value.class})"]
|
206
|
-
|
207
|
-
message << (params[:invalid_message].is_a?(Proc) ? params[:invalid_message].(value) : params[:invalid_message].to_s)
|
208
|
-
raise validation_error_klass, message.compact.join(" ")
|
209
|
-
end
|
210
|
-
|
211
|
-
if value.is_a?(Array) && !value.instance_variable_get(COMPOSER_ASSIGNED_ARRAY_METHODS.(name))
|
212
|
-
_itself = itself
|
213
|
-
value.define_singleton_method(:<<) do |val|
|
214
|
-
array_proc.(super(val), _itself)
|
215
|
-
end
|
216
|
-
value.instance_variable_set(COMPOSER_ASSIGNED_ARRAY_METHODS.(name), true)
|
217
|
-
end
|
218
|
-
|
219
|
-
if blk
|
220
|
-
yield(name, value)
|
221
|
-
end
|
222
|
-
|
223
|
-
value
|
224
|
-
end
|
225
|
-
end
|
226
|
-
|
227
|
-
# retrieve the value for the name -- Or return the default value
|
228
|
-
def __composer_retrieval__(name:, default:, array_proc:)
|
229
|
-
define_method(:"#{name}") do
|
230
|
-
value = instance_variable_get(:"@#{name}")
|
231
|
-
return value if instance_variable_get(COMPOSER_ASSIGNED_ATTR_NAME.(name))
|
232
|
-
|
233
|
-
if default.is_a?(Array) && !default.instance_variable_get(COMPOSER_ASSIGNED_ARRAY_METHODS.(name))
|
234
|
-
_itself = itself
|
235
|
-
default.define_singleton_method(:<<) do |value|
|
236
|
-
array_proc.(super(value), _itself)
|
237
|
-
end
|
238
|
-
default.instance_variable_set(COMPOSER_ASSIGNED_ARRAY_METHODS.(name), true)
|
239
|
-
end
|
240
|
-
|
241
|
-
default == ClassComposer::DefaultObject ? ClassComposer::DefaultObject.value : default
|
242
|
-
end
|
243
|
-
end
|
244
|
-
|
245
|
-
# create validator method for incoming name
|
246
|
-
def __composer_validator_proc__(validator:, allowed:, name:, error_klass:)
|
247
|
-
if validator && !validator.is_a?(Proc)
|
248
|
-
raise error_klass, "Expected validator to be a Proc. Received [#{validator.class}]"
|
249
|
-
end
|
250
|
-
|
251
|
-
# Proc will validate the entire attribute -- Full assignment must occur before validate is called
|
252
|
-
Proc.new do |value|
|
253
|
-
begin
|
254
|
-
allow =
|
255
|
-
if allowed.is_a?(Array)
|
256
|
-
allowed.include?(value.class)
|
257
|
-
else
|
258
|
-
allowed == value.class
|
259
|
-
end
|
260
|
-
# order is important -- Do not run validator if it is the default object
|
261
|
-
# Default object will likely raise an error if there is a custom validator
|
262
|
-
(allowed.include?(ClassComposer::DefaultObject) && value == ClassComposer::DefaultObject) || (allow && validator.(value))
|
263
|
-
rescue StandardError => e
|
264
|
-
raise error_klass, "#{e} occurred during validation for value [#{value}]. Check custom validator for #{name}"
|
265
|
-
end
|
266
|
-
end
|
267
|
-
end
|
268
|
-
end
|
269
19
|
end
|
270
20
|
end
|
data/lib/class_composer.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: class_composer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matt Taylor
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-12-
|
11
|
+
date: 2024-12-18 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Compose configurations for any class.
|
14
14
|
email:
|
@@ -43,6 +43,8 @@ files:
|
|
43
43
|
- lib/class_composer/default_object.rb
|
44
44
|
- lib/class_composer/generate_config.rb
|
45
45
|
- lib/class_composer/generator.rb
|
46
|
+
- lib/class_composer/generator/class_methods.rb
|
47
|
+
- lib/class_composer/generator/instance_methods.rb
|
46
48
|
- lib/class_composer/version.rb
|
47
49
|
homepage: https://github.com/matt-taylor/class_composer
|
48
50
|
licenses:
|