sinclair 1.9.0 → 1.11.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 +4 -4
- data/.rubocop.yml +7 -1
- data/README.md +414 -324
- data/config/check_specs.yml +3 -4
- data/config/yardstick.yml +9 -1
- data/lib/sinclair/configurable.rb +2 -0
- data/lib/sinclair/equals_checker.rb +2 -0
- data/lib/sinclair/matchers/add_class_method.rb +26 -36
- data/lib/sinclair/matchers/add_instance_method.rb +59 -59
- data/lib/sinclair/matchers/add_method.rb +33 -35
- data/lib/sinclair/matchers/change_class_method.rb +22 -16
- data/lib/sinclair/matchers/change_instance_method.rb +46 -16
- data/lib/sinclair/matchers.rb +2 -8
- data/lib/sinclair/method_builder/base.rb +17 -2
- data/lib/sinclair/method_builder/call_method_builder.rb +49 -0
- data/lib/sinclair/method_builder.rb +5 -17
- data/lib/sinclair/method_definition/block_definition.rb +2 -0
- data/lib/sinclair/method_definition/call_definition.rb +49 -0
- data/lib/sinclair/method_definition/string_definition.rb +2 -2
- data/lib/sinclair/method_definition.rb +80 -26
- data/lib/sinclair/method_definitions.rb +25 -7
- data/lib/sinclair/options/builder.rb +8 -0
- data/lib/sinclair/options.rb +1 -13
- data/lib/sinclair/version.rb +1 -1
- data/lib/sinclair.rb +149 -62
- data/spec/integration/readme/sinclair/types_of_definition_spec.rb +47 -0
- data/spec/integration/yard/sinclair/add_class_method_spec.rb +51 -0
- data/spec/integration/yard/sinclair/add_method_spec.rb +60 -0
- data/spec/integration/yard/sinclair/eval_and_add_method_spec.rb +26 -0
- data/spec/integration/yard/sinclair/matchers/change_class_method_spec.rb +24 -0
- data/spec/integration/yard/sinclair/matchers/change_instance_method_spec.rb +40 -0
- data/spec/integration/yard/sinclair_spec.rb +0 -83
- data/spec/lib/sinclair/method_builder/base_spec.rb +15 -0
- data/spec/lib/sinclair/method_builder/call_method_builder_spec.rb +76 -0
- data/spec/lib/sinclair/method_builder_spec.rb +63 -20
- data/spec/lib/sinclair/method_definition/call_definition_spec.rb +33 -0
- data/spec/lib/sinclair/method_definition_spec.rb +129 -0
- data/spec/lib/sinclair/method_definitions_spec.rb +79 -1
- data/spec/lib/sinclair/options/builder_spec.rb +13 -0
- data/spec/lib/sinclair/options/class_methods_spec.rb +23 -8
- data/spec/lib/sinclair_spec.rb +6 -160
- data/spec/support/models/dummy_builder.rb +4 -1
- data/spec/support/models/dummy_class_builder.rb +3 -0
- data/spec/support/models/person.rb +1 -1
- data/spec/support/shared_examples/attribute_accessor.rb +103 -0
- data/spec/support/shared_examples/sinclair.rb +112 -0
- metadata +15 -2
data/README.md
CHANGED
@@ -13,22 +13,24 @@ This gem helps the creation of complex gems/concerns
|
|
13
13
|
that enables creation of methods on the fly through class
|
14
14
|
methods
|
15
15
|
|
16
|
-
|
16
|
+
Current Release: [1.11.0](https://github.com/darthjee/sinclair/tree/1.11.0)
|
17
|
+
|
18
|
+
[Next release](https://github.com/darthjee/sinclair/compare/1.11.0...master)
|
17
19
|
|
18
20
|
Yard Documentation
|
19
21
|
-------------------
|
20
|
-
[https://www.rubydoc.info/gems/sinclair/1.
|
22
|
+
[https://www.rubydoc.info/gems/sinclair/1.11.0](https://www.rubydoc.info/gems/sinclair/1.11.0)
|
21
23
|
|
22
24
|
Installation
|
23
25
|
---------------
|
24
26
|
|
25
|
-
-
|
27
|
+
- Install it
|
26
28
|
|
27
29
|
```ruby
|
28
30
|
gem install sinclair
|
29
31
|
```
|
30
32
|
|
31
|
-
-
|
33
|
+
- Or add Sinclair to your `Gemfile` and `bundle install`:
|
32
34
|
|
33
35
|
```ruby
|
34
36
|
gem 'sinclair'
|
@@ -41,193 +43,202 @@ Installation
|
|
41
43
|
Usage
|
42
44
|
---------------
|
43
45
|
### Sinclair builder
|
44
|
-
Sinclair can actually be used in several ways
|
45
|
-
adding methods to your class on the fly
|
46
|
-
|
46
|
+
Sinclair can actually be used in several ways
|
47
|
+
- as a stand alone object capable of adding methods to your class on the fly
|
48
|
+
- as a builder inside a class method
|
49
|
+
- extending the builder for more complex logics
|
47
50
|
|
48
|
-
|
49
|
-
|
51
|
+
<details>
|
52
|
+
<summary>Stand Alone usage creating methods on the fly</summary>
|
50
53
|
|
51
|
-
|
52
|
-
|
54
|
+
```ruby
|
55
|
+
class Clazz
|
56
|
+
end
|
53
57
|
|
54
|
-
|
58
|
+
builder = Sinclair.new(Clazz)
|
55
59
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
60
|
+
builder.add_method(:twenty, '10 + 10')
|
61
|
+
builder.add_method(:eighty) { 4 * twenty }
|
62
|
+
builder.add_class_method(:one_hundred) { 100 }
|
63
|
+
builder.add_class_method(:one_hundred_twenty, 'one_hundred + 20')
|
64
|
+
builder.build
|
61
65
|
|
62
|
-
|
66
|
+
instance = Clazz.new
|
63
67
|
|
64
|
-
|
65
|
-
|
68
|
+
puts "Twenty => #{instance.twenty}" # Twenty => 20
|
69
|
+
puts "Eighty => #{instance.eighty}" # Eighty => 80
|
66
70
|
|
67
|
-
|
68
|
-
|
71
|
+
puts "One Hundred => #{Clazz.one_hundred}" # One Hundred => 100
|
72
|
+
puts "One Hundred => #{Clazz.one_hundred_twenty}" # One Hundred Twenty => 120
|
69
73
|
```
|
74
|
+
</details>
|
70
75
|
|
71
|
-
|
76
|
+
<details>
|
77
|
+
<summary>Builder in class method</summary>
|
72
78
|
|
73
79
|
```ruby
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
class << self
|
78
|
-
def parse(attribute, path: [])
|
79
|
-
builder = Sinclair.new(self)
|
80
|
+
class HttpJsonModel
|
81
|
+
attr_reader :json
|
80
82
|
|
81
|
-
|
83
|
+
class << self
|
84
|
+
def parse(attribute, path: [])
|
85
|
+
builder = Sinclair.new(self)
|
82
86
|
|
83
|
-
|
84
|
-
keys.inject(hash) { |h, key| h[key] }
|
85
|
-
end
|
87
|
+
keys = (path + [attribute]).map(&:to_s)
|
86
88
|
|
87
|
-
|
89
|
+
builder.add_method(attribute) do
|
90
|
+
keys.inject(hash) { |h, key| h[key] }
|
88
91
|
end
|
89
|
-
end
|
90
|
-
|
91
|
-
def initialize(json)
|
92
|
-
@json = json
|
93
|
-
end
|
94
92
|
|
95
|
-
|
96
|
-
@hash ||= JSON.parse(json)
|
93
|
+
builder.build
|
97
94
|
end
|
98
95
|
end
|
99
96
|
|
100
|
-
|
101
|
-
|
102
|
-
parse :name, path: [:personal_information]
|
103
|
-
parse :age, path: [:personal_information]
|
104
|
-
parse :username, path: [:digital_information]
|
105
|
-
parse :email, path: [:digital_information]
|
97
|
+
def initialize(json)
|
98
|
+
@json = json
|
106
99
|
end
|
107
100
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
101
|
+
def hash
|
102
|
+
@hash ||= JSON.parse(json)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
class HttpPerson < HttpJsonModel
|
107
|
+
parse :uid
|
108
|
+
parse :name, path: [:personal_information]
|
109
|
+
parse :age, path: [:personal_information]
|
110
|
+
parse :username, path: [:digital_information]
|
111
|
+
parse :email, path: [:digital_information]
|
112
|
+
end
|
113
|
+
|
114
|
+
json = <<-JSON
|
115
|
+
{
|
116
|
+
"uid": "12sof511",
|
117
|
+
"personal_information":{
|
118
|
+
"name":"Bob",
|
119
|
+
"age": 21
|
120
|
+
},
|
121
|
+
"digital_information":{
|
122
|
+
"username":"lordbob",
|
123
|
+
"email":"lord@bob.com"
|
119
124
|
}
|
120
|
-
|
125
|
+
}
|
126
|
+
JSON
|
121
127
|
|
122
|
-
|
128
|
+
person = HttpPerson.new(json)
|
123
129
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
130
|
+
person.uid # returns '12sof511'
|
131
|
+
person.name # returns 'Bob'
|
132
|
+
person.age # returns 21
|
133
|
+
person.username # returns 'lordbob'
|
134
|
+
person.email # returns 'lord@bob.com'
|
129
135
|
```
|
136
|
+
</details>
|
130
137
|
|
131
|
-
|
132
|
-
|
133
|
-
def env_prefix(new_prefix=nil)
|
134
|
-
@env_prefix = new_prefix if new_prefix
|
135
|
-
@env_prefix
|
136
|
-
end
|
138
|
+
<details>
|
139
|
+
<summary>Class method adding class methods</summary>
|
137
140
|
|
138
|
-
|
139
|
-
|
141
|
+
```ruby
|
142
|
+
module EnvSettings
|
143
|
+
def env_prefix(new_prefix=nil)
|
144
|
+
@env_prefix = new_prefix if new_prefix
|
145
|
+
@env_prefix
|
146
|
+
end
|
140
147
|
|
141
|
-
|
142
|
-
|
148
|
+
def from_env(*method_names)
|
149
|
+
builder = Sinclair.new(self)
|
143
150
|
|
144
|
-
|
145
|
-
|
146
|
-
end
|
151
|
+
method_names.each do |method_name|
|
152
|
+
env_key = [env_prefix, method_name].compact.join('_').upcase
|
147
153
|
|
148
|
-
|
154
|
+
builder.add_class_method(method_name, cached: true) do
|
155
|
+
ENV[env_key]
|
149
156
|
end
|
157
|
+
|
158
|
+
builder.build
|
150
159
|
end
|
151
160
|
end
|
161
|
+
end
|
152
162
|
|
153
|
-
|
154
|
-
|
163
|
+
class MyServerConfig
|
164
|
+
extend EnvSettings
|
155
165
|
|
156
|
-
|
166
|
+
env_prefix :server
|
157
167
|
|
158
|
-
|
159
|
-
|
168
|
+
from_env :host, :port
|
169
|
+
end
|
160
170
|
|
161
|
-
|
162
|
-
|
171
|
+
ENV['SERVER_HOST'] = 'myserver.com'
|
172
|
+
ENV['SERVER_PORT'] = '9090'
|
163
173
|
|
164
|
-
|
165
|
-
|
174
|
+
MyServerConfig.host # returns 'myserver.com'
|
175
|
+
MyServerConfig.port # returns '9090'
|
166
176
|
```
|
177
|
+
</details>
|
167
178
|
|
168
|
-
|
179
|
+
<details>
|
180
|
+
<summary>Extending the builder</summary>
|
169
181
|
|
170
182
|
```ruby
|
183
|
+
class ValidationBuilder < Sinclair
|
184
|
+
delegate :expected, to: :options_object
|
171
185
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
def initialize(klass, options={})
|
176
|
-
super
|
177
|
-
end
|
178
|
-
|
179
|
-
def add_validation(field)
|
180
|
-
add_method("#{field}_valid?", "#{field}.is_a?#{expected}")
|
181
|
-
end
|
186
|
+
def initialize(klass, options={})
|
187
|
+
super
|
188
|
+
end
|
182
189
|
|
183
|
-
|
184
|
-
|
185
|
-
end
|
190
|
+
def add_validation(field)
|
191
|
+
add_method("#{field}_valid?", "#{field}.is_a?#{expected}")
|
186
192
|
end
|
187
193
|
|
188
|
-
|
189
|
-
|
194
|
+
def add_accessors(fields)
|
195
|
+
klass.send(:attr_accessor, *fields)
|
196
|
+
end
|
197
|
+
end
|
190
198
|
|
191
|
-
|
192
|
-
|
193
|
-
builder = ::ValidationBuilder.new(self, expected: expected_class)
|
199
|
+
module MyConcern
|
200
|
+
extend ActiveSupport::Concern
|
194
201
|
|
195
|
-
|
196
|
-
|
202
|
+
class_methods do
|
203
|
+
def validate(*fields, expected_class)
|
204
|
+
builder = ::ValidationBuilder.new(self, expected: expected_class)
|
197
205
|
|
198
|
-
|
199
|
-
|
200
|
-
end
|
206
|
+
validatable_fields.concat(fields)
|
207
|
+
builder.add_accessors(fields)
|
201
208
|
|
202
|
-
|
209
|
+
fields.each do |field|
|
210
|
+
builder.add_validation(field)
|
203
211
|
end
|
204
212
|
|
205
|
-
|
206
|
-
@validatable_fields ||= []
|
207
|
-
end
|
213
|
+
builder.build
|
208
214
|
end
|
209
215
|
|
210
|
-
def
|
211
|
-
|
212
|
-
public_send("#{field}_valid?")
|
213
|
-
end
|
216
|
+
def validatable_fields
|
217
|
+
@validatable_fields ||= []
|
214
218
|
end
|
215
219
|
end
|
216
220
|
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
validate :age, :legs, Integer
|
221
|
-
|
222
|
-
def initialize(name: nil, surname: nil, age: nil, legs: nil)
|
223
|
-
@name = name
|
224
|
-
@surname = surname
|
225
|
-
@age = age
|
226
|
-
@legs = legs
|
221
|
+
def valid?
|
222
|
+
self.class.validatable_fields.all? do |field|
|
223
|
+
public_send("#{field}_valid?")
|
227
224
|
end
|
228
225
|
end
|
226
|
+
end
|
227
|
+
|
228
|
+
class MyClass
|
229
|
+
include MyConcern
|
230
|
+
validate :name, :surname, String
|
231
|
+
validate :age, :legs, Integer
|
232
|
+
|
233
|
+
def initialize(name: nil, surname: nil, age: nil, legs: nil)
|
234
|
+
@name = name
|
235
|
+
@surname = surname
|
236
|
+
@age = age
|
237
|
+
@legs = legs
|
238
|
+
end
|
239
|
+
end
|
229
240
|
|
230
|
-
|
241
|
+
instance = MyClass.new
|
231
242
|
```
|
232
243
|
|
233
244
|
the instance will respond to the methods
|
@@ -238,25 +249,73 @@ or by extending it for more complex logics
|
|
238
249
|
```valid?```.
|
239
250
|
|
240
251
|
```ruby
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
252
|
+
valid_object = MyClass.new(
|
253
|
+
name: :name,
|
254
|
+
surname: 'surname',
|
255
|
+
age: 20,
|
256
|
+
legs: 2
|
257
|
+
)
|
258
|
+
valid_object.valid? # returns true
|
259
|
+
```
|
260
|
+
|
261
|
+
```ruby
|
262
|
+
invalid_object = MyClass.new(
|
263
|
+
name: 'name',
|
264
|
+
surname: 'surname',
|
265
|
+
age: 20,
|
266
|
+
legs: 2
|
267
|
+
)
|
268
|
+
invalid_object.valid? # returns false
|
248
269
|
```
|
270
|
+
</details>
|
271
|
+
|
272
|
+
#### Different ways of adding the methods
|
273
|
+
There are different ways to add a method
|
274
|
+
<details>
|
275
|
+
<summary>Define method using block</summary>
|
249
276
|
|
250
277
|
```ruby
|
278
|
+
klass = Class.new
|
279
|
+
instance = klass.new
|
251
280
|
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
281
|
+
builder = Sinclair.new(klass)
|
282
|
+
builder.add_method(:random_number) { Random.rand(10..20) }
|
283
|
+
builder.build
|
284
|
+
|
285
|
+
instance.random_number # returns a number between 10 and 20
|
286
|
+
```
|
287
|
+
</details>
|
288
|
+
|
289
|
+
<details>
|
290
|
+
<summary>Define method using string</summary>
|
291
|
+
|
292
|
+
```ruby
|
293
|
+
klass = Class.new
|
294
|
+
instance = klass.new
|
295
|
+
|
296
|
+
builder = Sinclair.new(klass)
|
297
|
+
builder.add_method(:random_number, "Random.rand(10..20)")
|
298
|
+
builder.build
|
299
|
+
|
300
|
+
instance.random_number # returns a number between 10 and 20
|
301
|
+
```
|
302
|
+
</details>
|
303
|
+
|
304
|
+
<details>
|
305
|
+
<summary>Define method using a call to the class</summary>
|
306
|
+
|
307
|
+
```ruby
|
308
|
+
klass = Class.new
|
309
|
+
|
310
|
+
builder = Sinclair.new(klass)
|
311
|
+
builder.add_class_method(:attr_accessor, :number, type: :call)
|
312
|
+
builder.build
|
313
|
+
|
314
|
+
klass.number # returns nil
|
315
|
+
klass.number = 10
|
316
|
+
klass.number # returns 10
|
259
317
|
```
|
318
|
+
</details>
|
260
319
|
|
261
320
|
#### Caching the result
|
262
321
|
If wanted, the result of the method can be stored in an
|
@@ -265,87 +324,95 @@ instance variable with the same name.
|
|
265
324
|
When caching, you can cache with type `:full` so that even `nil`
|
266
325
|
values are cached
|
267
326
|
|
327
|
+
<details>
|
328
|
+
<summary>Example of simple cache usage</summary>
|
329
|
+
|
268
330
|
```ruby
|
269
|
-
|
270
|
-
|
271
|
-
|
331
|
+
class MyModel
|
332
|
+
attr_accessor :base, :expoent
|
333
|
+
end
|
272
334
|
|
273
|
-
|
335
|
+
builder = Sinclair.new(MyModel)
|
274
336
|
|
275
|
-
|
276
|
-
|
277
|
-
|
337
|
+
builder.add_method(:cached_power, cached: true) do
|
338
|
+
base ** expoent
|
339
|
+
end
|
278
340
|
|
279
|
-
|
280
|
-
|
281
|
-
|
341
|
+
# equivalent of builder.add_method(:cached_power) do
|
342
|
+
# @cached_power ||= base ** expoent
|
343
|
+
# end
|
282
344
|
|
283
|
-
|
345
|
+
builder.build
|
284
346
|
|
285
|
-
|
286
|
-
|
347
|
+
model.base = 3
|
348
|
+
model.expoent = 2
|
287
349
|
|
288
|
-
|
289
|
-
|
290
|
-
|
350
|
+
model.cached_power # returns 9
|
351
|
+
model.expoent = 3
|
352
|
+
model.cached_power # returns 9 (from cache)
|
291
353
|
```
|
354
|
+
</details>
|
355
|
+
|
356
|
+
<details>
|
357
|
+
<summary>Usage of different cache types</summary>
|
292
358
|
|
293
359
|
```ruby
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
end
|
360
|
+
module DefaultValueable
|
361
|
+
def default_reader(*methods, value:, accept_nil: false)
|
362
|
+
DefaultValueBuilder.new(
|
363
|
+
self, value: value, accept_nil: accept_nil
|
364
|
+
).add_default_values(*methods)
|
300
365
|
end
|
366
|
+
end
|
301
367
|
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
methods.each do |method|
|
307
|
-
add_method(method, cached: cache_type) { default_value }
|
308
|
-
end
|
368
|
+
class DefaultValueBuilder < Sinclair
|
369
|
+
def add_default_values(*methods)
|
370
|
+
default_value = value
|
309
371
|
|
310
|
-
|
372
|
+
methods.each do |method|
|
373
|
+
add_method(method, cached: cache_type) { default_value }
|
311
374
|
end
|
312
375
|
|
313
|
-
|
376
|
+
build
|
377
|
+
end
|
314
378
|
|
315
|
-
|
379
|
+
private
|
316
380
|
|
317
|
-
|
318
|
-
|
319
|
-
|
381
|
+
delegate :accept_nil, :value, to: :options_object
|
382
|
+
|
383
|
+
def cache_type
|
384
|
+
accept_nil ? :full : :simple
|
320
385
|
end
|
386
|
+
end
|
321
387
|
|
322
|
-
|
323
|
-
|
388
|
+
class Server
|
389
|
+
extend DefaultValueable
|
324
390
|
|
325
|
-
|
391
|
+
attr_writer :host, :port
|
326
392
|
|
327
|
-
|
328
|
-
|
393
|
+
default_reader :host, value: 'server.com', accept_nil: false
|
394
|
+
default_reader :port, value: 80, accept_nil: true
|
329
395
|
|
330
|
-
|
331
|
-
|
396
|
+
def url
|
397
|
+
return "http://#{host}" unless port
|
332
398
|
|
333
|
-
|
334
|
-
end
|
399
|
+
"http://#{host}:#{port}"
|
335
400
|
end
|
401
|
+
end
|
336
402
|
|
337
|
-
|
403
|
+
server = Server.new
|
338
404
|
|
339
|
-
|
405
|
+
server.url # returns 'http://server.com:80'
|
340
406
|
|
341
|
-
|
342
|
-
|
343
|
-
|
407
|
+
server.host = 'interstella.com'
|
408
|
+
server.port = 5555
|
409
|
+
server.url # returns 'http://interstella.com:5555'
|
344
410
|
|
345
|
-
|
346
|
-
|
347
|
-
|
411
|
+
server.host = nil
|
412
|
+
server.port = nil
|
413
|
+
server.url # return 'http://server.com'
|
348
414
|
```
|
415
|
+
</details>
|
349
416
|
|
350
417
|
### Sinclair::Configurable
|
351
418
|
|
@@ -356,214 +423,236 @@ Configurations are read-only objects that can only be set using
|
|
356
423
|
the `configurable#configure` method which accepts a block or
|
357
424
|
hash
|
358
425
|
|
426
|
+
<details>
|
427
|
+
<summary>Using configurable</summary>
|
428
|
+
|
359
429
|
```ruby
|
360
|
-
|
361
|
-
|
430
|
+
module MyConfigurable
|
431
|
+
extend Sinclair::Configurable
|
362
432
|
|
363
|
-
|
364
|
-
|
365
|
-
|
433
|
+
# port is defaulted to 80
|
434
|
+
configurable_with :host, port: 80
|
435
|
+
end
|
366
436
|
|
367
|
-
|
368
|
-
|
369
|
-
|
437
|
+
MyConfigurable.configure(port: 5555) do |config|
|
438
|
+
config.host 'interstella.art'
|
439
|
+
end
|
370
440
|
|
371
|
-
|
372
|
-
|
441
|
+
MyConfigurable.config.host # returns 'interstella.art'
|
442
|
+
MyConfigurable.config.port # returns 5555
|
373
443
|
|
374
|
-
|
375
|
-
|
444
|
+
# Configurable enables options that can be passed
|
445
|
+
MyConfigurable.as_options.host # returns 'interstella.art'
|
376
446
|
|
377
|
-
|
378
|
-
|
447
|
+
# Configurable enables options that can be passed with custom values
|
448
|
+
MyConfigurable.as_options(host: 'other').host # returns 'other'
|
379
449
|
|
380
|
-
|
450
|
+
MyConfigurable.reset_config
|
381
451
|
|
382
|
-
|
383
|
-
|
452
|
+
MyConfigurable.config.host # returns nil
|
453
|
+
MyConfigurable.config.port # returns 80
|
384
454
|
```
|
455
|
+
</details>
|
385
456
|
|
386
457
|
Configurations can also be done through custom classes
|
387
458
|
|
388
|
-
|
389
|
-
|
390
|
-
config_attributes :host, :port
|
459
|
+
<details>
|
460
|
+
<summary>Using configration class</summary>
|
391
461
|
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
462
|
+
```ruby
|
463
|
+
class MyServerConfig < Sinclair::Config
|
464
|
+
config_attributes :host, :port
|
465
|
+
|
466
|
+
def url
|
467
|
+
if @port
|
468
|
+
"http://#{@host}:#{@port}"
|
469
|
+
else
|
470
|
+
"http://#{@host}"
|
398
471
|
end
|
399
472
|
end
|
473
|
+
end
|
400
474
|
|
401
|
-
|
402
|
-
|
475
|
+
class Client
|
476
|
+
extend Sinclair::Configurable
|
403
477
|
|
404
|
-
|
405
|
-
|
478
|
+
configurable_by MyServerConfig
|
479
|
+
end
|
406
480
|
|
407
|
-
|
408
|
-
|
409
|
-
|
481
|
+
Client.configure do
|
482
|
+
host 'interstella.com'
|
483
|
+
end
|
410
484
|
|
411
|
-
|
485
|
+
Client.config.url # returns 'http://interstella.com'
|
412
486
|
|
413
|
-
|
414
|
-
|
415
|
-
|
487
|
+
Client.configure do |config|
|
488
|
+
config.port 8080
|
489
|
+
end
|
416
490
|
|
417
|
-
|
491
|
+
Client.config.url # returns 'http://interstella.com:8080'
|
418
492
|
```
|
493
|
+
</details>
|
419
494
|
|
420
495
|
### Sinclair::EnvSettable
|
421
496
|
|
422
497
|
Settable allows classes to extract configuration from environments through
|
423
498
|
a simple meta-programable way
|
424
499
|
|
500
|
+
<details>
|
501
|
+
<summary>Using env settable example</summary>
|
502
|
+
|
425
503
|
```ruby
|
426
|
-
|
427
|
-
|
428
|
-
|
504
|
+
class ServiceClient
|
505
|
+
extend Sinclair::EnvSettable
|
506
|
+
attr_reader :username, :password, :host, :port
|
429
507
|
|
430
|
-
|
508
|
+
settings_prefix 'SERVICE'
|
431
509
|
|
432
|
-
|
510
|
+
with_settings :username, :password, port: 80, hostname: 'my-host.com'
|
433
511
|
|
434
|
-
|
435
|
-
|
436
|
-
|
512
|
+
def self.default
|
513
|
+
@default ||= new
|
514
|
+
end
|
437
515
|
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
end
|
516
|
+
def initialize(
|
517
|
+
username: self.class.username,
|
518
|
+
password: self.class.password,
|
519
|
+
port: self.class.port,
|
520
|
+
hostname: self.class.hostname
|
521
|
+
)
|
522
|
+
@username = username
|
523
|
+
@password = password
|
524
|
+
@port = port
|
525
|
+
@hostname = hostname
|
449
526
|
end
|
527
|
+
end
|
450
528
|
|
451
|
-
|
452
|
-
|
529
|
+
ENV['SERVICE_USERNAME'] = 'my-login'
|
530
|
+
ENV['SERVICE_HOSTNAME'] = 'host.com'
|
453
531
|
|
454
|
-
|
532
|
+
ServiceClient.default # returns #<ServiceClient:0x0000556fa1b366e8 @username="my-login", @password=nil, @port=80, @hostname="host.com">'
|
455
533
|
```
|
534
|
+
</details>
|
456
535
|
|
457
536
|
### Sinclair::Options
|
458
537
|
Options allows projects to have an easy to configure option object
|
459
538
|
|
539
|
+
<details>
|
540
|
+
<summary>Example of using Options</summary>
|
541
|
+
|
460
542
|
```ruby
|
461
|
-
|
462
|
-
|
543
|
+
class ConnectionOptions < Sinclair::Options
|
544
|
+
with_options :timeout, :retries, port: 443, protocol: 'https'
|
463
545
|
|
464
|
-
|
465
|
-
|
546
|
+
# skip_validation if you dont want to validate intialization arguments
|
547
|
+
end
|
466
548
|
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
549
|
+
options = ConnectionOptions.new(
|
550
|
+
timeout: 10,
|
551
|
+
protocol: 'http'
|
552
|
+
)
|
471
553
|
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
554
|
+
options.timeout # returns 10
|
555
|
+
options.retries # returns nil
|
556
|
+
options.protocol # returns 'http'
|
557
|
+
options.port # returns 443
|
476
558
|
|
477
|
-
|
559
|
+
ConnectionOptions.new(invalid: 10) # raises Sinclair::Exception::InvalidOptions
|
478
560
|
```
|
561
|
+
</details>
|
479
562
|
|
480
563
|
### Sinclair::Comparable
|
481
564
|
Comparable allows a class to implement quickly a `==` method comparing given attributes
|
482
565
|
|
566
|
+
<details>
|
567
|
+
<summary>Example of Comparable usage</summary>
|
568
|
+
|
483
569
|
```ruby
|
484
|
-
|
485
|
-
|
570
|
+
class SampleModel
|
571
|
+
include Sinclair::Comparable
|
486
572
|
|
487
|
-
|
488
|
-
|
573
|
+
comparable_by :name
|
574
|
+
attr_reader :name, :age
|
489
575
|
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
end
|
576
|
+
def initialize(name: nil, age: nil)
|
577
|
+
@name = name
|
578
|
+
@age = age
|
494
579
|
end
|
580
|
+
end
|
495
581
|
|
496
|
-
|
497
|
-
|
582
|
+
model1 = model_class.new(name: 'jack', age: 21)
|
583
|
+
model2 = model_class.new(name: 'jack', age: 23)
|
498
584
|
|
499
|
-
|
585
|
+
model1 == model2 # returns true
|
500
586
|
```
|
587
|
+
</details>
|
501
588
|
|
502
589
|
RSspec matcher
|
503
590
|
---------------
|
504
591
|
|
505
592
|
You can use the provided matcher to check that your builder is adding a method correctly
|
506
593
|
|
594
|
+
<details>
|
595
|
+
<summary>Sample of specs over adding methods</summary>
|
596
|
+
|
507
597
|
```ruby
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
598
|
+
class DefaultValue
|
599
|
+
delegate :build, to: :builder
|
600
|
+
attr_reader :klass, :method, :value, :class_method
|
601
|
+
|
602
|
+
def initialize(klass, method, value, class_method: false)
|
603
|
+
@klass = klass
|
604
|
+
@method = method
|
605
|
+
@value = value
|
606
|
+
@class_method = class_method
|
607
|
+
end
|
518
608
|
|
519
|
-
|
609
|
+
private
|
520
610
|
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
end
|
611
|
+
def builder
|
612
|
+
@builder ||= Sinclair.new(klass).tap do |b|
|
613
|
+
if class_method
|
614
|
+
b.add_class_method(method) { value }
|
615
|
+
else
|
616
|
+
b.add_method(method) { value }
|
528
617
|
end
|
529
618
|
end
|
530
619
|
end
|
620
|
+
end
|
531
621
|
|
532
|
-
|
533
|
-
|
622
|
+
RSpec.describe Sinclair::Matchers do
|
623
|
+
subject(:builder_class) { DefaultValue }
|
534
624
|
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
625
|
+
let(:klass) { Class.new }
|
626
|
+
let(:method) { :the_method }
|
627
|
+
let(:value) { Random.rand(100) }
|
628
|
+
let(:builder) { builder_class.new(klass, method, value) }
|
629
|
+
let(:instance) { klass.new }
|
540
630
|
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
end
|
631
|
+
context 'when the builder runs' do
|
632
|
+
it do
|
633
|
+
expect { builder.build }.to add_method(method).to(instance)
|
545
634
|
end
|
635
|
+
end
|
546
636
|
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
end
|
637
|
+
context 'when the builder runs' do
|
638
|
+
it do
|
639
|
+
expect { builder.build }.to add_method(method).to(klass)
|
551
640
|
end
|
641
|
+
end
|
552
642
|
|
553
|
-
|
554
|
-
|
643
|
+
context 'when adding class methods' do
|
644
|
+
subject(:builder) { builder_class.new(klass, method, value, class_method: true) }
|
555
645
|
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
end
|
646
|
+
context 'when the builder runs' do
|
647
|
+
it do
|
648
|
+
expect { builder.build }.to add_class_method(method).to(klass)
|
560
649
|
end
|
561
650
|
end
|
562
651
|
end
|
652
|
+
end
|
563
653
|
```
|
564
654
|
|
565
655
|
```bash
|
566
|
-
|
567
656
|
> bundle exec rspec
|
568
657
|
```
|
569
658
|
|
@@ -577,10 +666,11 @@ Sinclair::Matchers
|
|
577
666
|
when the builder runs
|
578
667
|
should add method class_method 'the_method' to #<Class:0x000055e5d9b95d88>
|
579
668
|
```
|
669
|
+
</details>
|
580
670
|
|
581
671
|
Projects Using
|
582
672
|
---------------
|
583
673
|
|
584
|
-
-
|
585
|
-
-
|
586
|
-
-
|
674
|
+
- [Arstotzka](https://github.com/darthjee/arstotzka)
|
675
|
+
- [Azeroth](https://github.com/darthjee/azeroth)
|
676
|
+
- [Magicka](https://github.com/darthjee/magicka)
|