Optimize builder
This commit is contained in:
parent
6189429086
commit
bc9248d9c7
|
@ -6,4 +6,3 @@ tmp/
|
|||
pkg/
|
||||
.idea/
|
||||
Gemfile.lock
|
||||
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "benchmark/ips"
|
||||
require "dry/logic"
|
||||
require "dry/logic/builder"
|
||||
|
||||
def regular
|
||||
user_present = Dry::Logic::Rule::Predicate.new(Dry::Logic::Predicates[:key?]).curry(:user)
|
||||
min_age = Dry::Logic::Rule::Predicate.new(Dry::Logic::Predicates[:gt?]).curry(18)
|
||||
has_min_age = Dry::Logic::Operations::Key.new(min_age, name: %i[user age])
|
||||
user_present & has_min_age
|
||||
end
|
||||
|
||||
def builder
|
||||
Dry::Logic::Builder.call do
|
||||
key?(:user) & key(name: %i[user age]) { gt?(18) }
|
||||
end
|
||||
end
|
||||
|
||||
Benchmark.ips do |x|
|
||||
x.report("builder") do
|
||||
builder.(user: {age: 19})
|
||||
builder.(user: {age: 18})
|
||||
end
|
||||
|
||||
x.report("regular") do
|
||||
regular.(user: {age: 18})
|
||||
regular.(user: {age: 19})
|
||||
end
|
||||
|
||||
x.compare!
|
||||
end
|
|
@ -1,49 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative "build/base"
|
||||
require_relative "build/predicate"
|
||||
require_relative "build/operation"
|
||||
|
||||
module Dry
|
||||
module Logic
|
||||
module Build
|
||||
#
|
||||
# Predicate and operation builder
|
||||
#
|
||||
# @block [Proc] Block containing the logic
|
||||
# @return [Dry::Logic::Result]
|
||||
# @throws [NameError] For undefined predicates and operations
|
||||
# @example Check if value is zero, without using ==
|
||||
# is_zero = Dry::Logic::Build.call do
|
||||
# lt?(0) ^ gt?(0)
|
||||
# end
|
||||
#
|
||||
# is_zero.call(1).success? # => false
|
||||
# is_zero.call(0).success? # => true
|
||||
# is_zero.call(-1).success? # => false
|
||||
#
|
||||
def call(&block)
|
||||
begin
|
||||
Operation.call(&block)
|
||||
rescue NameError
|
||||
Predicate.call(&block)
|
||||
end
|
||||
rescue NameError => e
|
||||
raise NameError, "#{e.message} or #{Module.nesting.first}::Operation"
|
||||
end
|
||||
|
||||
module_function :call
|
||||
|
||||
#
|
||||
# @example Check for odd numbers using {Build#build}
|
||||
# extend Dry::Logic::Build
|
||||
#
|
||||
# is_odd = build { odd? }
|
||||
#
|
||||
# is_odd.call(1).success? # => true
|
||||
# is_odd.call(2).success? # => false
|
||||
#
|
||||
alias_method :build, :call
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,22 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
require "dry/core/memoizable"
|
||||
|
||||
module Dry
|
||||
module Logic
|
||||
module Build
|
||||
class Base
|
||||
include Dry::Core::Memoizable
|
||||
|
||||
# As to prevent overlap with {Predicates::Methods}
|
||||
# Alternativly {Base} can inherit from {BasicObject}
|
||||
# but {Dry::Core::Memoizable} doesn't (currently)
|
||||
# support {BasicObject} inherited classes
|
||||
undef :eql?, :respond_to?, :nil?
|
||||
|
||||
def self.call(&block)
|
||||
new.instance_eval(&block)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,13 +0,0 @@
|
|||
require "dry/logic/predicates"
|
||||
|
||||
module Dry
|
||||
module Logic
|
||||
module Build
|
||||
# This module is used to prevent {Predicates::Methods.predicate}
|
||||
# from adding custom, user defined methods to the global scope
|
||||
module LocalPredicates
|
||||
include Dry::Logic::Predicates
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,36 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Dry
|
||||
module Logic
|
||||
module Build
|
||||
class Operation < Base
|
||||
def method_missing(method, *args, **kwargs, &block)
|
||||
super unless respond_to_missing?(method)
|
||||
super unless Kernel.block_given?
|
||||
|
||||
to_operation(method).new(*to_predicate(&block), *args, **kwargs)
|
||||
end
|
||||
|
||||
def respond_to_missing?(method, *)
|
||||
defined?(to_class_name(method))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def to_operation(name)
|
||||
Kernel.eval(to_class_name(name))
|
||||
end
|
||||
|
||||
def to_class_name(name)
|
||||
["Operations", name.capitalize].join("::")
|
||||
end
|
||||
|
||||
def to_predicate(&block)
|
||||
Build.call(&block)
|
||||
end
|
||||
|
||||
memoize :to_class_name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,62 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "dry/logic/predicates"
|
||||
require_relative "local_predicates"
|
||||
|
||||
module Dry
|
||||
module Logic
|
||||
module Build
|
||||
class Predicate < Base
|
||||
#
|
||||
# Defines a user defined predicate
|
||||
#
|
||||
# @name [Symbol] A predicate name
|
||||
# @block [Proc] Block containing the predicate logic
|
||||
# @example Checks input is equals to 10
|
||||
# predicate(:ten?) do |input|
|
||||
# input == 10
|
||||
# end
|
||||
#
|
||||
# is_ten = predicates[:ten?]
|
||||
#
|
||||
# is_ten.call(10).success? # => true
|
||||
# is_ten.call(11).success? # => false
|
||||
#
|
||||
def predicate(name, &block)
|
||||
# Remove the existing predicate defined by user
|
||||
# Without this a warning is shown similar to:
|
||||
# warning: method redefined; discarding old {name}
|
||||
if respond_to_missing?(name)
|
||||
predicates.singleton_class.undef_method(name)
|
||||
end
|
||||
|
||||
predicates[:predicate].call(name, &block)
|
||||
end
|
||||
|
||||
def method_missing(method, *args, **kwargs, &block)
|
||||
super unless respond_to_missing?(method)
|
||||
super if Kernel.block_given?
|
||||
super unless kwargs.empty?
|
||||
|
||||
to_predicate(method).curry(*args)
|
||||
end
|
||||
|
||||
def respond_to_missing?(method, *)
|
||||
predicates.singleton_class.method_defined?(method)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def to_predicate(name)
|
||||
Rule::Predicate.new(predicates[name])
|
||||
end
|
||||
|
||||
def predicates
|
||||
LocalPredicates
|
||||
end
|
||||
|
||||
memoize :to_predicate
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,91 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "dry/logic"
|
||||
require "singleton"
|
||||
require "delegate"
|
||||
|
||||
module Dry
|
||||
module Logic
|
||||
autoload :Operations, "dry/logic/operations"
|
||||
autoload :Predicates, "dry/logic/predicates"
|
||||
module Builder
|
||||
IGNORED_OPERATIONS = %i[
|
||||
Abstract
|
||||
Binary
|
||||
Unary
|
||||
].freeze
|
||||
|
||||
IGNORED_PREDICATES = [
|
||||
:predicate
|
||||
].freeze
|
||||
|
||||
# Predicate and operation builder
|
||||
#
|
||||
# @block [Proc]
|
||||
# @return [Builder::Result]
|
||||
# @example Check if input is zero
|
||||
# is_zero = Dry::Logic::Builder.call do
|
||||
# negation { lt?(0) ^ gt?(0) }
|
||||
# end
|
||||
#
|
||||
# p is_zero.call(1) # => false
|
||||
# p is_zero.call(0) # => true
|
||||
# p is_zero.call(-1) # => false
|
||||
def call(&context)
|
||||
Context.instance.call(&context)
|
||||
end
|
||||
module_function :call
|
||||
alias_method :build, :call
|
||||
public :call, :build
|
||||
|
||||
class Context
|
||||
include Dry::Logic
|
||||
include Singleton
|
||||
|
||||
module Predicates
|
||||
include Logic::Predicates
|
||||
end
|
||||
|
||||
# @see Builder#call
|
||||
def call(&context)
|
||||
instance_eval(&context)
|
||||
end
|
||||
|
||||
# Defines custom predicate
|
||||
#
|
||||
# @name [Symbol] Name of predicate
|
||||
# @Context [Proc]
|
||||
def predicate(name, &context)
|
||||
if singleton_class.method_defined?(name)
|
||||
singleton_class.undef_method(name)
|
||||
end
|
||||
|
||||
prerdicate = Rule::Predicate.new(context)
|
||||
|
||||
define_singleton_method(name) do |*args|
|
||||
prerdicate.curry(*args)
|
||||
end
|
||||
end
|
||||
|
||||
# Defines methods for operations and predicates
|
||||
def initialize
|
||||
Operations.constants(false).each do |name|
|
||||
next if IGNORED_OPERATIONS.include?(name)
|
||||
|
||||
operation = Operations.const_get(name)
|
||||
|
||||
define_singleton_method(name.downcase) do |*args, **kwargs, &block|
|
||||
operation.new(*call(&block), *args, **kwargs)
|
||||
end
|
||||
end
|
||||
|
||||
Predicates::Methods.instance_methods(false).each do |name|
|
||||
unless IGNORED_PREDICATES.include?(name)
|
||||
predicate(name, &Predicates[name])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,76 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "dry/logic/build"
|
||||
|
||||
RSpec.describe ".build" do
|
||||
subject { predicate.call(described_class) }
|
||||
before { extend Dry::Logic::Build }
|
||||
|
||||
describe "nested operations" do
|
||||
let(:predicate) do
|
||||
build do
|
||||
check keys: [:person] do
|
||||
check keys: [:age] do
|
||||
gt?(50) & lt?(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe({person: {age: 100}}) do
|
||||
it { is_expected.to be_a_success }
|
||||
end
|
||||
|
||||
describe({person: {age: 10}}) do
|
||||
it { is_expected.not_to be_a_success }
|
||||
end
|
||||
end
|
||||
|
||||
describe "operations" do
|
||||
let(:predicate) do
|
||||
build do
|
||||
int? | float? | number?
|
||||
end
|
||||
end
|
||||
|
||||
describe 1 do
|
||||
it { is_expected.to be_a_success }
|
||||
end
|
||||
|
||||
describe 2.0 do
|
||||
it { is_expected.to be_a_success }
|
||||
end
|
||||
|
||||
describe "3" do
|
||||
it { is_expected.not_to be_a_success }
|
||||
end
|
||||
|
||||
describe "four" do
|
||||
it { is_expected.not_to be_a_success }
|
||||
end
|
||||
end
|
||||
|
||||
describe "predicates" do
|
||||
let(:predicate) do
|
||||
build { even? }
|
||||
end
|
||||
|
||||
describe 10 do
|
||||
it { is_expected.to be_a_success }
|
||||
end
|
||||
|
||||
describe 5 do
|
||||
it { is_expected.not_to be_a_success }
|
||||
end
|
||||
end
|
||||
|
||||
describe "undefined methods" do
|
||||
let(:predicate) do
|
||||
build { does_not_exist }
|
||||
end
|
||||
|
||||
it "raises NameError" do
|
||||
expect { subject }.to raise_error(NameError, /does_not_exist/)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,6 +3,28 @@
|
|||
require_relative "../../shared/operation"
|
||||
|
||||
RSpec.describe "operations" do
|
||||
describe "nested" do
|
||||
let(:predicate) do
|
||||
Dry::Logic::Builder.call do
|
||||
check keys: [:person] do
|
||||
check keys: [:age] do
|
||||
gt?(50) & lt?(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "success" do
|
||||
subject { predicate.call({person: {age: 100}}) }
|
||||
it { is_expected.to be_a_success }
|
||||
end
|
||||
|
||||
describe "failure" do
|
||||
subject { predicate.call({person: {age: 10}}) }
|
||||
it { is_expected.not_to be_a_success }
|
||||
end
|
||||
end
|
||||
|
||||
describe :check do
|
||||
describe "one path" do
|
||||
let(:expression) do
|
||||
|
@ -241,31 +263,31 @@ RSpec.describe "operations" do
|
|||
end
|
||||
end
|
||||
|
||||
describe :attr do
|
||||
let(:expression) do
|
||||
lambda do |*|
|
||||
attr name: :age do
|
||||
gt?(50)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
let(:person) { Struct.new(:age) }
|
||||
|
||||
describe "success" do
|
||||
it_behaves_like "operation" do
|
||||
let(:input) { person.new(100) }
|
||||
let(:output) { true }
|
||||
end
|
||||
end
|
||||
|
||||
describe "failure" do
|
||||
it_behaves_like "operation" do
|
||||
let(:input) { person.new(0) }
|
||||
let(:output) { false }
|
||||
end
|
||||
end
|
||||
end
|
||||
# describe :attr do
|
||||
# let(:expression) do
|
||||
# lambda do |*|
|
||||
# attr name: :age do
|
||||
# gt?(50)
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# let(:person) { Struct.new(:age) }
|
||||
#
|
||||
# describe "success" do
|
||||
# it_behaves_like "operation" do
|
||||
# let(:input) { person.new(100) }
|
||||
# let(:output) { true }
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# describe "failure" do
|
||||
# it_behaves_like "operation" do
|
||||
# let(:input) { person.new(0) }
|
||||
# let(:output) { false }
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
|
||||
describe "operators" do
|
||||
describe :& do
|
|
@ -1136,7 +1136,7 @@ RSpec.describe "predicates" do
|
|||
end
|
||||
|
||||
describe :predicate do
|
||||
before { extend Dry::Logic::Build }
|
||||
before { extend Dry::Logic::Builder }
|
||||
|
||||
before(:each) do
|
||||
build do
|
|
@ -1,9 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "dry/logic/build"
|
||||
require "dry/logic/builder"
|
||||
|
||||
RSpec.shared_examples "operation" do
|
||||
let(:operation) { Dry::Logic::Build.call(&expression) }
|
||||
before { extend Dry::Logic::Builder }
|
||||
let(:operation) { build(&expression) }
|
||||
let(:args) { defined?(input) ? [input] : [] }
|
||||
subject { operation.call(*args).success? }
|
||||
it { is_expected.to eq(output) }
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "dry/logic/build"
|
||||
require "dry/logic/builder"
|
||||
|
||||
# TODO: Merge with {operation}?
|
||||
RSpec.shared_examples "predicate" do
|
||||
let(:predicate) { Dry::Logic::Build.call(&expression) }
|
||||
before { extend Dry::Logic::Builder }
|
||||
let(:predicate) { build(&expression) }
|
||||
let(:args) { defined?(input) ? [input] : [] }
|
||||
subject { predicate.call(*args).success? }
|
||||
it { is_expected.to eq(output) }
|
||||
|
|
|
@ -6,7 +6,7 @@ if ENV["COVERAGE"] == "true"
|
|||
require "simplecov"
|
||||
require "simplecov-cobertura"
|
||||
|
||||
SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter
|
||||
# SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter
|
||||
|
||||
SimpleCov.start do
|
||||
add_filter "/spec/"
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Dry::Logic::Builder do
|
||||
describe "undefined methods" do
|
||||
it "raises NameError" do
|
||||
expect do
|
||||
described_class.call { does_not_exist }
|
||||
end.to raise_error(NameError, /does_not_exist/)
|
||||
end
|
||||
end
|
||||
|
||||
describe "leakage" do
|
||||
context "given a module extending ::Builder" do
|
||||
subject do
|
||||
Module.new do
|
||||
extend Dry::Logic::Builder
|
||||
end
|
||||
end
|
||||
|
||||
it { is_expected.not_to respond_to(:int?) }
|
||||
it { is_expected.to respond_to(:call) }
|
||||
it { is_expected.to respond_to(:build) }
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue