class_composer 1.0.2 → 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/.circleci/config.yml +2 -2
- data/.gitignore +1 -0
- data/CHANGELOG.md +23 -4
- data/Dockerfile +1 -1
- data/Gemfile +5 -5
- data/Gemfile.lock +24 -30
- data/README.md +14 -126
- data/bin/setup +0 -2
- data/class_composer.gemspec +1 -8
- data/docker-compose.yml +0 -2
- data/docs/array_usage.md +25 -0
- data/docs/basic_composer.md +133 -0
- data/docs/basic_composer_example.md +35 -0
- data/docs/composer_blocking.md +84 -0
- data/docs/freezing.md +58 -0
- data/docs/generating_initializer.md +74 -0
- data/lib/class_composer/generate_config.rb +143 -0
- 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 +9 -118
- data/lib/class_composer/version.rb +1 -1
- data/lib/class_composer.rb +0 -1
- metadata +15 -62
data/docs/freezing.md
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# Freezing ClassComposer
|
2
|
+
|
3
|
+
`ClassComposer` provides a simple way to freeze instances of its classes. Freezing can help ensure that configurations do not change during the life of the script or application.
|
4
|
+
|
5
|
+
Different behaviors are available when a user attempts to change a composed item after the instance has been frozen
|
6
|
+
|
7
|
+
## Allowed Options:
|
8
|
+
### Behavior:
|
9
|
+
- Required: When `&block` is nil, behavior is required
|
10
|
+
- Description: The behavior ClassComposer should enact when a composed item tries to get changed
|
11
|
+
- Type: Symbol [:raise, :log_and_allow, :log_and_skip]
|
12
|
+
|
13
|
+
### Children:
|
14
|
+
- Required: false
|
15
|
+
- Description: Any ClassComposed item that includes `ClassComposer::Generator` is considered a nested Child. When option set to true, We will iterate the tree and set all child instances to the same behavior as the parent. One stop shop to freeze all nested configuration
|
16
|
+
- Type: Boolean
|
17
|
+
|
18
|
+
### Block
|
19
|
+
- Required: When `behavior` is nil, block is required
|
20
|
+
- Description: Custom behavior tailored to your use case. For example, In test, maybe you raise, but production maybe you allow
|
21
|
+
- Type: Passed in block, Return `true` to allow the variable to get set. Return `false` to not allow the variable to get set
|
22
|
+
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
MyCoolEngine.config.class_composer_freeze_objects!(children: true) do |instance, key|
|
26
|
+
if Rails.staging?
|
27
|
+
# allow the variable to get set in staging
|
28
|
+
Rails.logger("Yikes! you are changing a config variable after boot. We will honor this")
|
29
|
+
true
|
30
|
+
elsif Rails.prod?
|
31
|
+
# disallow the variable to get set in prod
|
32
|
+
Rails.logger("Yikes! you are changing a config variable after boot. We will NOT honor this")
|
33
|
+
false
|
34
|
+
else
|
35
|
+
raise Error, "Cant change value on #{instance.class} for key. Please change"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
40
|
+
## Usage
|
41
|
+
|
42
|
+
### Rails Engine
|
43
|
+
When building out a complex nested configuration structure for a Rails Engine, you may want to ensure changes to the configuration do not occur after the Rails App runs its initializers. As example code, this can get added to your `*engine.rb` file
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
# MyCoolEngine.config is the location of the config instance
|
47
|
+
# Assign Defaults must get run first otherwise Lazily loaded objects will run into failure
|
48
|
+
|
49
|
+
# Run after Rails loads the initializes and environment files
|
50
|
+
# Ensures User has already set their desired config before we lock this down
|
51
|
+
initializer "my_cool_engine.config.instantiate", after: :load_config_initializers do |_app|
|
52
|
+
# ensure defaults are instantiated and all variables are assigned
|
53
|
+
MyCoolEngine.config.class_composer_assign_defaults!(children: true)
|
54
|
+
|
55
|
+
# Now that we can confirm all variables are defined, freeze all objects an their children
|
56
|
+
MyCoolEngine.config.class_composer_freeze_objects!(behavior: :raise, children: true)
|
57
|
+
end
|
58
|
+
```
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# Generating Initializer
|
2
|
+
|
3
|
+
Generating an initializer can help ensure that all users understand all potential configuration options without searching the codebase.
|
4
|
+
|
5
|
+
This generation will add both [Basic Composer Options](basic_composer.md) and [Composer Blocking Options](composer_blocking.md) to a configuration file.
|
6
|
+
|
7
|
+
The file output will show show assignment to all default values. Additionally all lines are commented out so the User can
|
8
|
+
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
class LoginStrategy
|
12
|
+
include ClassComposer::Generator
|
13
|
+
|
14
|
+
add_composer :password_regex, allowed: Regexp, default: /\A\w{6,20}\z/, desc: "Password must include valid characters between 6 and 20 in length"
|
15
|
+
|
16
|
+
add_composer :username_length, allowed: Integer, default: 10
|
17
|
+
|
18
|
+
add_composer :type, allowed: String, default: "plain_text"
|
19
|
+
end
|
20
|
+
|
21
|
+
class LockableStrategy
|
22
|
+
include ClassComposer::Generator
|
23
|
+
|
24
|
+
add_composer :enable, default: false, allowed: [TrueClass, FalseClass], desc: "By default Lockable Strategy is disabled."
|
25
|
+
add_composer :password_attempts, default: 10, allowed: Integer, desc: "Max password attempts before the account is locked"
|
26
|
+
end
|
27
|
+
|
28
|
+
class AppConfiguration
|
29
|
+
include ClassComposer::Generator
|
30
|
+
|
31
|
+
add_composer :login, allowed: LoginStrategy, default: LoginStrategy.new, desc: "Login Strategy for my Application"
|
32
|
+
|
33
|
+
add_composer_blocking :lockable, composer_class: LockableStrategy, enable_attr: :enable, desc: "Lock Strategy for my Application. By default this is disabled"
|
34
|
+
end
|
35
|
+
|
36
|
+
puts AppConfiguration.composer_generate_config(wrapping: "MyApplication.configure")
|
37
|
+
|
38
|
+
----
|
39
|
+
|
40
|
+
=begin
|
41
|
+
This configuration files lists all the configuration options available.
|
42
|
+
To change the default value, uncomment the line and change the value.
|
43
|
+
Please take note: Values set as `=` to a config variable are the current default values when none is assigned
|
44
|
+
=end
|
45
|
+
|
46
|
+
MyApplication.configure do |config|
|
47
|
+
# ### Block to configure Login ###
|
48
|
+
# Login Strategy for my Application
|
49
|
+
# config.with_login do |login_config|
|
50
|
+
# Password must include valid characters between 6 and 20 in length: [Regexp]
|
51
|
+
# login_config.password_regex = (?-mix:\A\w{6,20}\z)
|
52
|
+
|
53
|
+
# login_config.username_length = 10
|
54
|
+
|
55
|
+
# login_config.type = "plain_text"
|
56
|
+
# end
|
57
|
+
|
58
|
+
# ### Block to configure Lockable ###
|
59
|
+
# Lock Strategy for my Application. By default this is disabled
|
60
|
+
# When using the block, the enable flag will automatically get set to true
|
61
|
+
# config.with_lockable do |lockable_config|
|
62
|
+
# By default Lockable Strategy is disabled.: [TrueClass, FalseClass]
|
63
|
+
# lockable_config.enable = false
|
64
|
+
|
65
|
+
# Max password attempts before the account is locked: [Integer]
|
66
|
+
# lockable_config.password_attempts = 10
|
67
|
+
# end
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
71
|
+
## Usage Applications
|
72
|
+
### Rails Generator
|
73
|
+
Are you building an Engine or a Gem that requires custom configuration. This code can easily help downstream users understand exactly what options are available to them to configure your Engine/Gem.
|
74
|
+
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClassComposer
|
4
|
+
class GenerateConfig
|
5
|
+
attr_reader :instance
|
6
|
+
NOTICE = <<~HEREDOC
|
7
|
+
=begin
|
8
|
+
This configuration files lists all the configuration options available.
|
9
|
+
To change the default value, uncomment the line and change the value.
|
10
|
+
Please take note: Values set as `=` to a config variable are the current default values when none is assigned
|
11
|
+
=end
|
12
|
+
HEREDOC
|
13
|
+
|
14
|
+
def initialize(instance:)
|
15
|
+
raise ArgumentError, ":instance class (#{instance}) must include ClassComposer::Generator. It does not" unless instance.include?(ClassComposer::Generator)
|
16
|
+
|
17
|
+
@instance = instance
|
18
|
+
end
|
19
|
+
|
20
|
+
def execute(wrapping:, require_file:, space_count: 1, config_name: "config")
|
21
|
+
mapping = instance.composer_mapping
|
22
|
+
generated_config = generate(mapping:, space_count:, demeters_deep:[config_name])
|
23
|
+
|
24
|
+
stringified = ""
|
25
|
+
stringified += "require \"#{require_file}\"\n\n" if require_file
|
26
|
+
stringified += NOTICE
|
27
|
+
stringified += "\n"
|
28
|
+
stringified += "#{wrapping} do |#{config_name}|\n"
|
29
|
+
flattened_config = generated_config.flatten(1).map { _1.join(" ") }
|
30
|
+
flattened_config.pop if flattened_config[-1] == ""
|
31
|
+
|
32
|
+
stringified += flattened_config.join("\n")
|
33
|
+
stringified += "\nend"
|
34
|
+
stringified
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def generate(mapping:, space_count:, demeters_deep:)
|
40
|
+
mapping.map do |key, metadata|
|
41
|
+
if blocking_attributes = metadata[:blocking_attributes]
|
42
|
+
if children = metadata[:children]
|
43
|
+
do_block = "#{key}_config"
|
44
|
+
blocking(key:, do_block:, metadata:, space_count:, demeters_deep:, blocking_attributes:) do
|
45
|
+
generate(mapping: children.first, space_count: space_count + 2, demeters_deep: [do_block])
|
46
|
+
end
|
47
|
+
else
|
48
|
+
[]
|
49
|
+
end
|
50
|
+
elsif children = metadata[:children]
|
51
|
+
config_prepend = demeters_deep + [key]
|
52
|
+
children_config = []
|
53
|
+
if desc = metadata[:desc]
|
54
|
+
children_config << spec_child_description(space_count:, desc:, key:)
|
55
|
+
end
|
56
|
+
|
57
|
+
children.each do |child|
|
58
|
+
children_config += generate(mapping: child, space_count:, demeters_deep: config_prepend)
|
59
|
+
end
|
60
|
+
|
61
|
+
children_config.flatten(1)
|
62
|
+
else
|
63
|
+
spec(key:, metadata:, space_count:, demeters_deep:)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def spec_child_description(space_count:, desc:, key:)
|
69
|
+
base = "#########"
|
70
|
+
length = base.length * 2 + 4 + key.capitalize.length
|
71
|
+
|
72
|
+
[
|
73
|
+
[prepending(space_count),"#" * length],
|
74
|
+
[prepending(space_count),"##{" " * (length - 2)}#" ],
|
75
|
+
[prepending(space_count), "#{base} #{key.capitalize} #{base}"],
|
76
|
+
[prepending(space_count),"##{" " * (length - 2)}#" ],
|
77
|
+
[prepending(space_count),"#" * length],
|
78
|
+
[prepending(space_count), "## #{desc}"],
|
79
|
+
[],
|
80
|
+
]
|
81
|
+
end
|
82
|
+
|
83
|
+
def blocking(key:, do_block:, metadata:, space_count:, demeters_deep:, blocking_attributes:)
|
84
|
+
config = concat_demeter_with_key(blocking_attributes[:block_name], demeters_deep)
|
85
|
+
values = [
|
86
|
+
[prepending(space_count), "### Block to configure #{key.to_s.split("_").map {_1.capitalize}.join(" ")} ###"],
|
87
|
+
[prepending(space_count), metadata[:desc]],
|
88
|
+
]
|
89
|
+
values << [prepending(space_count), "When using the block, the #{blocking_attributes[:enable_attr]} flag will automatically get set to true"] if blocking_attributes[:enable_attr]
|
90
|
+
values << [prepending(space_count), config, "do", "|#{do_block}|"]
|
91
|
+
|
92
|
+
values += yield.flatten(1)
|
93
|
+
values.pop if values[-1] == [""]
|
94
|
+
values << [prepending(space_count), "end"]
|
95
|
+
|
96
|
+
values << [""]
|
97
|
+
end
|
98
|
+
|
99
|
+
def spec(key:, metadata:, space_count:, demeters_deep:)
|
100
|
+
config = concat_demeter_with_key(key, demeters_deep)
|
101
|
+
|
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)
|
111
|
+
default = "\"#{metadata[:default]}\""
|
112
|
+
else
|
113
|
+
default = custom_case(metadata[:default])
|
114
|
+
end
|
115
|
+
arr = []
|
116
|
+
|
117
|
+
arr << [prepending(space_count), "#{metadata[:desc]}: #{(metadata[:allowed] - [ClassComposer::DefaultObject])}"] if metadata[:desc]
|
118
|
+
arr <<[prepending(space_count), config, "=", default]
|
119
|
+
arr << [""]
|
120
|
+
|
121
|
+
arr
|
122
|
+
end
|
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
|
+
|
135
|
+
def prepending(space_count)
|
136
|
+
"#{" " * space_count}#"
|
137
|
+
end
|
138
|
+
|
139
|
+
def concat_demeter_with_key(key, demeters_deep)
|
140
|
+
(demeters_deep + ["#{key}"]).join(".")
|
141
|
+
end
|
142
|
+
end
|
143
|
+
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
|