dry-logic 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1a6ff887f61b513496991a96d7903685886ab29dae71cb4f75185cd7866fd4b2
4
- data.tar.gz: 9e063f346341b395ac21645f84319e619af19151e62bd97413badf246440d353
3
+ metadata.gz: 12cf783095366932df786fea023d69be64d48e7b91c03a279186d9be7ed098b4
4
+ data.tar.gz: e324cfb405828951649cd948055dc4fbfb84eb65f50156cee29695785d8b065f
5
5
  SHA512:
6
- metadata.gz: f643a0238e3b67c91c83af1e90cb4398279b4c01957ce663a5186ae8437a87e94c9d9cb2f7a2aabca1c7edb47be28a531317a044e58e270ffff59c44e6a9ccf5
7
- data.tar.gz: 17b0ff20fc5c41258a537ad0c57e959cd3359765801261157772a62ee5d1ce525c15bca2ce92c8ee624b1fda67a5cf6cbf4228af552cf99efc8e8428026dbccb
6
+ metadata.gz: 476e8b141e0868cc3bdcdee4c7d695809ae056d6b7977b0b49db6af47990a1a285b33350ef6a789670576d64bcb8495b0068228a812d6a7727b98250445c1485
7
+ data.tar.gz: c3de162dc7c3f75b7ee0c2e321406cc9e9f5f17d0e73c7ef416bb40981b037ce3c4c846a9f55cb5a3602ae3deb2a4162e3a502c20f87cd1009b97f4c8ecdc690
@@ -1,23 +1,15 @@
1
- engines:
1
+ version: "2"
2
+
3
+ prepare:
4
+ fetch:
5
+ - url: "https://raw.githubusercontent.com/dry-rb/devtools/master/.rubocop.yml"
6
+ path: ".rubocop.yml"
7
+
8
+ exclude_patterns:
9
+ - "benchmarks/"
10
+ - "examples/"
11
+ - "spec/"
12
+
13
+ plugins:
2
14
  rubocop:
3
15
  enabled: true
4
- checks:
5
- Rubocop/Metrics/LineLength:
6
- enabled: true
7
- max: 100
8
- Rubocop/Style/Documentation:
9
- enabled: false
10
- Rubocop/Lint/HandleExceptions:
11
- enabled: true
12
- exclude:
13
- - rakelib/*.rake
14
- Rubocop/Style/FileName:
15
- enabled: true
16
- exclude:
17
- - 'lib/dry-logic.rb'
18
- ratings:
19
- paths:
20
- - lib/**/*.rb
21
- exclude_paths:
22
- - spec/**/*
23
- - examples/**/*
data/.gitignore CHANGED
@@ -5,3 +5,4 @@ vendor/bundle
5
5
  tmp/
6
6
  .idea/
7
7
  Gemfile.lock
8
+ .rubocop.yml
@@ -1,8 +1,6 @@
1
1
  language: ruby
2
2
  cache: bundler
3
- sudo: false
4
3
  bundler_args: --without benchmarks tools
5
- before_install: gem update --system
6
4
  before_script:
7
5
  - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
8
6
  - chmod +x ./cc-test-reporter
@@ -12,19 +10,22 @@ after_script:
12
10
  script:
13
11
  - bundle exec rake
14
12
  rvm:
15
- - 2.6.0
16
- - 2.5.3
17
- - 2.4.5
18
- - 2.3.8
19
- - jruby-9.2.5.0
13
+ - 2.6.2
14
+ - 2.5.5
15
+ - 2.4.6
16
+ - jruby-9.2.6.0
17
+ - truffleruby
20
18
  env:
21
19
  global:
22
20
  - COVERAGE=true
21
+ matrix:
22
+ allow_failures:
23
+ - rvm: truffleruby
23
24
  notifications:
24
25
  email: false
25
26
  webhooks:
26
27
  urls:
27
- - https://webhooks.gitter.im/e/19098b4253a72c9796db
28
+ - https://dry-rb.zulipchat.com/api/v1/external/travis?api_key=SY8LLz6fShd4TJeLDXEdT0eBEgRqT2lv&stream=notifications&topic=ci
28
29
  on_success: change # options: [always|never|change] default: always
29
30
  on_failure: always # options: [always|never|change] default: always
30
31
  on_start: false # default: false
@@ -1,3 +1,15 @@
1
+ # v0.6.0 2019-04-04
2
+
3
+ ### Added
4
+
5
+ * Generating hints can be disabled by building `Operations::And` with `hints: false` option set (solnic)
6
+
7
+ ### Changed
8
+
9
+ * `Rule` construction has been optimized so that currying and application is multiple-times faster (flash-gordon)
10
+
11
+ [Compare v0.5.0...v0.6.0](https://github.com/dry-rb/dry-logic/compare/v0.5.0...v0.6.0)
12
+
1
13
  # v0.5.0 2019-01-29
2
14
 
3
15
  ### Added
data/Gemfile CHANGED
@@ -7,11 +7,7 @@ group :test do
7
7
  end
8
8
 
9
9
  group :tools do
10
- gem 'rubocop'
11
- gem 'byebug', platform: :mri
12
-
13
- unless ENV['TRAVIS']
14
- gem 'mutant', github: 'mbj/mutant'
15
- gem 'mutant-rspec', github: 'mbj/mutant'
16
- end
10
+ gem 'pry-byebug', platform: :mri
11
+ gem 'benchmark-ips', platform: :mri
12
+ gem 'hotch', platform: :mri
17
13
  end
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  [gem]: https://rubygems.org/gems/dry-logic
2
2
  [travis]: https://travis-ci.org/dry-rb/dry-logic
3
3
  [codeclimate]: https://codeclimate.com/github/dry-rb/dry-logic
4
- [coveralls]: https://coveralls.io/r/dry-rb/dry-logic
4
+ [chat]: https://dry-rb.zulipchat.com
5
5
  [inchpages]: http://inch-ci.org/github/dry-rb/dry-logic
6
6
 
7
- # dry-logic [![Join the chat at https://gitter.im/dry-rb/chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/dry-rb/chat)
7
+ # dry-schema [![Join the chat at https://dry-rb.zulipchat.com](https://img.shields.io/badge/dry--rb-join%20chat-%23346b7a.svg)][chat]
8
8
 
9
9
  [![Gem Version](https://badge.fury.io/rb/dry-logic.svg)][gem]
10
10
  [![Build Status](https://travis-ci.org/dry-rb/dry-logic.svg?branch=master)][travis]
@@ -15,7 +15,7 @@
15
15
  Predicate logic and rule composition used by:
16
16
 
17
17
  * [dry-types](https://github.com/dry-rb/dry-types) for constrained types
18
- * [dry-validation](https://github.com/dry-rb/dry-validation) for composing validation rules
18
+ * [dry-schema](https://github.com/dry-rb/dry-schema) and [dry-validation](https://github.com/dry-rb/dry-validation) for composing validation rules
19
19
  * your project...?
20
20
 
21
21
  ## Links
@@ -0,0 +1,28 @@
1
+ require_relative 'setup'
2
+
3
+ unless Dry::Logic::Rule.respond_to?(:build)
4
+ Dry::Logic::Rule.singleton_class.alias_method(:build, :new)
5
+ end
6
+
7
+ predicates = Dry::Logic::Predicates
8
+
9
+ type_check = Dry::Logic::Rule.build(predicates[:type?]).curry(Integer)
10
+ int_check = Dry::Logic::Rule.build(predicates[:int?])
11
+ key_check = Dry::Logic::Rule.build(predicates[:key?]).curry(:user)
12
+ with_user = { user: {} }
13
+ without_user = {}
14
+
15
+ comparison = Dry::Logic::Rule.build(predicates[:gteq?]).curry(18)
16
+
17
+ Benchmark.ips do |x|
18
+ x.report("type check - success") { type_check.(0) }
19
+ x.report("type check - failure") { type_check.('0') }
20
+ x.report("int check - success") { int_check.(0) }
21
+ x.report("int check - failure") { int_check.('0') }
22
+ x.report("key check - success") { key_check.(with_user) }
23
+ x.report("key check - failure") { key_check.(without_user) }
24
+ x.report("comparison - success") { comparison.(20) }
25
+ x.report("comparison - failure") { comparison.(17) }
26
+
27
+ x.compare!
28
+ end
@@ -0,0 +1,11 @@
1
+ require 'benchmark/ips'
2
+ require 'hotch'
3
+ ENV['HOTCH_VIEWER'] ||= 'open'
4
+
5
+ require 'dry/logic'
6
+ require 'dry/logic/predicates'
7
+
8
+ def profile(&block)
9
+ Hotch(filter: 'Dry', &block)
10
+ end
11
+
@@ -13,8 +13,8 @@ Gem::Specification.new do |spec|
13
13
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
14
14
  spec.require_paths = ['lib']
15
15
 
16
+ spec.add_runtime_dependency 'concurrent-ruby', '~> 1.0'
16
17
  spec.add_runtime_dependency 'dry-core', '~> 0.2'
17
- spec.add_runtime_dependency 'dry-container', '~> 0.2', '>= 0.2.6'
18
18
  spec.add_runtime_dependency 'dry-equalizer', '~> 0.2'
19
19
 
20
20
  spec.add_development_dependency 'bundler'
@@ -3,9 +3,9 @@ require 'dry/logic/predicates'
3
3
 
4
4
  include Dry::Logic
5
5
 
6
- user_present = Rule::Predicate.new(Predicates[:key?]).curry(:user)
6
+ user_present = Rule::Predicate.build(Predicates[:key?]).curry(:user)
7
7
 
8
- has_min_age = Operations::Key.new(Rule::Predicate.new(Predicates[:gt?]).curry(18), name: [:user, :age])
8
+ has_min_age = Operations::Key.new(Rule::Predicate.build(Predicates[:gt?]).curry(18), name: [:user, :age])
9
9
 
10
10
  user_rule = user_present & has_min_age
11
11
 
@@ -5,10 +5,17 @@ module Dry
5
5
  module Logic
6
6
  module Operations
7
7
  class And < Binary
8
+ attr_reader :hints
9
+
10
+ def initialize(*)
11
+ super
12
+ @hints = options.fetch(:hints, true)
13
+ end
14
+
8
15
  def type
9
16
  :and
10
17
  end
11
- alias_method :operator, :type
18
+ alias operator type
12
19
 
13
20
  def call(input)
14
21
  left_result = left.(input)
@@ -22,7 +29,10 @@ module Dry
22
29
  Result.new(false, id) { right_result.ast(input) }
23
30
  end
24
31
  else
25
- Result.new(false, id) { [type, [left_result.to_ast, [:hint, right.ast(input)]]] }
32
+ Result.new(false, id) do
33
+ left_ast = left_result.to_ast
34
+ hints ? [type, [left_ast, [:hint, right.ast(input)]]] : left_ast
35
+ end
26
36
  end
27
37
  end
28
38
 
@@ -1,15 +1,17 @@
1
+ require 'concurrent/map'
1
2
  require 'dry/core/constants'
2
3
  require 'dry/equalizer'
3
4
  require 'dry/logic/operations'
4
5
  require 'dry/logic/result'
6
+ require 'dry/logic/rule/interface'
5
7
 
6
8
  module Dry
7
9
  module Logic
8
10
  def self.Rule(*args, **options, &block)
9
11
  if args.any?
10
- Rule.new(*args, Rule::DEFAULT_OPTIONS.merge(options))
12
+ Rule.build(*args, **options)
11
13
  elsif block
12
- Rule.new(block, Rule::DEFAULT_OPTIONS.merge(options))
14
+ Rule.build(block, **options)
13
15
  end
14
16
  end
15
17
 
@@ -18,8 +20,6 @@ module Dry
18
20
  include Dry::Equalizer(:predicate, :options)
19
21
  include Operators
20
22
 
21
- DEFAULT_OPTIONS = { args: [].freeze }.freeze
22
-
23
23
  attr_reader :predicate
24
24
 
25
25
  attr_reader :options
@@ -28,10 +28,27 @@ module Dry
28
28
 
29
29
  attr_reader :arity
30
30
 
31
- def initialize(predicate, options = DEFAULT_OPTIONS)
31
+ def self.interfaces
32
+ @interfaces ||= ::Concurrent::Map.new
33
+ end
34
+
35
+ def self.specialize(arity, curried, base = Rule)
36
+ base.interfaces.fetch_or_store([arity, curried]) do
37
+ interface = Interface.new(arity, curried)
38
+ klass = Class.new(base) { include interface }
39
+ base.const_set("#{base.name.split('::').last}#{interface.name}", klass)
40
+ klass
41
+ end
42
+ end
43
+
44
+ def self.build(predicate, args: EMPTY_ARRAY, arity: predicate.arity, **options)
45
+ specialize(arity, args.size).new(predicate, { args: args, arity: arity, **options })
46
+ end
47
+
48
+ def initialize(predicate, options = EMPTY_HASH)
32
49
  @predicate = predicate
33
50
  @options = options
34
- @args = options[:args]
51
+ @args = options[:args] || EMPTY_ARRAY
35
52
  @arity = options[:arity] || predicate.arity
36
53
  end
37
54
 
@@ -43,29 +60,15 @@ module Dry
43
60
  options[:id]
44
61
  end
45
62
 
46
- def call(*input)
47
- Result.new(self[*input], id) { ast(*input) }
48
- end
49
-
50
- def [](*input)
51
- arity == 0 ? predicate.() : predicate[*args, *input]
52
- end
53
-
54
63
  def curry(*new_args)
55
- all_args = args + new_args
56
-
57
- if all_args.size > arity
58
- raise ArgumentError, "wrong number of arguments (#{all_args.size} for #{arity})"
59
- else
60
- with(args: all_args)
61
- end
64
+ with(args: args + new_args)
62
65
  end
63
66
 
64
67
  def bind(object)
65
- if UnboundMethod === predicate
66
- self.class.new(predicate.bind(object), options)
68
+ if predicate.respond_to?(:bind)
69
+ self.class.build(predicate.bind(object), options)
67
70
  else
68
- self.class.new(
71
+ self.class.build(
69
72
  -> *args { object.instance_exec(*args, &predicate) },
70
73
  options.merge(arity: arity, parameters: parameters)
71
74
  )
@@ -77,7 +80,7 @@ module Dry
77
80
  end
78
81
 
79
82
  def with(new_opts)
80
- self.class.new(predicate, options.merge(new_opts))
83
+ self.class.build(predicate, options.merge(new_opts))
81
84
  end
82
85
 
83
86
  def parameters
@@ -0,0 +1,126 @@
1
+ module Dry
2
+ module Logic
3
+ class Rule
4
+ class Interface < ::Module
5
+ attr_reader :arity
6
+
7
+ attr_reader :curried
8
+
9
+ def initialize(arity, curried)
10
+ @arity = arity
11
+ @curried = curried
12
+
13
+ if !variable_arity? && curried > arity
14
+ raise ArgumentError, "wrong number of arguments (#{curried} for #{arity})"
15
+ end
16
+
17
+ define_constructor if curried?
18
+
19
+ if variable_arity?
20
+ define_splat_application
21
+ else
22
+ define_fixed_application
23
+ end
24
+ end
25
+
26
+ def constant?
27
+ arity.zero?
28
+ end
29
+
30
+ def variable_arity?
31
+ arity.equal?(-1)
32
+ end
33
+
34
+ def curried?
35
+ !curried.zero?
36
+ end
37
+
38
+ def unapplied
39
+ if variable_arity?
40
+ -1
41
+ else
42
+ arity - curried
43
+ end
44
+ end
45
+
46
+ def name
47
+ if constant?
48
+ 'Constant'
49
+ else
50
+ arity_str = variable_arity? ? 'VariableArity' : "#{arity}Arity"
51
+ curried_str = curried? ? "#{curried}Curried" : EMPTY_STRING
52
+
53
+ "#{arity_str}#{curried_str}"
54
+ end
55
+ end
56
+
57
+ def define_constructor
58
+ assignment =
59
+ if curried.equal?(1)
60
+ '@arg0 = @args[0]'
61
+ else
62
+ "#{curried_args.join(', ')} = @args"
63
+ end
64
+
65
+ module_eval(<<~RUBY, __FILE__, __LINE__ + 1)
66
+ def initialize(*)
67
+ super
68
+
69
+ #{assignment}
70
+ end
71
+ RUBY
72
+ end
73
+
74
+ def define_splat_application
75
+ application =
76
+ if curried?
77
+ "@predicate[#{curried_args.join(', ')}, *input]"
78
+ else
79
+ '@predicate[*input]'
80
+ end
81
+
82
+ module_eval(<<~RUBY, __FILE__, __LINE__ + 1)
83
+ def call(*input)
84
+ if #{application}
85
+ Result::SUCCESS
86
+ else
87
+ Result.new(false, id) { ast(*input) }
88
+ end
89
+ end
90
+
91
+ def [](*input)
92
+ #{application}
93
+ end
94
+ RUBY
95
+ end
96
+
97
+ def define_fixed_application
98
+ parameters = unapplied_args.join(', ')
99
+ application = "@predicate[#{ (curried_args + unapplied_args).join(', ') }]"
100
+
101
+ module_eval(<<~RUBY, __FILE__, __LINE__ + 1)
102
+ def call(#{parameters})
103
+ if #{application}
104
+ Result::SUCCESS
105
+ else
106
+ Result.new(false, id) { ast(#{parameters}) }
107
+ end
108
+ end
109
+
110
+ def [](#{parameters})
111
+ #{application}
112
+ end
113
+ RUBY
114
+ end
115
+
116
+ def curried_args
117
+ @curried_args ||= ::Array.new(curried) { |i| "@arg#{i}" }
118
+ end
119
+
120
+ def unapplied_args
121
+ @unapplied_args ||= ::Array.new(unapplied) { |i| "input#{i}" }
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -3,6 +3,10 @@ require 'dry/logic/rule'
3
3
  module Dry
4
4
  module Logic
5
5
  class Rule::Predicate < Rule
6
+ def self.specialize(arity, curried, base = Predicate)
7
+ super
8
+ end
9
+
6
10
  def type
7
11
  :predicate
8
12
  end
@@ -52,7 +52,7 @@ module Dry
52
52
 
53
53
  def visit_predicate(node)
54
54
  name, params = node
55
- predicate = Rule::Predicate.new(predicates[name])
55
+ predicate = Rule::Predicate.build(predicates[name])
56
56
 
57
57
  if params.size > 1
58
58
  args = params.map(&:last).reject { |val| val == Undefined }
@@ -1,5 +1,5 @@
1
1
  module Dry
2
2
  module Logic
3
- VERSION = '0.5.0'.freeze
3
+ VERSION = '0.6.0'.freeze
4
4
  end
5
5
  end
@@ -9,7 +9,7 @@ RSpec.describe Result do
9
9
  end
10
10
 
11
11
  context 'with a predicate' do
12
- let(:rule) { Rule::Predicate.new(gt?, args: [18]) }
12
+ let(:rule) { Rule::Predicate.build(gt?, args: [18]) }
13
13
  let(:input) { 17 }
14
14
  let(:output) { 'gt?(18, 17)' }
15
15
 
@@ -17,7 +17,7 @@ RSpec.describe Result do
17
17
  end
18
18
 
19
19
  context 'with AND operation' do
20
- let(:rule) { Rule::Predicate.new(array?).and(Rule::Predicate.new(empty?)) }
20
+ let(:rule) { Rule::Predicate.build(array?).and(Rule::Predicate.build(empty?)) }
21
21
  let(:input) { '' }
22
22
  let(:output) { 'array?("") AND empty?("")' }
23
23
 
@@ -25,7 +25,7 @@ RSpec.describe Result do
25
25
  end
26
26
 
27
27
  context 'with OR operation' do
28
- let(:rule) { Rule::Predicate.new(array?).or(Rule::Predicate.new(empty?)) }
28
+ let(:rule) { Rule::Predicate.build(array?).or(Rule::Predicate.build(empty?)) }
29
29
  let(:input) { 123 }
30
30
  let(:output) { 'array?(123) OR empty?(123)' }
31
31
 
@@ -33,7 +33,7 @@ RSpec.describe Result do
33
33
  end
34
34
 
35
35
  context 'with XOR operation' do
36
- let(:rule) { Rule::Predicate.new(array?).xor(Rule::Predicate.new(empty?)) }
36
+ let(:rule) { Rule::Predicate.build(array?).xor(Rule::Predicate.build(empty?)) }
37
37
  let(:input) { [] }
38
38
  let(:output) { 'array?([]) XOR empty?([])' }
39
39
 
@@ -41,7 +41,7 @@ RSpec.describe Result do
41
41
  end
42
42
 
43
43
  context 'with THEN operation' do
44
- let(:rule) { Rule::Predicate.new(array?).then(Rule::Predicate.new(empty?)) }
44
+ let(:rule) { Rule::Predicate.build(array?).then(Rule::Predicate.build(empty?)) }
45
45
  let(:input) { [1, 2, 3] }
46
46
  let(:output) { 'empty?([1, 2, 3])' }
47
47
 
@@ -49,7 +49,7 @@ RSpec.describe Result do
49
49
  end
50
50
 
51
51
  context 'with NOT operation' do
52
- let(:rule) { Operations::Negation.new(Rule::Predicate.new(array?)) }
52
+ let(:rule) { Operations::Negation.new(Rule::Predicate.build(array?)) }
53
53
  let(:input) { 'foo' }
54
54
  let(:output) { 'not(array?("foo"))' }
55
55
 
@@ -1,11 +1,12 @@
1
1
  shared_examples_for Dry::Logic::Rule do
2
- let(:predicate) { double(:predicate, arity: 2, name: predicate_name) }
2
+ let(:arity) { 2 }
3
+ let(:predicate) { double(:predicate, arity: arity, name: predicate_name) }
3
4
  let(:rule_type) { described_class }
4
5
  let(:predicate_name) { :good? }
5
6
 
6
7
  describe '#arity' do
7
8
  it 'returns its predicate arity' do
8
- rule = rule_type.new(predicate)
9
+ rule = rule_type.build(predicate)
9
10
 
10
11
  expect(rule.arity).to be(2)
11
12
  end
@@ -13,15 +14,17 @@ shared_examples_for Dry::Logic::Rule do
13
14
 
14
15
  describe '#parameters' do
15
16
  it 'returns a list of args with their names' do
16
- rule = rule_type.new(-> foo, bar { true }, args: [312])
17
+ rule = rule_type.build(-> foo, bar { true }, args: [312])
17
18
 
18
19
  expect(rule.parameters).to eql([[:req, :foo], [:req, :bar]])
19
20
  end
20
21
  end
21
22
 
22
23
  describe '#call' do
24
+ let(:arity) { 1 }
25
+
23
26
  it 'returns success for valid input' do
24
- rule = rule_type.new(predicate)
27
+ rule = rule_type.build(predicate)
25
28
 
26
29
  expect(predicate).to receive(:[]).with(2).and_return(true)
27
30
 
@@ -29,7 +32,7 @@ shared_examples_for Dry::Logic::Rule do
29
32
  end
30
33
 
31
34
  it 'returns failure for invalid input' do
32
- rule = rule_type.new(predicate)
35
+ rule = rule_type.build(predicate)
33
36
 
34
37
  expect(predicate).to receive(:[]).with(2).and_return(false)
35
38
 
@@ -38,8 +41,10 @@ shared_examples_for Dry::Logic::Rule do
38
41
  end
39
42
 
40
43
  describe '#[]' do
44
+ let(:arity) { 1 }
45
+
41
46
  it 'delegates to its predicate' do
42
- rule = rule_type.new(predicate)
47
+ rule = rule_type.build(predicate)
43
48
 
44
49
  expect(predicate).to receive(:[]).with(2).and_return(true)
45
50
  expect(rule[2]).to be(true)
@@ -48,7 +53,7 @@ shared_examples_for Dry::Logic::Rule do
48
53
 
49
54
  describe '#curry' do
50
55
  it 'returns a curried rule' do
51
- rule = rule_type.new(predicate).curry(3)
56
+ rule = rule_type.build(predicate).curry(3)
52
57
 
53
58
  expect(predicate).to receive(:[]).with(3, 2).and_return(true)
54
59
  expect(rule.args).to eql([3])
@@ -59,7 +64,7 @@ shared_examples_for Dry::Logic::Rule do
59
64
  it 'raises argument error when arity does not match' do
60
65
  expect(predicate).to receive(:arity).and_return(2)
61
66
 
62
- expect { rule_type.new(predicate).curry(3, 2, 1) }.to raise_error(
67
+ expect { rule_type.build(predicate).curry(3, 2, 1) }.to raise_error(
63
68
  ArgumentError, 'wrong number of arguments (3 for 2)'
64
69
  )
65
70
  end
@@ -12,7 +12,7 @@ if RUBY_ENGINE == 'ruby' && ENV['COVERAGE'] == 'true'
12
12
  end
13
13
 
14
14
  begin
15
- require 'byebug'
15
+ require 'pry-byebug'
16
16
  rescue LoadError; end
17
17
 
18
18
  require 'dry-logic'
@@ -3,8 +3,8 @@ RSpec.describe Operations::And do
3
3
 
4
4
  include_context 'predicates'
5
5
 
6
- let(:left) { Rule::Predicate.new(int?) }
7
- let(:right) { Rule::Predicate.new(gt?).curry(18) }
6
+ let(:left) { Rule::Predicate.build(int?) }
7
+ let(:right) { Rule::Predicate.build(gt?).curry(18) }
8
8
 
9
9
  describe '#call' do
10
10
  it 'calls left and right' do
@@ -24,6 +24,10 @@ RSpec.describe Operations::And do
24
24
  [:and, [[:predicate, [:int?, [[:input, '18']]]], [:hint, [:predicate, [:gt?, [[:num, 18], [:input, '18']]]]]]]
25
25
  )
26
26
 
27
+ expect(operation.with(hints: false).('18').to_ast).to eql(
28
+ [:predicate, [:int?, [[:input, '18']]]]
29
+ )
30
+
27
31
  expect(operation.(18).to_ast).to eql(
28
32
  [:predicate, [:gt?, [[:num, 18], [:input, 18]]]]
29
33
  )
@@ -41,7 +45,7 @@ RSpec.describe Operations::And do
41
45
  end
42
46
 
43
47
  describe '#and' do
44
- let(:other) { Rule::Predicate.new(lt?).curry(30) }
48
+ let(:other) { Rule::Predicate.build(lt?).curry(30) }
45
49
 
46
50
  it 'creates and with the other' do
47
51
  expect(operation.and(other).(31)).to be_failure
@@ -49,7 +53,7 @@ RSpec.describe Operations::And do
49
53
  end
50
54
 
51
55
  describe '#or' do
52
- let(:other) { Rule::Predicate.new(lt?).curry(14) }
56
+ let(:other) { Rule::Predicate.build(lt?).curry(14) }
53
57
 
54
58
  it 'creates or with the other' do
55
59
  expect(operation.or(other).(13)).to be_success
@@ -1,5 +1,5 @@
1
1
  RSpec.describe Operations::Attr do
2
- subject(:operation) { Operations::Attr.new(Rule::Predicate.new(str?), name: :name) }
2
+ subject(:operation) { Operations::Attr.new(Rule::Predicate.build(str?), name: :name) }
3
3
 
4
4
  include_context 'predicates'
5
5
 
@@ -13,7 +13,7 @@ RSpec.describe Operations::Attr do
13
13
  end
14
14
 
15
15
  describe '#and' do
16
- let(:other) { Operations::Attr.new(Rule::Predicate.new(min_size?).curry(3), name: :name) }
16
+ let(:other) { Operations::Attr.new(Rule::Predicate.build(min_size?).curry(3), name: :name) }
17
17
 
18
18
  it 'returns and where value is passed to the right' do
19
19
  present_and_string = operation.and(other)
@@ -4,7 +4,7 @@ RSpec.describe Operations::Check do
4
4
  describe '#call' do
5
5
  context 'with 1-level nesting' do
6
6
  subject(:operation) do
7
- Operations::Check.new(Rule::Predicate.new(eql?).curry(1), id: :compare, keys: [:num])
7
+ Operations::Check.new(Rule::Predicate.build(eql?).curry(1), id: :compare, keys: [:num])
8
8
  end
9
9
 
10
10
  it 'applies predicate to args extracted from the input' do
@@ -15,7 +15,7 @@ RSpec.describe Operations::Check do
15
15
 
16
16
  context 'with 2-levels nesting' do
17
17
  subject(:operation) do
18
- Operations::Check.new(Rule::Predicate.new(eql?), id: :compare, keys: [[:nums, :left], [:nums, :right]])
18
+ Operations::Check.new(Rule::Predicate.build(eql?), id: :compare, keys: [[:nums, :left], [:nums, :right]])
19
19
  end
20
20
 
21
21
  it 'applies predicate to args extracted from the input' do
@@ -37,7 +37,7 @@ RSpec.describe Operations::Check do
37
37
 
38
38
  describe '#to_ast' do
39
39
  subject(:operation) do
40
- Operations::Check.new(Rule::Predicate.new(str?), keys: [:email])
40
+ Operations::Check.new(Rule::Predicate.build(str?), keys: [:email])
41
41
  end
42
42
 
43
43
  it 'returns ast' do
@@ -3,7 +3,7 @@ RSpec.describe Operations::Each do
3
3
 
4
4
  include_context 'predicates'
5
5
 
6
- let(:is_string) { Rule::Predicate.new(str?) }
6
+ let(:is_string) { Rule::Predicate.build(str?) }
7
7
 
8
8
  describe '#call' do
9
9
  it 'applies its rules to all elements in the input' do
@@ -3,8 +3,8 @@ RSpec.describe Operations::Implication do
3
3
 
4
4
  include_context 'predicates'
5
5
 
6
- let(:left) { Rule::Predicate.new(int?) }
7
- let(:right) { Rule::Predicate.new(gt?).curry(18) }
6
+ let(:left) { Rule::Predicate.build(int?) }
7
+ let(:right) { Rule::Predicate.build(gt?).curry(18) }
8
8
 
9
9
  describe '#call' do
10
10
  it 'calls left and right' do
@@ -4,7 +4,7 @@ RSpec.describe Operations::Key do
4
4
  include_context 'predicates'
5
5
 
6
6
  let(:predicate) do
7
- Rule::Predicate.new(key?).curry(:age)
7
+ Rule::Predicate.build(key?).curry(:age)
8
8
  end
9
9
 
10
10
  describe '#call' do
@@ -32,7 +32,7 @@ RSpec.describe Operations::Key do
32
32
  end
33
33
 
34
34
  let(:predicate) do
35
- Operations::Set.new(Rule::Predicate.new(key?).curry(:city), Rule::Predicate.new(key?).curry(:zipcode))
35
+ Operations::Set.new(Rule::Predicate.build(key?).curry(:city), Rule::Predicate.build(key?).curry(:zipcode))
36
36
  end
37
37
 
38
38
  it 'applies set rule to the value that passes' do
@@ -60,7 +60,7 @@ RSpec.describe Operations::Key do
60
60
  end
61
61
 
62
62
  let(:predicate) do
63
- Operations::Each.new(Rule::Predicate.new(str?))
63
+ Operations::Each.new(Rule::Predicate.build(str?))
64
64
  end
65
65
 
66
66
  it 'applies each rule to the value that passses' do
@@ -108,11 +108,11 @@ RSpec.describe Operations::Key do
108
108
 
109
109
  describe '#and' do
110
110
  subject(:operation) do
111
- Operations::Key.new(Rule::Predicate.new(str?), name: [:user, :name])
111
+ Operations::Key.new(Rule::Predicate.build(str?), name: [:user, :name])
112
112
  end
113
113
 
114
114
  let(:other) do
115
- Operations::Key.new(Rule::Predicate.new(filled?), name: [:user, :name])
115
+ Operations::Key.new(Rule::Predicate.build(filled?), name: [:user, :name])
116
116
  end
117
117
 
118
118
  it 'returns and rule where value is passed to the right' do
@@ -3,7 +3,7 @@ RSpec.describe Operations::Negation do
3
3
 
4
4
  include_context 'predicates'
5
5
 
6
- let(:is_int) { Rule::Predicate.new(int?) }
6
+ let(:is_int) { Rule::Predicate.build(int?) }
7
7
 
8
8
  describe '#call' do
9
9
  it 'negates its rule' do
@@ -3,11 +3,11 @@ RSpec.describe Operations::Or do
3
3
 
4
4
  include_context 'predicates'
5
5
 
6
- let(:left) { Rule::Predicate.new(nil?) }
7
- let(:right) { Rule::Predicate.new(gt?).curry(18) }
6
+ let(:left) { Rule::Predicate.build(nil?) }
7
+ let(:right) { Rule::Predicate.build(gt?).curry(18) }
8
8
 
9
9
  let(:other) do
10
- Rule::Predicate.new(int?) & Rule::Predicate.new(lt?).curry(14)
10
+ Rule::Predicate.build(int?) & Rule::Predicate.build(lt?).curry(14)
11
11
  end
12
12
 
13
13
  describe '#call' do
@@ -3,8 +3,8 @@ RSpec.describe Operations::Set do
3
3
 
4
4
  include_context 'predicates'
5
5
 
6
- let(:is_int) { Rule::Predicate.new(int?) }
7
- let(:gt_18) { Rule::Predicate.new(gt?, args: [18]) }
6
+ let(:is_int) { Rule::Predicate.build(int?) }
7
+ let(:gt_18) { Rule::Predicate.build(gt?, args: [18]) }
8
8
 
9
9
  describe '#call' do
10
10
  it 'applies all its rules to the input' do
@@ -3,11 +3,11 @@ RSpec.describe Operations::Xor do
3
3
 
4
4
  include_context 'predicates'
5
5
 
6
- let(:left) { Rule::Predicate.new(array?) }
7
- let(:right) { Rule::Predicate.new(empty?) }
6
+ let(:left) { Rule::Predicate.build(array?) }
7
+ let(:right) { Rule::Predicate.build(empty?) }
8
8
 
9
9
  let(:other) do
10
- Rule::Predicate.new(str?)
10
+ Rule::Predicate.build(str?)
11
11
  end
12
12
 
13
13
  describe '#call' do
@@ -1,5 +1,5 @@
1
1
  RSpec.describe Rule::Predicate do
2
- subject(:rule) { Rule::Predicate.new(predicate) }
2
+ subject(:rule) { Rule::Predicate.build(predicate) }
3
3
 
4
4
  let(:predicate) { str? }
5
5
 
@@ -27,11 +27,11 @@ RSpec.describe Rule::Predicate do
27
27
  end
28
28
 
29
29
  context 'with a result' do
30
- it 'returns success ast' do
31
- expect(rule.('foo').to_ast).to eql([:predicate, [:str?, [[:input, 'foo']]]])
30
+ it 'returns success' do
31
+ expect(rule.('foo')).to be_success
32
32
  end
33
33
 
34
- it 'returns ast' do
34
+ it 'returns failure ast' do
35
35
  expect(rule.(5).to_ast).to eql([:predicate, [:str?, [[:input, 5]]]])
36
36
  end
37
37
  end
@@ -13,7 +13,7 @@ RSpec.describe Dry::Logic::RuleCompiler, '#call' do
13
13
 
14
14
  let(:predicate) { double(:predicate, name: :test?, arity: 2).as_null_object }
15
15
 
16
- let(:rule) { Rule::Predicate.new(predicate) }
16
+ let(:rule) { Rule::Predicate.build(predicate) }
17
17
  let(:key_op) { Operations::Key.new(rule, name: :email) }
18
18
  let(:attr_op) { Operations::Attr.new(rule, name: :email) }
19
19
  let(:check_op) { Operations::Check.new(rule, keys: [:email]) }
@@ -1,11 +1,11 @@
1
1
  RSpec.describe Dry::Logic::Rule do
2
- subject(:rule) { Rule.new(predicate, options) }
2
+ subject(:rule) { Rule.build(predicate, options) }
3
3
 
4
4
  let(:predicate) { -> { true } }
5
5
  let(:options) { {} }
6
6
 
7
7
  let(:schema) do
8
- Class.new(BasicObject) do
8
+ Class.new do
9
9
  define_method(:class, Kernel.instance_method(:class))
10
10
 
11
11
  def method_missing(m, *)
@@ -34,29 +34,29 @@ RSpec.describe Dry::Logic::Rule do
34
34
 
35
35
  describe '.new' do
36
36
  it 'accepts an :id' do
37
- expect(Rule.new(predicate, id: :check_num).id).to be(:check_num)
37
+ expect(Rule.build(predicate, id: :check_num).id).to be(:check_num)
38
38
  end
39
39
  end
40
40
 
41
41
  describe 'with a function returning truthy value' do
42
42
  it 'is successful for valid input' do
43
- expect(Rule.new(-> val { val }).('true')).to be_success
43
+ expect(Rule.build(-> val { val }).('true')).to be_success
44
44
  end
45
45
 
46
46
  it 'is not successful for invalid input' do
47
- expect(Rule.new(-> val { val }).(nil)).to be_failure
47
+ expect(Rule.build(-> val { val }).(nil)).to be_failure
48
48
  end
49
49
  end
50
50
 
51
51
  describe '#ast' do
52
52
  it 'returns predicate node with :id' do
53
- expect(Rule.new(-> value { true }).with(id: :email?).ast('oops')).to eql(
53
+ expect(Rule.build(-> value { true }).with(id: :email?).ast('oops')).to eql(
54
54
  [:predicate, [:email?, [[:value, 'oops']]]]
55
55
  )
56
56
  end
57
57
 
58
58
  it 'returns predicate node with undefined args' do
59
- expect(Rule.new(-> value { true }).with(id: :email?).ast).to eql(
59
+ expect(Rule.build(-> value { true }).with(id: :email?).ast).to eql(
60
60
  [:predicate, [:email?, [[:value, Undefined]]]]
61
61
  )
62
62
  end
@@ -120,7 +120,7 @@ RSpec.describe Dry::Logic::Rule do
120
120
 
121
121
  describe '#eval_args' do
122
122
  context 'with an unbound method' do
123
- let(:options) { { args: [1, klass.instance_method(:num), :foo] } }
123
+ let(:options) { { args: [1, klass.instance_method(:num), :foo], arity: 3 } }
124
124
  let(:klass) { Class.new { def num; 7; end } }
125
125
  let(:object) { klass.new }
126
126
 
@@ -130,7 +130,7 @@ RSpec.describe Dry::Logic::Rule do
130
130
  end
131
131
 
132
132
  context 'with a schema instance' do
133
- let(:options) { { args: [1, schema, :foo] } }
133
+ let(:options) { { args: [1, schema, :foo], arity: 3 } }
134
134
  let(:object) { Object.new }
135
135
 
136
136
  it 'returns a new with its predicate executed in the context of the provided object' do
@@ -138,4 +138,65 @@ RSpec.describe Dry::Logic::Rule do
138
138
  end
139
139
  end
140
140
  end
141
+
142
+ describe 'arity specialization' do
143
+ describe '0-arity rule' do
144
+ let(:options) { { args: [1], arity: 1 } }
145
+ let(:predicate) { :odd?.to_proc }
146
+
147
+ it 'generates interface with the right arity' do
148
+ expect(rule.method(:call).arity).to be_zero
149
+ expect(rule.method(:[]).arity).to be_zero
150
+ expect(rule[]).to be(true)
151
+ expect(rule.()).to be_success
152
+ end
153
+ end
154
+
155
+ describe '1-arity rule' do
156
+ let(:options) { { args: [1], arity: 2 } }
157
+ let(:predicate) { -> a, b { a + b } }
158
+
159
+ it 'generates interface with the right arity' do
160
+ expect(rule.method(:call).arity).to be(1)
161
+ expect(rule.method(:[]).arity).to be(1)
162
+ expect(rule[10]).to be(11)
163
+ expect(rule.(1)).to be_success
164
+ end
165
+ end
166
+
167
+ describe 'currying' do
168
+ let(:options) { { args: [], arity: 2 } }
169
+ let(:predicate) { -> a, b { a + b } }
170
+ let(:rule) { super().curry(1) }
171
+
172
+ it 'generates correct arity on currying' do
173
+ expect(rule.method(:call).arity).to be(1)
174
+ expect(rule.method(:[]).arity).to be(1)
175
+ expect(rule[10]).to be(11)
176
+ expect(rule.(1)).to be_success
177
+ end
178
+ end
179
+
180
+ describe 'arbitrary arity' do
181
+ let(:arity) { rand(20) }
182
+ let(:curried) { arity.zero? ? 0 : rand(arity) }
183
+
184
+ let(:options) { { args: [1] * curried, arity: arity } }
185
+ let(:predicate) { double(:predicate) }
186
+
187
+ it 'generates correct arity' do
188
+ expect(rule.method(:call).arity).to be(arity - curried)
189
+ expect(rule.method(:[]).arity).to be(arity - curried)
190
+ end
191
+ end
192
+
193
+ describe '-1 arity' do
194
+ let(:options) { { args: [], arity: -1 } }
195
+
196
+ it 'accepts variable number of arguments' do
197
+ expect(rule.method(:call).arity).to be(-1)
198
+ expect(rule.method(:[]).arity).to be(-1)
199
+ end
200
+ end
201
+ end
141
202
  end
metadata CHANGED
@@ -1,39 +1,36 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dry-logic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Solnica
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-01-29 00:00:00.000000000 Z
11
+ date: 2019-04-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: dry-core
14
+ name: concurrent-ruby
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0.2'
19
+ version: '1.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0.2'
26
+ version: '1.0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: dry-container
28
+ name: dry-core
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
33
  version: '0.2'
34
- - - ">="
35
- - !ruby/object:Gem::Version
36
- version: 0.2.6
37
34
  type: :runtime
38
35
  prerelease: false
39
36
  version_requirements: !ruby/object:Gem::Requirement
@@ -41,9 +38,6 @@ dependencies:
41
38
  - - "~>"
42
39
  - !ruby/object:Gem::Version
43
40
  version: '0.2'
44
- - - ">="
45
- - !ruby/object:Gem::Version
46
- version: 0.2.6
47
41
  - !ruby/object:Gem::Dependency
48
42
  name: dry-equalizer
49
43
  requirement: !ruby/object:Gem::Requirement
@@ -110,8 +104,6 @@ files:
110
104
  - ".codeclimate.yml"
111
105
  - ".gitignore"
112
106
  - ".rspec"
113
- - ".rubocop.yml"
114
- - ".rubocop_todo.yml"
115
107
  - ".travis.yml"
116
108
  - CHANGELOG.md
117
109
  - CONTRIBUTING.md
@@ -119,6 +111,8 @@ files:
119
111
  - LICENSE
120
112
  - README.md
121
113
  - Rakefile
114
+ - benchmarks/rule_application.rb
115
+ - benchmarks/setup.rb
122
116
  - bin/console
123
117
  - dry-logic.gemspec
124
118
  - examples/basic.rb
@@ -144,6 +138,7 @@ files:
144
138
  - lib/dry/logic/predicates.rb
145
139
  - lib/dry/logic/result.rb
146
140
  - lib/dry/logic/rule.rb
141
+ - lib/dry/logic/rule/interface.rb
147
142
  - lib/dry/logic/rule/predicate.rb
148
143
  - lib/dry/logic/rule_compiler.rb
149
144
  - lib/dry/logic/version.rb
@@ -221,7 +216,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
221
216
  - !ruby/object:Gem::Version
222
217
  version: '0'
223
218
  requirements: []
224
- rubygems_version: 3.0.2
219
+ rubygems_version: 3.0.3
225
220
  signing_key:
226
221
  specification_version: 4
227
222
  summary: Predicate logic with rule composition
@@ -1,16 +0,0 @@
1
- # Generated by `rubocop --auto-gen-config`
2
- inherit_from: .rubocop_todo.yml
3
-
4
- Metrics/LineLength:
5
- Max: 100
6
-
7
- Style/Documentation:
8
- Enabled: false
9
-
10
- Lint/HandleExceptions:
11
- Exclude:
12
- - rakelib/*.rake
13
-
14
- Style/FileName:
15
- Exclude:
16
- - lib/dry-logic.rb
@@ -1,7 +0,0 @@
1
- # This configuration was generated by
2
- # `rubocop --auto-gen-config`
3
- # on 2015-10-30 01:32:46 +0000 using RuboCop version 0.34.2.
4
- # The point is for the user to remove these configuration records
5
- # one by one as the offenses are removed from the code base.
6
- # Note that changes in the inspected code, or installation of new
7
- # versions of RuboCop, may require this file to be generated again.