attr_magic 0.1.2
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/.gitignore +26 -0
- data/.rspec +3 -0
- data/.yardopts +6 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +52 -0
- data/MIT-LICENSE +20 -0
- data/README-ru.md +212 -0
- data/README.md +216 -0
- data/Rakefile +15 -0
- data/attr_magic.gemspec +17 -0
- data/lib/attr_magic/instance_methods.rb +78 -0
- data/lib/attr_magic/version.rb +4 -0
- data/lib/attr_magic.rb +54 -0
- data/libx/README.md +4 -0
- data/libx/rspec_magic/config.rb +17 -0
- data/libx/rspec_magic/stable/alias_method.rb +27 -0
- data/libx/rspec_magic/stable/context_when.rb +108 -0
- data/libx/rspec_magic/stable/use_letset.rb +101 -0
- data/libx/rspec_magic/stable/use_method_discovery.rb +65 -0
- data/libx/rspec_magic/stable.rb +2 -0
- data/libx/rspec_magic.rb +9 -0
- data/spec/lib/attr_magic_spec.rb +140 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/support/its.rb +2 -0
- data/spec/support/rspec_magic.rb +2 -0
- data/spec/support/self.rb +2 -0
- data/spec/support/simplecov.rb +11 -0
- metadata +71 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c5c17464091f895e95a549137979f6be96d48eae
|
4
|
+
data.tar.gz: f17e82aad2ebf8d6e28937c46b71e373038f488f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4c689dbcef43970967cfb0db1c2fb4ce98cfdb132ed7cea4da80a6acb8f0ed447bd47680cc496d0db0c0ad9eb36f1a2eaf9f7164c2a685eeb563bd96e813bbd4
|
7
|
+
data.tar.gz: b4772ada55e0fb12de57f589b06b1d9418b1e20717a3ca7326980d4bd2aa86931e773a924fc1814629b14e172ee2b61f362469bbbd0b28d4bc190fbf9ea1bcbf
|
data/.gitignore
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
|
2
|
+
# General Ruby, sorted by first letter.
|
3
|
+
/.bundle/
|
4
|
+
/coverage/
|
5
|
+
/doc/
|
6
|
+
/vendor/bundle/
|
7
|
+
/.yardoc/
|
8
|
+
|
9
|
+
# Project-specific.
|
10
|
+
/*.gem
|
11
|
+
|
12
|
+
# Old etc.
|
13
|
+
*.old*
|
14
|
+
*.orig
|
15
|
+
/*.patch
|
16
|
+
*.ref*
|
17
|
+
|
18
|
+
# Nonpersistent stuff.
|
19
|
+
TODO.md
|
20
|
+
|
21
|
+
# Shared VS Code settings.
|
22
|
+
/.vscode/*
|
23
|
+
!/.vscode/*.json
|
24
|
+
|
25
|
+
# Custom VS Code settings.
|
26
|
+
/*.code-workspace
|
data/.rspec
ADDED
data/.yardopts
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
GEM
|
2
|
+
remote: https://rubygems.org/
|
3
|
+
specs:
|
4
|
+
activesupport (5.2.8.1)
|
5
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
6
|
+
i18n (>= 0.7, < 2)
|
7
|
+
minitest (~> 5.1)
|
8
|
+
tzinfo (~> 1.1)
|
9
|
+
concurrent-ruby (1.2.2)
|
10
|
+
diff-lcs (1.5.0)
|
11
|
+
docile (1.3.5)
|
12
|
+
i18n (1.14.1)
|
13
|
+
concurrent-ruby (~> 1.0)
|
14
|
+
its (0.2.0)
|
15
|
+
rspec-core
|
16
|
+
json (2.6.3)
|
17
|
+
minitest (5.15.0)
|
18
|
+
rspec (3.12.0)
|
19
|
+
rspec-core (~> 3.12.0)
|
20
|
+
rspec-expectations (~> 3.12.0)
|
21
|
+
rspec-mocks (~> 3.12.0)
|
22
|
+
rspec-core (3.12.2)
|
23
|
+
rspec-support (~> 3.12.0)
|
24
|
+
rspec-expectations (3.12.3)
|
25
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
26
|
+
rspec-support (~> 3.12.0)
|
27
|
+
rspec-mocks (3.12.6)
|
28
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
29
|
+
rspec-support (~> 3.12.0)
|
30
|
+
rspec-support (3.12.1)
|
31
|
+
simplecov (0.17.1)
|
32
|
+
docile (~> 1.1)
|
33
|
+
json (>= 1.8, < 3)
|
34
|
+
simplecov-html (~> 0.10.0)
|
35
|
+
simplecov-html (0.10.2)
|
36
|
+
thread_safe (0.3.6)
|
37
|
+
tzinfo (1.2.11)
|
38
|
+
thread_safe (~> 0.1)
|
39
|
+
yard (0.9.34)
|
40
|
+
|
41
|
+
PLATFORMS
|
42
|
+
ruby
|
43
|
+
|
44
|
+
DEPENDENCIES
|
45
|
+
activesupport
|
46
|
+
its
|
47
|
+
rspec
|
48
|
+
simplecov
|
49
|
+
yard
|
50
|
+
|
51
|
+
BUNDLED WITH
|
52
|
+
1.17.3
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2017-2023 Alex Fortuna
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README-ru.md
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
|
2
|
+
# Инструменты для реализации атрибутов-вычислителей
|
3
|
+
|
4
|
+
<!-- @import "[TOC]" {cmd="toc" depthFrom=2 depthTo=6 orderedList=false} -->
|
5
|
+
|
6
|
+
<!-- code_chunk_output -->
|
7
|
+
|
8
|
+
- [Введение](#введение)
|
9
|
+
- [Что даёт AttrMagic?](#что-даёт-attrmagic)
|
10
|
+
- [Установка](#установка)
|
11
|
+
- [Пример использования](#пример-использования)
|
12
|
+
- [`#igetset`](#igetset)
|
13
|
+
- [`#require_attr`](#require_attr)
|
14
|
+
- [`#igetwrite`](#igetwrite)
|
15
|
+
- [Copyright](#copyright)
|
16
|
+
|
17
|
+
<!-- /code_chunk_output -->
|
18
|
+
|
19
|
+
## Введение
|
20
|
+
|
21
|
+
*An English version of this text is also available: [README.md](README.md).*
|
22
|
+
|
23
|
+
Атрибут-вычислитель (lazy attribute) — это метод-accessor, который производит некое вычисление при первом вызове,
|
24
|
+
мемоизирует результат в instance-переменную и возвращает результат. Например:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
class Person
|
28
|
+
# @return [String]
|
29
|
+
attr_accessor :first_name, :last_name
|
30
|
+
|
31
|
+
attr_writer :full_name, :is_admin
|
32
|
+
|
33
|
+
def initialize(attrs = {})
|
34
|
+
attrs.each { |k, v| public_send("#{k}=", v) }
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [String]
|
38
|
+
def full_name
|
39
|
+
@full_name ||= begin
|
40
|
+
[first_name, last_name].compact.join(" ").strip
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [Boolean]
|
45
|
+
def is_admin
|
46
|
+
return @is_admin if defined? @is_admin
|
47
|
+
@is_admin = !!full_name.match(/admin/i)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
```
|
51
|
+
|
52
|
+
В примере выше `#full_name` и `#is_admin` — атрибуты-вычислители.
|
53
|
+
|
54
|
+
### Что даёт AttrMagic?
|
55
|
+
|
56
|
+
Классам, содержащим атрибуты-вычислители, AttrMagic даёт:
|
57
|
+
|
58
|
+
1. `#igetset` и `#igetwrite` для простой мемоизации любых значений, включая `false` и `nil`.
|
59
|
+
2. `#require_attr` для валидации атрибутов, от которых зависит результат данного вычисления.
|
60
|
+
|
61
|
+
## Установка
|
62
|
+
|
63
|
+
Добавляем в `Gemfile` нашего проекта:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
gem "attr_magic"
|
67
|
+
#gem "attr_magic", git: "https://github.com/dadooda/attr_magic"
|
68
|
+
```
|
69
|
+
|
70
|
+
## Пример использования
|
71
|
+
|
72
|
+
Чтобы использовать фичу, загружаем её в класс:
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
class Person
|
76
|
+
AttrMagic.load(self)
|
77
|
+
…
|
78
|
+
end
|
79
|
+
```
|
80
|
+
|
81
|
+
Методы AttrMagic теперь доступны в классе `Person`.
|
82
|
+
Теперь рассмотрим, какие инструменты стали нам доступны.
|
83
|
+
|
84
|
+
### `#igetset`
|
85
|
+
|
86
|
+
В примере выше метод `#full_name` мемоизирует результат оператором `||=`.
|
87
|
+
Это вполне приемлемо, ведь результат вычисления — строка.
|
88
|
+
|
89
|
+
А вот реализация `#is_admin` гораздо более громоздка, ведь результат
|
90
|
+
может быть вычислен как `false`, стало быть `||=` не подойдёт.
|
91
|
+
|
92
|
+
Оба метода можно переписать с использованием `#igetset`:
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
class Person
|
96
|
+
…
|
97
|
+
def full_name
|
98
|
+
igetset(__method__) { [first_name, last_name].compact.join(" ").strip }
|
99
|
+
end
|
100
|
+
|
101
|
+
def is_admin
|
102
|
+
igetset(__method__) { !!full_name.match(/admin/i) }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
Методы приобрели короткий, единообразный, легко читаемый вид.
|
108
|
+
Теперь вычисление в `#is_admin` отчётливо видно, тогда как ранее оно тонуло в повторах
|
109
|
+
*is_admin* внутри метода, который и так назван этим словом.
|
110
|
+
|
111
|
+
### `#require_attr`
|
112
|
+
|
113
|
+
В примере выше метод `#first_name` возвращает пустую строку, даже если атрибуты
|
114
|
+
`first_name` и `last_name` не присвоены или пусты.
|
115
|
+
|
116
|
+
С точки зрения внятности это поведение «на грани фола».
|
117
|
+
Ведь, скорее всего, результат `#full_name` будет выводиться в блоке информации о персоне или при обращении к персоне.
|
118
|
+
Пустая строка, даже правомерно вычисленная, вызовет в этой ситуации, как минимум, непонимание.
|
119
|
+
|
120
|
+
Конечно, экземпляр `Person` не виноват, что перед вызовом `#full_name` в нём не присвоили `first_name` и `last_name`.
|
121
|
+
Как говорится, garbage in — garbage out.
|
122
|
+
|
123
|
+
Однако, чем тупо «вредничать», возвращая невнятную пустоту, `#full_name` мог бы
|
124
|
+
помочь разработчику выявить эту ситуацию, чётко о ней просигналив.
|
125
|
+
|
126
|
+
Предположим, мы решили, что для корректного вычисления `#full_name` нам необходим,
|
127
|
+
как минимум, непустой `first_name`. Реализация может выглядеть так:
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
class Person
|
131
|
+
…
|
132
|
+
def full_name
|
133
|
+
igetset(__method__) do
|
134
|
+
require_attr :first_name
|
135
|
+
[first_name, last_name].compact.join(" ").strip
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
```
|
140
|
+
|
141
|
+
Теперь посмотрим, как это работает:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
Person.new.full_name
|
145
|
+
# RuntimeError: Attribute `first_name` must not be nil: nil
|
146
|
+
```
|
147
|
+
|
148
|
+
Вроде неплохо. Вместо пустоты мы получили исключение с указанием на причину отказа:
|
149
|
+
не присвоен `first_name`. Смущают, однако, слова про `nil`, хотя речь идёт о строковом атрибуте.
|
150
|
+
А вдруг в `first_name` пустая строка, что тогда? Пробуем:
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
Person.new(first_name: "").full_name
|
154
|
+
# => ""
|
155
|
+
|
156
|
+
Person.new(first_name: " ").full_name
|
157
|
+
# => ""
|
158
|
+
```
|
159
|
+
|
160
|
+
На пустую строку наш `require_attr` пока не реагирует, хотя суть требования была в том,
|
161
|
+
чтобы `first_name` был именно *не пустой.* Чуть-чуть доработаем код:
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
# Нужно для `Object#present?`.
|
165
|
+
require "active_support/core_ext/object/blank"
|
166
|
+
|
167
|
+
class Person
|
168
|
+
…
|
169
|
+
def full_name
|
170
|
+
igetset(__method__) do
|
171
|
+
require_attr :first_name, :present?
|
172
|
+
#require_attr :first_name, :not_blank? # Можно так.
|
173
|
+
[first_name, last_name].compact.join(" ").strip
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
```
|
178
|
+
|
179
|
+
Снова пробуем вызвать:
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
Person.new.full_name
|
183
|
+
# RuntimeError: Attribute `first_name` must be present: nil
|
184
|
+
|
185
|
+
Person.new(first_name: " ").full_name
|
186
|
+
# RuntimeError: Attribute `first_name` must be present: " "
|
187
|
+
|
188
|
+
Person.new(first_name: "Joe").full_name
|
189
|
+
# => "Joe"
|
190
|
+
```
|
191
|
+
|
192
|
+
Теперь и сообщение чётче, и требование выполнено.
|
193
|
+
|
194
|
+
Мы узнали, что `#require_attr` даёт возможность выполнить тривиальную валидацию
|
195
|
+
соседнего атрибута, значение которого нужно в данном вычислении.
|
196
|
+
|
197
|
+
### `#igetwrite`
|
198
|
+
|
199
|
+
Описанный в примере выше, метод `#igetset` взаимодействует с instance-переменными напрямую:
|
200
|
+
проверяет наличие, читает, записывает. В большинстве случаев этого достаточно.
|
201
|
+
|
202
|
+
Бывает, однако, что мы добавляем наш метод-вычислитель в класс, требующий записи в свои атрибуты
|
203
|
+
строго через write-аксессоры вроде `#name=`.
|
204
|
+
|
205
|
+
В таких случаях помогает `#igetwrite`.
|
206
|
+
Выполнив вычисление, он записывает значение в объект через write accessor.
|
207
|
+
|
208
|
+
## Copyright
|
209
|
+
|
210
|
+
Продукт распространяется свободно на условиях лицензии MIT.
|
211
|
+
|
212
|
+
— © 2017-2023 Алексей Фортуна
|
data/README.md
ADDED
@@ -0,0 +1,216 @@
|
|
1
|
+
|
2
|
+
# The tools to ease lazy attribute implementation
|
3
|
+
|
4
|
+
<!-- @import "[TOC]" {cmd="toc" depthFrom=2 depthTo=6 orderedList=false} -->
|
5
|
+
|
6
|
+
<!-- code_chunk_output -->
|
7
|
+
|
8
|
+
- [Overview](#overview)
|
9
|
+
- [What does AttrMagic provide?](#what-does-attrmagic-provide)
|
10
|
+
- [Setup](#setup)
|
11
|
+
- [Usage example](#usage-example)
|
12
|
+
- [`#igetset`](#igetset)
|
13
|
+
- [`#require_attr`](#require_attr)
|
14
|
+
- [`#igetwrite`](#igetwrite)
|
15
|
+
- [Copyright](#copyright)
|
16
|
+
|
17
|
+
<!-- /code_chunk_output -->
|
18
|
+
|
19
|
+
## Overview
|
20
|
+
|
21
|
+
*Этот текст можно прочитать на русском языке: [README-ru.md](README-ru.md).*
|
22
|
+
|
23
|
+
A lazy attribute is an accessor method that performs some computation the first time it is called,
|
24
|
+
memoizes the result into an instance variable, and returns the result. Example:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
class Person
|
28
|
+
# @return [String]
|
29
|
+
attr_accessor :first_name, :last_name
|
30
|
+
|
31
|
+
attr_writer :full_name, :is_admin
|
32
|
+
|
33
|
+
def initialize(attrs = {})
|
34
|
+
attrs.each { |k, v| public_send("#{k}=", v) }
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [String]
|
38
|
+
def full_name
|
39
|
+
@full_name ||= begin
|
40
|
+
[first_name, last_name].compact.join(" ").strip
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [Boolean]
|
45
|
+
def is_admin
|
46
|
+
return @is_admin if defined? @is_admin
|
47
|
+
@is_admin = !!full_name.match(/admin/i)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
```
|
51
|
+
|
52
|
+
Methods `#full_name` и `#is_admin` are lazy attributes.
|
53
|
+
|
54
|
+
### What does AttrMagic provide?
|
55
|
+
|
56
|
+
For classes that have lazy attributes, AttrMagic provides:
|
57
|
+
|
58
|
+
1. `#igetset` and `#igetwrite` for simple memoization of any values, including `false` and `nil`.
|
59
|
+
2. `#require_attr` to validate attributes which are required by the given computation.
|
60
|
+
|
61
|
+
## Setup
|
62
|
+
|
63
|
+
Add to your project's `Gemfile`:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
gem "attr_magic"
|
67
|
+
#gem "attr_magic", git: "https://github.com/dadooda/attr_magic"
|
68
|
+
```
|
69
|
+
|
70
|
+
## Usage example
|
71
|
+
|
72
|
+
To use feature, let's load it into a class:
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
class Person
|
76
|
+
AttrMagic.load(self)
|
77
|
+
…
|
78
|
+
end
|
79
|
+
```
|
80
|
+
|
81
|
+
AttrMagic methods are now available in the `Person` class.
|
82
|
+
Let's see how we can use them.
|
83
|
+
|
84
|
+
### `#igetset`
|
85
|
+
|
86
|
+
In the example above, method `#full_name` memoizes the result with the `||=` operator.
|
87
|
+
This suits us, because the result of the computation is a string.
|
88
|
+
|
89
|
+
As to `#is_admin`, its much more verbose: the result can be `false`,
|
90
|
+
thus operator `||=` won't work.
|
91
|
+
|
92
|
+
Both methods can be written using `#igetset`:
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
class Person
|
96
|
+
…
|
97
|
+
def full_name
|
98
|
+
igetset(__method__) { [first_name, last_name].compact.join(" ").strip }
|
99
|
+
end
|
100
|
+
|
101
|
+
def is_admin
|
102
|
+
igetset(__method__) { !!full_name.match(/admin/i) }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
Now our methods are short, uniform, and easy to read.
|
108
|
+
|
109
|
+
Also, the computation in `#is_admin` is clearly visible, whereas previously it was obscured
|
110
|
+
by repetitions of *is_admin* inside the method already named by this word.
|
111
|
+
|
112
|
+
### `#require_attr`
|
113
|
+
|
114
|
+
In the example above, method `#first_name` returns an empty string even if attributes
|
115
|
+
`first_name` and `last_name` are unassigned or blank.
|
116
|
+
|
117
|
+
Such behavior cannot be considered completely sane.
|
118
|
+
Most likely, the result of `#full_name` will be displayed in the info block about a person
|
119
|
+
or be used when addressing a person.
|
120
|
+
An empty string, even “legitimately” computed, may cause confusion.
|
121
|
+
|
122
|
+
Of course, it's not the `Person` instance's fault that neither `first_name` nor `last_name`
|
123
|
+
were assigned values prior to calling `#full_name`. Garbage in — garbage out.
|
124
|
+
|
125
|
+
However, rather than behaving deliberately “harmful” by returning inarticulate blankness,
|
126
|
+
`#full_name` could be *helpful* by signalling about this situation and helping us tackle it promptly.
|
127
|
+
|
128
|
+
Suppose we decided, that in order to compute `#full_name` properly,
|
129
|
+
we at least need a non-blank `first_name`. An implementation could look like this:
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
class Person
|
133
|
+
…
|
134
|
+
def full_name
|
135
|
+
igetset(__method__) do
|
136
|
+
require_attr :first_name
|
137
|
+
[first_name, last_name].compact.join(" ").strip
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
```
|
142
|
+
|
143
|
+
Let's see how it works:
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
Person.new.full_name
|
147
|
+
# RuntimeError: Attribute `first_name` must not be nil: nil
|
148
|
+
```
|
149
|
+
|
150
|
+
Not bad! Instead of getting dumb blankness, we've got an exception pointing straight at the reason:
|
151
|
+
unassigned `first_name`. However, the `nil` phrasing might sound a bit confusing
|
152
|
+
when talking about string values.
|
153
|
+
Also, what if `first_name` is assigned, but is empty or blank? Let's see:
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
Person.new(first_name: "").full_name
|
157
|
+
# => ""
|
158
|
+
|
159
|
+
Person.new(first_name: " ").full_name
|
160
|
+
# => ""
|
161
|
+
```
|
162
|
+
|
163
|
+
Our `require_attr` doesn't react to an empty/blank string yet,
|
164
|
+
despite our decision to ensure that `first_name` is *non-blank*.
|
165
|
+
Let's tune the code a bit:
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
# We need this for `Object#present?`.
|
169
|
+
require "active_support/core_ext/object/blank"
|
170
|
+
|
171
|
+
class Person
|
172
|
+
…
|
173
|
+
def full_name
|
174
|
+
igetset(__method__) do
|
175
|
+
require_attr :first_name, :present?
|
176
|
+
#require_attr :first_name, :not_blank? # Also possible.
|
177
|
+
[first_name, last_name].compact.join(" ").strip
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
```
|
182
|
+
|
183
|
+
Let's try it again:
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
Person.new.full_name
|
187
|
+
# RuntimeError: Attribute `first_name` must be present: nil
|
188
|
+
|
189
|
+
Person.new(first_name: " ").full_name
|
190
|
+
# RuntimeError: Attribute `first_name` must be present: " "
|
191
|
+
|
192
|
+
Person.new(first_name: "Joe").full_name
|
193
|
+
# => "Joe"
|
194
|
+
```
|
195
|
+
|
196
|
+
Now the message is more meaningful and the requirement is met.
|
197
|
+
|
198
|
+
We've learned that `#require_attr` makes it possible to perform a trivial validation
|
199
|
+
of another attribute, needed for a given computation to succeed.
|
200
|
+
|
201
|
+
### `#igetwrite`
|
202
|
+
|
203
|
+
Method `#igetset`, described above, operates instance variables directly:
|
204
|
+
checks for being defined, gets and sets. This is sufficient in most cases.
|
205
|
+
|
206
|
+
Sometimes, however, proper setting of an attribute demands calling its write accessor,
|
207
|
+
such as `#name=`.
|
208
|
+
|
209
|
+
In such cases, `#igetwrite` comes in handy.
|
210
|
+
After performing the computation, it saves the value to the object by calling a write accessor.
|
211
|
+
|
212
|
+
## Copyright
|
213
|
+
|
214
|
+
The product is free software distributed under the terms of the MIT license.
|
215
|
+
|
216
|
+
— © 2017-2023 Alex Fortuna
|
data/Rakefile
ADDED
data/attr_magic.gemspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
|
2
|
+
require_relative "lib/attr_magic/version"
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "attr_magic"
|
6
|
+
s.summary = "The tools to ease lazy attribute implementation"
|
7
|
+
s.version = AttrMagic::VERSION
|
8
|
+
|
9
|
+
s.authors = ["Alex Fortuna"]
|
10
|
+
s.email = ["fortunadze@gmail.com"]
|
11
|
+
s.homepage = "https://github.com/dadooda/attr_magic"
|
12
|
+
s.license = "MIT"
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.require_paths = ["lib"]
|
16
|
+
s.test_files = `git ls-files -- {spec}/*`.split("\n")
|
17
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AttrMagic
|
4
|
+
module InstanceMethods
|
5
|
+
# Memoize a lazy attribute, given its computation block.
|
6
|
+
#
|
7
|
+
# def full_name
|
8
|
+
# igetset(__method__) { [first_name, last_name].compact.join(" ").strip }
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# @param [Symbol | String] name
|
12
|
+
# @return [mixed] The result of +compute+.
|
13
|
+
# @see #igetwrite
|
14
|
+
def igetset(name, &compute)
|
15
|
+
if instance_variable_defined?(k = "@#{name}")
|
16
|
+
instance_variable_get(k)
|
17
|
+
else
|
18
|
+
raise ArgumentError, "Code block must be given" unless compute
|
19
|
+
instance_variable_set(k, compute.call)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Same as {#igetset}, but this one calls an attribute writer to store the computed
|
24
|
+
# value into the object.
|
25
|
+
# @param [Symbol | String] name
|
26
|
+
# @return [mixed] The result of +compute+.
|
27
|
+
# @see #igetset
|
28
|
+
def igetwrite(name, &compute)
|
29
|
+
if instance_variable_defined?(k = "@#{name}")
|
30
|
+
instance_variable_get(k)
|
31
|
+
else
|
32
|
+
raise ArgumentError, "Code block must be given" unless compute
|
33
|
+
send("#{name}=", compute.call)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Require an attribute to be set, present, valid or not invalid.
|
38
|
+
#
|
39
|
+
# require_attr(:name) # Require not to be `.nil?`.
|
40
|
+
# require_attr(:obj, :valid) # Require to be `.valid`.
|
41
|
+
# require_attr(:items, :present?) # Require to be `.present?`.
|
42
|
+
# require_attr(:items, :not_empty?) # Require not to be `.empty?`.
|
43
|
+
#
|
44
|
+
# @param name [Symbol | String]
|
45
|
+
# @param predicate [Symbol | String]
|
46
|
+
# @return [mixed] Attribute value.
|
47
|
+
# @raise [RuntimeError]
|
48
|
+
def require_attr(name, predicate = :not_nil?)
|
49
|
+
# Declare in the scope.
|
50
|
+
m = nil
|
51
|
+
|
52
|
+
# `check` is a function returning `true` if the value is good.
|
53
|
+
m, verb, check = if ((sp = predicate.to_s).start_with? "not_")
|
54
|
+
[
|
55
|
+
sp[4..-1],
|
56
|
+
"must not",
|
57
|
+
-> (v) { !v.public_send(m) },
|
58
|
+
]
|
59
|
+
else
|
60
|
+
[
|
61
|
+
sp,
|
62
|
+
"must",
|
63
|
+
-> (v) { v.public_send(m) },
|
64
|
+
]
|
65
|
+
end
|
66
|
+
|
67
|
+
raise ArgumentError, "Invalid predicate: #{predicate.inspect}" if m.empty?
|
68
|
+
|
69
|
+
# NOTE: Shorten the error backtrace to the minimum.
|
70
|
+
|
71
|
+
# Get and check the value.
|
72
|
+
v = send(name)
|
73
|
+
check.(v) or raise "Attribute `#{name}` #{verb} be #{m.chomp('?')}: #{v.inspect}"
|
74
|
+
|
75
|
+
v
|
76
|
+
end
|
77
|
+
end # InstanceMethods
|
78
|
+
end
|
data/lib/attr_magic.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
"LODoc"
|
4
|
+
|
5
|
+
# Ease lazy attribute implementation to the owner class.
|
6
|
+
#
|
7
|
+
# = Usage
|
8
|
+
#
|
9
|
+
# class Klass
|
10
|
+
# AttrMagic.load(self)
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# = Features
|
14
|
+
#
|
15
|
+
# == Implement a lazy attribute reader
|
16
|
+
#
|
17
|
+
# class Person
|
18
|
+
# …
|
19
|
+
# attr_writer :full_name
|
20
|
+
#
|
21
|
+
# def full_name
|
22
|
+
# igetset(__method__) { [first_name, last_name].compact.join(" ").strip }
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# See {InstanceMethods#igetset}, {InstanceMethods#igetwrite}.
|
27
|
+
#
|
28
|
+
# == Validate an attribute
|
29
|
+
#
|
30
|
+
# class Person
|
31
|
+
# …
|
32
|
+
# def full_name
|
33
|
+
# igetset(__method__) do
|
34
|
+
# #require_attr :first_name # Will check for `nil` only.
|
35
|
+
# require_attr :first_name, :present?
|
36
|
+
# #require_attr :first_name, :not_blank? # Also possible.
|
37
|
+
# [first_name, last_name].compact.join(" ").strip
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# See {InstanceMethods#require_attr}.
|
43
|
+
module AttrMagic
|
44
|
+
# Load the feature into +owner+.
|
45
|
+
# @param owner [Class]
|
46
|
+
def self.load(owner)
|
47
|
+
return if owner < InstanceMethods
|
48
|
+
owner.send(:include, InstanceMethods)
|
49
|
+
owner.class_eval { private :igetset, :igetwrite, :require_attr }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Load all.
|
54
|
+
Dir[File.expand_path("attr_magic/**/*.rb", __dir__)].each { |fn| require fn }
|
data/libx/README.md
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
"LODoc"
|
4
|
+
|
5
|
+
module RSpecMagic
|
6
|
+
# Shared configuration.
|
7
|
+
module Config
|
8
|
+
class << self
|
9
|
+
attr_writer :spec_path
|
10
|
+
|
11
|
+
# @return [String]
|
12
|
+
def spec_path
|
13
|
+
@spec_path || raise("`#{__method__}` must be configured")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
"LODoc"
|
4
|
+
|
5
|
+
module RSpecMagic; module Stable
|
6
|
+
# Method alias matcher.
|
7
|
+
# Originally from https://gist.github.com/1950961, but heavily reworked consistency-wise.
|
8
|
+
#
|
9
|
+
# = Usage
|
10
|
+
#
|
11
|
+
# describe User do
|
12
|
+
# it { is_expected.to alias_method(:admin?, :is_admin) }
|
13
|
+
# end
|
14
|
+
module AliasMethod
|
15
|
+
end
|
16
|
+
|
17
|
+
# Activate.
|
18
|
+
defined?(RSpec) and RSpec::Matchers.define(:alias_method) do |new_name, old_name|
|
19
|
+
match do |subject|
|
20
|
+
expect(subject.method(new_name)).to eq subject.method(old_name)
|
21
|
+
end
|
22
|
+
|
23
|
+
description do
|
24
|
+
"have #{new_name.inspect} aliased to #{old_name.inspect}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end; end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
"LODoc"
|
4
|
+
|
5
|
+
module RSpecMagic; module Stable
|
6
|
+
# Create a self-descriptive "when …" context with one or more +let+ variables defined.
|
7
|
+
#
|
8
|
+
# = Usage
|
9
|
+
#
|
10
|
+
# The following:
|
11
|
+
#
|
12
|
+
# context_when name: "Joe", age: 25 do
|
13
|
+
# …
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# is identical to:
|
17
|
+
#
|
18
|
+
# context "when { name: \"Joe\", age: 25 }" do
|
19
|
+
# let(:name) { "Joe" }
|
20
|
+
# let(:age) { 25 }
|
21
|
+
# …
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# = Features
|
25
|
+
#
|
26
|
+
# Prepend +x+ to +context_when+ to exclude it:
|
27
|
+
#
|
28
|
+
# xcontext_when … do
|
29
|
+
# …
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# ----
|
33
|
+
#
|
34
|
+
# Define a custom formatter via +_context_when_formatter+:
|
35
|
+
#
|
36
|
+
# context "…" do
|
37
|
+
# def self._context_when_formatter(h)
|
38
|
+
# "when #{h.to_json}"
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# …
|
42
|
+
# end
|
43
|
+
module ContextWhen
|
44
|
+
# Default formatter for {#context_when}. Redefine at the context level if needed.
|
45
|
+
#
|
46
|
+
# describe "…" do
|
47
|
+
# def self._context_when_formatter(h)
|
48
|
+
# # Your custom formatter here.
|
49
|
+
# h.to_json
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# context_when … do
|
53
|
+
# …
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
# @param [Hash] h
|
57
|
+
# @return [String]
|
58
|
+
def _context_when_formatter(h)
|
59
|
+
# Extract labels for Proc arguments, if any.
|
60
|
+
labels = {}
|
61
|
+
h.each do |k, v|
|
62
|
+
if v.is_a? Proc
|
63
|
+
begin
|
64
|
+
labels[k] = h.fetch(lk = "#{k}_label".to_sym)
|
65
|
+
h.delete(lk)
|
66
|
+
rescue KeyError
|
67
|
+
raise ArgumentError, "`#{k}` is a `Proc`, `#{k}_label` must be given"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
pcs = h.map do |k, v|
|
73
|
+
[
|
74
|
+
k.is_a?(Symbol) ? "#{k}:" : "#{k.inspect} =>",
|
75
|
+
v.is_a?(Proc) ? labels[k] : v.inspect,
|
76
|
+
].join(" ")
|
77
|
+
end
|
78
|
+
|
79
|
+
"when { " + pcs.join(", ") + " }"
|
80
|
+
end
|
81
|
+
|
82
|
+
# Create a context.
|
83
|
+
# @param [Hash] h
|
84
|
+
def context_when(h, &block)
|
85
|
+
context _context_when_formatter(h) do
|
86
|
+
h.each do |k, v|
|
87
|
+
if v.is_a? Proc
|
88
|
+
let(k, &v)
|
89
|
+
else
|
90
|
+
# Generic scalar value.
|
91
|
+
let(k) { v }
|
92
|
+
end
|
93
|
+
end
|
94
|
+
class_eval(&block)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Create a temporarily excluded context.
|
99
|
+
def xcontext_when(h, &block)
|
100
|
+
xcontext _context_when_formatter(h) { class_eval(&block) }
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Activate.
|
105
|
+
defined?(RSpec) and RSpec.configure do |config|
|
106
|
+
config.extend ContextWhen
|
107
|
+
end
|
108
|
+
end; end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
"LODoc"
|
4
|
+
|
5
|
+
module RSpecMagic; module Stable
|
6
|
+
# Define methods to manage a set of custom +let+ variables which act as a distinct collection.
|
7
|
+
#
|
8
|
+
# RSpec.describe SomeKlass do
|
9
|
+
# use_letset :let_a, :attrs # (1)
|
10
|
+
# use_letset :let_p, :params # (2)
|
11
|
+
# ...
|
12
|
+
#
|
13
|
+
# In above examples, (1) provides our suite with:
|
14
|
+
#
|
15
|
+
# def self.let_a(let, &block)
|
16
|
+
# def attrs(include: [])
|
17
|
+
#
|
18
|
+
# , thus this now becomes possible:
|
19
|
+
#
|
20
|
+
# describe "attrs" do
|
21
|
+
# let_a(:name) { "Joe" }
|
22
|
+
# let_a(:age) { 25 }
|
23
|
+
# let(:gender) { :male }
|
24
|
+
# it do
|
25
|
+
# expect(name).to eq "Joe"
|
26
|
+
# expect(age).to eq 25
|
27
|
+
# expect(attrs).to eq(name: "Joe", age: 25)
|
28
|
+
# expect(attrs(include: [:gender])).to eq(name: "Joe", age: 25, gender: :male)
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# By not providing a block it's possible to <b>declare</b> a custom <tt>let</tt> variable
|
33
|
+
# and be able to redefine it later via regular <tt>let</tt>. This will work:
|
34
|
+
#
|
35
|
+
# describe "declarative (no block) usage" do
|
36
|
+
# let_a(:name)
|
37
|
+
#
|
38
|
+
# subject { attrs }
|
39
|
+
#
|
40
|
+
# context "when no other `let` value" do
|
41
|
+
# it { is_expected.to eq({}) }
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# context "when `let`" do
|
45
|
+
# let(:name) { "Joe" }
|
46
|
+
# it { is_expected.to eq(name: "Joe") }
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
module UseLetset
|
51
|
+
# Define the collection.
|
52
|
+
# @param let_method [Symbol]
|
53
|
+
# @param collection_let [Symbol]
|
54
|
+
# @return [void]
|
55
|
+
def use_letset(let_method, collection_let)
|
56
|
+
keys_m = "_#{collection_let}_keys".to_sym
|
57
|
+
|
58
|
+
# See "Implementation notes" on failed implementation of "collection only" mode.
|
59
|
+
|
60
|
+
# E.g. "_data_keys" or something.
|
61
|
+
define_singleton_method(keys_m) do
|
62
|
+
if instance_variable_defined?(k = "@#{keys_m}")
|
63
|
+
instance_variable_get(k)
|
64
|
+
else
|
65
|
+
# Start by copying superclass's known vars or default to `[]`.
|
66
|
+
instance_variable_set(k, (superclass.send(keys_m).dup rescue []))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
define_singleton_method let_method, ->(k, &block) do
|
71
|
+
(send(keys_m) << k).uniq!
|
72
|
+
# Create a `let` variable unless it's a declaration call (`let_a(:name)`).
|
73
|
+
let(k, &block) if block
|
74
|
+
end
|
75
|
+
|
76
|
+
define_method(collection_let) do
|
77
|
+
{}.tap do |h|
|
78
|
+
self.class.send(keys_m).each do |k|
|
79
|
+
h[k] = public_send(k) if respond_to?(k)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Activate.
|
87
|
+
defined?(RSpec) and RSpec.configure do |config|
|
88
|
+
config.extend UseLetset
|
89
|
+
end
|
90
|
+
end; end
|
91
|
+
|
92
|
+
#
|
93
|
+
# Implementation notes:
|
94
|
+
#
|
95
|
+
# * There was once an idea to support `use_letset` in "collection only" mode. Say, `let_a` appends
|
96
|
+
# to `attrs`, but doesn't publish a let variable. This change IS COMPLETELY NOT IN LINE with
|
97
|
+
# RSpec design. Let variables are methods and the collection is built by probing for those
|
98
|
+
# methods. "Collection only" would require a complete redesign. It's easier to implement another
|
99
|
+
# helper method for that, or, even better, do it with straight Ruby right in the test where
|
100
|
+
# needed. The need for "collection only" mode is incredibly rare, say, specific serializer
|
101
|
+
# tests.
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
"LODoc"
|
4
|
+
|
5
|
+
module RSpecMagic; module Stable
|
6
|
+
# Provide an automatic <tt>let</tt> variable which contains a method name discovered from
|
7
|
+
# its string representation.
|
8
|
+
#
|
9
|
+
# describe "instance methods" do
|
10
|
+
# use_method_discovery :m
|
11
|
+
#
|
12
|
+
# describe "#name" do
|
13
|
+
# it { expect(m).to eq :name }
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# describe "#surname" do
|
17
|
+
# it { expect(m).to eq :surname }
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
module UseMethodDiscovery
|
22
|
+
# Enable the discovery mechanics.
|
23
|
+
# @param [Symbol] method_let
|
24
|
+
def use_method_discovery(method_let)
|
25
|
+
# This context and all sub-contexts will respond to A and return B. "Signature" is based on
|
26
|
+
# invocation arguments which can vary as we use the feature more intensively. Signature method
|
27
|
+
# is the same, thus it shadows higher level definitions completely.
|
28
|
+
signature = { method_let: method_let }
|
29
|
+
define_singleton_method(:_umd_signature) { signature }
|
30
|
+
|
31
|
+
let(method_let) do
|
32
|
+
# NOTE: `self.class` responds to signature method, no need to probe and rescue.
|
33
|
+
if (sig = (klass = self.class)._umd_signature) != signature
|
34
|
+
raise "`#{method_let}` is shadowed by `#{sig.fetch(:method_let)}` in this context"
|
35
|
+
end
|
36
|
+
|
37
|
+
# NOTE: Better not `return` from the loop to keep it debuggable in case logic changes.
|
38
|
+
found = nil
|
39
|
+
while (klass._umd_signature rescue nil) == signature
|
40
|
+
found = self.class.send(:_use_method_discovery_parser, klass.description.to_s) and break
|
41
|
+
klass = klass.superclass
|
42
|
+
end
|
43
|
+
|
44
|
+
found or raise "No method-like descriptions found to use as `#{method_let}`"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# The parser used by {.use_method_discovery}.
|
51
|
+
# @param [String] input
|
52
|
+
# @return [Symbol] Method name, if parsed okay.
|
53
|
+
# @return [nil] If input isn't method-like.
|
54
|
+
def _use_method_discovery_parser(input)
|
55
|
+
if (mat = input.match(/^(?:(?:#|\.|::)(\w+(?:\?|!|=|)|\[\])|(?:DELETE|GET|PUT|POST) (\w+))$/))
|
56
|
+
(mat[1] || mat[2]).to_sym
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end # module
|
60
|
+
|
61
|
+
# Activate.
|
62
|
+
defined?(RSpec) and RSpec.configure do |config|
|
63
|
+
config.extend UseMethodDiscovery
|
64
|
+
end
|
65
|
+
end; end
|
data/libx/rspec_magic.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
|
2
|
+
# Load public features.
|
3
|
+
require_relative "rspec_magic/config"
|
4
|
+
|
5
|
+
# # Load a full set of stable features.
|
6
|
+
# require_relative "rspec_magic/alias_method"
|
7
|
+
# require_relative "rspec_magic/context_when"
|
8
|
+
# require_relative "rspec_magic/use_letset"
|
9
|
+
# require_relative "rspec_magic/use_method_discovery"
|
@@ -0,0 +1,140 @@
|
|
1
|
+
|
2
|
+
RSpec.describe AttrMagic do
|
3
|
+
use_method_discovery :m
|
4
|
+
|
5
|
+
describe "#iget*" do
|
6
|
+
let(:klass) do
|
7
|
+
feature = described_class
|
8
|
+
Class.new do
|
9
|
+
feature.load(self)
|
10
|
+
|
11
|
+
attr_writer :c
|
12
|
+
|
13
|
+
def a
|
14
|
+
igetset(:a) do
|
15
|
+
calls << :a_block
|
16
|
+
false
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def b
|
21
|
+
igetset(:b) do
|
22
|
+
calls << :b_block
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def c
|
28
|
+
igetwrite(:c) do
|
29
|
+
calls << :c_block
|
30
|
+
"c"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def c=(value)
|
35
|
+
calls << :c_writer
|
36
|
+
@c = value + "!" # Custom modification by the writer.
|
37
|
+
end
|
38
|
+
|
39
|
+
def calls
|
40
|
+
@log ||= []
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "#igetset" do
|
46
|
+
it "generally works" do
|
47
|
+
obj = klass.new
|
48
|
+
expect(obj.calls).to eq []
|
49
|
+
expect(obj.a).to be false
|
50
|
+
expect(obj.a).to be false
|
51
|
+
expect(obj.calls).to eq [:a_block]
|
52
|
+
|
53
|
+
obj = klass.new
|
54
|
+
expect(obj.calls).to eq []
|
55
|
+
expect(obj.b).to be nil
|
56
|
+
expect(obj.b).to be nil
|
57
|
+
expect(obj.calls).to eq [:b_block]
|
58
|
+
end
|
59
|
+
end # describe "#igetset"
|
60
|
+
|
61
|
+
describe "#igetwrite" do
|
62
|
+
it "generally works" do
|
63
|
+
obj = klass.new
|
64
|
+
expect(obj.calls).to eq []
|
65
|
+
expect(obj.c).to eq "c!"
|
66
|
+
expect(obj.c).to eq "c!"
|
67
|
+
expect(obj.c).to eq "c!"
|
68
|
+
expect(obj.calls).to eq [:c_block, :c_writer]
|
69
|
+
end
|
70
|
+
end # describe "#igetwrite"
|
71
|
+
end # describe "iget*"
|
72
|
+
|
73
|
+
describe "#require_attr" do
|
74
|
+
let(:klass) do
|
75
|
+
feature = described_class
|
76
|
+
Class.new do
|
77
|
+
feature.load(self)
|
78
|
+
|
79
|
+
attr_accessor :a
|
80
|
+
|
81
|
+
def initialize(attrs = {})
|
82
|
+
attrs.each { |k, v| public_send("#{k}=", v) }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
let(:obj) { klass.new(a: value) }
|
88
|
+
|
89
|
+
subject { obj.send(m, :a, predicate) }
|
90
|
+
|
91
|
+
context "when valid arguments" do
|
92
|
+
# NOTE: We don't nest contexts and provide a linear sequence of [value, predicate] for
|
93
|
+
# smarter reporting.
|
94
|
+
context_when value: nil do
|
95
|
+
subject { obj.send(m, :a) }
|
96
|
+
it { expect { subject }.to raise_error(RuntimeError, /attribute.+`a`.+not.+nil: nil/i) }
|
97
|
+
end
|
98
|
+
|
99
|
+
context_when value: nil, predicate: "not_nil?" do
|
100
|
+
it { expect { subject }.to raise_error(RuntimeError, /attribute.+`a`.+not.+nil: nil/i) }
|
101
|
+
end
|
102
|
+
|
103
|
+
context_when value: [], predicate: :not_empty? do
|
104
|
+
it { expect { subject }.to raise_error(RuntimeError, /attribute.+`a`.+not.+empty: \[\]/i) }
|
105
|
+
end
|
106
|
+
|
107
|
+
context_when value: 1, predicate: :odd? do
|
108
|
+
it { is_expected.to eq 1 }
|
109
|
+
end
|
110
|
+
|
111
|
+
context_when value: 1, predicate: :even? do
|
112
|
+
it { expect { subject }.to raise_error(RuntimeError, /attribute.+`a`.+even: 1/i) }
|
113
|
+
end
|
114
|
+
|
115
|
+
context_when value: [], predicate: :valid do
|
116
|
+
let(:a) { double "a" }
|
117
|
+
it do
|
118
|
+
expect(obj).to receive(:a).and_return(a)
|
119
|
+
expect(a).to receive(:valid).and_return(false)
|
120
|
+
expect { subject }.to raise_error(RuntimeError, /must be valid/i)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# NOTE: This is a valid case in a duck-typed language which Ruby is.
|
125
|
+
context_when value: [], predicate: "joyful?" do
|
126
|
+
it { expect { subject }.to raise_error(NoMethodError, /joyful\?/) }
|
127
|
+
end
|
128
|
+
end # context "when valid arguments"
|
129
|
+
|
130
|
+
context "when invalid arguments" do
|
131
|
+
context_when value: [], predicate: "" do
|
132
|
+
it { expect { subject }.to raise_error(ArgumentError, /invalid predicate/i) }
|
133
|
+
end
|
134
|
+
|
135
|
+
context_when value: [], predicate: "not_" do
|
136
|
+
it { expect { subject }.to raise_error(ArgumentError, /invalid predicate/i) }
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end # describe "#require_attr"
|
140
|
+
end # describe
|
data/spec/spec_helper.rb
ADDED
data/spec/support/its.rb
ADDED
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: attr_magic
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alex Fortuna
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-01-28 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email:
|
15
|
+
- fortunadze@gmail.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- ".gitignore"
|
21
|
+
- ".rspec"
|
22
|
+
- ".yardopts"
|
23
|
+
- Gemfile
|
24
|
+
- Gemfile.lock
|
25
|
+
- MIT-LICENSE
|
26
|
+
- README-ru.md
|
27
|
+
- README.md
|
28
|
+
- Rakefile
|
29
|
+
- attr_magic.gemspec
|
30
|
+
- lib/attr_magic.rb
|
31
|
+
- lib/attr_magic/instance_methods.rb
|
32
|
+
- lib/attr_magic/version.rb
|
33
|
+
- libx/README.md
|
34
|
+
- libx/rspec_magic.rb
|
35
|
+
- libx/rspec_magic/config.rb
|
36
|
+
- libx/rspec_magic/stable.rb
|
37
|
+
- libx/rspec_magic/stable/alias_method.rb
|
38
|
+
- libx/rspec_magic/stable/context_when.rb
|
39
|
+
- libx/rspec_magic/stable/use_letset.rb
|
40
|
+
- libx/rspec_magic/stable/use_method_discovery.rb
|
41
|
+
- spec/lib/attr_magic_spec.rb
|
42
|
+
- spec/spec_helper.rb
|
43
|
+
- spec/support/its.rb
|
44
|
+
- spec/support/rspec_magic.rb
|
45
|
+
- spec/support/self.rb
|
46
|
+
- spec/support/simplecov.rb
|
47
|
+
homepage: https://github.com/dadooda/attr_magic
|
48
|
+
licenses:
|
49
|
+
- MIT
|
50
|
+
metadata: {}
|
51
|
+
post_install_message:
|
52
|
+
rdoc_options: []
|
53
|
+
require_paths:
|
54
|
+
- lib
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '0'
|
65
|
+
requirements: []
|
66
|
+
rubyforge_project:
|
67
|
+
rubygems_version: 2.5.2
|
68
|
+
signing_key:
|
69
|
+
specification_version: 4
|
70
|
+
summary: The tools to ease lazy attribute implementation
|
71
|
+
test_files: []
|