Class: Philiprehberger::SchemaValidator::Schema

Inherits:
Object
  • Object
show all
Includes:
Constraints
Defined in:
lib/philiprehberger/schema_validator/schema.rb

Constant Summary collapse

TYPES =
%i[string integer float boolean array hash].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(&block) ⇒ Schema

Returns a new instance of Schema.



12
13
14
15
16
17
18
# File 'lib/philiprehberger/schema_validator/schema.rb', line 12

def initialize(&block)
  @fields = {}
  @nested_schemas = {}
  @cross_validators = []
  @strict = false
  instance_eval(&block) if block
end

Class Method Details

.omit(base, *field_names) ⇒ Schema

Create a sub-schema excluding the specified fields

Parameters:

  • base (Schema)

    the base schema

  • field_names (Array<Symbol>)

    fields to exclude

Returns:

  • (Schema)

    new schema without excluded fields



75
76
77
78
79
80
81
# File 'lib/philiprehberger/schema_validator/schema.rb', line 75

def self.omit(base, *field_names)
  new_schema = new {} # rubocop:disable Lint/EmptyBlock
  base.instance_variable_get(:@fields).each do |name, field|
    new_schema.instance_variable_get(:@fields)[name] = field unless field_names.include?(name)
  end
  new_schema
end

.pick(base, *field_names) ⇒ Schema

Create a sub-schema with only the specified fields

Parameters:

  • base (Schema)

    the base schema

  • field_names (Array<Symbol>)

    fields to include

Returns:

  • (Schema)

    new schema with only selected fields



62
63
64
65
66
67
68
# File 'lib/philiprehberger/schema_validator/schema.rb', line 62

def self.pick(base, *field_names)
  new_schema = new {} # rubocop:disable Lint/EmptyBlock
  base.instance_variable_get(:@fields).each do |name, field|
    new_schema.instance_variable_get(:@fields)[name] = field if field_names.include?(name)
  end
  new_schema
end

Instance Method Details

#depends_on(field, when_field:) ⇒ Object

Declare a conditional field dependency

Parameters:

  • field (Symbol)

    the field that becomes required

  • when_field (Hash)

    condition: { field_name => expected_value }



45
46
47
# File 'lib/philiprehberger/schema_validator/schema.rb', line 45

def depends_on(field, when_field:)
  (@dependencies ||= []) << { field: field, when_field: when_field }
end

#exclusive_group(name, fields) ⇒ Object

Declare mutually exclusive fields

Parameters:

  • name (Symbol)

    group name

  • fields (Array<Symbol>)

    fields that are mutually exclusive



53
54
55
# File 'lib/philiprehberger/schema_validator/schema.rb', line 53

def exclusive_group(name, fields)
  (@exclusive_groups ||= []) << { name: name, fields: fields }
end

#fieldsObject



130
131
132
# File 'lib/philiprehberger/schema_validator/schema.rb', line 130

def fields
  @fields.keys
end

#merge(&block) ⇒ Object



160
161
162
163
164
165
166
167
168
# File 'lib/philiprehberger/schema_validator/schema.rb', line 160

def merge(&block)
  new_schema = Schema.new
  new_schema.instance_variable_set(:@fields, @fields.dup)
  new_schema.instance_variable_set(:@nested_schemas, @nested_schemas.dup)
  new_schema.instance_variable_set(:@cross_validators, @cross_validators.dup)
  new_schema.instance_variable_set(:@strict, @strict)
  new_schema.instance_eval(&block) if block
  new_schema
end

#nested(name, **opts) ⇒ Object



83
84
85
86
# File 'lib/philiprehberger/schema_validator/schema.rb', line 83

def nested(name, **opts, &)
  sub_schema = Schema.new(&)
  @nested_schemas[name] = { schema: sub_schema, required: opts.fetch(:required, false) }
end

#strict!void

This method returns an undefined value.

Enable strict mode — unknown keys in validated data will produce errors



30
31
32
# File 'lib/philiprehberger/schema_validator/schema.rb', line 30

def strict!
  @strict = true
end

#strict?Boolean

Whether this schema rejects unknown keys

Returns:

  • (Boolean)


37
38
39
# File 'lib/philiprehberger/schema_validator/schema.rb', line 37

def strict?
  @strict == true
end

#to_json_schemaHash

Export a simplified JSON Schema (draft 7) representation

Returns:

  • (Hash)

    a hash compatible with JSON Schema draft 7



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/philiprehberger/schema_validator/schema.rb', line 137

def to_json_schema
  properties = {}
  required_fields = []

  @fields.each do |name, field|
    properties[name.to_s] = field_to_json_schema(field)
    required_fields << name.to_s if field.required?
  end

  @nested_schemas.each do |name, config|
    properties[name.to_s] = config[:schema].to_json_schema
    required_fields << name.to_s if config[:required]
  end

  schema = {
    'type' => 'object',
    'properties' => properties
  }
  schema['required'] = required_fields unless required_fields.empty?
  schema['additionalProperties'] = false if strict?
  schema
end

#validate(data = nil, &block) ⇒ Object



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/philiprehberger/schema_validator/schema.rb', line 88

def validate(data = nil, &block)
  if block
    @cross_validators << block
    return
  end

  errors = []
  @fields.each_value { |field| validate_field(field, data, errors) }
  @nested_schemas.each { |name, config| validate_nested(name, config, data, errors) }
  @cross_validators.each { |cv| cv.call(data, errors) }
  validate_unknown_keys(data, errors) if strict?
  result = Result.new(errors: errors)
  validate_dependencies(data, result) if @dependencies&.any?
  validate_exclusive_groups(data, result) if @exclusive_groups&.any?
  result
end

#validate!(data) ⇒ Object

Raises:



105
106
107
108
109
110
# File 'lib/philiprehberger/schema_validator/schema.rb', line 105

def validate!(data)
  result = validate(data)
  raise ValidationError, result.errors.join(', ') unless result.valid?

  result
end

#validate_and_coerce(data) ⇒ Hash{Symbol => Object}

Validate and coerce in a single call

Attempts type coercion on any field whose coercion is well defined and succeeds, leaving other values as provided. Defaults are applied for absent fields. Returns a hash containing the validity flag, the (best-effort) coerced payload, and the error list from ‘#validate`.

Parameters:

  • data (Hash)

Returns:

  • (Hash{Symbol => Object})

    { valid: Boolean, values: Hash, errors: Array<String> }



120
121
122
123
124
125
126
127
128
# File 'lib/philiprehberger/schema_validator/schema.rb', line 120

def validate_and_coerce(data)
  coerced = begin
    coerce_payload(data)
  rescue StandardError
    data
  end
  result = validate(coerced)
  { valid: result.valid?, values: coerced, errors: result.errors }
end