diff --git a/lib/datacaster/and_node.rb b/lib/datacaster/and_node.rb index 8869887..05dbddb 100644 --- a/lib/datacaster/and_node.rb +++ b/lib/datacaster/and_node.rb @@ -1,19 +1,21 @@ module Datacaster class AndNode < Base - def initialize(left, right) - @left = left - @right = right + def initialize(*casters) + @casters = casters end def cast(object, runtime:) - left_result = @left.with_runtime(runtime).(object) - return left_result unless left_result.valid? - - @right.with_runtime(runtime).(left_result.value) + Datacaster.ValidResult( + @casters.reduce(object) do |result, caster| + caster_result = caster.with_runtime(runtime).(result) + return caster_result unless caster_result.valid? + caster_result.value + end + ) end def inspect - "#" + "#" end end end diff --git a/lib/datacaster/context_nodes/structure_cleaner.rb b/lib/datacaster/context_nodes/structure_cleaner.rb index f389341..8628202 100644 --- a/lib/datacaster/context_nodes/structure_cleaner.rb +++ b/lib/datacaster/context_nodes/structure_cleaner.rb @@ -5,7 +5,7 @@ def initialize(base, strategy = :fail) super(base) unless %i[fail remove pass].include?(strategy) - raise ArgumentError.new("Strategy should be :fail (return error on extra keys), :remove (remove extra keys) or :pass (ignore presence of extra keys)") + raise ArgumentError.new("Strategy should be :fail (return error on extra keys), :remove (remove extra keys) or :pass (ignore presence of extra keys), instead got #{strategy.inspect}") end @strategy = strategy diff --git a/lib/datacaster/predefined.rb b/lib/datacaster/predefined.rb index a77adc7..92daa21 100644 --- a/lib/datacaster/predefined.rb +++ b/lib/datacaster/predefined.rb @@ -45,6 +45,14 @@ def hash_schema(fields, error_key = nil) ) end + def strict_hash_schema(fields, error_key = nil) + schema(hash_schema(fields, error_key)) + end + + def choosy_hash_schema(fields, error_key = nil) + choosy_schema(hash_schema(fields, error_key)) + end + def transform_to_hash(fields) HashMapper.new(fields.transform_values { |x| DefinitionDSL.expand(x) }) end @@ -54,15 +62,15 @@ def validate(active_model_validations) end def schema(base) - ContextNodes::StructureCleaner.new(base, strategy: :fail) + ContextNodes::StructureCleaner.new(base, :fail) end def choosy_schema(base) - ContextNodes::StructureCleaner.new(base, strategy: :remove) + ContextNodes::StructureCleaner.new(base, :remove) end def partial_schema(base) - ContextNodes::StructureCleaner.new(base, strategy: :pass) + ContextNodes::StructureCleaner.new(base, :pass) end # 'Meta' types diff --git a/lib/datacaster/transaction.rb b/lib/datacaster/transaction.rb new file mode 100644 index 0000000..b2d9619 --- /dev/null +++ b/lib/datacaster/transaction.rb @@ -0,0 +1,33 @@ +module Datacaster + module Transaction + module ClassMethods + def perform(caster) + @_caster = ContextNodes::StructureCleaner.new(caster, :fail) + end + + def perform_partial(caster) + @_caster = ContextNodes::StructureCleaner.new(caster, :pass) + end + + def perform_choosy(caster) + @_caster = ContextNodes::StructureCleaner.new(caster, :remove) + end + + def call(*args, **kwargs) + new.call(*args, **kwargs) + end + end + + def self.included(base) + base.extend Datacaster::Predefined + base.extend ClassMethods + base.include Datacaster::Mixin + end + + def cast(object, runtime:) + self.class.instance_variable_get(:@_caster). + with_runtime(runtime). + (object) + end + end +end diff --git a/spec/datacaster_spec.rb b/spec/datacaster_spec.rb index 68aa338..dbea811 100644 --- a/spec/datacaster_spec.rb +++ b/spec/datacaster_spec.rb @@ -428,6 +428,34 @@ end end + describe "and node with .steps(...)" do + subject do + described_class.schema do + steps( + string, + check { |x| x.length <= 5 }, + compare("test") + ) + end + end + + it "returns Success when all are valid" do + expect(subject.("test").to_dry_result).to eq Success("test") + end + + it "returns first ErrorResult, when first is ErrorResult" do + expect(subject.(:not_even_string).to_dry_result).to eq Failure(["is not a string"]) + end + + it "returns second ErrorResult, when second is ErrorResult" do + expect(subject.("too long").to_dry_result).to eq Failure(["is invalid"]) + end + + it "returns the last ErrorResult, when the last is ErrorResult" do + expect(subject.("tset").to_dry_result).to eq Failure(['does not equal "test"']) + end + end + describe "or (|) node" do subject do described_class.schema { string | integer } diff --git a/spec/transaction_spec.rb b/spec/transaction_spec.rb new file mode 100644 index 0000000..5f5bf9a --- /dev/null +++ b/spec/transaction_spec.rb @@ -0,0 +1,57 @@ +RSpec.describe Datacaster::Transaction do + include Dry::Monads[:result] + + before(:all) do + @i18n_module = Datacaster::Config.i18n_module + Datacaster::Config.i18n_module = Datacaster::SubstituteI18n + end + + after(:all) do + Datacaster::Config.i18n_module = @i18n_module + end + + it "returns true for Datacaster.instance?" do + tx = + Class.new do + include Datacaster::Transaction + end + + expect(Datacaster.instance?(tx.new)).to eq true + end + + it "performs basic casting with Datacaster::Predefined casters" do + tx = + Class.new do + include Datacaster::Transaction + + perform integer + end + + expect(tx.new.(5).to_dry_result).to eq Success(5) + expect(tx.(5).to_dry_result).to eq Success(5) + end + + it "performs multi-step casting" do + tx = + Class.new do + include Datacaster::Transaction + + unwrap = transform { |x| x.to_h } + typecast = hash_schema(name: string, email: string) + send_email = transform do |email| + {address: email, result: true} + end + + perform steps( + unwrap, + typecast, + transform_to_hash(email: pick(:email) & send_email) + ) + end + + expect(tx.(name: 'John', email: 'john@example.org').to_dry_result).to eq Success( + name: 'John', + email: {address: 'john@example.org', result: true} + ) + end +end