dry-logic 0.5.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1a6ff887f61b513496991a96d7903685886ab29dae71cb4f75185cd7866fd4b2
4
- data.tar.gz: 9e063f346341b395ac21645f84319e619af19151e62bd97413badf246440d353
3
+ metadata.gz: cc0793c95681757b6d069c1d7c6bc59278457069edac35f329845dc283e69c22
4
+ data.tar.gz: e46cd2a8f4e03c78778f35e51a3c616a2dc32881eb703aa6b441da1d34603733
5
5
  SHA512:
6
- metadata.gz: f643a0238e3b67c91c83af1e90cb4398279b4c01957ce663a5186ae8437a87e94c9d9cb2f7a2aabca1c7edb47be28a531317a044e58e270ffff59c44e6a9ccf5
7
- data.tar.gz: 17b0ff20fc5c41258a537ad0c57e959cd3359765801261157772a62ee5d1ce525c15bca2ce92c8ee624b1fda67a5cf6cbf4228af552cf99efc8e8428026dbccb
6
+ metadata.gz: 732504942a364347befcdc7bee439777a0fd60b470b288cd793d9a21835ff13cc5399b2b6fd514ca0d586708fe044acd664dc2403b6a6a821851b4183533f494
7
+ data.tar.gz: 811ba73afd3bc3f0bb13511ca69fa6af54a24638a7dc02cbc867df7529a276ed7b559c4cae7863b8b21a529b2095f8bbfc33eb027ea6f80e1d29076656cf13de
@@ -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
@@ -3,5 +3,7 @@ coverage
3
3
  /.bundle
4
4
  vendor/bundle
5
5
  tmp/
6
+ pkg/
6
7
  .idea/
7
8
  Gemfile.lock
9
+ .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.3
14
+ - 2.5.5
15
+ - 2.4.6
16
+ - jruby-9.2.7.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,27 @@
1
+ # v1.0.0 2019-04-23
2
+
3
+ Version bump to 1.0.0
4
+
5
+ [Compare v0.6.1...v1.0.0](https://github.com/dry-rb/dry-logic/compare/v0.6.1...v1.0.0)
6
+
7
+ # v0.6.1 2019-04-18
8
+
9
+ * Fix a regression in dry-validation 0.x for argument-less predicates (flash-gordon)
10
+
11
+ [Compare v0.6.0...v0.6.1](https://github.com/dry-rb/dry-logic/compare/v0.6.0...v0.6.1)
12
+
13
+ # v0.6.0 2019-04-04
14
+
15
+ ### Added
16
+
17
+ * Generating hints can be disabled by building `Operations::And` with `hints: false` option set (solnic)
18
+
19
+ ### Changed
20
+
21
+ * `Rule` construction has been optimized so that currying and application is multiple-times faster (flash-gordon)
22
+
23
+ [Compare v0.5.0...v0.6.0](https://github.com/dry-rb/dry-logic/compare/v0.5.0...v0.6.0)
24
+
1
25
  # v0.5.0 2019-01-29
2
26
 
3
27
  ### 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-logic [![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,144 @@
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
+ elsif constant?
22
+ define_constant_application
23
+ else
24
+ define_fixed_application
25
+ end
26
+ end
27
+
28
+ def constant?
29
+ arity.zero?
30
+ end
31
+
32
+ def variable_arity?
33
+ arity.equal?(-1)
34
+ end
35
+
36
+ def curried?
37
+ !curried.zero?
38
+ end
39
+
40
+ def unapplied
41
+ if variable_arity?
42
+ -1
43
+ else
44
+ arity - curried
45
+ end
46
+ end
47
+
48
+ def name
49
+ if constant?
50
+ 'Constant'
51
+ else
52
+ arity_str = variable_arity? ? 'VariableArity' : "#{arity}Arity"
53
+ curried_str = curried? ? "#{curried}Curried" : EMPTY_STRING
54
+
55
+ "#{arity_str}#{curried_str}"
56
+ end
57
+ end
58
+
59
+ def define_constructor
60
+ assignment =
61
+ if curried.equal?(1)
62
+ '@arg0 = @args[0]'
63
+ else
64
+ "#{curried_args.join(', ')} = @args"
65
+ end
66
+
67
+ module_eval(<<~RUBY, __FILE__, __LINE__ + 1)
68
+ def initialize(*)
69
+ super
70
+
71
+ #{assignment}
72
+ end
73
+ RUBY
74
+ end
75
+
76
+ def define_constant_application
77
+ module_exec do
78
+ def call(*)
79
+ if @predicate[]
80
+ Result::SUCCESS
81
+ else
82
+ Result.new(false, id) { ast }
83
+ end
84
+ end
85
+
86
+ def [](*)
87
+ @predicate[]
88
+ end
89
+ end
90
+ end
91
+
92
+ def define_splat_application
93
+ application =
94
+ if curried?
95
+ "@predicate[#{curried_args.join(', ')}, *input]"
96
+ else
97
+ '@predicate[*input]'
98
+ end
99
+
100
+ module_eval(<<~RUBY, __FILE__, __LINE__ + 1)
101
+ def call(*input)
102
+ if #{application}
103
+ Result::SUCCESS
104
+ else
105
+ Result.new(false, id) { ast(*input) }
106
+ end
107
+ end
108
+
109
+ def [](*input)
110
+ #{application}
111
+ end
112
+ RUBY
113
+ end
114
+
115
+ def define_fixed_application
116
+ parameters = unapplied_args.join(', ')
117
+ application = "@predicate[#{ (curried_args + unapplied_args).join(', ') }]"
118
+
119
+ module_eval(<<~RUBY, __FILE__, __LINE__ + 1)
120
+ def call(#{parameters})
121
+ if #{application}
122
+ Result::SUCCESS
123
+ else
124
+ Result.new(false, id) { ast(#{parameters}) }
125
+ end
126
+ end
127
+
128
+ def [](#{parameters})
129
+ #{application}
130
+ end
131
+ RUBY
132
+ end
133
+
134
+ def curried_args
135
+ @curried_args ||= ::Array.new(curried) { |i| "@arg#{i}" }
136
+ end
137
+
138
+ def unapplied_args
139
+ @unapplied_args ||= ::Array.new(unapplied) { |i| "input#{i}" }
140
+ end
141
+ end
142
+ end
143
+ end
144
+ 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 = '1.0.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,74 @@ 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
+
202
+ describe 'constants' do
203
+ let(:options) { { args: [], arity: 0 } }
204
+
205
+ fit 'accepts variable number of arguments' do
206
+ expect(rule.method(:call).arity).to be(-1)
207
+ expect(rule.method(:[]).arity).to be(-1)
208
+ end
209
+ end
210
+ end
141
211
  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: 1.0.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-23 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.