Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extension info, not work with macros maybe #367

Open
ermolaev opened this issue Jun 30, 2021 · 3 comments
Open

Extension info, not work with macros maybe #367

ermolaev opened this issue Jun 30, 2021 · 3 comments

Comments

@ermolaev
Copy link

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 ermolaev changed the title Extension info, not work with macros maybe attribute Extension info, not work with macros maybe Jun 30, 2021
@Jane-Terziev
Copy link

@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

@ermolaev
Copy link
Author

@Jane-Terziev looks good, and it works, you can create PR?
I think @solnic will give more detailed feedback, on PR review

@santiagodoldan
Copy link

I opened #471, looking for feedback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants