calificador 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +97 -0
- data/lib/calificador.rb +44 -0
- data/lib/calificador/assertor.rb +134 -0
- data/lib/calificador/build/attribute.rb +20 -0
- data/lib/calificador/build/attribute_container.rb +103 -0
- data/lib/calificador/build/attribute_evaluator.rb +85 -0
- data/lib/calificador/build/factory.rb +132 -0
- data/lib/calificador/build/trait.rb +20 -0
- data/lib/calificador/key.rb +59 -0
- data/lib/calificador/minitest/minitest_patches.rb +27 -0
- data/lib/calificador/spec/basic_context.rb +353 -0
- data/lib/calificador/spec/class_method_context.rb +42 -0
- data/lib/calificador/spec/condition_context.rb +10 -0
- data/lib/calificador/spec/examine_context.rb +29 -0
- data/lib/calificador/spec/instance_method_context.rb +38 -0
- data/lib/calificador/spec/test_environment.rb +141 -0
- data/lib/calificador/spec/test_method.rb +64 -0
- data/lib/calificador/spec/test_root.rb +44 -0
- data/lib/calificador/spec/type_context.rb +29 -0
- data/lib/calificador/spec/value_override.rb +37 -0
- data/lib/calificador/test.rb +12 -0
- data/lib/calificador/test_mixin.rb +264 -0
- data/lib/calificador/util/call_formatter.rb +41 -0
- data/lib/calificador/util/class_mixin.rb +14 -0
- data/lib/calificador/util/core_extensions.rb +135 -0
- data/lib/calificador/util/missing.rb +26 -0
- data/lib/calificador/util/nil.rb +59 -0
- data/lib/calificador/version.rb +5 -0
- metadata +257 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6f7600ea7641198f91585844a8701550ba999fb7b2e91c49a4d0c4e1728429d7
|
4
|
+
data.tar.gz: 9b81427ec6996cb065868cac1d0406f0af6262163c442f33b193709d838498a9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 27229e0b80d7da65f9c9c039070b2aa42981a9d64408e2c687ec7ee1af24a82ec70eb678d03ff8e3c1d57272cc8ce45734236991b4b325ea824a2597afb356b2
|
7
|
+
data.tar.gz: 2d0ef75a9ed7549a0deaeb5d5b62c8d277db62b8dc2c3a7ac3f1f5c8e33e9b325f3a56a7c426eff5a469dd491cdf2839c5007170f92321f3d26e94ff2484507b
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2020 Jochen Seeber
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
## What is it?
|
2
|
+
|
3
|
+
A small Gem that lets you write concise and readable unit tests. It is heavily inspired by [Minitest], [Factory Bot], and [Assertive Expressive] but tries to boil everything down to be as concise as possible:
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
module ReadMe
|
7
|
+
require "calificador"
|
8
|
+
|
9
|
+
# Define a test class
|
10
|
+
class HighTea
|
11
|
+
attr_accessor :scones, :posh
|
12
|
+
|
13
|
+
def eat_scone
|
14
|
+
raise "Out of scones" if scones == 0
|
15
|
+
|
16
|
+
@scones -= 1
|
17
|
+
end
|
18
|
+
|
19
|
+
def tea
|
20
|
+
@posh ? "Darjeeling First Flush" : "Earl Grey"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Unit test
|
25
|
+
class HighTeaTest < Calificador::Test
|
26
|
+
examines HighTea
|
27
|
+
|
28
|
+
# Define a factory for the test subject
|
29
|
+
factory HighTea do
|
30
|
+
scones { 2 }
|
31
|
+
posh { false }
|
32
|
+
|
33
|
+
# Use raits to define variants of your test subject
|
34
|
+
trait :style do
|
35
|
+
posh { true }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Define test method
|
40
|
+
must "have tea and scones" do
|
41
|
+
# Write assertions using plain Ruby methods instead of spec DSL methods
|
42
|
+
refute { subject.tea }.nil?
|
43
|
+
assert { subject.scones } > 0
|
44
|
+
end
|
45
|
+
|
46
|
+
# Get nice test names. This one will be called "HighTea must have me let a scone"
|
47
|
+
must "let me have a scone" do
|
48
|
+
count = subject.scones
|
49
|
+
subject.eat_scone
|
50
|
+
assert { subject.scones } == count - 1
|
51
|
+
end
|
52
|
+
|
53
|
+
# Adjust test subject using traits or properties for minor variations
|
54
|
+
must "complain if out of scones", scones: 0 do
|
55
|
+
assert { subject.eat_scone }.raises?(StandardError)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Create subcontexts for variations of the test subject using traits and properties
|
59
|
+
with :style do
|
60
|
+
# Still nice test names. This one is "HighTea with style should have expensive tea"
|
61
|
+
must "have expensive tea" do
|
62
|
+
assert { subject.tea }.include?("First Flush")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
ReadMe::HighTeaTest.run_all_tests
|
69
|
+
```
|
70
|
+
|
71
|
+
[Minitest]: https://github.com/seattlerb/minitest
|
72
|
+
[Factory Bot]: https://github.com/thoughtbot/factory_bot
|
73
|
+
[Shoulda Context]: https://github.com/thoughtbot/shoulda-context
|
74
|
+
[Assertive Expressive]: https://github.com/rubyworks/ae
|
75
|
+
|
76
|
+
## Why?
|
77
|
+
|
78
|
+
Calificador is an experiment in getting rid of as much mental load, boilerplate and distractions as possible. It tries to create a simple, easy to learn and easy to understand DSL for unit tests.
|
79
|
+
|
80
|
+
Only a handful of DSL methods are required:
|
81
|
+
|
82
|
+
* Test structure: examine, must, where/with/without
|
83
|
+
* Factory methods: factory, traits
|
84
|
+
* Assertions: assert, refute and raises?
|
85
|
+
|
86
|
+
It also tries to keep things simple by avoiding all the special assertion and expectation methods you need to learn for other test frameworks. Instead of having to learn that `include` is the RSpec matcher for `include?`, or Minitest's `assert_match` is used to assert regular expression matches, you can write `refute { something }.include? "value"` or `assert { something }.match %r{pattern}` using Ruby's normal `String#match` method.
|
87
|
+
|
88
|
+
|
89
|
+
## Frequently asked questions
|
90
|
+
|
91
|
+
Q: What's with the name?
|
92
|
+
|
93
|
+
A: In the [Spanish Inquisition](https://en.wikipedia.org/wiki/Spanish_Inquisition), a defendant was examined by calificadores, who determined if there was heresy involved.
|
94
|
+
|
95
|
+
Q: The Spanish Inquisition? I did not expect that.
|
96
|
+
|
97
|
+
A: Well, [nobody expects the Spanish Inquisition](https://www.youtube.com/watch?v=sAn7baRbhx4).
|
data/lib/calificador.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "zeitwerk"
|
4
|
+
require "minitest"
|
5
|
+
|
6
|
+
loader = Zeitwerk::Loader.for_gem
|
7
|
+
loader.setup
|
8
|
+
|
9
|
+
# Main module
|
10
|
+
module Calificador
|
11
|
+
using Calificador::Util::CoreExtensions
|
12
|
+
|
13
|
+
MISSING = Util::Missing.instance
|
14
|
+
|
15
|
+
class << self
|
16
|
+
attr_writer :call_formatter
|
17
|
+
|
18
|
+
def call_formatter
|
19
|
+
@call_formatter ||= Calificador::Util::CallFormatter.new
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.test(subject_type, trait: Key::INHERITED_TRAIT, &block)
|
24
|
+
raise "Subject type must be a #{Module}" unless subject_type.is_a?(Module)
|
25
|
+
|
26
|
+
test_class = Class.new(Calificador::Test)
|
27
|
+
test_class_name = "#{subject_type.base_name}Test"
|
28
|
+
test_module = subject_type.parent_module
|
29
|
+
|
30
|
+
if test_module
|
31
|
+
test_module.const_set(test_class_name, test_class)
|
32
|
+
else
|
33
|
+
Kernel.const_set(test_class_name, test_class)
|
34
|
+
end
|
35
|
+
|
36
|
+
test_class.examines(subject_type, trait)
|
37
|
+
test_class.class_eval(&block)
|
38
|
+
|
39
|
+
test_class
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
Minitest::Assertion.prepend(Calificador::Minitest::MinitestPatches::AssertionMethods)
|
44
|
+
Class.include(Calificador::Util::ClassMixin)
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "minitest"
|
4
|
+
|
5
|
+
module Calificador
|
6
|
+
class Assertor < BasicObject
|
7
|
+
def initialize(test:, negated: false, block: nil)
|
8
|
+
@test = test
|
9
|
+
@negated = negated ? true : false
|
10
|
+
@block = block
|
11
|
+
@value = MISSING
|
12
|
+
@triggered = false
|
13
|
+
end
|
14
|
+
|
15
|
+
def respond_to?(method, include_all = false)
|
16
|
+
__value.respond_to?(method, include_all) || super
|
17
|
+
end
|
18
|
+
|
19
|
+
def method_missing(method, *arguments, **options, &block) # rubocop:disable Style/MissingRespondToMissing
|
20
|
+
if __value.respond_to?(method)
|
21
|
+
__check(method: method, arguments: arguments, options: options, &block)
|
22
|
+
else
|
23
|
+
super
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def ==(other)
|
28
|
+
message = ::Kernel.proc do
|
29
|
+
actual = ::Calificador.call_formatter.value(value: __value)
|
30
|
+
expected = ::Calificador.call_formatter.value(value: other)
|
31
|
+
|
32
|
+
"Expected #{actual} (#{__value.class}) to#{@negated ? " not" : ""} be equal to #{expected} (#{other.class})"
|
33
|
+
end
|
34
|
+
|
35
|
+
__check(method: :"==", message: message, arguments: [other])
|
36
|
+
end
|
37
|
+
|
38
|
+
def !=(other)
|
39
|
+
message = ::Kernel.proc do
|
40
|
+
actual = ::Calificador.call_formatter.value(value: __value)
|
41
|
+
expected = ::Calificador.call_formatter.value(value: other)
|
42
|
+
|
43
|
+
"Expected #{actual} (#{__value.class}) to#{@negated ? "" : " not"} be equal to #{expected} (#{other.class})"
|
44
|
+
end
|
45
|
+
|
46
|
+
__check(method: :"!=", message: message, arguments: [other])
|
47
|
+
end
|
48
|
+
|
49
|
+
def identical?(other)
|
50
|
+
message = ::Kernel.proc do
|
51
|
+
actual = ::Calificador.call_formatter.value(value: __value)
|
52
|
+
expected = ::Calificador.call_formatter.value(value: other)
|
53
|
+
|
54
|
+
"Expected #{actual} (#{__value.class}) to#{@negated ? " not" : ""} be identical to #{expected} (#{other.class})"
|
55
|
+
end
|
56
|
+
|
57
|
+
__check(method: :"equal?", message: message, arguments: [other])
|
58
|
+
end
|
59
|
+
|
60
|
+
def not
|
61
|
+
Assertor.new(test: @test, negated: !@negated, block: @block)
|
62
|
+
end
|
63
|
+
|
64
|
+
def raises?(*exception_classes, &block)
|
65
|
+
@triggered = true
|
66
|
+
|
67
|
+
block ||= @block
|
68
|
+
|
69
|
+
::Kernel.raise ::ArgumentError, "Exception classes must not be empty" if exception_classes.empty?
|
70
|
+
::Kernel.raise ::ArgumentError, "Exception assert must have a block" if block.nil?
|
71
|
+
|
72
|
+
message = ::Kernel.proc do
|
73
|
+
actual = ::Calificador.call_formatter.value(value: @block)
|
74
|
+
"Expected #{actual} (#{@block}) to#{@negated ? " not" : ""} raise #{exception_classes.join(", ")}"
|
75
|
+
end
|
76
|
+
|
77
|
+
begin
|
78
|
+
result = block.call
|
79
|
+
|
80
|
+
@test.assert(@negated, message)
|
81
|
+
|
82
|
+
result
|
83
|
+
rescue *exception_classes => e
|
84
|
+
@test.refute(@negated, @test.exception_details(e, message.call))
|
85
|
+
e
|
86
|
+
rescue ::Minitest::Assertion, ::SignalException, ::SystemExit
|
87
|
+
::Kernel.raise
|
88
|
+
rescue ::Exception => e # rubocop:disable Lint/RescueException
|
89
|
+
@test.assert(@negated, @test.exception_details(e, message.call))
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def __check_triggered
|
94
|
+
unless @triggered
|
95
|
+
::Kernel.raise ::StandardError, <<~MESSAGE.gsub("\n", " ")
|
96
|
+
Assertor (#{@block.source_location.join(":")}) was not triggered. You probably need to call a method to
|
97
|
+
check for something, our your check must be changed to use a method not defined on BasicObject.
|
98
|
+
MESSAGE
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
protected
|
103
|
+
|
104
|
+
def __value
|
105
|
+
if @value.equal?(MISSING)
|
106
|
+
::Kernel.raise ::StandardError, "No block set for assertion" if @block.nil?
|
107
|
+
|
108
|
+
@value = @block.call
|
109
|
+
end
|
110
|
+
|
111
|
+
@value
|
112
|
+
end
|
113
|
+
|
114
|
+
def __check(method:, message: nil, arguments: [], options: {}, &block)
|
115
|
+
@triggered = true
|
116
|
+
|
117
|
+
result = __value.send(method, *arguments, **options, &block)
|
118
|
+
|
119
|
+
message ||= ::Kernel.proc do
|
120
|
+
actual = ::Calificador.call_formatter.value(value: __value)
|
121
|
+
call = ::Calificador.call_formatter.method(method: method, arguments: arguments, options: options)
|
122
|
+
"Expected #{actual} (#{__value.class}) to#{@negated ? " not" : ""} #{call}"
|
123
|
+
end
|
124
|
+
|
125
|
+
if @negated
|
126
|
+
@test.refute(result, message)
|
127
|
+
else
|
128
|
+
@test.assert(result, message)
|
129
|
+
end
|
130
|
+
|
131
|
+
result
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Calificador
|
4
|
+
module Build
|
5
|
+
# Attribute description
|
6
|
+
class Attribute
|
7
|
+
TYPES = %i[property transient init].freeze
|
8
|
+
|
9
|
+
attr_reader :name, :type, :config
|
10
|
+
|
11
|
+
def initialize(name:, type:, config:)
|
12
|
+
raise "Illegal property type #{type}. Valid types are #{TYPES.join(", ")}" unless TYPES.include?(type.to_sym)
|
13
|
+
|
14
|
+
@name = name.to_sym
|
15
|
+
@type = type.to_sym
|
16
|
+
@config = config
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
using Calificador::Util::CoreExtensions
|
4
|
+
|
5
|
+
module Calificador
|
6
|
+
module Build
|
7
|
+
# Factory calss
|
8
|
+
class AttributeContainer
|
9
|
+
class Dsl
|
10
|
+
def initialize(delegate:)
|
11
|
+
@delegate = delegate
|
12
|
+
@property_type = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_attribute(name, type: nil, &config)
|
16
|
+
type ||= @delegate.parent&.attribute(name: name)&.type || @property_type || :property
|
17
|
+
@delegate.add_attribute(Attribute.new(name: name, type: type, config: config))
|
18
|
+
end
|
19
|
+
|
20
|
+
def transient(&block)
|
21
|
+
raise ArgumentError, "Transient requires a block" if block.nil?
|
22
|
+
|
23
|
+
old_property_type = @property_type
|
24
|
+
@property_type = :transient
|
25
|
+
|
26
|
+
begin
|
27
|
+
instance_exec(self, &block)
|
28
|
+
ensure
|
29
|
+
@property_type = old_property_type
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def init_with(&block)
|
34
|
+
raise "Initializer requires a block to create the object" if block.nil?
|
35
|
+
|
36
|
+
@delegate.init_with = block
|
37
|
+
end
|
38
|
+
|
39
|
+
def before_create(&block)
|
40
|
+
raise "Before requires a block to call" if block.nil?
|
41
|
+
|
42
|
+
@delegate.before_create = block
|
43
|
+
end
|
44
|
+
|
45
|
+
def after_create(&block)
|
46
|
+
raise "After requires a block to call" if block.nil?
|
47
|
+
|
48
|
+
@delegate.after_create = block
|
49
|
+
end
|
50
|
+
|
51
|
+
def respond_to_missing?(method, include_all = false)
|
52
|
+
if method.start_with?("__")
|
53
|
+
super
|
54
|
+
else
|
55
|
+
true
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def method_missing(method, *arguments, &block)
|
60
|
+
if method.start_with?("__")
|
61
|
+
super
|
62
|
+
else
|
63
|
+
unless arguments.empty?
|
64
|
+
raise ::ArgumentError, <<~ERROR
|
65
|
+
Attribute #{method} cannot have arguments. Please use a block to configure the value
|
66
|
+
ERROR
|
67
|
+
end
|
68
|
+
|
69
|
+
raise ::ArgumentError, "Attribute #{method} must have a block to provide the value" if block.nil?
|
70
|
+
|
71
|
+
add_attribute(method, &block)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
attr_reader :parent, :description
|
77
|
+
attr_accessor :init_with, :before_create, :after_create
|
78
|
+
|
79
|
+
def initialize(parent:, description:)
|
80
|
+
@parent = parent
|
81
|
+
@description = description.dup.freeze
|
82
|
+
@attributes = {}
|
83
|
+
@init_with = nil
|
84
|
+
@before_create = nil
|
85
|
+
@after_create = nil
|
86
|
+
end
|
87
|
+
|
88
|
+
def attributes
|
89
|
+
@attributes.dup.freeze
|
90
|
+
end
|
91
|
+
|
92
|
+
def attribute(name:)
|
93
|
+
@attributes[name]
|
94
|
+
end
|
95
|
+
|
96
|
+
def add_attribute(attribute)
|
97
|
+
raise KeyError, "Duplicate attribute name #{name}" if @attributes.key?(attribute.name)
|
98
|
+
|
99
|
+
@attributes[attribute.name] = attribute
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
using Calificador::Util::CoreExtensions
|
4
|
+
|
5
|
+
module Calificador
|
6
|
+
module Build
|
7
|
+
class AttributeEvaluator
|
8
|
+
EVALUATING = Object.new.freeze
|
9
|
+
|
10
|
+
class Proxy
|
11
|
+
def initialize(delegate:)
|
12
|
+
@delegate = delegate
|
13
|
+
end
|
14
|
+
|
15
|
+
def create(type, trait = nil)
|
16
|
+
key = Key[type, trait]
|
17
|
+
@delegate.create_object(key: key)
|
18
|
+
end
|
19
|
+
|
20
|
+
def respond_to_missing?(method, include_all)
|
21
|
+
@delegate.attribute?(name: method) ? true : super
|
22
|
+
end
|
23
|
+
|
24
|
+
def method_missing(method, *arguments, &block)
|
25
|
+
if @delegate.attribute?(name: method)
|
26
|
+
raise ArgumentError, "Getter must not be called with arguments" unless arguments.empty?
|
27
|
+
|
28
|
+
@delegate.value(name: method)
|
29
|
+
else
|
30
|
+
super
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
attr_reader :attributes, :values
|
36
|
+
|
37
|
+
def initialize(context:)
|
38
|
+
@context = context
|
39
|
+
@attributes = {}
|
40
|
+
@values = {}
|
41
|
+
@proxy = Proxy.new(delegate: self)
|
42
|
+
end
|
43
|
+
|
44
|
+
def create_object(key:)
|
45
|
+
@context.create_object(key: key)
|
46
|
+
end
|
47
|
+
|
48
|
+
def add_values(values)
|
49
|
+
@values.merge!(values)
|
50
|
+
end
|
51
|
+
|
52
|
+
def add_attributes(attributes)
|
53
|
+
attributes.each do |attribute|
|
54
|
+
@attributes[attribute.name] = attribute
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def value(name:)
|
59
|
+
result = @values.fetch(name) do
|
60
|
+
@values[name] = EVALUATING
|
61
|
+
|
62
|
+
begin
|
63
|
+
config = @attributes.fetch(name).config
|
64
|
+
@values[name] = evaluate(&config)
|
65
|
+
rescue StandardError
|
66
|
+
@values.delete(name)
|
67
|
+
raise
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
raise StandardError, "Endless recursion while evaluating attribute #{name}" if result == EVALUATING
|
72
|
+
|
73
|
+
result
|
74
|
+
end
|
75
|
+
|
76
|
+
def evaluate(*arguments, **options, &block)
|
77
|
+
block.invoke_with_target(@proxy, *arguments, **options)
|
78
|
+
end
|
79
|
+
|
80
|
+
def attribute?(name:)
|
81
|
+
@attributes.key?(name)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|