Skip to content

Commit

Permalink
Introduce configurable input processors (closes #66)
Browse files Browse the repository at this point in the history
  • Loading branch information
solnic committed Mar 11, 2016
1 parent 7cf8784 commit d65082d
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,17 @@

module Dry
module Validation
class InputTypeCompiler
class InputProcessorCompiler
attr_reader :type_compiler

FORM_TYPES = {
default: 'string',
none?: 'form.nil',
bool?: 'form.bool',
str?: 'string',
int?: 'form.int',
float?: 'form.float',
decimal?: 'form.decimal',
date?: 'form.date',
date_time?: 'form.date_time',
time?: 'form.time'
}.freeze

CONST_TYPES = {
NilClass => 'form.nil',
String => 'string',
Fixnum => 'form.int',
Integer => 'form.int',
Float => 'form.float',
BigDecimal => 'form.decimal',
Array => 'form.array',
Hash => 'form.hash',
Date => 'form.date',
DateTime => 'form.date_time',
Time => 'form.time',
TrueClass => 'form.true',
FalseClass => 'form.false'
}.freeze

DEFAULT_TYPE_NODE = [[:type, 'string']].freeze

def initialize
@type_compiler = Dry::Types::Compiler.new(Dry::Types)
end

def call(ast)
type_compiler.([:type, ['hash', [:symbolized, schema_ast(ast)]]])
type_compiler.(hash_node(schema_ast(ast)))
end

def schema_ast(ast)
Expand All @@ -54,7 +25,7 @@ def visit(node, *args)
end

def visit_schema(node, *args)
[:type, ['hash', [:symbolized, node.input_type_ast]]]
hash_node(node.input_processor_ast(:form))
end

def visit_or(node, *args)
Expand All @@ -72,7 +43,7 @@ def visit_and(node, first = true)
if result.size == 1
result.first
else
(result - DEFAULT_TYPE_NODE).first
(result - self.class::DEFAULT_TYPE_NODE).first
end
end
end
Expand All @@ -92,26 +63,36 @@ def visit_val(node, *args)
end

def visit_set(node, *)
[:type, ['form.hash', [:symbolized, node.map { |n| visit(n) }]]]
hash_node(node.map { |n| visit(n) })
end

def visit_each(node, *args)
[:type, ['form.array', visit(node, *args)]]
array_node(visit(node, *args))
end

def visit_predicate(node, *args)
id, args = node
default = FORM_TYPES[:default]

if id == :key?
args[0]
elsif id == :type?
else
type(id, args)
end
end

def type(predicate, args)
default = self.class::PREDICATE_MAP[:default]

if predicate == :type?
const = args[0]
[:type, CONST_TYPES[const] || Types.identifier(const)]
[:type, self.class::CONST_MAP[const] || Types.identifier(const)]
else
[:type, FORM_TYPES[id] || default]
[:type, self.class::PREDICATE_MAP[predicate] || default]
end
end
end
end
end

require 'dry/validation/input_processor_compiler/sanitizer'
require 'dry/validation/input_processor_compiler/form'
42 changes: 42 additions & 0 deletions lib/dry/validation/input_processor_compiler/form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
module Dry
module Validation
class InputProcessorCompiler::Form < InputProcessorCompiler
PREDICATE_MAP = {
default: 'string',
none?: 'form.nil',
bool?: 'form.bool',
str?: 'string',
int?: 'form.int',
float?: 'form.float',
decimal?: 'form.decimal',
date?: 'form.date',
date_time?: 'form.date_time',
time?: 'form.time'
}.freeze

CONST_MAP = {
NilClass => 'form.nil',
String => 'string',
Fixnum => 'form.int',
Integer => 'form.int',
Float => 'form.float',
BigDecimal => 'form.decimal',
Array => 'form.array',
Hash => 'form.hash',
Date => 'form.date',
DateTime => 'form.date_time',
Time => 'form.time',
TrueClass => 'form.true',
FalseClass => 'form.false'
}.freeze

def hash_node(schema)
[:type, ['form.hash', [:symbolized, schema]]]
end

def array_node(members)
[:type, ['form.array', members]]
end
end
end
end
42 changes: 42 additions & 0 deletions lib/dry/validation/input_processor_compiler/sanitizer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
module Dry
module Validation
class InputProcessorCompiler::Sanitizer < InputProcessorCompiler
PREDICATE_MAP = {
default: 'string',
none?: 'nil',
bool?: 'bool',
str?: 'string',
int?: 'int',
float?: 'float',
decimal?: 'decimal',
date?: 'date',
date_time?: 'date_time',
time?: 'time'
}.freeze

CONST_MAP = {
NilClass => 'nil',
String => 'string',
Fixnum => 'int',
Integer => 'int',
Float => 'float',
BigDecimal => 'decimal',
Array => 'array',
Hash => 'hash',
Date => 'date',
DateTime => 'date_time',
Time => 'time',
TrueClass => 'true',
FalseClass => 'false'
}.freeze
end

def hash_node(schema)
[:type, ['hash', [:schema, schema]]]
end

def array_node(members)
[:type, ['array', members]]
end
end
end
39 changes: 31 additions & 8 deletions lib/dry/validation/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@
require 'dry/validation/error_compiler'
require 'dry/validation/hint_compiler'

require 'dry/validation/input_processor_compiler'

module Dry
module Validation
class Schema
extend Dry::Configurable

NOOP_INPUT_PROCESSOR = -> input { input }

setting :path
setting :predicates, Types::Predicates
setting :messages, :yaml
Expand All @@ -26,6 +30,13 @@ class Schema
setting :checks, []
setting :option_names, []

setting :input_processor, :noop

setting :input_processor_map, {
sanitizer: InputProcessorCompiler::Sanitizer.new,
form: InputProcessorCompiler::Form.new
}

def self.new(rules = config.rules, **options)
super(rules, default_options.merge(options))
end
Expand Down Expand Up @@ -77,16 +88,23 @@ def self.hint_compiler
@hint_compiler ||= HintCompiler.new(messages, rules: rule_ast)
end

def self.input_type_compiler
@input_type_compiler = InputTypeCompiler.new
def self.input_processor
@input_processor ||=
begin
if input_processor_compiler
input_processor_compiler.(rule_ast)
else
NOOP_INPUT_PROCESSOR
end
end
end

def self.input_type_ast
input_type_compiler.schema_ast(rule_ast)
def self.input_processor_ast(type)
config.input_processor_map.fetch(type).schema_ast(rule_ast)
end

def self.input_type
@input_type ||= input_type_compiler.(rule_ast)
def self.input_processor_compiler
@input_processor_comp ||= config.input_processor_map[config.input_processor]
end

def self.rule_ast
Expand All @@ -97,6 +115,7 @@ def self.default_options
{ predicates: predicates,
error_compiler: error_compiler,
hint_compiler: hint_compiler,
input_processor: input_processor,
checks: config.checks }
end

Expand All @@ -106,6 +125,8 @@ def self.default_options

attr_reader :predicates

attr_reader :input_processor

attr_reader :rule_compiler

attr_reader :error_compiler
Expand All @@ -124,6 +145,7 @@ def initialize(rules, options)
@error_compiler = options.fetch(:error_compiler)
@hint_compiler = options.fetch(:hint_compiler)
@predicates = options.fetch(:predicates)
@input_processor = options.fetch(:input_processor, NOOP_INPUT_PROCESSOR)

initialize_options(options)
initialize_rules(rules)
Expand All @@ -137,7 +159,8 @@ def with(new_options)
end

def call(input)
Result.new(input, errors(input), error_compiler, hint_compiler)
processed_input = input_processor[input]
Result.new(processed_input, apply(processed_input), error_compiler, hint_compiler)
end

def [](name)
Expand All @@ -152,7 +175,7 @@ def [](name)

private

def errors(input)
def apply(input)
results = rule_results(input)

results.merge!(check_results(input, results)) unless checks.empty?
Expand Down
11 changes: 2 additions & 9 deletions lib/dry/validation/schema/form.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
require 'dry/validation/schema'
require 'dry/validation/input_type_compiler'

module Dry
module Validation
class Schema::Form < Schema
option :input_type

def self.default_options
super.merge(input_type: input_type)
end

def call(input)
super(input_type[input])
configure do |config|
config.input_processor = :form
end
end
end
Expand Down
4 changes: 3 additions & 1 deletion spec/integration/schema/form_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ def email?(value)
end

it 'validates presence of an email and min age value' do
expect(schema.('email' => '', 'age' => '18').messages).to eql(
result = schema.('email' => '', 'age' => '18')

expect(result.messages).to eql(
address: ['is missing'],
age: ['must be greater than 18'],
email: ['must be filled']
Expand Down
19 changes: 19 additions & 0 deletions spec/integration/schema/input_processor_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
RSpec.describe Dry::Validation::Schema, 'setting input processor in schema' do
subject(:schema) do
Dry::Validation.Schema do
configure do
config.input_processor = :sanitizer
end

key(:email).required

key(:age).maybe(:int?, gt?: 18)
end
end

it 'rejects unspecified keys' do
expect(schema.(email: 'jane@doe', age: 19, such: 'key').output).to eql(
email: 'jane@doe', age: 19
)
end
end
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
require 'dry/validation/input_type_compiler'

RSpec.describe Dry::Validation::InputTypeCompiler, '#call' do
subject(:compiler) { Dry::Validation::InputTypeCompiler.new }
RSpec.describe Dry::Validation::InputProcessorCompiler::Form, '#call' do
subject(:compiler) { Dry::Validation::InputProcessorCompiler::Form.new }

let(:rule_ast) do
[
Expand Down

0 comments on commit d65082d

Please sign in to comment.