Skip to content

Commit

Permalink
Introduce AST error compiler
Browse files Browse the repository at this point in the history
It makes message compiler a specific case of a generic error compiler,
just like new ast error compiler.

References dry-rb/dry-validation#604
  • Loading branch information
waiting-for-dev committed Dec 23, 2019
1 parent c3fa27d commit e2a58f8
Show file tree
Hide file tree
Showing 13 changed files with 265 additions and 109 deletions.
18 changes: 18 additions & 0 deletions lib/dry/schema/ast_error_compiler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

require 'dry/schema/error_compiler'
require 'dry/schema/ast_error_set'

module Dry
module Schema
# Compiles rule results AST into machine-readable format
#
# @api private
class AstErrorCompiler < ErrorCompiler
# @api private
def call(ast)
AstErrorSet[ast]
end
end
end
end
21 changes: 21 additions & 0 deletions lib/dry/schema/ast_error_set.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

require 'dry/schema/error_set'

module Dry
module Schema
# A set of AST errors used to generate machine-readable errors
#
# @see Result#message_set
#
# @api public
class AstErrorSet < ErrorSet
private

# @api private
def errors_map(errors = self.errors)
errors
end
end
end
end
10 changes: 10 additions & 0 deletions lib/dry/schema/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ class Config
# @api public
setting(:types, Dry::Types)

# @!method error_compiler
#
# Return configured error_compiler
#
# @return [Dry::Schema::ErrorCompiler]
#
# @api public
setting(:error_compiler, :message)


# @!method messages
#
# Return configuration for message backend
Expand Down
20 changes: 20 additions & 0 deletions lib/dry/schema/error_compiler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module Dry
module Schema
# Compiles rule results AST into some type of errors
#
# @api private
class ErrorCompiler
# @api private
def call(_ast)
raise NotImplementedError
end

# @api private
def with(*args)
self
end
end
end
end
110 changes: 110 additions & 0 deletions lib/dry/schema/error_set.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# frozen_string_literal: true

require 'dry/equalizer'

module Dry
module Schema
# A set of generic errors
#
# @see Result#message_set
#
# @api public
class ErrorSet
include Enumerable
include Dry::Equalizer(:errors, :options)

# A list of compiled errors
#
# @return [Array<Any>]
attr_reader :errors

# Options hash
#
# @return [Hash]
attr_reader :options

# @api private
def self.[](errors, options = EMPTY_HASH)
new(errors, options)
end

# @api private
def initialize(errors, options = EMPTY_HASH)
@errors = errors
@options = options
end

# Iterate over errors
#
# @example
# result.errors.each do |message|
# puts message.text
# end
#
# @return [Array]
#
# @api public
def each(&block)
return self if empty?
return to_enum unless block

errors.each(&block)
end

# Dump message set to a hash
#
# @return [Hash<Symbol=>Array<String>>]
#
# @api public
def to_h
@to_h ||= errors_map
end
alias_method :to_hash, :to_h

# Get a list of errors for the given key
#
# @param [Symbol] key
#
# @return [Array<String>]
#
# @api public
def [](key)
to_h[key]
end

# Get a list of errors for the given key
#
# @param [Symbol] key
#
# @return [Array<String>]
#
# @raise KeyError
#
# @api public
def fetch(key)
self[key] || raise(KeyError, "+#{key}+ error was not found")
end

# Check if an error set is empty
#
# @return [Boolean]
#
# @api public
def empty?
@empty ||= errors.empty?
end

# @api private
def freeze
to_h
empty?
super
end

# @api private
def errors_map(_errors)
raise NotImplementedError
end
end
end
end
15 changes: 8 additions & 7 deletions lib/dry/schema/extensions/hints/message_set_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ module MessageSetMethods
attr_reader :failures

# @api private
def initialize(messages, options = EMPTY_HASH)
def initialize(errors, options = EMPTY_HASH)
super
@hints = messages.select(&:hint?)
@hints = errors.select(&:hint?)
@failures = options.fetch(:failures, true)
end

Expand All @@ -34,22 +34,23 @@ def initialize(messages, options = EMPTY_HASH)
#
# @api public
def to_h
@to_h ||= failures ? messages_map : messages_map(hints)
@to_h ||= failures ? errors_map : errors_map(hints)
end
alias_method :to_hash, :to_h

private

# @api private
def unique_paths
messages.uniq(&:path).map(&:path)
errors.uniq(&:path).map(&:path)
end

# @api private
def messages_map(messages = self.messages)
def errors_map(errors = self.errors)
return EMPTY_HASH if empty?

messages.reduce(placeholders) { |hash, msg|
initialize_placeholders!
errors.reduce(placeholders) { |hash, msg|
node = msg.path.reduce(hash) { |a, e| a.is_a?(Hash) ? a[e] : a.last[e] }
(node[0].is_a?(::Array) ? node[0] : node) << msg.dump
hash
Expand All @@ -61,7 +62,7 @@ def messages_map(messages = self.messages)
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/PerceivedComplexity
def initialize_placeholders!
@placeholders = unique_paths.each_with_object(EMPTY_HASH.dup) { |path, hash|
@placeholders ||= unique_paths.each_with_object(EMPTY_HASH.dup) { |path, hash|
curr_idx = 0
last_idx = path.size - 1
node = hash
Expand Down
5 changes: 3 additions & 2 deletions lib/dry/schema/message_compiler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require 'dry/initializer'

require 'dry/schema/error_compiler'
require 'dry/schema/constants'
require 'dry/schema/message'
require 'dry/schema/message_set'
Expand All @@ -12,7 +13,7 @@ module Schema
# Compiles rule results AST into human-readable format
#
# @api private
class MessageCompiler
class MessageCompiler < ErrorCompiler
extend Dry::Initializer

resolve_key_predicate = proc { |node, opts|
Expand Down Expand Up @@ -65,7 +66,7 @@ def call(ast)
current_messages = EMPTY_ARRAY.dup
compiled_messages = ast.map { |node| visit(node, EMPTY_OPTS.dup(current_messages)) }

MessageSet[compiled_messages, failures: options.fetch(:failures, true)]
MessageSet[compiled_messages.flatten, failures: options.fetch(:failures, true)]
end

# @api private
Expand Down
3 changes: 2 additions & 1 deletion lib/dry/schema/message_compiler/visitor_opts.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# frozen_string_literal: true

require 'dry/schema/error_compiler'
require 'dry/schema/constants'
require 'dry/schema/message'

module Dry
module Schema
# @api private
class MessageCompiler
class MessageCompiler < ErrorCompiler
# Optimized option hash used by visitor methods in message compiler
#
# @api private
Expand Down
Loading

0 comments on commit e2a58f8

Please sign in to comment.