Class: Servus::Support::Validator

Inherits:
Object
  • Object
show all
Defined in:
lib/servus/support/validator.rb

Overview

Handles JSON Schema validation for service arguments and results.

The Validator class provides automatic validation of service inputs and outputs against JSON Schema definitions. Schemas can be defined as inline constants (ARGUMENTS_SCHEMA, RESULT_SCHEMA) or as external JSON files.

Examples:

Inline schema validation

class MyService < Servus::Base
  ARGUMENTS_SCHEMA = {
    type: "object",
    required: ["user_id"],
    properties: {
      user_id: { type: "integer" }
    }
  }
end

File-based schema validation

# app/schemas/services/my_service/arguments.json
# { "type": "object", "required": ["user_id"], ... }

See Also:

Class Method Summary collapse

Class Method Details

.cacheHash

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.

Returns the current schema cache.

Returns:

  • (Hash)

    cache mapping schema paths to loaded schemas



179
180
181
# File 'lib/servus/support/validator.rb', line 179

def self.cache
  @schema_cache
end

.clear_cache!Hash

Clears the schema cache.

Useful in development when schema files are modified, or in tests to ensure fresh schema loading between test cases.

Examples:

In a test suite

before(:each) do
  Servus::Support::Validator.clear_cache!
end

Returns:

  • (Hash)

    empty hash



171
172
173
# File 'lib/servus/support/validator.rb', line 171

def self.clear_cache!
  @schema_cache = {}
end

.fetch_schema_from_sources(dsl_schema, inline_schema_constant, schema_path) ⇒ Hash?

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.

Fetches schema from DSL, inline constant, or file.

Implements the schema resolution precedence:

  1. DSL-defined schema (if provided)
  2. Inline constant (if provided)
  3. File at schema_path (if exists)
  4. nil (no schema found)

Parameters:

  • dsl_schema (Hash, nil)

    schema from DSL method (e.g., schema arguments: Hash)

  • inline_schema_constant (Hash, nil)

    inline schema constant (e.g., ARGUMENTS_SCHEMA)

  • schema_path (String)

    file path to external schema JSON

Returns:

  • (Hash, nil)

    schema with indifferent access, or nil if not found



197
198
199
200
201
202
203
204
205
# File 'lib/servus/support/validator.rb', line 197

def self.fetch_schema_from_sources(dsl_schema, inline_schema_constant, schema_path)
  if dsl_schema
    dsl_schema.with_indifferent_access
  elsif inline_schema_constant
    inline_schema_constant.with_indifferent_access
  elsif File.exist?(schema_path)
    JSON.load_file(schema_path).with_indifferent_access
  end
end

.load_schema(service_class, type) ⇒ Hash?

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 and caches a schema for a service.

Implements a three-tier lookup strategy:

  1. Check for schema defined via DSL method (service_class.arguments_schema/result_schema)
  2. Check for inline constant (ARGUMENTS_SCHEMA or RESULT_SCHEMA)
  3. Fall back to JSON file in app/schemas/services/namespace/type.json

Schemas are cached after first load for performance.

rubocop:disable Metrics/MethodLength

Parameters:

  • service_class (Class)

    the service class

  • type (String)

    schema type ("arguments" or "result")

Returns:

  • (Hash, nil)

    the schema hash, or nil if no schema found



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/servus/support/validator.rb', line 135

def self.load_schema(service_class, type)
  # Get service path based on class name (e.g., "process_payment" from "Servus::ProcessPayment::Service")
  service_namespace = parse_service_namespace(service_class)
  schema_path = Servus.config.schema_path_for(service_namespace, type)

  # Return from cache if available
  return @schema_cache[schema_path] if @schema_cache.key?(schema_path)

  # Check for DSL-defined schema first
  dsl_schema = if type == 'arguments'
                 service_class.arguments_schema
               else
                 service_class.result_schema
               end

  inline_schema_constant_name = "#{service_class}::#{type.upcase}_SCHEMA"
  inline_schema_constant = if Object.const_defined?(inline_schema_constant_name)
                             Object.const_get(inline_schema_constant_name)
                           end

  @schema_cache[schema_path] = fetch_schema_from_sources(dsl_schema, inline_schema_constant, schema_path)
  @schema_cache[schema_path]
end

.parse_service_namespace(service_class) ⇒ String

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.

Converts service class name to file path namespace.

Transforms a class name like "Services::ProcessPayment::Service" into "services/process_payment" for locating schema files.

Examples:

parse_service_namespace(Services::ProcessPayment::Service)
# => "services/process_payment"

Parameters:

  • service_class (Class)

    the service class

Returns:

  • (String)

    underscored namespace path



220
221
222
223
224
# File 'lib/servus/support/validator.rb', line 220

def self.parse_service_namespace(service_class)
  service_class.name.split('::')[..-2].map do |s|
    s.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
  end.join('/')
end

.validate_arguments!(service_class, args) ⇒ Boolean

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.

Validates service arguments against the ARGUMENTS_SCHEMA.

Checks arguments against either an inline ARGUMENTS_SCHEMA constant or a file-based schema at app/schemas/services/namespace/arguments.json. Validation is skipped if no schema is defined.

Examples:

Validator.validate_arguments!(MyService, { user_id: 123 })

Parameters:

  • service_class (Class)

    the service class being validated

  • args (Hash)

    keyword arguments passed to the service

Returns:

  • (Boolean)

    true if validation passes

Raises:



46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/servus/support/validator.rb', line 46

def self.validate_arguments!(service_class, args)
  schema = load_schema(service_class, 'arguments')
  return true unless schema # Skip validation if no schema exists

  serialized_result = args.as_json
  validation_errors = JSON::Validator.fully_validate(schema, serialized_result)

  if validation_errors.any?
    error_message = "Invalid arguments for #{service_class.name}: #{validation_errors.join(', ')}"
    raise Servus::Base::ValidationError, error_message
  end

  true
end

.validate_event_payload!(handler_class, payload) ⇒ Boolean

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.

Validates event payload against the handler's payload schema.

Examples:

Validator.validate_event_payload!(MyEventHandler, { user_id: 123 })

Parameters:

  • handler_class (Class)

    the event handler class

  • payload (Hash)

    the event payload to validate

Returns:

  • (Boolean)

    true if validation passes

Raises:



105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/servus/support/validator.rb', line 105

def self.validate_event_payload!(handler_class, payload)
  schema = handler_class.payload_schema
  return true unless schema

  serialized_payload = payload.as_json
  validation_errors = JSON::Validator.fully_validate(schema, serialized_payload)

  if validation_errors.any?
    raise Servus::Support::Errors::ValidationError,
          "Invalid payload for event :#{handler_class.event_name}: #{validation_errors.join(', ')}"
  end

  true
end

.validate_result!(service_class, result) ⇒ Servus::Support::Response

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.

Validates service result data against the RESULT_SCHEMA.

Checks the result.data against either an inline RESULT_SCHEMA constant or a file-based schema at app/schemas/services/namespace/result.json. Only validates successful responses; failures are skipped.

Examples:

Validator.validate_result!(MyService, response)

Parameters:

  • service_class (Class)

    the service class being validated

  • result (Servus::Support::Response)

    the response object to validate

Returns:

Raises:



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/servus/support/validator.rb', line 76

def self.validate_result!(service_class, result)
  return result unless result.success?

  schema = load_schema(service_class, 'result')
  return result unless schema # Skip validation if no schema exists

  serialized_result = result.data.as_json
  validation_errors = JSON::Validator.fully_validate(schema, serialized_result)

  if validation_errors.any?
    error_message = "Invalid result structure from #{service_class.name}: #{validation_errors.join(', ')}"
    raise Servus::Base::ValidationError, error_message
  end

  result
end