Extension info, not work with macros `maybe`
Describe the bug
UserSchema.info raised an error if schema has .maybe
To Reproduce
UserSchema = Dry::Schema.JSON do
optional(:age).maybe(:integer)
end
UserSchema.info
undefined method `visit_not' for #<Dry::Schema::Info::SchemaCompiler:0x00005625dd027ef8>
Did you mean? visit_and
visit_set
visit_key
lib/dry/schema/extensions/info/schema_compiler.rb:46:in `public_send'
lib/dry/schema/extensions/info/schema_compiler.rb:46:in `visit'
lib/dry/schema/extensions/info/schema_compiler.rb:74:in `block in visit_implication'
Expected behavior
A clear and concise description of what you expected to happen.
My environment
- Affects my production application: YES
- Ruby version: 2.7
- OS: linux
@ermolaev As a quick fix, I think you can copy the following class: https://github.com/dry-rb/dry-schema/blob/d0f601c370f6e399e83067cbf321947904d735fd/lib/dry/schema/extensions/info/schema_compiler.rb
and add the following method:
def visit_not(_node, opts = {})
key = opts[:key]
keys[key][:nullable] = true
end
And instead of calling .info, use
> UserSchema = Dry::Schema.JSON do
> optional(:age).maybe(:integer)
> end
> compiler = Dry::SchemaCompiler.new
> compiler.call(UserSchema.to_ast)
> compiler.keys
=> {:age=>{:required=>false, :nullable=>true, :type=>"integer"}}
I've also noticed an issue with the following definition:
required(:field).array(:int?)
This returns the field with type integer, and there is no indication that the definition is an array.
You could also do a workaround by overriding the visit_predicate method and adding an array flag.
def visit_predicate(node, opts = {})
name, rest = node
key = opts[:key]
if name.equal?(:key?)
keys[rest[0][1]] = { required: opts.fetch(:required, true) }
elsif name.equal?(:array?)
keys[key][:array] = true
else
type = PREDICATE_TO_TYPE[name]
keys[key][:type] = type if type
end
end
Also, keep in mind that EMPTY_HASH will not be available in your custom class namespace, you can replace it with {}.
The full file should look something like this:
module Dry
class SchemaCompiler
PREDICATE_TO_TYPE = {
array?: 'array',
bool?: 'boolean',
date?: 'date',
date_time?: 'datetime',
decimal?: 'float',
float?: 'float',
hash?: 'hash',
int?: 'integer',
nil?: 'nil',
str?: 'string',
time?: 'time'
}.freeze
# @api private
attr_reader :keys
# @api private
def initialize
@keys = {}
end
# @api private
def to_h
{ keys: keys }
end
# @api private
def call(ast)
visit(ast)
end
# @api private
def visit(node, opts = {})
meth, rest = node
public_send(:"visit_#{meth}", rest, opts)
end
# @api private
def visit_set(node, opts = {})
target = (key = opts[:key]) ? self.class.new : self
node.map { |child| target.visit(child, opts) }
return unless key
target_info = opts[:member] ? { member: target.to_h } : target.to_h
type = opts[:member] ? 'array' : 'hash'
keys.update(key => { **keys[key], type: type, **target_info })
end
# @api private
def visit_and(node, opts = {})
left, right = node
visit(left, opts)
visit(right, opts)
end
def visit_not(_node, opts = {})
key = opts[:key]
keys[key][:nullable] = true
end
# @api private
def visit_implication(node, opts = {})
node.each do |el|
visit(el, opts.merge(required: false))
end
end
# @api private
def visit_each(node, opts = {})
visit(node, opts.merge(member: true))
end
# @api private
def visit_key(node, opts = {})
name, rest = node
visit(rest, opts.merge(key: name, required: true))
end
# @api private
def visit_predicate(node, opts = {})
name, rest = node
key = opts[:key]
if name.equal?(:key?)
keys[rest[0][1]] = { required: opts.fetch(:required, true) }
elsif name.equal?(:array?)
keys[key][:array] = true
else
type = PREDICATE_TO_TYPE[name]
keys[key][:type] = type if type
end
end
end
end
@Jane-Terziev looks good, and it works, you can create PR? I think @solnic will give more detailed feedback, on PR review
I opened #471, looking for feedback