Dear Russian friends, please watch President Zelenskyy's speech addressed to you. πŸ‡ΊπŸ‡¦Help our brave mates in Ukraine with a donation.

Rule AST

The DSL in dry-schema is used to create rule objects that are provided by dry-logic. These rules are built using an AST, which uses simple data structures to represent predicates and how they are composed into complex rules and operations.

The AST can be used to convert it into another representation - for example meta-data that can be used to produce documentation.

Accessing the AST

To access schema's rule AST use Schema#to_ast method:

schema = Dry::Schema.Params do
  optional(:age).filled(:integer, gt?: 18)

# => [:set,
#  [[:and,
#    [[:predicate, [:key?, [[:name, :email], [:input, Undefined]]]],
#     [:key, [:email, [:and, [[:predicate, [:str?, [[:input, Undefined]]]], [:predicate, [:filled?, [[:input, Undefined]]]]]]]]]],
#   [:implication,
#    [[:predicate, [:key?, [[:name, :age], [:input, Undefined]]]],
#     [:key,
#      [:age,
#       [:and,
#        [[:and, [[:predicate, [:int?, [[:input, Undefined]]]], [:predicate, [:filled?, [[:input, Undefined]]]]]],
#         [:predicate, [:gt?, [[:num, 18], [:input, Undefined]]]]]]]]]]]]

Writing an AST compiler

Even though such a data structure may look scary, it's actually very easy to write a compiler that will turn it into something useful. Let's say you want to generate meta-data about the schema and use it for documentation purposes. To do this, you can write an AST compiler.

Here's a simple example to give you the idea:

require 'dry/schema'

class DocCompiler
  def visit(node)
    meth, rest = node
    public_send(:"visit_#{meth}", rest)

  def visit_set(nodes) { |node| visit(node) }.flatten(1)

  def visit_and(node)
    left, right = node
    [visit(left), visit(right)].compact

  def visit_key(node)
    name, rest = node

    predicates = visit(rest).flatten(1).reduce(:merge)
    validations = { |name, args| predicate_description(name, args) }.compact

    { key: name, validations: validations }

  def visit_implication(node)
    _, right =
    right.merge(optional: true)

  def visit_predicate(node)
    name, args = node

    return if name.equal?(:key?)

    { name => { |v| v.equal?(Dry::Schema::Undefined) } }

  def predicate_description(name, args)
    case name
    when :str? then "must be a string"
    when :filled? then "must be filled"
    when :int? then "must be an integer"
    when :gt? then "must be greater than #{args[0]}"
      raise NotImplementedError, "#{name} not supported yet"

With such a compiler we can now turn schema's rule AST into a list of hashes that describe keys and their validations:

compiler =

# [
#   {:key=>:email, :validations=>["must be filled", "must be a string"]},
#   {:key=>:age, :validations=>["must be filled", "must be an integer", "must be greater than 18"], :optional=>true}
# ]

octocatEdit on GitHub