Skip to content

Instantly share code, notes, and snippets.

@felixyz
Last active May 23, 2020 18:40
Show Gist options
  • Save felixyz/62130f9f6b9aa309c3ddfb9fd534a0fc to your computer and use it in GitHub Desktop.
Save felixyz/62130f9f6b9aa309c3ddfb9fd534a0fc to your computer and use it in GitHub Desktop.
dry-validation form => json schema
module DryJsonSchema
# Transforms a dry-validation form to json-schema-compatible objects (hash or array)
# Usage:
# DryJsonSchema::Converter.(my_form)
# => { :type => :object,
# :properties => { :abc => { :type => :integer },
# :xyz => { :type => :integer }
# },
# :required => [:abc]}
class Converter
class << self
def call(dry_schema, params = nil, &block)
properties, required_keys = convert_schema dry_schema, params
yield_parameters properties, required_keys, block if block_given?
{
type: :object,
properties: properties,
required: required_keys,
additionalProperties: false
}
end
private
def yield_parameters(parameters, required_keys, block)
parameters.each do |name, properties|
required = required_keys.include? name
block.(name, properties.merge(required: required))
end
end
def convert_schema(dry_schema, params)
properties = Hash.new { |h, k| h[k] = {} }
initial = { properties: properties, required_keys: [] }
extracted = dry_schema
.rules
.select { |rule_key, _| params.nil? || params.include?(rule_key) }
.each_with_object(initial) do |(rule_key, root), acc|
required, definition = required_and_definition(root)
acc[:properties][rule_key] = definition
acc[:required_keys] << rule_key if required
end
[extracted[:properties], extracted[:required_keys]]
end
def required_and_definition(root)
case root
when Dry::Logic::Operations::And
[true, property_definition(root.right)]
when Dry::Logic::Operations::Implication
[false, property_definition(root.right)]
end
end
# In the future, this could return *both* enum and type objects, along with others
def property_definition(rule)
return array_definition rule if array?(rule)
return enum_definition rule if enum?(rule)
return pattern_definition rule if pattern?(rule)
type_definition rule
end
def array_definition(rule)
type = type_definition rule.left
item_type = type_definition rule.right.rule
type.merge(items: item_type)
end
def enum_definition(rule)
{
enum: rule.right.rule.options[:args].flatten,
type: :string # Not good enough
}
end
def pattern_definition(rule)
{ type: :string, pattern: rule.right.rule.options[:args] }
end
def type_definition(rule)
case rule
when Dry::Logic::Operations::Implication
type = [:null, type_from_predicate(rule.right.rule)]
when Dry::Logic::Operations::And
type = type_from_predicate(rule.right.rule)
end
{ type: type }
end
TYPES = {
array?: :array,
bool?: :boolean,
date_time?: :string,
decimal?: :number,
float?: :number,
int?: :integer,
str?: :string
}.freeze
def type_from_predicate(predicate)
name = predicate.name
raise ArgumentError, "Unknown type: #{name}" unless TYPES.keys.include? name
TYPES[name]
end
def array?(rule)
rule.left.is_a? Dry::Logic::Operations::And
end
def enum?(rule)
rule.right.rule.name == :included_in?
end
def pattern?(rule)
rule.right.rule.name == :format?
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment