Class: MediaTypes::Scheme

Inherits:
Object
  • Object
show all
Defined in:
lib/media_types/scheme.rb,
lib/media_types/scheme/links.rb,
lib/media_types/scheme/rules.rb,
lib/media_types/scheme/any_of.rb,
lib/media_types/scheme/errors.rb,
lib/media_types/scheme/allow_nil.rb,
lib/media_types/scheme/attribute.rb,
lib/media_types/scheme/not_strict.rb,
lib/media_types/scheme/output_type_guard.rb,
lib/media_types/scheme/missing_validation.rb,
lib/media_types/scheme/output_empty_guard.rb,
lib/media_types/scheme/validation_options.rb,
lib/media_types/scheme/enumeration_context.rb,
lib/media_types/scheme/enumeration_of_type.rb,
lib/media_types/scheme/rules_exhausted_guard.rb,
lib/media_types/scheme/output_iterator_with_predicate.rb

Overview

Media Type Schemes can validate content to a media type, by itself. Used by the ‘validations` dsl.

Examples:

A scheme to test against


class MyMedia
  include MediaTypes::Dsl

  validations do
    attribute :foo do
      collection :bar, String
    end
    attribute :number, Numeric
  end
end

MyMedia.valid?({ foo: { bar: ['test'] }, number: 42 })
#=> true

See Also:

Defined Under Namespace

Classes: AnyOverwritingNotStrictError, Attribute, CaseEqualityWithList, ConflictingTypeDefinitionError, DuplicateAnyRuleError, DuplicateKeyError, DuplicateNotStrictRuleError, DuplicateStringKeyError, DuplicateSymbolKeyError, EmptyOutputError, EnumerationContext, EnumerationOfType, ExhaustedOutputError, KeyTypeError, Links, MissingValidation, NotStrict, NotStrictOverwritingAnyError, OutputEmptyGuard, OutputIteratorWithPredicate, OutputTypeGuard, OutputTypeMismatch, OverwritingRuleError, Rules, RulesExhaustedGuard, StrictValidationError, StringOverwritingSymbolError, SymbolOverwritingStringError, ValidationError, ValidationOptions

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(allow_empty: false, expected_type: ::Object, current_type: nil, registry: nil, &block) ⇒ Scheme

Creates a new scheme

Parameters:

  • allow_empty (TrueClass, FalseClass) (defaults to: false)

    if true allows to be empty, if false raises EmptyOutputError if empty

  • expected_type (NilClass, Class) (defaults to: ::Object)

    forces the type to be this type, if given

See Also:



97
98
99
100
101
102
103
104
105
106
# File 'lib/media_types/scheme.rb', line 97

def initialize(allow_empty: false, expected_type: ::Object, current_type: nil, registry: nil, &block)
  self.rules = Rules.new(allow_empty: allow_empty, expected_type: expected_type)
  self.type_attributes = {}
  self.fixtures = []
  self.asserted_sane = false
  @registry = registry
  @current_type = current_type

  instance_exec(&block) if block_given?
end

Instance Attribute Details

#asserted_saneObject Also known as: asserted_sane?

Returns the value of attribute asserted_sane.



109
110
111
# File 'lib/media_types/scheme.rb', line 109

def asserted_sane
  @asserted_sane
end

#fixturesObject

Returns the value of attribute fixtures.



108
109
110
# File 'lib/media_types/scheme.rb', line 108

def fixtures
  @fixtures
end

#rulesObject

Returns the value of attribute rules.



109
110
111
# File 'lib/media_types/scheme.rb', line 109

def rules
  @rules
end

#type_attributesObject

Returns the value of attribute type_attributes.



108
109
110
# File 'lib/media_types/scheme.rb', line 108

def type_attributes
  @type_attributes
end

Class Method Details

.AllowNil(klazz) ⇒ CaseEqualityWithList

noinspection RubyClassMethodNamingConvention

Allows the wrapped klazz to be nil

Parameters:

  • klazz (Class)

    the class that it must be the if it is not NilClass

Returns:



15
16
17
# File 'lib/media_types/scheme/allow_nil.rb', line 15

def AllowNil(klazz) # rubocop:disable Naming/MethodName
  AnyOf(NilClass, klazz)
end

.AnyOf(*klazzes) ⇒ CaseEqualityWithList

noinspection RubyClassMethodNamingConvention

Allows it to be any of the wrapped klazzes

Parameters:

  • klazzes (Array<Class>)

    the classes that are valid for it

Returns:



26
27
28
# File 'lib/media_types/scheme/any_of.rb', line 26

def AnyOf(*klazzes) # rubocop:disable Naming/MethodName
  CaseEqualityWithList.new(klazzes)
end

Instance Method Details

#AllowNil(klazz) ⇒ CaseEqualityWithList

noinspection RubyInstanceMethodNamingConvention

Allows the wrapped klazz to be nil

Parameters:

  • klazz (Class)

    the class that it must be the if it is not NilClass

Returns:



26
27
28
# File 'lib/media_types/scheme/allow_nil.rb', line 26

def AllowNil(klazz) # rubocop:disable Naming/MethodName
  self.class.AllowNil(klazz)
end

#any(scheme = nil, expected_type: ::Hash, allow_empty: false, &block) ⇒ Object

Allow for any key.

The +&block+ defines the Schema for each value.

Examples:

Add a collection named foo, expecting any key with a defined value


class MyMedia
  include MediaTypes::Dsl

  validations do
    collection :foo do
      any do
        attribute :bar, String
      end
    end
  end
end

MyMedia.valid?({ foo: [{ anything: { bar: 'my-string' }, other_thing: { bar: 'other-string' } }] })
# => true

Any key, but all of them String or Numeric


class MyMedia
  include MediaTypes::Dsl

  validations do
    any AnyOf(String, Numeric)
  end
end

MyMedia.valid?({ foo: 'my-string', bar: 42 })
# => true

Parameters:

  • scheme (Scheme, NilClass) (defaults to: nil)

    scheme to use if no &block is given

  • allow_empty (TrueClass, FalseClass) (defaults to: false)

    if true, empty (no key/value present) is allowed

  • expected_type (Class) (defaults to: ::Hash)

    forces the validated object to have this type

Raises:

See Also:



264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/media_types/scheme.rb', line 264

def any(scheme = nil, expected_type: ::Hash, allow_empty: false, &block)
  raise ConflictingTypeDefinitionError, 'You cannot apply a block to a non-hash typed property, either remove the type or the block' if scheme != ::Hash && block_given? && !scheme.nil?

  unless block_given?
    if scheme.is_a?(Scheme)
      return rules.default = scheme
    end

    return rules.default = Attribute.new(scheme)
  end

  rules.default = Scheme.new(allow_empty: allow_empty, expected_type: expected_type, registry: @registry, current_type: @current_type, &block)
end

#AnyOf(*klazzes) ⇒ CaseEqualityWithList

noinspection RubyInstanceMethodNamingConvention

Allows it to be any of the wrapped klazzes

Parameters:

  • klazzes (Array<Class>)

    the classes that are valid for it

Returns:



37
38
39
# File 'lib/media_types/scheme/any_of.rb', line 37

def AnyOf(*klazzes) # rubocop:disable Naming/MethodName
  self.class.AnyOf(*klazzes)
end

#assert_fail(fixture, loose: false) ⇒ Object



461
462
463
464
# File 'lib/media_types/scheme.rb', line 461

def assert_fail(fixture, loose: false)
  reduced_stack = remove_current_dir_from_stack(caller_locations)
  @fixtures << FixtureData.new(caller: reduced_stack.first, fixture: fixture, expect_to_pass: false, loose: loose)
end

#assert_pass(fixture, loose: false) ⇒ Object



456
457
458
459
# File 'lib/media_types/scheme.rb', line 456

def assert_pass(fixture, loose: false)
  reduced_stack = remove_current_dir_from_stack(caller_locations)
  @fixtures << FixtureData.new(caller: reduced_stack.first, fixture: fixture, expect_to_pass: true, loose: loose)
end

#attribute(key, type = nil, optional: false, **opts, &block) ⇒ Object

Adds an attribute to the schema

If a +block+ is given, uses that to test against instead of +type+

Examples:

Add an attribute named foo, expecting a string


class MyMedia
  include MediaTypes::Dsl

  validations do
    attribute :foo, String
  end
end

MyMedia.valid?({ foo: 'my-string' })
# => true

Add an attribute named foo, expecting nested scheme


class MyMedia
  include MediaTypes::Dsl

  validations do
    attribute :foo do
      attribute :bar, String
    end
  end
end

MyMedia.valid?({ foo: { bar: 'my-string' }})
# => true

Parameters:

  • key (Symbol)

    the attribute name

  • opts (Hash)

    options to pass to Scheme or Attribute

  • type (Class, #===, Scheme) (defaults to: nil)

    The type of the value, can be anything that responds to #===, or scheme to use if no &block is given. Defaults to Object without a &block and to Hash with a &block. or scheme to use if no &block is given. Defaults to Object without a &block and to Hash with a &block.

Raises:

See Also:



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/media_types/scheme.rb', line 208

def attribute(key, type = nil, optional: false, **opts, &block)
  raise ConflictingTypeDefinitionError, 'You cannot apply a block to a non-hash typed attribute, either remove the type or the block' if type != ::Hash && block_given? && !type.nil?

  type ||= ::Object

  if block_given?
    return collection(key, expected_type: ::Hash, optional: optional, **opts, &block)
  end

  if type.is_a?(Scheme)
    return rules.add(key, type, optional: optional)
  end

  rules.add(key, Attribute.new(type, **opts, &block), optional: optional)
end

#collection(key, scheme = nil, view: nil, allow_empty: false, expected_type: ::Array, optional: false, &block) ⇒ Object

Expect a collection such as an array or hash.

The +block+ defines the Schema for each item in that collection.

Examples:

Collection with an array of string


class MyMedia
  include MediaTypes::Dsl

  validations do
    collection :foo, String
  end
end

MyMedia.valid?({ collection: ['foo', 'bar'] })
# => true

Collection with defined scheme


class MyMedia
  include MediaTypes::Dsl

  validations do
    collection :foo do
      attribute :required, String
      attribute :number, Numeric
    end
  end
end

MyMedia.valid?({ foo: [{ required: 'test', number: 42 }, { required: 'other', number: 0 }] })
# => true

Parameters:

  • key (Symbol)

    key of the collection (same as #attribute)

  • scheme (NilClass, Scheme, Class) (defaults to: nil)

    scheme to use if no &block is given, or type of each item in collection

  • allow_empty (TrueClass, FalseClass) (defaults to: false)

    if true accepts 0 items in an enumerable

  • expected_type (Class) (defaults to: ::Array)

    forces the value of this collection to be this type, defaults to Array.

Raises:

See Also:



353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
# File 'lib/media_types/scheme.rb', line 353

def collection(key, scheme = nil, view: nil, allow_empty: false, expected_type: ::Array, optional: false, &block)
  raise ConflictingTypeDefinitionError, 'You cannot apply a block to a non-hash typed collection, either remove the type or the block' if scheme != ::Hash && block_given? && !scheme.nil?

  unless block_given?
    if scheme.nil?
      dependent_key = @current_type.as_key.dup
      dependent_key[1] = view

      unless @registry.has_key? dependent_key
        raise Errors::CollectionDefinitionNotFound.new(@current_type.override_suffix('json').to_s, @current_type.view(view).override_suffix('json').to_s)
      end
      scheme = @registry[dependent_key]
    end

    return rules.add(
      key,
      EnumerationOfType.new(
        scheme,
        enumeration_type: expected_type,
        allow_empty: allow_empty
      ),
      optional: optional
    )
  end

  rules.add(key, Scheme.new(allow_empty: allow_empty, expected_type: expected_type, registry: @registry, current_type: @current_type, &block), optional: optional)
end

#emptyObject

Mark object as a valid empty object

Examples:

Empty object


class MyMedia
  include MediaTypes::Dsl

  validations do
    empty
  end
end


444
445
# File 'lib/media_types/scheme.rb', line 444

def empty
end

#index(optional: false) ⇒ Object

Expect an index of links



384
385
386
387
388
# File 'lib/media_types/scheme.rb', line 384

def index(optional: false)
  collection(:_links, optional: optional) do
    link :_self
  end
end

#inspect(indentation = 0) ⇒ Object



447
448
449
450
451
452
453
454
# File 'lib/media_types/scheme.rb', line 447

def inspect(indentation = 0)
  tabs = '  ' * indentation
  [
    "#{tabs}[Scheme]",
    rules.inspect(indentation + 1),
    "#{tabs}[/Scheme]"
  ].join("\n")
end

Expect a link

Examples:

Links as defined in HAL, JSON-Links and other specs


class MyMedia
  include MediaTypes::Dsl

  validations do
    link :_self
    link :image
  end
end

MyMedia.valid?({ _links: { self: { href: 'https://example.org/s' }, image: { href: 'https://image.org/i' }} })
# => true

Link with extra attributes


class MyMedia
  include MediaTypes::Dsl

  validations do
    link :image do
      attribute :templated, TrueClass
    end
  end
end

MyMedia.valid?({ _links: { image: { href: 'https://image.org/{md5}', templated: true }} })
# => true

See Also:



424
425
426
427
428
429
430
# File 'lib/media_types/scheme.rb', line 424

def link(*args, **opts, &block)
  rules.fetch(:_links) do
    Links.new.tap do |links|
      rules.add(:_links, links)
    end
  end.link(*args, **opts, &block)
end

#merge(scheme, &block) ⇒ Object

Merges a scheme into this scheme without changing the incoming scheme

Parameters:

  • scheme (Scheme)

    the scheme to merge into this



283
284
285
286
# File 'lib/media_types/scheme.rb', line 283

def merge(scheme, &block)
  self.rules = rules.merge(scheme.send(:rules))
  instance_exec(&block) if block_given?
end

#not_strictObject

Allow for extra keys in the schema/collection even when passing strict: true to #validate!

Examples:

Allow for extra keys in collection


class MyMedia
  include MediaTypes::Dsl

  validations do
    collection :foo do
      attribute :required, String
      not_strict
    end
  end
end

MyMedia.valid?({ foo: [{ required: 'test', bar: 42 }] })
# => true

See Also:



309
310
311
# File 'lib/media_types/scheme.rb', line 309

def not_strict
  rules.default = NotStrict.new
end

#remove_current_dir_from_stack(stack) ⇒ Object

Removes all calls originating in current dir from given stack We need this so that we find out the caller of an assert_pass/fail in the caller_locations Which gets polluted by Scheme consecutively executing blocks within the validation blocks



469
470
471
# File 'lib/media_types/scheme.rb', line 469

def remove_current_dir_from_stack(stack)
  stack.reject { |location| location.path.include?(__dir__) }
end

#run_fixture_validations(expect_symbol_keys, backtrace = []) ⇒ Object

Raises:



506
507
508
509
510
511
512
513
514
# File 'lib/media_types/scheme.rb', line 506

def run_fixture_validations(expect_symbol_keys, backtrace = [])
  fixture_errors = validate_scheme_fixtures(expect_symbol_keys, backtrace)
  fixture_errors += validate_nested_scheme_fixtures(expect_symbol_keys, backtrace)
  fixture_errors += validate_default_scheme_fixtures(expect_symbol_keys, backtrace)

  raise AssertionError, fixture_errors unless fixture_errors.empty?

  self.asserted_sane = true
end

#valid?(output, **opts) ⇒ TrueClass, FalseClass

Checks if the output is valid

Parameters:

  • output (#each)

    the output to test against

  • opts (Hash)

    the options as defined below

  • exhaustive (Hash)

    a customizable set of options

  • strict (Hash)

    a customizable set of options

Returns:

  • (TrueClass, FalseClass)

    true if valid, false otherwise



123
124
125
126
127
128
129
# File 'lib/media_types/scheme.rb', line 123

def valid?(output, **opts)
  validate(output, **opts)
rescue ExhaustedOutputError
  !opts.fetch(:exhaustive) { true }
rescue ValidationError
  false
end

#validate(output, options = nil, **opts) ⇒ TrueClass

Validates the output and raises on certain validation errors

Parameters:

  • output (#each)

    output to validate

  • opts (Hash)

    a customizable set of options

  • opts[Array<String>] (Hash)

    a customizable set of options

Options Hash (**opts):

  • exhaustive (TrueClass, FalseClass)

    if true, the entire schema needs to be consumed

  • strict (TrueClass, FalseClass)

    if true, no extra keys may be present in output

Returns:

  • (TrueClass)

Raises:

  • ExhaustedOutputError

  • StrictValidationError

  • EmptyOutputError

  • CollectionTypeError

  • ValidationError

See Also:



149
150
151
152
153
154
155
156
# File 'lib/media_types/scheme.rb', line 149

def validate(output, options = nil, **opts)
  options ||= ValidationOptions.new(**opts)
  options.context = output

  catch(:end) do
    validate!(output, options, context: nil)
  end
end

#validate!(output, call_options, **_opts) ⇒ Object



161
162
163
164
165
# File 'lib/media_types/scheme.rb', line 161

def validate!(output, call_options, **_opts)
  OutputTypeGuard.call(output, call_options, rules: rules)
  OutputEmptyGuard.call(output, call_options, rules: rules)
  RulesExhaustedGuard.call(output, call_options, rules: rules)
end

#validate_default_scheme_fixtures(expect_symbol_keys, backtrace) ⇒ Object



497
498
499
500
501
502
503
504
# File 'lib/media_types/scheme.rb', line 497

def validate_default_scheme_fixtures(expect_symbol_keys, backtrace)
  return [] unless @rules.default.is_a?(Scheme)

  @rules.default.run_fixture_validations(expect_symbol_keys, backtrace.dup.append('*'))
  []
rescue AssertionError => e
  e.fixture_errors
end

#validate_fixture(fixture_data, expect_symbol_keys, backtrace = []) ⇒ Object



516
517
518
519
520
521
522
523
524
525
526
527
528
# File 'lib/media_types/scheme.rb', line 516

def validate_fixture(fixture_data, expect_symbol_keys, backtrace = [])
  json = JSON.parse(fixture_data.fixture, { symbolize_names: expect_symbol_keys })
  expected_key_type = expect_symbol_keys ? Symbol : String

  begin
    validate(json, expected_key_type: expected_key_type, backtrace: backtrace, loose: fixture_data.loose)
    unless fixture_data.expect_to_pass?
      raise UnexpectedValidationResultError.new(fixture_data.caller, 'No error encounterd whilst expecting to')
    end
  rescue MediaTypes::Scheme::ValidationError => e
    raise UnexpectedValidationResultError.new(fixture_data.caller, e) if fixture_data.expect_to_pass?
  end
end

#validate_nested_scheme_fixtures(expect_symbol_keys, backtrace) ⇒ Object



484
485
486
487
488
489
490
491
492
493
494
495
# File 'lib/media_types/scheme.rb', line 484

def validate_nested_scheme_fixtures(expect_symbol_keys, backtrace)
  @rules.flat_map do |key, rule|
    next unless rule.is_a?(Scheme) || rule.is_a?(Links)

    begin
      rule.run_fixture_validations(expect_symbol_keys, backtrace.dup.append(key))
      nil
    rescue AssertionError => e
      e.fixture_errors
    end
  end.compact
end

#validate_scheme_fixtures(expect_symbol_keys, backtrace) ⇒ Object



473
474
475
476
477
478
479
480
481
482
# File 'lib/media_types/scheme.rb', line 473

def validate_scheme_fixtures(expect_symbol_keys, backtrace)
  @fixtures.map do |fixture_data|
    begin
      validate_fixture(fixture_data, expect_symbol_keys, backtrace)
      nil
    rescue UnexpectedValidationResultError => e
      e
    end
  end.compact
end