module Recurly::Schema::SchemaValidator

This module is responsible for validating that the raw data passed in to attributes matches the schema belonging to this class. It should be mixed in to the Request class.

Public Instance Methods

get_did_you_mean(schema, misspelled_attr) click to toggle source

Gets the closest term to the misspelled attribute

# File lib/recurly/schema/schema_validator.rb, line 61
def get_did_you_mean(schema, misspelled_attr)
  closest = schema.attributes.keys.sort_by do |v|
    levenshtein_distance(v, misspelled_attr)
  end.first

  if closest && levenshtein_distance(closest, misspelled_attr) <= 4
    closest
  end
end
validate!() click to toggle source

Validates the attributes and throws an error if something is wrong.

@example

Recurly::Requests::PlanCreate.new(code: 'plan123').validate!
#=> {:code=>"plan123"}

@example

Recurly::Requests::PlanCreate.new(code: 3.14).validate!
#=> ArgumentError: Attribute 'code' on the resource Recurly::Requests::PlanCreate is type Float but should be a String.

@example

Recurly::Requests::PlanCreate.new(kode: 'plan123').validate!
#=> ArgumentError: Attribute 'kode' does not exist on request Recurly::Requests::PlanCreate. Did you mean 'code'?

@raise [ArgumentError] if the attribute data does not match the schema.

# File lib/recurly/schema/schema_validator.rb, line 20
def validate!
  attributes.each do |attr_name, val|
    schema_attr = schema.get_attribute(attr_name)
    if schema_attr.nil?
      err_msg = "Attribute '#{attr_name}' does not exist on request #{self.class.name}."
      if did_you_mean = get_did_you_mean(schema, attr_name)
        err_msg << " Did you mean '#{did_you_mean}'?"
      end
      raise ArgumentError, err_msg
    else
      validate_attribute!(attr_name, schema_attr, val)
    end
  end
end
validate_attribute!(name, schema_attr, val) click to toggle source

Validates an individual attribute

# File lib/recurly/schema/schema_validator.rb, line 36
def validate_attribute!(name, schema_attr, val)
  unless val.nil? || schema_attr.is_valid?(val)
    # If it's safely castable, the json deserializer or server
    # will take care of it for us
    unless safely_castable?(val.class, schema_attr.type)
      expected = case schema_attr
        when Schema::ArrayAttribute
          "Array of #{schema_attr.type}s"
        else
          schema_attr.type
        end

      raise ArgumentError, "Attribute '#{name}' on the resource #{self.class.name} is type #{val.class} but should be a #{expected}"
    end
  end

  # This is the convention for a recurly object
  if schema_attr.is_a?(Schema::ResourceAttribute) && val.is_a?(Hash)
    # Using send because the initializer may be private
    instance = schema_attr.recurly_class.send(:new, val)
    instance.validate!
  end
end

Private Instance Methods

levenshtein_distance(str1, str2) click to toggle source

This code is copied directly from the did_you mean gem which is based directly on the Text gem implementation.

did_you_mean: Copyright © 2014-2016 Yuki Nishijima. Text: Copyright © 2006-2013 Paul Battley, Michael Neumann, Tim Fletcher.

Returns a value representing the “cost” of transforming str1 into str2

# File lib/recurly/schema/schema_validator.rb, line 99
def levenshtein_distance(str1, str2)
  str1 = str1.to_s unless str1.is_a? String
  str2 = str2.to_s unless str2.is_a? String
  n = str1.length
  m = str2.length
  return m if n.zero?
  return n if m.zero?

  d = (0..m).to_a
  x = nil

  # to avoid duplicating an enumerable object, create it outside of the loop
  str2_codepoints = str2.codepoints

  str1.each_codepoint.with_index(1) do |char1, i|
    j = 0
    while j < m
      cost = (char1 == str2_codepoints[j]) ? 0 : 1
      x = min3(
        d[j + 1] + 1, # insertion
        i + 1,      # deletion
        d[j] + cost # substitution
      )
      d[j] = i
      i = x

      j += 1
    end
    d[m] = x
  end

  x
end
min3(a, b, c) click to toggle source
# File lib/recurly/schema/schema_validator.rb, line 133
def min3(a, b, c)
  if a < b && a < c
    a
  elsif b < c
    b
  else
    c
  end
end
safely_castable?(from_type, to_type) click to toggle source
# File lib/recurly/schema/schema_validator.rb, line 73
def safely_castable?(from_type, to_type)
  # TODO we can drop this switch when 2.3 support is dropped
  int_class = if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.4.0")
      :Integer
    else
      :Fixnum
    end
  int_class = Kernel.const_get(int_class)

  case [from_type, to_type]
  when [Symbol, String]
    true
  when [int_class, Float]
    true
  else
    false
  end
end