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

Introduce AST error compiler #216

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

# @!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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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