Class: SpecForge::Normalizer

Inherits:
Object
  • Object
show all
Extended by:
Default
Defined in:
lib/spec_forge/normalizer.rb,
lib/spec_forge/normalizer/default.rb,
lib/spec_forge/normalizer/structure.rb,
lib/spec_forge/normalizer/validators.rb,
lib/spec_forge/normalizer/transformers.rb

Overview

Validates and transforms input data against structure definitions

The Normalizer system ensures that YAML input conforms to expected structures, applying defaults, type checking, and custom validations. Structure definitions are loaded from YAML files in the normalizers/ directory.

Defined Under Namespace

Modules: Default Classes: Structure, Transformers, Validators

Constant Summary collapse

LABELS =

Mapping of structure names to their human-readable labels

Returns:

  • (Hash<Symbol, String>)
{
  factory_reference: "factory reference",
  global_context: "global context"
}.freeze

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(label, input, structure:) ⇒ Normalizer

Creates a normalizer for normalizing Hash data based on a structure

Parameters:

  • label (String)

    A label that describes the data itself

  • input (Hash)

    The data to normalize

  • structure (Hash)

    The structure to normalize the data to



217
218
219
220
221
# File 'lib/spec_forge/normalizer.rb', line 217

def initialize(label, input, structure:)
  @label = label
  @input = input
  @structure = structure
end

Class Attribute Details

.structuresHash<Symbol, Hash> (readonly)

Collection of structure definitions used for validation

Contains all the structure definitions loaded from YAML files, indexed by their name. Each structure defines the expected format, types, and validation rules for a specific data structure.

Examples:

Accessing a structure definition

spec_structure = SpecForge::Normalizer.structures[:spec]
url_definition = spec_structure[:structure][:url]

Returns:

  • (Hash<Symbol, Hash>)

    Hash mapping structure names to their definitions



36
37
38
# File 'lib/spec_forge/normalizer.rb', line 36

def structures
  @structures
end

Class Method Details

.default(name = nil, structure: nil, include_optional: false) ⇒ Hash

Returns the default values for a structure

Creates a hash of defaults based on a structure definition. Handles optional values, nested structures, and type-specific default generation.

Examples:

Getting defaults for a predefined structure

SpecForge::Normalizer.default(:spec)
# => {debug: false, variables: {}, headers: {}, ...}

Getting defaults for a custom structure

structure = {name: {type: String, default: "Unnamed"}}
SpecForge::Normalizer.default(structure: structure)
# => {name: "Unnamed"}

Parameters:

  • name (Symbol, nil) (defaults to: nil)

    Name of a predefined structure to use

  • structure (Hash, nil) (defaults to: nil)

    Custom structure definition (used if name not provided)

  • include_optional (Boolean) (defaults to: false)

    Whether to include non-required fields with no default

Returns:

  • (Hash)

    A hash of default values based on the structure



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/spec_forge/normalizer.rb', line 132

def default(name = nil, structure: nil, include_optional: false)
  structure ||= @structures[name.to_sym]

  if !structure.is_a?(Hash)
    message =
      if name.present?
        "No normalizer structure exists with name #{name.in_quotes}"
      else
        "The provided normalizer structure must be a Hash. Got #{structure.inspect}"
      end

    raise ArgumentError, message
  end

  default_from_structure(structure, include_optional:)
end

.load_from_filesHash

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Loads normalizer structure definitions from YAML files

Reads YAML files in the normalizers directory and creates structure definitions for use in validation and normalization.

Returns:

  • (Hash)

    A hash of loaded structure definitions



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/spec_forge/normalizer.rb', line 159

def load_from_files
  base_path = Pathname.new(File.expand_path("normalizers", __dir__))
  paths = Dir[base_path.join("**/*.yml")].sort

  @structures =
    paths.each_with_object({}) do |path, hash|
      path = Pathname.new(path)

      # Include the directory name in the path to include normalizers in directories
      name = path.relative_path_from(base_path).to_s.delete_suffix(".yml").to_sym

      input = YAML.safe_load_file(path, symbolize_names: true, aliases: true)
      raise Error, "Normalizer defined at #{path.to_s.in_quotes} is empty" if input.blank?

      hash[name] = Structure.new(input, label: LABELS[name] || name.to_s.humanize.downcase)
    end
end

.normalize(input, using:, label: nil) ⇒ Array<Hash, Set>

Normalizes input data against a structure without raising errors

Validates and transforms input data according to a structure definition, collecting any validation errors rather than raising them. This method is the underlying implementation used by normalize! but returns errors instead of raising them.

Parameters:

  • input (Hash)

    The data to normalize

  • using (Symbol, Hash)

    Either a predefined structure name or a custom structure

  • label (String, nil) (defaults to: nil)

    A descriptive label for error messages

Returns:

  • (Array<Hash, Set>)

    A two-element array containing:

    1. The normalized data
    2. A set of any validation errors encountered

Raises:



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/spec_forge/normalizer.rb', line 80

def normalize(input, using:, label: nil)
  # Since normalization is based on a structured hash, :using can be passed a Hash
  # to skip using a predefined normalizer.
  if using.is_a?(Hash)
    structure = using

    if label.blank?
      raise ArgumentError, "A label must be provided when using a custom structure"
    end
  else
    structure = @structures[using.to_sym]

    # We have a predefined structure and structures all have labels
    label ||= structure.label
  end

  # Ensure we have a structure
  if !structure.is_a?(Hash)
    structures = @structures.keys.map(&:in_quotes).to_or_sentence

    raise ArgumentError,
      "Invalid structure or name. Got #{using}, expected one of #{structures}"
  end

  # This is checked down here because it felt like it belonged...
  # and because of that pesky label
  raise Error::InvalidTypeError.new(input, Hash, for: label) unless input.is_a?(Hash)

  new(label, input, structure:).normalize
end

.normalize!(input, using:, label: nil) ⇒ Hash Also known as: validate!

Normalizes input data against a structure with error raising

Same as #normalize but raises an error if validation fails.

Examples:

Using a predefined structure

SpecForge::Normalizer.normalize!({url: "/users"}, using: :spec)

Using a custom structure

structure = {name: {type: String}}
SpecForge::Normalizer.normalize!({name: "Test"}, using: structure, label: "custom")

Parameters:

  • input (Hash)

    The data to normalize

  • using (Symbol, Hash)

    Either a predefined structure name or a custom structure

  • label (String, nil) (defaults to: nil)

    A descriptive label for error messages

Returns:

  • (Hash)

    The normalized data

Raises:



58
59
60
# File 'lib/spec_forge/normalizer.rb', line 58

def normalize!(input, using:, label: nil)
  raise_errors! { normalize(input, using:, label:) }
end

.raise_errors! { ... } ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Raises any errors collected by the block

Yields:

  • Block that returns [output, errors]

Yield Returns:

  • (Array<Object, Set>)

    The result and any errors

Returns:

  • (Object)

    The normalized output if successful

Raises:



189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/spec_forge/normalizer.rb', line 189

def raise_errors!(&block)
  errors = Set.new

  begin
    output, new_errors = yield
    errors.merge(new_errors) if new_errors.size > 0
  rescue => e
    errors << e
  end

  raise Error::InvalidStructureError.new(errors) if errors.size > 0

  output
end

Instance Method Details

#normalizeArray<Hash, Set>

Normalizes the data according to the defined structure

Returns:

  • (Array<Hash, Set>)

    The normalized data and any errors



228
229
230
231
232
233
234
235
# File 'lib/spec_forge/normalizer.rb', line 228

def normalize
  case @input
  when Hash
    normalize_hash
  when Array
    normalize_array
  end
end