calificador 0.1.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 +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
|