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.
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