sinclair 1.10.0 → 1.12.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 +6 -0
- data/README.md +444 -324
- data/config/check_specs.yml +5 -5
- data/config/yardstick.yml +4 -0
- data/lib/sinclair/matchers/add_class_method.rb +0 -1
- data/lib/sinclair/method_builder/base.rb +32 -2
- data/lib/sinclair/method_builder/block_method_builder.rb +1 -12
- data/lib/sinclair/method_builder/call_method_builder.rb +10 -27
- data/lib/sinclair/method_builder/string_method_builder.rb +2 -29
- data/lib/sinclair/method_builder.rb +4 -20
- data/lib/sinclair/method_definition/block_definition.rb +2 -0
- data/lib/sinclair/method_definition/call_definition.rb +15 -18
- data/lib/sinclair/method_definition/parameter_builder.rb +89 -0
- data/lib/sinclair/method_definition/parameter_helper.rb +124 -0
- data/lib/sinclair/method_definition/string_definition.rb +26 -2
- data/lib/sinclair/method_definition.rb +42 -3
- data/lib/sinclair/method_definitions.rb +20 -22
- data/lib/sinclair/version.rb +1 -1
- data/lib/sinclair.rb +149 -62
- data/spec/integration/readme/sinclair/types_of_definition_spec.rb +61 -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_spec.rb +0 -83
- data/spec/lib/sinclair/method_builder/base_spec.rb +15 -0
- data/spec/lib/sinclair/method_builder/string_method_builder_spec.rb +24 -1
- data/spec/lib/sinclair/method_definition/call_definition_spec.rb +14 -17
- data/spec/lib/sinclair/method_definition/parameter_builder_spec.rb +81 -0
- data/spec/lib/sinclair/method_definition/string_definition_spec.rb +60 -29
- data/spec/lib/sinclair/method_definition_spec.rb +77 -2
- data/spec/lib/sinclair/method_definitions_spec.rb +15 -16
- data/spec/lib/sinclair_spec.rb +6 -160
- data/spec/support/models/dummy_builder.rb +5 -1
- data/spec/support/models/dummy_class_builder.rb +4 -0
- data/spec/support/models/person.rb +1 -1
- data/spec/support/shared_examples/sinclair.rb +118 -0
- metadata +11 -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.12.0](https://github.com/darthjee/sinclair/tree/1.12.0)
|
17
|
+
|
18
|
+
[Next release](https://github.com/darthjee/sinclair/compare/1.12.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.12.0](https://www.rubydoc.info/gems/sinclair/1.12.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
|
-
|
80
|
+
class HttpJsonModel
|
81
|
+
attr_reader :json
|
76
82
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
keys = (path + [attribute]).map(&:to_s)
|
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
92
|
|
91
|
-
|
92
|
-
@json = json
|
93
|
-
end
|
94
|
-
|
95
|
-
def hash
|
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,103 @@ 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
|
248
259
|
```
|
249
260
|
|
250
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
|
269
|
+
```
|
270
|
+
</details>
|
251
271
|
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
272
|
+
#### Different ways of adding the methods
|
273
|
+
There are different ways to add a method, each accepting different options
|
274
|
+
|
275
|
+
<details>
|
276
|
+
<summary>Define method using block</summary>
|
277
|
+
|
278
|
+
Block methods accepts, as option
|
279
|
+
- [cache](#caching-the-result): defining the cashing of results
|
280
|
+
|
281
|
+
```ruby
|
282
|
+
klass = Class.new
|
283
|
+
instance = klass.new
|
284
|
+
|
285
|
+
builder = Sinclair.new(klass)
|
286
|
+
builder.add_method(:random_number) { Random.rand(10..20) }
|
287
|
+
builder.build
|
288
|
+
|
289
|
+
instance.random_number # returns a number between 10 and 20
|
290
|
+
```
|
291
|
+
</details>
|
292
|
+
|
293
|
+
<details>
|
294
|
+
<summary>Define method using string</summary>
|
295
|
+
|
296
|
+
String methods accepts, as option
|
297
|
+
- [cache](#caching-the-result): defining the cashing of results
|
298
|
+
- parameters: defining accepted parameters
|
299
|
+
- named_parameters: defining accepted named parameters
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
# Example without parameters
|
303
|
+
|
304
|
+
class MyClass
|
305
|
+
end
|
306
|
+
instance = MyClass.new
|
307
|
+
|
308
|
+
builder = Sinclair.new(MyClass)
|
309
|
+
builder.add_method(:random_number, "Random.rand(10..20)")
|
310
|
+
builder.build
|
311
|
+
|
312
|
+
instance.random_number # returns a number between 10 and 20
|
313
|
+
```
|
314
|
+
|
315
|
+
```ruby
|
316
|
+
# Example with parameters
|
317
|
+
|
318
|
+
class MyClass
|
319
|
+
end
|
320
|
+
|
321
|
+
builder = described_class.new(MyClass)
|
322
|
+
builder.add_class_method(
|
323
|
+
:function, 'a ** b + c', parameters: [:a], named_parameters: [:b, { c: 15 }]
|
324
|
+
)
|
325
|
+
builder.build
|
326
|
+
|
327
|
+
MyClass.function(10, b: 2) # returns 115
|
328
|
+
```
|
329
|
+
</details>
|
330
|
+
|
331
|
+
<details>
|
332
|
+
<summary>Define method using a call to the class</summary>
|
333
|
+
|
334
|
+
Call method definitions right now have no options available
|
335
|
+
|
336
|
+
```ruby
|
337
|
+
class MyClass
|
338
|
+
end
|
339
|
+
|
340
|
+
builder = Sinclair.new(MyClass)
|
341
|
+
builder.add_class_method(:attr_accessor, :number, type: :call)
|
342
|
+
builder.build
|
343
|
+
|
344
|
+
MyClass.number # returns nil
|
345
|
+
MyClass.number = 10
|
346
|
+
MyClass.number # returns 10
|
259
347
|
```
|
348
|
+
</details>
|
260
349
|
|
261
350
|
#### Caching the result
|
262
351
|
If wanted, the result of the method can be stored in an
|
@@ -265,87 +354,95 @@ instance variable with the same name.
|
|
265
354
|
When caching, you can cache with type `:full` so that even `nil`
|
266
355
|
values are cached
|
267
356
|
|
357
|
+
<details>
|
358
|
+
<summary>Example of simple cache usage</summary>
|
359
|
+
|
268
360
|
```ruby
|
269
|
-
|
270
|
-
|
271
|
-
|
361
|
+
class MyModel
|
362
|
+
attr_accessor :base, :expoent
|
363
|
+
end
|
272
364
|
|
273
|
-
|
365
|
+
builder = Sinclair.new(MyModel)
|
274
366
|
|
275
|
-
|
276
|
-
|
277
|
-
|
367
|
+
builder.add_method(:cached_power, cached: true) do
|
368
|
+
base ** expoent
|
369
|
+
end
|
278
370
|
|
279
|
-
|
280
|
-
|
281
|
-
|
371
|
+
# equivalent of builder.add_method(:cached_power) do
|
372
|
+
# @cached_power ||= base ** expoent
|
373
|
+
# end
|
282
374
|
|
283
|
-
|
375
|
+
builder.build
|
284
376
|
|
285
|
-
|
286
|
-
|
377
|
+
model.base = 3
|
378
|
+
model.expoent = 2
|
287
379
|
|
288
|
-
|
289
|
-
|
290
|
-
|
380
|
+
model.cached_power # returns 9
|
381
|
+
model.expoent = 3
|
382
|
+
model.cached_power # returns 9 (from cache)
|
291
383
|
```
|
384
|
+
</details>
|
385
|
+
|
386
|
+
<details>
|
387
|
+
<summary>Usage of different cache types</summary>
|
292
388
|
|
293
389
|
```ruby
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
end
|
390
|
+
module DefaultValueable
|
391
|
+
def default_reader(*methods, value:, accept_nil: false)
|
392
|
+
DefaultValueBuilder.new(
|
393
|
+
self, value: value, accept_nil: accept_nil
|
394
|
+
).add_default_values(*methods)
|
300
395
|
end
|
396
|
+
end
|
301
397
|
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
methods.each do |method|
|
307
|
-
add_method(method, cached: cache_type) { default_value }
|
308
|
-
end
|
398
|
+
class DefaultValueBuilder < Sinclair
|
399
|
+
def add_default_values(*methods)
|
400
|
+
default_value = value
|
309
401
|
|
310
|
-
|
402
|
+
methods.each do |method|
|
403
|
+
add_method(method, cached: cache_type) { default_value }
|
311
404
|
end
|
312
405
|
|
313
|
-
|
406
|
+
build
|
407
|
+
end
|
314
408
|
|
315
|
-
|
409
|
+
private
|
316
410
|
|
317
|
-
|
318
|
-
|
319
|
-
|
411
|
+
delegate :accept_nil, :value, to: :options_object
|
412
|
+
|
413
|
+
def cache_type
|
414
|
+
accept_nil ? :full : :simple
|
320
415
|
end
|
416
|
+
end
|
321
417
|
|
322
|
-
|
323
|
-
|
418
|
+
class Server
|
419
|
+
extend DefaultValueable
|
324
420
|
|
325
|
-
|
421
|
+
attr_writer :host, :port
|
326
422
|
|
327
|
-
|
328
|
-
|
423
|
+
default_reader :host, value: 'server.com', accept_nil: false
|
424
|
+
default_reader :port, value: 80, accept_nil: true
|
329
425
|
|
330
|
-
|
331
|
-
|
426
|
+
def url
|
427
|
+
return "http://#{host}" unless port
|
332
428
|
|
333
|
-
|
334
|
-
end
|
429
|
+
"http://#{host}:#{port}"
|
335
430
|
end
|
431
|
+
end
|
336
432
|
|
337
|
-
|
433
|
+
server = Server.new
|
338
434
|
|
339
|
-
|
435
|
+
server.url # returns 'http://server.com:80'
|
340
436
|
|
341
|
-
|
342
|
-
|
343
|
-
|
437
|
+
server.host = 'interstella.com'
|
438
|
+
server.port = 5555
|
439
|
+
server.url # returns 'http://interstella.com:5555'
|
344
440
|
|
345
|
-
|
346
|
-
|
347
|
-
|
441
|
+
server.host = nil
|
442
|
+
server.port = nil
|
443
|
+
server.url # return 'http://server.com'
|
348
444
|
```
|
445
|
+
</details>
|
349
446
|
|
350
447
|
### Sinclair::Configurable
|
351
448
|
|
@@ -356,214 +453,236 @@ Configurations are read-only objects that can only be set using
|
|
356
453
|
the `configurable#configure` method which accepts a block or
|
357
454
|
hash
|
358
455
|
|
456
|
+
<details>
|
457
|
+
<summary>Using configurable</summary>
|
458
|
+
|
359
459
|
```ruby
|
360
|
-
|
361
|
-
|
460
|
+
module MyConfigurable
|
461
|
+
extend Sinclair::Configurable
|
362
462
|
|
363
|
-
|
364
|
-
|
365
|
-
|
463
|
+
# port is defaulted to 80
|
464
|
+
configurable_with :host, port: 80
|
465
|
+
end
|
366
466
|
|
367
|
-
|
368
|
-
|
369
|
-
|
467
|
+
MyConfigurable.configure(port: 5555) do |config|
|
468
|
+
config.host 'interstella.art'
|
469
|
+
end
|
370
470
|
|
371
|
-
|
372
|
-
|
471
|
+
MyConfigurable.config.host # returns 'interstella.art'
|
472
|
+
MyConfigurable.config.port # returns 5555
|
373
473
|
|
374
|
-
|
375
|
-
|
474
|
+
# Configurable enables options that can be passed
|
475
|
+
MyConfigurable.as_options.host # returns 'interstella.art'
|
376
476
|
|
377
|
-
|
378
|
-
|
477
|
+
# Configurable enables options that can be passed with custom values
|
478
|
+
MyConfigurable.as_options(host: 'other').host # returns 'other'
|
379
479
|
|
380
|
-
|
480
|
+
MyConfigurable.reset_config
|
381
481
|
|
382
|
-
|
383
|
-
|
482
|
+
MyConfigurable.config.host # returns nil
|
483
|
+
MyConfigurable.config.port # returns 80
|
384
484
|
```
|
485
|
+
</details>
|
385
486
|
|
386
487
|
Configurations can also be done through custom classes
|
387
488
|
|
388
|
-
|
389
|
-
|
390
|
-
config_attributes :host, :port
|
489
|
+
<details>
|
490
|
+
<summary>Using configration class</summary>
|
391
491
|
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
492
|
+
```ruby
|
493
|
+
class MyServerConfig < Sinclair::Config
|
494
|
+
config_attributes :host, :port
|
495
|
+
|
496
|
+
def url
|
497
|
+
if @port
|
498
|
+
"http://#{@host}:#{@port}"
|
499
|
+
else
|
500
|
+
"http://#{@host}"
|
398
501
|
end
|
399
502
|
end
|
503
|
+
end
|
400
504
|
|
401
|
-
|
402
|
-
|
505
|
+
class Client
|
506
|
+
extend Sinclair::Configurable
|
403
507
|
|
404
|
-
|
405
|
-
|
508
|
+
configurable_by MyServerConfig
|
509
|
+
end
|
406
510
|
|
407
|
-
|
408
|
-
|
409
|
-
|
511
|
+
Client.configure do
|
512
|
+
host 'interstella.com'
|
513
|
+
end
|
410
514
|
|
411
|
-
|
515
|
+
Client.config.url # returns 'http://interstella.com'
|
412
516
|
|
413
|
-
|
414
|
-
|
415
|
-
|
517
|
+
Client.configure do |config|
|
518
|
+
config.port 8080
|
519
|
+
end
|
416
520
|
|
417
|
-
|
521
|
+
Client.config.url # returns 'http://interstella.com:8080'
|
418
522
|
```
|
523
|
+
</details>
|
419
524
|
|
420
525
|
### Sinclair::EnvSettable
|
421
526
|
|
422
527
|
Settable allows classes to extract configuration from environments through
|
423
528
|
a simple meta-programable way
|
424
529
|
|
530
|
+
<details>
|
531
|
+
<summary>Using env settable example</summary>
|
532
|
+
|
425
533
|
```ruby
|
426
|
-
|
427
|
-
|
428
|
-
|
534
|
+
class ServiceClient
|
535
|
+
extend Sinclair::EnvSettable
|
536
|
+
attr_reader :username, :password, :host, :port
|
429
537
|
|
430
|
-
|
538
|
+
settings_prefix 'SERVICE'
|
431
539
|
|
432
|
-
|
540
|
+
with_settings :username, :password, port: 80, hostname: 'my-host.com'
|
433
541
|
|
434
|
-
|
435
|
-
|
436
|
-
|
542
|
+
def self.default
|
543
|
+
@default ||= new
|
544
|
+
end
|
437
545
|
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
end
|
546
|
+
def initialize(
|
547
|
+
username: self.class.username,
|
548
|
+
password: self.class.password,
|
549
|
+
port: self.class.port,
|
550
|
+
hostname: self.class.hostname
|
551
|
+
)
|
552
|
+
@username = username
|
553
|
+
@password = password
|
554
|
+
@port = port
|
555
|
+
@hostname = hostname
|
449
556
|
end
|
557
|
+
end
|
450
558
|
|
451
|
-
|
452
|
-
|
559
|
+
ENV['SERVICE_USERNAME'] = 'my-login'
|
560
|
+
ENV['SERVICE_HOSTNAME'] = 'host.com'
|
453
561
|
|
454
|
-
|
562
|
+
ServiceClient.default # returns #<ServiceClient:0x0000556fa1b366e8 @username="my-login", @password=nil, @port=80, @hostname="host.com">'
|
455
563
|
```
|
564
|
+
</details>
|
456
565
|
|
457
566
|
### Sinclair::Options
|
458
567
|
Options allows projects to have an easy to configure option object
|
459
568
|
|
569
|
+
<details>
|
570
|
+
<summary>Example of using Options</summary>
|
571
|
+
|
460
572
|
```ruby
|
461
|
-
|
462
|
-
|
573
|
+
class ConnectionOptions < Sinclair::Options
|
574
|
+
with_options :timeout, :retries, port: 443, protocol: 'https'
|
463
575
|
|
464
|
-
|
465
|
-
|
576
|
+
# skip_validation if you dont want to validate intialization arguments
|
577
|
+
end
|
466
578
|
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
579
|
+
options = ConnectionOptions.new(
|
580
|
+
timeout: 10,
|
581
|
+
protocol: 'http'
|
582
|
+
)
|
471
583
|
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
584
|
+
options.timeout # returns 10
|
585
|
+
options.retries # returns nil
|
586
|
+
options.protocol # returns 'http'
|
587
|
+
options.port # returns 443
|
476
588
|
|
477
|
-
|
589
|
+
ConnectionOptions.new(invalid: 10) # raises Sinclair::Exception::InvalidOptions
|
478
590
|
```
|
591
|
+
</details>
|
479
592
|
|
480
593
|
### Sinclair::Comparable
|
481
594
|
Comparable allows a class to implement quickly a `==` method comparing given attributes
|
482
595
|
|
596
|
+
<details>
|
597
|
+
<summary>Example of Comparable usage</summary>
|
598
|
+
|
483
599
|
```ruby
|
484
|
-
|
485
|
-
|
600
|
+
class SampleModel
|
601
|
+
include Sinclair::Comparable
|
486
602
|
|
487
|
-
|
488
|
-
|
603
|
+
comparable_by :name
|
604
|
+
attr_reader :name, :age
|
489
605
|
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
end
|
606
|
+
def initialize(name: nil, age: nil)
|
607
|
+
@name = name
|
608
|
+
@age = age
|
494
609
|
end
|
610
|
+
end
|
495
611
|
|
496
|
-
|
497
|
-
|
612
|
+
model1 = model_class.new(name: 'jack', age: 21)
|
613
|
+
model2 = model_class.new(name: 'jack', age: 23)
|
498
614
|
|
499
|
-
|
615
|
+
model1 == model2 # returns true
|
500
616
|
```
|
617
|
+
</details>
|
501
618
|
|
502
619
|
RSspec matcher
|
503
620
|
---------------
|
504
621
|
|
505
622
|
You can use the provided matcher to check that your builder is adding a method correctly
|
506
623
|
|
624
|
+
<details>
|
625
|
+
<summary>Sample of specs over adding methods</summary>
|
626
|
+
|
507
627
|
```ruby
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
628
|
+
class DefaultValue
|
629
|
+
delegate :build, to: :builder
|
630
|
+
attr_reader :klass, :method, :value, :class_method
|
631
|
+
|
632
|
+
def initialize(klass, method, value, class_method: false)
|
633
|
+
@klass = klass
|
634
|
+
@method = method
|
635
|
+
@value = value
|
636
|
+
@class_method = class_method
|
637
|
+
end
|
518
638
|
|
519
|
-
|
639
|
+
private
|
520
640
|
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
end
|
641
|
+
def builder
|
642
|
+
@builder ||= Sinclair.new(klass).tap do |b|
|
643
|
+
if class_method
|
644
|
+
b.add_class_method(method) { value }
|
645
|
+
else
|
646
|
+
b.add_method(method) { value }
|
528
647
|
end
|
529
648
|
end
|
530
649
|
end
|
650
|
+
end
|
531
651
|
|
532
|
-
|
533
|
-
|
652
|
+
RSpec.describe Sinclair::Matchers do
|
653
|
+
subject(:builder_class) { DefaultValue }
|
534
654
|
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
655
|
+
let(:klass) { Class.new }
|
656
|
+
let(:method) { :the_method }
|
657
|
+
let(:value) { Random.rand(100) }
|
658
|
+
let(:builder) { builder_class.new(klass, method, value) }
|
659
|
+
let(:instance) { klass.new }
|
540
660
|
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
end
|
661
|
+
context 'when the builder runs' do
|
662
|
+
it do
|
663
|
+
expect { builder.build }.to add_method(method).to(instance)
|
545
664
|
end
|
665
|
+
end
|
546
666
|
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
end
|
667
|
+
context 'when the builder runs' do
|
668
|
+
it do
|
669
|
+
expect { builder.build }.to add_method(method).to(klass)
|
551
670
|
end
|
671
|
+
end
|
552
672
|
|
553
|
-
|
554
|
-
|
673
|
+
context 'when adding class methods' do
|
674
|
+
subject(:builder) { builder_class.new(klass, method, value, class_method: true) }
|
555
675
|
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
end
|
676
|
+
context 'when the builder runs' do
|
677
|
+
it do
|
678
|
+
expect { builder.build }.to add_class_method(method).to(klass)
|
560
679
|
end
|
561
680
|
end
|
562
681
|
end
|
682
|
+
end
|
563
683
|
```
|
564
684
|
|
565
685
|
```bash
|
566
|
-
|
567
686
|
> bundle exec rspec
|
568
687
|
```
|
569
688
|
|
@@ -577,10 +696,11 @@ Sinclair::Matchers
|
|
577
696
|
when the builder runs
|
578
697
|
should add method class_method 'the_method' to #<Class:0x000055e5d9b95d88>
|
579
698
|
```
|
699
|
+
</details>
|
580
700
|
|
581
701
|
Projects Using
|
582
702
|
---------------
|
583
703
|
|
584
|
-
-
|
585
|
-
-
|
586
|
-
-
|
704
|
+
- [Arstotzka](https://github.com/darthjee/arstotzka)
|
705
|
+
- [Azeroth](https://github.com/darthjee/azeroth)
|
706
|
+
- [Magicka](https://github.com/darthjee/magicka)
|