class_composer 2.0.0 → 2.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 +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:
|