diff --git a/lib/dry/logic/operations.rb b/lib/dry/logic/operations.rb index c974e25..e35de14 100644 --- a/lib/dry/logic/operations.rb +++ b/lib/dry/logic/operations.rb @@ -7,6 +7,7 @@ require 'dry/logic/operations/key' require 'dry/logic/operations/attr' require 'dry/logic/operations/each' +require 'dry/logic/operations/part' require 'dry/logic/operations/set' require 'dry/logic/operations/check' diff --git a/lib/dry/logic/operations/multiple.rb b/lib/dry/logic/operations/multiple.rb new file mode 100644 index 0000000..cf9faec --- /dev/null +++ b/lib/dry/logic/operations/multiple.rb @@ -0,0 +1,17 @@ +require 'dry/logic/operations/abstract' + +module Dry + module Logic + module Operations + class Multiple < Abstract + def ast(input = Undefined) + [type, rules.map { |rule| rule.ast(input) }] + end + + def to_s + "#{type}(#{rules.map(&:to_s).join(', ')})" + end + end + end + end +end diff --git a/lib/dry/logic/operations/part.rb b/lib/dry/logic/operations/part.rb new file mode 100644 index 0000000..71dc12c --- /dev/null +++ b/lib/dry/logic/operations/part.rb @@ -0,0 +1,29 @@ +require 'dry/logic/operations/multiple' +require 'dry/logic/result' + +module Dry + module Logic + module Operations + class Part < Multiple + def type + :part + end + + def call(input) + results = rules.map { |rule| rule.(input) } + success = results.any?(&:success?) + + return Result::SUCCESS if success + + Result.new(success, id) do + [type, results.map { |failure| failure.to_ast }] + end + end + + def [](input) + rules.map { |rule| rule[input] }.any? + end + end + end + end +end diff --git a/lib/dry/logic/operations/set.rb b/lib/dry/logic/operations/set.rb index cc75c29..52bca73 100644 --- a/lib/dry/logic/operations/set.rb +++ b/lib/dry/logic/operations/set.rb @@ -1,10 +1,10 @@ -require 'dry/logic/operations/abstract' +require 'dry/logic/operations/multiple' require 'dry/logic/result' module Dry module Logic module Operations - class Set < Abstract + class Set < Multiple def type :set end @@ -21,14 +21,6 @@ def call(input) def [](input) rules.map { |rule| rule[input] }.all? end - - def ast(input = Undefined) - [type, rules.map { |rule| rule.ast(input) }] - end - - def to_s - "#{type}(#{rules.map(&:to_s).join(', ')})" - end end end end diff --git a/lib/dry/logic/result.rb b/lib/dry/logic/result.rb index 890fa9f..d921a7c 100644 --- a/lib/dry/logic/result.rb +++ b/lib/dry/logic/result.rb @@ -93,6 +93,14 @@ def visit_not(node) def visit_hint(node) visit(node) end + + def visit_set(nodes) + "set(#{nodes.map { |node| visit(node) }.join(', ')})" + end + + def visit_part(nodes) + "part(#{nodes.map { |node| visit(node) }.join(', ')})" + end end end end diff --git a/lib/dry/logic/rule_compiler.rb b/lib/dry/logic/rule_compiler.rb index 779fa94..5aa1027 100644 --- a/lib/dry/logic/rule_compiler.rb +++ b/lib/dry/logic/rule_compiler.rb @@ -40,6 +40,10 @@ def visit_attr(node) Operations::Attr.new(visit(predicate), name: name) end + def visit_part(node) + Operations::Part.new(*call(node)) + end + def visit_set(node) Operations::Set.new(*call(node)) end diff --git a/spec/shared/predicates.rb b/spec/shared/predicates.rb index f58d354..7823314 100644 --- a/spec/shared/predicates.rb +++ b/spec/shared/predicates.rb @@ -34,6 +34,8 @@ let(:case?) { Dry::Logic::Predicates[:case?] } let(:equal?) { Dry::Logic::Predicates[:equal?] } + + let(:odd?) { Dry::Logic::Predicates[:odd?] } end RSpec.shared_examples 'a passing predicate' do diff --git a/spec/unit/operations/part_spec.rb b/spec/unit/operations/part_spec.rb new file mode 100644 index 0000000..7c13bca --- /dev/null +++ b/spec/unit/operations/part_spec.rb @@ -0,0 +1,42 @@ +RSpec.describe Operations::Part do + subject(:operation) { Operations::Part.new(is_odd, gt_18) } + + include_context 'predicates' + + let(:is_odd) { Rule::Predicate.new(odd?) } + let(:gt_18) { Rule::Predicate.new(gt?, args: [18]) } + + describe '#call' do + it 'applies at least one of its rules to the input' do + expect(operation.(20)).to be_success + expect(operation.(17)).to be_success + expect(operation.(16)).to be_failure + end + end + + describe '#to_ast' do + it 'returns ast' do + expect(operation.to_ast).to eql( + [:part, [[:predicate, [:odd?, [[:input, Undefined]]]], [:predicate, [:gt?, [[:num, 18], [:input, Undefined]]]]]] + ) + end + + it 'returns result ast' do + expect(operation.(16).to_ast).to eql( + [:part, [[:predicate, [:odd?, [[:input, 16]]]], [:predicate, [:gt?, [[:num, 18], [:input, 16]]]]]] + ) + end + + it 'returns result ast with an :id' do + expect(operation.with(id: :age).(16).to_ast).to eql( + [:failure, [:age, [:part, [[:predicate, [:odd?, [[:input, 16]]]], [:predicate, [:gt?, [[:num, 18], [:input, 16]]]]]]]] + ) + end + end + + describe '#to_s' do + it 'returns string representation' do + expect(operation.to_s).to eql('part(odd?, gt?(18))') + end + end +end