class OpenUI::Rails::Actions::Schema

Validates the ‘args` of an action submission against a small, explicit contract. It is intentionally not a general-purpose schema language: it only ensures the shape a host handler relies on, so a malformed payload is rejected before any side effect.

Attributes

optional[R]
required[R]

Public Class Methods

any() click to toggle source

A schema that accepts any JSON object.

# File lib/openui/rails/actions/schema.rb, line 26
def self.any
  new(required: {}, optional: {}, allow_extra: true)
end
define(required: {}, optional: {}, allow_extra: false) click to toggle source
# File lib/openui/rails/actions/schema.rb, line 30
def self.define(required: {}, optional: {}, allow_extra: false)
  new(required: required || {}, optional: optional || {}, allow_extra: allow_extra)
end
from(value) click to toggle source

Build a schema from a Hash of required types, an existing Schema or a callable predicate.

# File lib/openui/rails/actions/schema.rb, line 36
def self.from(value)
  case value
  when nil then any
  when Schema, Callable then value
  when Hash then new(required: value, allow_extra: false)
  else
    unless value.respond_to?(:call)
      raise ConfigurationError, "schema must be a Hash, Schema, or respond to #call"
    end

    Callable.new(value)
  end
end
new(required: {}, optional: {}, allow_extra: false) click to toggle source
# File lib/openui/rails/actions/schema.rb, line 52
def initialize(required: {}, optional: {}, allow_extra: false)
  @required = required
  @optional = optional
  @allow_extra = allow_extra
end

Public Instance Methods

allow_extra?(= @allow_extra) click to toggle source
# File lib/openui/rails/actions/schema.rb, line 58
  def allow_extra? = @allow_extra

  def validate!(args)
    raise InvalidActionError, "action arguments must be an object" unless args.is_a?(Hash)

    values = normalize(args)
    validate_required(values)
    validate_optional(values)
    validate_extras(values.keys) unless allow_extra?
    args
  end

  private

  def normalize(args)
    args.each_with_object({}) { |(key, value), memo| memo[key.to_s] = value }
  end

  def validate_required(values)
    required.each do |key, expected|
      name = key.to_s
      raise InvalidActionError, "missing required argument #{name}" unless values.key?(name)
      next if type_matches?(values[name], expected)

      raise InvalidActionError, "argument #{name} has an invalid value"
    end
  end

  def validate_optional(values)
    optional.each do |key, expected|
      name = key.to_s
      next unless values.key?(name)
      next if type_matches?(values[name], expected)

      raise InvalidActionError, "argument #{name} has an invalid value"
    end
  end

  def validate_extras(keys)
    allowed = (required.keys + optional.keys).map(&:to_s)
    extra = keys - allowed
    return if extra.empty?

    raise InvalidActionError, "unexpected arguments: #{extra.join(", ")}"
  end

  def type_matches?(value, expected)
    case expected
    when Array then expected.any? { |candidate| type_matches?(value, candidate) }
    when Regexp then value.is_a?(String) && expected.match?(value)
    when Module then value.is_a?(expected)
    else expected.respond_to?(:call) ? expected.call(value) == true : false
    end
  end
end
normalize(args) click to toggle source
# File lib/openui/rails/actions/schema.rb, line 72
def normalize(args)
  args.each_with_object({}) { |(key, value), memo| memo[key.to_s] = value }
end
type_matches?(value, expected) click to toggle source
# File lib/openui/rails/actions/schema.rb, line 104
def type_matches?(value, expected)
  case expected
  when Array then expected.any? { |candidate| type_matches?(value, candidate) }
  when Regexp then value.is_a?(String) && expected.match?(value)
  when Module then value.is_a?(expected)
  else expected.respond_to?(:call) ? expected.call(value) == true : false
  end
end
validate!(args) click to toggle source
# File lib/openui/rails/actions/schema.rb, line 60
def validate!(args)
  raise InvalidActionError, "action arguments must be an object" unless args.is_a?(Hash)

  values = normalize(args)
  validate_required(values)
  validate_optional(values)
  validate_extras(values.keys) unless allow_extra?
  args
end
validate_extras(keys) click to toggle source
# File lib/openui/rails/actions/schema.rb, line 96
def validate_extras(keys)
  allowed = (required.keys + optional.keys).map(&:to_s)
  extra = keys - allowed
  return if extra.empty?

  raise InvalidActionError, "unexpected arguments: #{extra.join(", ")}"
end
validate_optional(values) click to toggle source
# File lib/openui/rails/actions/schema.rb, line 86
def validate_optional(values)
  optional.each do |key, expected|
    name = key.to_s
    next unless values.key?(name)
    next if type_matches?(values[name], expected)

    raise InvalidActionError, "argument #{name} has an invalid value"
  end
end
validate_required(values) click to toggle source
# File lib/openui/rails/actions/schema.rb, line 76
def validate_required(values)
  required.each do |key, expected|
    name = key.to_s
    raise InvalidActionError, "missing required argument #{name}" unless values.key?(name)
    next if type_matches?(values[name], expected)

    raise InvalidActionError, "argument #{name} has an invalid value"
  end
end