diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index bde7680..9fc061c 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - ruby-version: ["2.7", "3.0", "3.1", "3.2", "jruby-9.3", "jruby-9.4"] + ruby-version: ["2.7", "3.0", "3.1", "3.2", "3.3", "jruby-9.3", "jruby-9.4"] steps: - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..0fa4ae4 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.3.0 \ No newline at end of file diff --git a/Changelog.md b/Changelog.md index d963af5..8594a7c 100644 --- a/Changelog.md +++ b/Changelog.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## [1.1.0] - 2023-11-22 +### Changes +- Improved performance + ## [1.0.0] - 2023-02-14 ### Removed - Removed deprecated version of `GreaterThanEqual` definition that had a typo in it (GreaterThenEqual) diff --git a/Gemfile.lock b/Gemfile.lock index 6f16e3c..1acb9b9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,36 +1,44 @@ PATH remote: . specs: - definition (1.0.0) + definition (1.1.0) activesupport i18n GEM remote: https://rubygems.org/ specs: - activesupport (6.1.7.3) + activesupport (7.1.3) + base64 + bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) minitest (>= 5.1) + mutex_m tzinfo (~> 2.0) - zeitwerk (~> 2.3) approvals (0.0.26) json (~> 2.0) nokogiri (~> 1.8) thor (~> 1.0) ast (2.4.2) awesome_print (1.9.2) - benchmark-ips (2.10.0) + base64 (0.2.0) + benchmark-ips (2.13.0) + bigdecimal (3.1.6) coderay (1.1.3) - concurrent-ruby (1.2.2) + concurrent-ruby (1.2.3) + connection_pool (2.4.1) diff-lcs (1.5.0) - ffi (1.15.5) - ffi (1.15.5-java) + drb (2.2.0) + ruby2_keywords + ffi (1.16.3) formatador (1.1.0) fuubar (2.5.1) rspec-core (~> 3.0) ruby-progressbar (~> 1.4) - guard (2.18.0) + guard (2.18.1) formatador (>= 0.2.4) listen (>= 2.7, < 4.0) lumberjack (>= 1.0.12, < 2.0) @@ -44,47 +52,37 @@ GEM guard (~> 2.1) guard-compat (~> 1.1) rspec (>= 2.99.0, < 4.0) - i18n (1.12.0) + i18n (1.14.1) concurrent-ruby (~> 1.0) - jar-dependencies (0.4.1) - jaro_winkler (1.5.4) - jaro_winkler (1.5.4-java) - json (2.6.3) - json (2.6.3-java) + jaro_winkler (1.5.6) + json (2.7.1) listen (3.8.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - lumberjack (1.2.8) + lumberjack (1.2.10) method_source (1.0.0) - mini_portile2 (2.8.1) - minitest (5.18.0) + minitest (5.21.2) + mutex_m (0.2.0) nenv (0.3.0) - nokogiri (1.13.10) - mini_portile2 (~> 2.8.0) + nokogiri (1.16.0-arm64-darwin) racc (~> 1.4) - nokogiri (1.13.10-java) + nokogiri (1.16.0-x86_64-linux) racc (~> 1.4) notiffany (0.1.3) nenv (~> 0.1) shellany (~> 0.0) - parallel (1.22.1) - parser (3.2.1.0) + parallel (1.24.0) + parser (3.3.0.5) ast (~> 2.4.1) + racc pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) - pry (0.14.2-java) - coderay (~> 1.1) - method_source (~> 1.0) - spoon (~> 0.0) - psych (4.0.6) + psych (5.1.2) stringio - psych (4.0.6-java) - jar-dependencies (>= 0.1.7) - racc (1.6.2) - racc (1.6.2-java) + racc (1.7.3) rainbow (3.1.1) - rake (13.0.6) + rake (13.1.0) rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) @@ -92,18 +90,18 @@ GEM rspec-core (~> 3.12.0) rspec-expectations (~> 3.12.0) rspec-mocks (~> 3.12.0) - rspec-core (3.12.1) + rspec-core (3.12.2) rspec-support (~> 3.12.0) - rspec-expectations (3.12.2) + rspec-expectations (3.12.3) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) rspec-its (1.3.0) rspec-core (>= 3.0.0) rspec-expectations (>= 3.0.0) - rspec-mocks (3.12.3) + rspec-mocks (3.12.6) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-support (3.12.0) + rspec-support (3.12.1) rspec_junit_formatter (0.6.0) rspec-core (>= 2, < 4, != 2.12.0) rubocop (0.66.0) @@ -117,21 +115,19 @@ GEM rubocop-rspec (1.32.0) rubocop (>= 0.60.0) rubocop_runner (2.2.1) - ruby-progressbar (1.11.0) + ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) shellany (0.0.1) - spoon (0.0.6) - ffi - stringio (3.0.5) - thor (1.2.1) - timecop (0.9.6) + stringio (3.1.0) + thor (1.3.0) + timecop (0.9.8) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (1.5.0) - zeitwerk (2.6.7) PLATFORMS - ruby - universal-java-11 + arm64-darwin-23 + x86_64-linux DEPENDENCIES approvals (~> 0.0) @@ -152,4 +148,4 @@ DEPENDENCIES timecop BUNDLED WITH - 2.4.8 + 2.4.21 diff --git a/benchmark/model.rb b/benchmark/model.rb index ebcd03c..542a9dc 100644 --- a/benchmark/model.rb +++ b/benchmark/model.rb @@ -4,7 +4,7 @@ gemfile do source "https://rubygems.org" - gem "dry-struct", "~> 1.4" + gem "dry-struct", "~> 1.6" gem "awesome_print" gem "benchmark-ips" gem "pry" @@ -21,6 +21,16 @@ class DryStructModel < Dry::Struct attribute :app_name, Dry::Types["strict.string"].optional.default(nil) attribute :app_branch, Dry::Types["strict.string"].optional.default(nil) attribute :platform, Dry::Types["strict.string"].optional.default(nil) + attribute :user, Dry::Types["strict.hash"].schema( + name: Dry::Types["strict.string"], + age: Dry::Types["coercible.integer"] + ) + attribute :array, Dry::Types["strict.array"].of( + Dry::Types["strict.string"].enum("a", "b", "c", "d") + ).optional.default(nil) + attribute :and_attribute, Dry::Types["strict.integer"].constrained(gt: 1) + attribute :or_attribute, Dry::Types["strict.integer"] | Dry::Types["strict.float"] + attribute :bool_attribute, Dry::Types["strict.bool"] end class DefinitionModel < Definition::Model @@ -30,13 +40,40 @@ class DefinitionModel < Definition::Model optional :app_name, Definition.Type(String) optional :app_branch, Definition.Type(String) optional :platform, Definition.Type(String) + required(:user, Definition.Keys do + required :name, Definition.Type(String) + required :age, Definition.CoercibleType(Integer) + end) + optional :array, Definition.Nilable(Definition.Each(Definition.Enum("a", "b", "c", "d"))) + optional :and_attribute, Definition.Nilable(Definition.And( + Definition.Type(Integer), + Definition.GreaterThan(1) + )) + required :or_attribute, Definition.Nilable(Definition.Or( + Definition.Type(Integer), + Definition.Type(Float) + )) + required :bool_attribute, Definition.Boolean end puts "Benchmark with valid input data:" -valid_data = { id: 1, app_key: "com.test", app_version: "1.0.0", app_name: "testapp" } +valid_data = { + id: 1, + app_key: "com.test", + app_version: "1.0.0", + app_name: "testapp", + user: { + name: "John Doe", + age: "65" + }, + array: %w[a b c d a], + and_attribute: 2, + or_attribute: 3.4, + bool_attribute: true +} Benchmark.ips do |x| - x.config(time: 5, warmup: 2) + x.config(time: 20, warmup: 5) x.report("definition") do DefinitionModel.new(**valid_data) @@ -52,7 +89,7 @@ class DefinitionModel < Definition::Model puts "Benchmark with invalid input data:" invalid_data = { id: "abc", app_key: "com.test", app_name: "testapp" } Benchmark.ips do |x| - x.config(time: 5, warmup: 2) + x.config(time: 20, warmup: 5) x.report("definition") do DefinitionModel.new(**invalid_data) diff --git a/lib/definition/conform_result.rb b/lib/definition/conform_result.rb index 05497fd..4b73984 100644 --- a/lib/definition/conform_result.rb +++ b/lib/definition/conform_result.rb @@ -5,14 +5,14 @@ module Definition class ConformResult def initialize(value, errors: []) - self.value = value - self.conform_errors = errors + @value = value + @conform_errors = errors end - attr_accessor :value + attr_reader :value def passed? - conform_errors.empty? + @conform_errors.empty? end alias conformed? passed? @@ -21,7 +21,7 @@ def error_message end def leaf_errors - conform_errors.map(&:leaf_errors).flatten + @conform_errors.map(&:leaf_errors).flatten end def errors @@ -46,7 +46,7 @@ def error_hash end def error_tree - conform_errors + @conform_errors end private @@ -71,7 +71,5 @@ def find_next_parent_key_error(error) end nil end - - attr_accessor :conform_errors end end diff --git a/lib/definition/dsl/nil.rb b/lib/definition/dsl/nil.rb index 8432078..97477c1 100644 --- a/lib/definition/dsl/nil.rb +++ b/lib/definition/dsl/nil.rb @@ -6,9 +6,7 @@ module Nil # Example: # Nil def Nil # rubocop:disable Naming/MethodName - Types::Lambda.new(:nil) do |value| - conform_with(value) if value.nil? - end + Types::Nil.new end end end diff --git a/lib/definition/types.rb b/lib/definition/types.rb index c95ca38..f4461f2 100644 --- a/lib/definition/types.rb +++ b/lib/definition/types.rb @@ -7,3 +7,4 @@ require "definition/types/lambda" require "definition/types/type" require "definition/types/each" +require "definition/types/nil" diff --git a/lib/definition/types/and.rb b/lib/definition/types/and.rb index 30a102d..b7265f6 100644 --- a/lib/definition/types/and.rb +++ b/lib/definition/types/and.rb @@ -21,46 +21,23 @@ def initialize(name, *args) end def conform(value) - Conformer.new(self).conform(value) + last_result = nil + definitions.each do |definition| + last_result = definition.conform(last_result.nil? ? value : last_result.value) + next if last_result.passed? + + return ConformResult.new(last_result.value, errors: [ + ConformError.new(self, "Not all definitions are valid for '#{name}'", + sub_errors: last_result.error_tree) + ]) + end + + ConformResult.new(last_result.value) end def error_renderer ErrorRenderers::Leaf end - - class Conformer - def initialize(definition) - self.definition = definition - end - - def conform(value) - results = conform_all(value) - - if results.all?(&:conformed?) - ConformResult.new(results.last.value) - else - ConformResult.new(value, errors: [ - ConformError.new(definition, "Not all definitions are valid for '#{definition.name}'", - sub_errors: results.map(&:error_tree).flatten) - ]) - end - end - - private - - attr_accessor :definition - - def conform_all(value) - results = [] - definition.definitions.each do |definition| - result = definition.conform(value) - value = result.value - results << result - break unless result.passed? - end - results - end - end end end end diff --git a/lib/definition/types/each.rb b/lib/definition/types/each.rb index eaad8fb..3a0afd9 100644 --- a/lib/definition/types/each.rb +++ b/lib/definition/types/each.rb @@ -13,64 +13,50 @@ def initialize(name, definition:) super(name) end - def conform(value) - Conformer.new(self).conform(value) - end + def conform(values) + return non_array_error(values) unless values.is_a?(Array) - def error_renderer - ErrorRenderers::Leaf - end + errors = false - class Conformer - def initialize(definition) - self.definition = definition + results = values.map do |value| + result = item_definition.conform(value) + errors = true unless result.passed? + result end - def conform(value) - return non_array_error(value) unless value.is_a?(Array) - - results = conform_all(value) - - if results.all?(&:conformed?) - ConformResult.new(results.map(&:value)) - else - ConformResult.new(value, errors: [ConformError.new(definition, - "Not all items conform with '#{definition.name}'", - sub_errors: errors(results))]) - end - end + return ConformResult.new(results.map(&:value)) unless errors - private + ConformResult.new(values, errors: [ConformError.new(self, + "Not all items conform with '#{name}'", + sub_errors: convert_errors(results))]) + end - attr_accessor :definition + def error_renderer + ErrorRenderers::Leaf + end - def errors(results) - errors = [] - results.each_with_index do |result, index| - next if result.passed? + private - errors << KeyConformError.new( - definition, - "Item #{result.value.inspect} did not conform to #{definition.name}", - key: index, - sub_errors: result.error_tree - ) - end - errors - end + def convert_errors(results) + errors = [] + results.each_with_index do |result, index| + next if result.passed? - def conform_all(values) - values.map do |value| - definition.item_definition.conform(value) - end + errors << KeyConformError.new( + self, + "Item #{result.value.inspect} did not conform to #{name}", + key: index, + sub_errors: result.error_tree + ) end + errors + end - def non_array_error(value) - ConformResult.new(value, errors: [ - ConformError.new(definition, - "Non-Array value does not conform with #{definition.name}") - ]) - end + def non_array_error(value) + ConformResult.new(value, errors: [ + ConformError.new(self, + "Non-Array value does not conform with #{name}") + ]) end end end diff --git a/lib/definition/types/keys.rb b/lib/definition/types/keys.rb index 01939e0..dcd3c12 100644 --- a/lib/definition/types/keys.rb +++ b/lib/definition/types/keys.rb @@ -9,11 +9,11 @@ module Types class Keys < Base module Dsl def required(key, definition) - required_definitions << { key: key, definition: definition } + required_definitions[key] = definition end def optional(key, definition, **opts) - optional_definitions << { key: key, definition: definition } + optional_definitions[key] = definition default(key, opts[:default]) if opts.key?(:default) end @@ -31,8 +31,8 @@ def include(other) ensure_keys_do_not_interfere(other) - self.required_definitions += other.required_definitions - self.optional_definitions += other.optional_definitions + required_definitions.merge!(other.required_definitions) + optional_definitions.merge!(other.optional_definitions) defaults.merge!(other.defaults) end @@ -57,114 +57,93 @@ def ensure_keys_do_not_interfere(other) def initialize(name, req: {}, opt: {}, defaults: {}, options: {}) super(name) - self.required_definitions = req.map { |key, definition| { key: key, definition: definition } } - self.optional_definitions = opt.map { |key, definition| { key: key, definition: definition } } + self.required_definitions = req + self.optional_definitions = opt self.defaults = defaults self.ignore_extra_keys = options.fetch(:ignore_extra_keys, false) end - def conform(value) - Conformer.new(self, value).conform - end + def conform(input_value) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity + # input_value is duplicated because we don't want to modify the user object that is passed into this function. + # The following logic will iterate over each definition and delete the key associated with the definition from + # the input value. + # In the end if there are still keys on the object and 'ignore_extra_keys' is false, an error will be raised. + value = input_value.dup + result_value = {} + errors = [] - def keys - (required_definitions + optional_definitions).map { |hash| hash[:key] } - end + return wrong_type_result(value) unless value.is_a?(Hash) - class Conformer - def initialize(definition, value) - self.definition = definition - self.value = value - self.errors = [] - @conform_result_value = {} # This will be the output value after conforming - @not_conformed_value_keys = value.dup # Used to track which keys are left over in the end (unexpected keys) - end + required_definitions.each do |key, definition| + if value.key?(key) + result = definition.conform(value.delete(key)) + result_value[key] = result.value + next if result.passed? - def conform - return invalid_input_result unless valid_input_type? - - values = conform_all_keys - add_extra_key_errors unless definition.ignore_extra_keys - - ConformResult.new(values, errors: errors) + errors.push(key_error(definition, key, result)) + else + errors << missing_key_error(key) + end end - private - - attr_accessor :errors + optional_definitions.each do |key, definition| + if value.key?(key) + result = definition.conform(value.delete(key)) + result_value[key] = result.value + next if result.passed? - def invalid_input_result - errors = [ConformError.new(definition, - "#{definition.name} is not a Hash", - i18n_key: "keys.not_a_hash")] - ConformResult.new(value, errors: errors) - end - - def valid_input_type? - value.is_a?(Hash) + errors.push(key_error(definition, key, result)) + elsif defaults.key?(key) + result_value[key] = defaults.fetch(key) + end end - def add_extra_key_errors - extra_keys = @not_conformed_value_keys.keys - return if extra_keys.empty? - - extra_keys.each do |key| - errors.push(KeyConformError.new( - definition, - "#{definition.name} has extra key: #{key.inspect}", - key: key, - i18n_key: "keys.has_extra_key" - )) + if !ignore_extra_keys && !value.keys.empty? + value.keys.each do |key| + errors << extra_key_error(key) end end - def conform_all_keys - conform_definitions(definition.required_definitions, required: true) - conform_definitions(definition.optional_definitions, required: false) + ConformResult.new(result_value, errors: errors) + end - @conform_result_value - end + def keys + required_definitions.keys + optional_definitions.keys + end - def conform_definitions(keys, required:) - keys.each do |hash| - key = hash[:key] - key_definition = hash[:definition] - conform_definition(key, key_definition, required: required) - end - end + private - # Rubcop rules are disabled for performance optimization purposes - def conform_definition(key, key_definition, required:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength - @not_conformed_value_keys.delete(key) # Keys left over in that hash at the end are considered unexpected - - # If the input value is missing a key: - # a) add a missing key error if it is a required key - # b) otherwise initialize the missing key in the output value if a default value is configured - unless value.key?(key) - errors.push(missing_key_error(key)) if required - @conform_result_value[key] = definition.defaults[key] if definition.defaults.key?(key) - return - end - - # If the input value has a key then its value is conformed against the configured definition - result = key_definition.conform(value[key]) - @conform_result_value[key] = result.value - return if result.passed? + def wrong_type_result(value) + ConformResult.new(value, errors: [ + ConformError.new( + self, + "#{name} is not a Hash", + i18n_key: "keys.not_a_hash" + ) + ]) + end - errors.push(KeyConformError.new(key_definition, - "#{definition.name} fails validation for key #{key}", - key: key, - sub_errors: result.error_tree)) - end + def extra_key_error(key) + KeyConformError.new( + self, + "#{name} has extra key: #{key.inspect}", + key: key, + i18n_key: "keys.has_extra_key" + ) + end - def missing_key_error(key) - KeyConformError.new(definition, - "#{definition.name} is missing key #{key.inspect}", - key: key, - i18n_key: "keys.has_missing_key") - end + def missing_key_error(key) + KeyConformError.new(self, + "#{name} is missing key #{key.inspect}", + key: key, + i18n_key: "keys.has_missing_key") + end - attr_accessor :definition, :value + def key_error(definition, key, result) + KeyConformError.new(definition, + "#{name} fails validation for key #{key}", + key: key, + sub_errors: result.error_tree) end end end diff --git a/lib/definition/types/lambda.rb b/lib/definition/types/lambda.rb index adbef6d..2d409cd 100644 --- a/lib/definition/types/lambda.rb +++ b/lib/definition/types/lambda.rb @@ -28,41 +28,39 @@ def conform_with(value) end def fail_with(error_message) - self.error_message = error_message + @error_message = error_message end end include Dsl def initialize(definition) - self.definition = definition + @definition = definition end def conform(value) - lambda_result = instance_exec(value, &definition.conformity_test_lambda) + lambda_result = instance_exec(value, &@definition.conformity_test_lambda) return lambda_result if lambda_result.is_a?(ConformResult) - failure_result_with(value, error_message) + failure_result_with(value) end private - attr_accessor :definition, :error_message - def standard_error_message - "Did not pass test for #{definition.name}" + "Did not pass test for #{@definition.name}" end def contextual_error_message - return standard_error_message if definition.context.empty? + return standard_error_message if @definition.context.empty? - "#{standard_error_message} (#{definition.context.values.join(',')})" + "#{standard_error_message} (#{@definition.context.values.join(',')})" end - def failure_result_with(value, error_message) + def failure_result_with(value) ConformResult.new(value, errors: [ - ConformError.new(definition, + ConformError.new(@definition, contextual_error_message, - translated_message: error_message) + translated_message: @error_message) ]) end end diff --git a/lib/definition/types/nil.rb b/lib/definition/types/nil.rb new file mode 100644 index 0000000..e429a58 --- /dev/null +++ b/lib/definition/types/nil.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require "definition/types/base" + +module Definition + module Types + class Nil < Base + def initialize + super(:nil) + end + + def conform(value) + if value.nil? + ConformResult.new(value) + else + ConformResult.new(value, errors: [ + ConformError.new(self, "Did not pass test for nil") + ]) + end + end + end + end +end diff --git a/lib/definition/types/or.rb b/lib/definition/types/or.rb index 72a5f1a..55008b5 100644 --- a/lib/definition/types/or.rb +++ b/lib/definition/types/or.rb @@ -13,55 +13,31 @@ def validate(definition) end include Dsl - attr_accessor :definitions + attr_reader :definitions def initialize(name, *args) - self.definitions = *args + @definitions = *args super(name) end def conform(value) - Conformer.new(self).conform(value) + last_result = nil + @definitions.each do |definition| + last_result = definition.conform(value) + return last_result if last_result.passed? + end + + ConformResult.new(value, errors: [ + ConformError.new(self, + "None of the definitions are valid for '#{name}'."\ + " Errors for last tested definition:", + sub_errors: last_result.error_tree) + ]) end def error_renderer ErrorRenderers::Leaf end - - class Conformer - def initialize(definition) - self.definition = definition - end - - def conform(value) - result = first_successful_conform_or_errors(value) - if result.is_a?(ConformResult) - result - else - error = ConformError.new(definition, - "None of the definitions are valid for '#{definition.name}'."\ - " Errors for last tested definition:", - sub_errors: result) - ConformResult.new(value, errors: [error]) - end - end - - private - - def first_successful_conform_or_errors(value) - errors = [] - definition.definitions.each do |definition| - result = definition.conform(value) - return result if result.passed? - - errors = result.error_tree - end - - errors.flatten - end - - attr_accessor :definition - end end end end diff --git a/lib/definition/value_object.rb b/lib/definition/value_object.rb index 752d6bb..6e41255 100644 --- a/lib/definition/value_object.rb +++ b/lib/definition/value_object.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "delegate" + module Definition class InvalidValueObjectError < StandardError attr_accessor :conform_result diff --git a/lib/definition/version.rb b/lib/definition/version.rb index 3b58329..e2c8e4d 100644 --- a/lib/definition/version.rb +++ b/lib/definition/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Definition - VERSION = "1.0.0" + VERSION = "1.1.0" end diff --git a/spec/lib/definition/types/and_spec.rb b/spec/lib/definition/types/and_spec.rb index ffd1bb0..2e21e87 100644 --- a/spec/lib/definition/types/and_spec.rb +++ b/spec/lib/definition/types/and_spec.rb @@ -33,7 +33,7 @@ end end - context "with value that fails one definition" do + context "with value that fails last definition" do let(:definition1) do conforming_definition end @@ -55,6 +55,28 @@ end end + context "with value that fails first definition" do + let(:definition1) do + failing_definition(:def1, translated_error_message: "def1 error") + end + let(:definition2) do + conforming_definition + end + let(:value) { 6 } + + it "does not conform" do + expect(conform).to not_conform_with( + "Not all definitions are valid for 'and_test': { Did not pass test for def1 }" + ) + end + + it "produces a good translated error message" do + expect(conform.errors.map(&:translated_error)).to eql( + ["def1 error"] + ) + end + end + context "with value that fails no definition" do let(:definition1) do conforming_definition @@ -73,19 +95,23 @@ describe "with coersion" do subject(:definition) do described_class.new("and_test", - definition_int, - definition_float).conform("1.3") + definition_1, + definition_2).conform(2) end - let(:definition_int) do - conforming_definition(conform_with: 1) + let(:definition_1) do + Definition.Lambda(:conforming_def) do |value| + conform_with(value * 2) + end end - let(:definition_float) do - conforming_definition(conform_with: 1.0) + let(:definition_2) do + Definition.Lambda(:conforming_def) do |value| + conform_with(value.to_s) + end end - it "conforms" do - expect(definition).to conform_with(1.0) + it "conforms by passing on the corerced value output from def1 to def2" do + expect(definition).to conform_with("4") end end end