Class: Servactory::ToolKit::DynamicOptions::ConsistsOf

Inherits:
Must
  • Object
show all
Defined in:
lib/servactory/tool_kit/dynamic_options/consists_of.rb

Overview

Validates that collection elements are of specified types.

## Purpose

ConsistsOf ensures that all elements within an Array or collection attribute match one of the specified types. This is essential for validating homogeneous collections where each element must conform to expected types.

## Usage

This option is **included by default** for inputs, internals, and outputs. No registration required for basic usage.

To extend supported collection types (e.g., add ‘ActiveRecord::Relation`), use the `collection_mode_class_names` configuration:

“‘ruby configuration do

collection_mode_class_names([ActiveRecord::Relation])

end “‘

## Simple Mode

Specify type directly as the option value:

“‘ruby class ProcessUsersService < ApplicationService::Base

input :user_ids, type: Array, consists_of: Integer
input :tags, type: Array, consists_of: [String, Symbol]
input :scores, type: Array, consists_of: Float

end “‘

## Advanced Mode

Specify type with custom error message using a hash. Note: Advanced mode uses ‘:type` key (not `:is` like other options).

With static message:

“‘ruby input :ids, type: Array, consists_of:

type: String,
message: "Input `ids` must be an array of `String`"

“‘

With dynamic lambda message:

“‘ruby input :ids, type: Array, consists_of:

type: String,
message: lambda do |input:, option_value:, **|
  "Input `#{input.name` must be an array of `#')`"
end

} “‘

Lambda receives the following parameters:

  • For inputs: ‘input:, value:, option_value:, reason:, **`

  • For internals: ‘internal:, value:, option_value:, reason:, **`

  • For outputs: ‘output:, value:, option_value:, reason:, **`

## Validation Rules

  • Collection must be of a registered collection type (Array, Set, etc.)

  • All elements are flattened before validation (nested arrays supported)

  • Empty collections pass validation for optional input attributes only

  • For internal/output attributes, presence check is always performed

  • For optional inputs with non-empty collections, presence check is performed

  • Multiple types can be specified as an array

## Important Notes

  • Use ‘consists_of: false` to disable validation

  • NilClass in types allows nil elements in the collection

  • Nested arrays are automatically flattened for validation

  • Advanced mode uses ‘:type` key (not `:is` like other options)

  • Numeric types use exact class matching (Integer != Float)

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Must

#equivalent_with, #initialize, #must, #must_content_message_with, #must_content_value_with, #must_content_with

Constructor Details

This class inherits a constructor from Servactory::ToolKit::DynamicOptions::Must

Class Method Details

.use(option_name = :consists_of, collection_mode_class_names:) ⇒ Servactory::Maintenance::Attributes::OptionHelper

Creates a ConsistsOf validator instance.

Parameters:

  • option_name (Symbol) (defaults to: :consists_of)

    The option name (default: :consists_of)

  • collection_mode_class_names (Array<Class>)

    Valid collection types

Returns:



93
94
95
96
97
# File 'lib/servactory/tool_kit/dynamic_options/consists_of.rb', line 93

def self.use(option_name = :consists_of, collection_mode_class_names:)
  instance = new(option_name, :type, false)
  instance.assign(collection_mode_class_names)
  instance.must(:consists_of)
end

Instance Method Details

#assign(collection_mode_class_names) ⇒ void

This method returns an undefined value.

Assigns the list of valid collection class names.

Parameters:

  • collection_mode_class_names (Array<Class>)

    Collection types to accept



103
104
105
# File 'lib/servactory/tool_kit/dynamic_options/consists_of.rb', line 103

def assign(collection_mode_class_names)
  @collection_mode_class_names = collection_mode_class_names
end

#common_condition_with(attribute:, value:, option:) ⇒ Boolean, Array

Common validation logic for all attribute types.

Parameters:

  • attribute (Object)

    The attribute being validated

  • value (Object)

    Collection value to validate

  • option (WorkOption)

    Type configuration

Returns:

  • (Boolean, Array)

    true if valid, or [false, reason]



143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/servactory/tool_kit/dynamic_options/consists_of.rb', line 143

def common_condition_with(attribute:, value:, option:)
  # Validation disabled.
  return true if option.value == false

  # Attribute must be a collection type.
  return [false, :wrong_type] unless @collection_mode_class_names.intersect?(attribute.types)

  # Flatten nested arrays for uniform validation.
  values = value.respond_to?(:flatten) ? value&.flatten : value

  validate_for!(attribute:, values:, option:)
end

#condition_for_input_with(input:, value:, option:) ⇒ Boolean, Array

Validates element types for input attribute.

Parameters:

  • input (Object)

    Input attribute object

  • value (Object)

    Collection value to validate

  • option (WorkOption)

    Type configuration

Returns:

  • (Boolean, Array)

    true if valid, or [false, reason]



113
114
115
# File 'lib/servactory/tool_kit/dynamic_options/consists_of.rb', line 113

def condition_for_input_with(input:, value:, option:)
  common_condition_with(attribute: input, value:, option:)
end

#condition_for_internal_with(internal:, value:, option:) ⇒ Boolean, Array

Validates element types for internal attribute.

Parameters:

  • internal (Object)

    Internal attribute object

  • value (Object)

    Collection value to validate

  • option (WorkOption)

    Type configuration

Returns:

  • (Boolean, Array)

    true if valid, or [false, reason]



123
124
125
# File 'lib/servactory/tool_kit/dynamic_options/consists_of.rb', line 123

def condition_for_internal_with(internal:, value:, option:)
  common_condition_with(attribute: internal, value:, option:)
end

#condition_for_output_with(output:, value:, option:) ⇒ Boolean, Array

Validates element types for output attribute.

Parameters:

  • output (Object)

    Output attribute object

  • value (Object)

    Collection value to validate

  • option (WorkOption)

    Type configuration

Returns:

  • (Boolean, Array)

    true if valid, or [false, reason]



133
134
135
# File 'lib/servactory/tool_kit/dynamic_options/consists_of.rb', line 133

def condition_for_output_with(output:, value:, option:)
  common_condition_with(attribute: output, value:, option:)
end

#fails_presence_validation?(attribute:, values:, consists_of_types:) ⇒ Boolean

Checks if collection fails presence validation.

Parameters:

  • attribute (Object)

    The attribute being validated

  • values (Array)

    Collection elements

  • consists_of_types (Array<Class>)

    Allowed types

Returns:

  • (Boolean)

    true if validation fails



185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/servactory/tool_kit/dynamic_options/consists_of.rb', line 185

def fails_presence_validation?(attribute:, values:, consists_of_types:)
  # NilClass in types allows nil elements.
  return false if consists_of_types.include?(NilClass)

  check_present = proc { _1 && !values.all?(&:present?) }

  [
    check_present[attribute.input? && (attribute.required? || (attribute.optional? && values.present?))],
    check_present[attribute.internal?],
    check_present[attribute.output?]
  ].any?
end

#given_type_for(values:, option_value:) ⇒ String

Extracts type names of elements that don’t match expected types.

Parameters:

  • values (Array, nil)

    Collection elements

  • option_value (Object)

    Expected types

Returns:

  • (String)

    Comma-separated list of unexpected type names



274
275
276
277
278
279
280
# File 'lib/servactory/tool_kit/dynamic_options/consists_of.rb', line 274

def given_type_for(values:, option_value:)
  return "NilClass" if values.nil?

  values = values&.flatten if values.respond_to?(:flatten)

  values.filter { |value| Array(option_value).uniq.exclude?(value.class) }.map(&:class).uniq.join(", ")
end

#message_for_input_with(service:, input:, value:, option_name:, option_value:, reason:) ⇒ String

Generates error message for input validation failure.

Parameters:

  • service (Object)

    Service context

  • input (Object)

    Input attribute

  • value (Object)

    Failed value

  • option_name (Symbol)

    Option name

  • option_value (Object)

    Expected types

  • reason (Symbol)

    Failure reason

Returns:

  • (String)

    Localized error message



209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/servactory/tool_kit/dynamic_options/consists_of.rb', line 209

def message_for_input_with(service:, input:, value:, option_name:, option_value:, reason:, **)
  i18n_key = "inputs.validations.must.dynamic_options.consists_of"
  i18n_key += reason.present? ? ".#{reason}" : ".default"

  service.translate(
    i18n_key,
    service_class_name: service.class_name,
    input_name: input.name,
    option_name:,
    expected_type: Array(option_value).uniq.join(", "),
    given_type: given_type_for(values: value, option_value:)
  )
end

#message_for_internal_with(service:, internal:, value:, option_name:, option_value:, reason:) ⇒ String

Generates error message for internal validation failure.

Parameters:

  • service (Object)

    Service context

  • internal (Object)

    Internal attribute

  • value (Object)

    Failed value

  • option_name (Symbol)

    Option name

  • option_value (Object)

    Expected types

  • reason (Symbol)

    Failure reason

Returns:

  • (String)

    Localized error message



232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/servactory/tool_kit/dynamic_options/consists_of.rb', line 232

def message_for_internal_with(service:, internal:, value:, option_name:, option_value:, reason:, **)
  i18n_key = "internals.validations.must.dynamic_options.consists_of"
  i18n_key += reason.present? ? ".#{reason}" : ".default"

  service.translate(
    i18n_key,
    service_class_name: service.class_name,
    internal_name: internal.name,
    option_name:,
    expected_type: Array(option_value).uniq.join(", "),
    given_type: given_type_for(values: value, option_value:)
  )
end

#message_for_output_with(service:, output:, value:, option_name:, option_value:, reason:) ⇒ String

Generates error message for output validation failure.

Parameters:

  • service (Object)

    Service context

  • output (Object)

    Output attribute

  • value (Object)

    Failed value

  • option_name (Symbol)

    Option name

  • option_value (Object)

    Expected types

  • reason (Symbol)

    Failure reason

Returns:

  • (String)

    Localized error message



255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/servactory/tool_kit/dynamic_options/consists_of.rb', line 255

def message_for_output_with(service:, output:, value:, option_name:, option_value:, reason:, **)
  i18n_key = "outputs.validations.must.dynamic_options.consists_of"
  i18n_key += reason.present? ? ".#{reason}" : ".default"

  service.translate(
    i18n_key,
    service_class_name: service.class_name,
    output_name: output.name,
    option_name:,
    expected_type: Array(option_value).uniq.join(", "),
    given_type: given_type_for(values: value, option_value:)
  )
end

#validate_for!(attribute:, values:, option:) ⇒ Boolean, Array

Validates all elements against allowed types.

Parameters:

  • attribute (Object)

    The attribute being validated

  • values (Array)

    Flattened collection elements

  • option (WorkOption)

    Type configuration

Returns:

  • (Boolean, Array)

    true if valid, or [false, reason]



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/servactory/tool_kit/dynamic_options/consists_of.rb', line 162

def validate_for!(attribute:, values:, option:)
  consists_of_types = Array(option.value).uniq

  # Check presence requirements.
  return [false, :required] if fails_presence_validation?(attribute:, values:, consists_of_types:)

  # Empty optional collections are valid.
  return true if values.blank? && attribute.input? && attribute.optional?

  # Verify each element matches allowed types.
  return true if values.all? do |value|
    consists_of_types.include?(value.class)
  end

  [false, :wrong_element_type]
end