sinclair 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.circleci/config.yml +6 -9
- data/.github/copilot-instructions.md +66 -0
- data/.github/sinclair-usage.md +492 -0
- data/.rubocop_todo.yml +88 -1
- data/Dockerfile +2 -2
- data/Gemfile +13 -11
- data/README.md +42 -11
- data/lib/sinclair/class_methods.rb +4 -3
- data/lib/sinclair/config_builder.rb +3 -3
- data/lib/sinclair/method_definition.rb +13 -8
- data/lib/sinclair/model.rb +0 -2
- data/lib/sinclair/settable/builder.rb +12 -7
- data/lib/sinclair/settable/caster.rb +2 -0
- data/lib/sinclair/settable.rb +9 -0
- data/lib/sinclair/version.rb +1 -1
- data/lib/sinclair.rb +24 -12
- data/sinclair.png +0 -0
- data/spec/lib/sinclair/chain_settable_spec.rb +9 -1
- data/spec/lib/sinclair/env_settable_spec.rb +4 -0
- data/spec/lib/sinclair/settable/builder_spec.rb +4 -0
- data/spec/lib/sinclair/settable/caster_spec.rb +79 -0
- data/spec/lib/sinclair/settable_spec.rb +6 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/support/models/app_client.rb +2 -0
- data/spec/support/models/hash_app_client.rb +2 -0
- data/spec/support/models/my_app_client.rb +2 -0
- data/spec/support/models/non_default_app_client.rb +2 -0
- data/spec/support/shared_examples/settable.rb +137 -10
- metadata +6 -3
- data/sinclair.jpg +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 85ecbcdf5d1d0426ff0582775a7aba2808c2222c9f4d1dabdd0af6d772c61d0d
|
|
4
|
+
data.tar.gz: 4b89ed144b308319c375742e9a590055730a851f447d2c7226a70fa2142d305b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: acb1735bed6b21b9ed00dc6ec61f2b06b76095b04c933bc1338e21a81b3b026db5748d17e7b2af0a9108a55656e30aba832a0123c03a98123f560e65df175a17
|
|
7
|
+
data.tar.gz: c7e3a76d3201db9f73fa3b674794622ba71c780d545cc5108b618a30b53b7b3f551a25306745fa33768ef55c8ebca8bfb9dce5b8db21cd79dcffe3dcd221375a
|
data/.circleci/config.yml
CHANGED
|
@@ -22,26 +22,23 @@ workflows:
|
|
|
22
22
|
jobs:
|
|
23
23
|
test:
|
|
24
24
|
docker:
|
|
25
|
-
- image: darthjee/circleci_ruby_331:1.0
|
|
25
|
+
- image: darthjee/circleci_ruby_331:1.1.0
|
|
26
26
|
environment:
|
|
27
27
|
PROJECT: sinclair
|
|
28
28
|
steps:
|
|
29
29
|
- checkout
|
|
30
|
-
- run:
|
|
31
|
-
name: Prepare Coverage Test Report
|
|
32
|
-
command: cc-test-reporter before-build
|
|
33
30
|
- run:
|
|
34
31
|
name: Bundle Install
|
|
35
32
|
command: bundle install
|
|
36
33
|
- run:
|
|
37
34
|
name: RSpec
|
|
38
|
-
command: bundle exec rspec
|
|
35
|
+
command: CI=true bundle exec rspec
|
|
39
36
|
- run:
|
|
40
|
-
name:
|
|
41
|
-
command:
|
|
37
|
+
name: Upload coverage to Codacy
|
|
38
|
+
command: bash <(curl -Ls https://coverage.codacy.com/get.sh) report -r coverage/lcov/project.lcov
|
|
42
39
|
checks:
|
|
43
40
|
docker:
|
|
44
|
-
- image: darthjee/circleci_ruby_331:1.0
|
|
41
|
+
- image: darthjee/circleci_ruby_331:1.1.0
|
|
45
42
|
environment:
|
|
46
43
|
PROJECT: sinclair
|
|
47
44
|
steps:
|
|
@@ -66,7 +63,7 @@ jobs:
|
|
|
66
63
|
command: check_specs
|
|
67
64
|
build-and-release:
|
|
68
65
|
docker:
|
|
69
|
-
- image: darthjee/circleci_ruby_331:1.0
|
|
66
|
+
- image: darthjee/circleci_ruby_331:1.1.0
|
|
70
67
|
environment:
|
|
71
68
|
PROJECT: sinclair
|
|
72
69
|
steps:
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# GitHub Copilot Instructions for Sinclair
|
|
2
|
+
|
|
3
|
+
## Project Purpose
|
|
4
|
+
|
|
5
|
+
Sinclair is a Ruby gem that serves as a **foundation for developing other gems** by providing base classes and utility modules. It supplies:
|
|
6
|
+
|
|
7
|
+
- A method builder (`Sinclair`) for dynamically adding instance and class methods to any class.
|
|
8
|
+
- `Sinclair::Configurable` / `Sinclair::Config` for adding configuration to classes and modules.
|
|
9
|
+
- `Sinclair::Options` for structured, validated option objects.
|
|
10
|
+
- `Sinclair::EnvSettable` for reading environment variables through class methods.
|
|
11
|
+
- `Sinclair::Comparable` for easy `==` comparisons based on selected attributes.
|
|
12
|
+
- `Sinclair::Model` for quick creation of simple plain-Ruby model classes (using `initialize_with` inside the class body or `.for` as an inline subclassing helper).
|
|
13
|
+
- RSpec matchers (`Sinclair::Matchers`) to test method-building behaviour.
|
|
14
|
+
|
|
15
|
+
All PRs, code, comments, and documentation must be written in **English**.
|
|
16
|
+
|
|
17
|
+
## Development Workflow
|
|
18
|
+
|
|
19
|
+
Development runs inside **Docker** using **docker-compose**.
|
|
20
|
+
|
|
21
|
+
- **Enter the development environment:**
|
|
22
|
+
```bash
|
|
23
|
+
make dev
|
|
24
|
+
```
|
|
25
|
+
This runs `docker-compose run sinclair /bin/bash`, dropping you into an interactive shell inside the container with the project mounted at `/home/app/app`.
|
|
26
|
+
|
|
27
|
+
- The Docker image is built from `Dockerfile`; a CircleCI-specific image is available via `Dockerfile.circleci`.
|
|
28
|
+
|
|
29
|
+
## Tooling & CI
|
|
30
|
+
|
|
31
|
+
The CI pipeline (`.circleci/config.yml`) runs the following checks on every PR:
|
|
32
|
+
|
|
33
|
+
- **RSpec** – unit/integration test suite:
|
|
34
|
+
```bash
|
|
35
|
+
bundle exec rspec
|
|
36
|
+
```
|
|
37
|
+
- **Rubocop** – Ruby style and linting:
|
|
38
|
+
```bash
|
|
39
|
+
rubocop
|
|
40
|
+
```
|
|
41
|
+
- **Yardstick** – documentation coverage check:
|
|
42
|
+
```bash
|
|
43
|
+
bundle exec rake verify_measurements
|
|
44
|
+
```
|
|
45
|
+
- **YARD** – API documentation is generated with YARD:
|
|
46
|
+
```bash
|
|
47
|
+
yard
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
All four checks must pass before a PR can be merged.
|
|
51
|
+
|
|
52
|
+
## Testing Guidelines
|
|
53
|
+
|
|
54
|
+
- **Aim for at least one spec per source file.** Files explicitly excluded from this requirement are listed in `config/check_specs.yml`.
|
|
55
|
+
- **Avoid mocks.** Prefer real objects and stubs only when there is no reasonable alternative.
|
|
56
|
+
- **One expectation per example.** Keep each `it` block focused on a single assertion.
|
|
57
|
+
- Place specs under `spec/` mirroring the structure of `lib/`.
|
|
58
|
+
|
|
59
|
+
## Code Quality & Style
|
|
60
|
+
|
|
61
|
+
- Follow **Clean Code** principles: clear naming, small focused methods, and minimal duplication.
|
|
62
|
+
- Follow **Sandi Metz** Ruby rules:
|
|
63
|
+
- Classes should be small and have a **single responsibility**.
|
|
64
|
+
- Methods should be short and do one thing.
|
|
65
|
+
- Respect the **Law of Demeter** – avoid long method chains that reach through unrelated objects.
|
|
66
|
+
- Document all public methods and classes with **YARD** doc-comments.
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
# Sinclair – Usage Guide for Dependent Projects
|
|
2
|
+
|
|
3
|
+
This document describes how to use the **sinclair** gem in your project.
|
|
4
|
+
Copy this file into your project's `.github/` directory so that GitHub Copilot
|
|
5
|
+
is aware of the patterns and conventions Sinclair provides.
|
|
6
|
+
|
|
7
|
+
**Current release**: 3.0.1
|
|
8
|
+
**Docs**: <https://www.rubydoc.info/gems/sinclair/3.0.1>
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
Add to your `Gemfile`:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
gem 'sinclair'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
then run:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
bundle install
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Features Overview
|
|
29
|
+
|
|
30
|
+
| Feature | Class / Module | Purpose |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| Method builder | `Sinclair` | Add instance/class methods dynamically |
|
|
33
|
+
| Configuration | `Sinclair::Configurable` + `Sinclair::Config` | Read-only config with defaults |
|
|
34
|
+
| Options | `Sinclair::Options` | Validated parameter objects |
|
|
35
|
+
| Env variables | `Sinclair::EnvSettable` | Read ENV vars via class methods |
|
|
36
|
+
| Equality | `Sinclair::Comparable` | Attribute-based `==` |
|
|
37
|
+
| Plain models | `Sinclair::Model` | Quick data-model classes |
|
|
38
|
+
| Type casting | `Sinclair::Caster` | Extensible type transformations |
|
|
39
|
+
| RSpec matchers | `Sinclair::Matchers` | Test method-builder behaviour |
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 1. Sinclair – Dynamic Method Builder
|
|
44
|
+
|
|
45
|
+
`Sinclair` lets you add instance and class methods to any class at runtime.
|
|
46
|
+
Methods are queued with `add_method` / `add_class_method` and created only
|
|
47
|
+
when `build` is called.
|
|
48
|
+
|
|
49
|
+
### Stand-alone usage
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
class Clazz; end
|
|
53
|
+
|
|
54
|
+
builder = Sinclair.new(Clazz)
|
|
55
|
+
builder.add_method(:twenty, '10 + 10') # string-based
|
|
56
|
+
builder.add_method(:eighty) { 4 * twenty } # block-based
|
|
57
|
+
builder.add_class_method(:one_hundred) { 100 }
|
|
58
|
+
builder.build
|
|
59
|
+
|
|
60
|
+
instance = Clazz.new
|
|
61
|
+
instance.twenty # => 20
|
|
62
|
+
instance.eighty # => 80
|
|
63
|
+
Clazz.one_hundred # => 100
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Block DSL (`Sinclair.build`)
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
Sinclair.build(MyClass) do
|
|
70
|
+
add_method(:random_number) { Random.rand(10..20) }
|
|
71
|
+
add_class_method(:static_value) { 42 }
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### String method with parameters
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
Sinclair.build(MyClass) do
|
|
79
|
+
add_class_method(
|
|
80
|
+
:power, 'a ** b + c',
|
|
81
|
+
parameters: [:a],
|
|
82
|
+
named_parameters: [:b, { c: 15 }]
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
MyClass.power(10, b: 2) # => 115
|
|
87
|
+
MyClass.power(10, b: 2, c: 0) # => 100
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Call-based method (delegates to the class itself)
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
builder = Sinclair.new(MyClass)
|
|
94
|
+
builder.add_class_method(:attr_accessor, :number, type: :call)
|
|
95
|
+
builder.build
|
|
96
|
+
|
|
97
|
+
MyClass.number # => nil
|
|
98
|
+
MyClass.number = 10
|
|
99
|
+
MyClass.number # => 10
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Caching results
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
builder.add_method(:expensive, cached: true) { slow_computation }
|
|
106
|
+
# equivalent to: @expensive ||= slow_computation
|
|
107
|
+
|
|
108
|
+
builder.add_method(:nullable, cached: :full) { may_return_nil }
|
|
109
|
+
# caches even nil / false values
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Extending the builder
|
|
113
|
+
|
|
114
|
+
Subclass `Sinclair` to create domain-specific builders:
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
class ValidationBuilder < Sinclair
|
|
118
|
+
delegate :expected, to: :options_object
|
|
119
|
+
|
|
120
|
+
def add_validation(field)
|
|
121
|
+
add_method("#{field}_valid?", "#{field}.is_a?(#{expected})")
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
module Validatable
|
|
126
|
+
extend ActiveSupport::Concern
|
|
127
|
+
|
|
128
|
+
class_methods do
|
|
129
|
+
def validate(*fields, expected_class)
|
|
130
|
+
builder = ValidationBuilder.new(self, expected: expected_class)
|
|
131
|
+
fields.each { |f| builder.add_validation(f) }
|
|
132
|
+
builder.build
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
class MyModel
|
|
138
|
+
include Validatable
|
|
139
|
+
validate :name, String
|
|
140
|
+
validate :age, Integer
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 2. Sinclair::Configurable – Application Configuration
|
|
147
|
+
|
|
148
|
+
`Sinclair::Configurable` adds a read-only `config` object to any class or
|
|
149
|
+
module. Settings can only be changed through `configure`.
|
|
150
|
+
|
|
151
|
+
### Inline attributes
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
module MyApp
|
|
155
|
+
extend Sinclair::Configurable
|
|
156
|
+
|
|
157
|
+
configurable_with :host, port: 80, debug: false
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
MyApp.configure(port: 5555) do |config|
|
|
161
|
+
config.host 'example.com'
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
MyApp.config.host # => 'example.com'
|
|
165
|
+
MyApp.config.port # => 5555
|
|
166
|
+
|
|
167
|
+
MyApp.reset_config
|
|
168
|
+
MyApp.config.host # => nil
|
|
169
|
+
MyApp.config.port # => 80
|
|
170
|
+
|
|
171
|
+
# Convert to Options object (useful for passing around)
|
|
172
|
+
MyApp.as_options(host: 'other').host # => 'other'
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Custom config class
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
class ServerConfig < Sinclair::Config
|
|
179
|
+
config_attributes :host, :port
|
|
180
|
+
|
|
181
|
+
def url
|
|
182
|
+
@port ? "http://#{@host}:#{@port}" : "http://#{@host}"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
class Client
|
|
187
|
+
extend Sinclair::Configurable
|
|
188
|
+
configurable_by ServerConfig
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
Client.configure { host 'api.example.com' }
|
|
192
|
+
Client.config.url # => 'http://api.example.com'
|
|
193
|
+
|
|
194
|
+
Client.configure { port 8080 }
|
|
195
|
+
Client.config.url # => 'http://api.example.com:8080'
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## 3. Sinclair::Options – Validated Option Objects
|
|
201
|
+
|
|
202
|
+
`Sinclair::Options` creates structured option/parameter value objects with
|
|
203
|
+
defaults and validation against unknown keys.
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
class ConnectionOptions < Sinclair::Options
|
|
207
|
+
with_options :timeout, :retries, port: 443, protocol: 'https'
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
opts = ConnectionOptions.new(timeout: 30, protocol: 'http')
|
|
211
|
+
opts.timeout # => 30
|
|
212
|
+
opts.retries # => nil
|
|
213
|
+
opts.port # => 443 (default)
|
|
214
|
+
opts.protocol # => 'http'
|
|
215
|
+
opts.to_h # => { timeout: 30, retries: nil, port: 443, protocol: 'http' }
|
|
216
|
+
|
|
217
|
+
ConnectionOptions.new(unknown_key: 1)
|
|
218
|
+
# raises Sinclair::Exception::InvalidOptions
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Call `skip_validation` in the class body to allow unknown keys:
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
class LooseOptions < Sinclair::Options
|
|
225
|
+
with_options :name
|
|
226
|
+
skip_validation
|
|
227
|
+
end
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## 4. Sinclair::EnvSettable – Environment Variable Access
|
|
233
|
+
|
|
234
|
+
`EnvSettable` exposes environment variables as class-level methods, with
|
|
235
|
+
optional prefix and default values.
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
class ServiceClient
|
|
239
|
+
extend Sinclair::EnvSettable
|
|
240
|
+
|
|
241
|
+
settings_prefix 'SERVICE'
|
|
242
|
+
with_settings :username, :password, port: 80, hostname: 'my-host.com'
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
ENV['SERVICE_USERNAME'] = 'my-login'
|
|
246
|
+
ENV['SERVICE_HOSTNAME'] = 'host.com'
|
|
247
|
+
|
|
248
|
+
ServiceClient.username # => 'my-login'
|
|
249
|
+
ServiceClient.hostname # => 'host.com'
|
|
250
|
+
ServiceClient.port # => 80 (default – ENV var not set)
|
|
251
|
+
ServiceClient.password # => nil (ENV var not set, no default)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Type casting
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
class AppConfig
|
|
258
|
+
extend Sinclair::EnvSettable
|
|
259
|
+
|
|
260
|
+
settings_prefix 'APP'
|
|
261
|
+
setting_with_options :timeout, type: :integer, default: 30
|
|
262
|
+
setting_with_options :debug, type: :boolean
|
|
263
|
+
setting_with_options :rate, type: :float
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
ENV['APP_TIMEOUT'] = '60'
|
|
267
|
+
AppConfig.timeout # => 60 (Integer)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## 5. Sinclair::Comparable – Attribute-based Equality
|
|
273
|
+
|
|
274
|
+
Include `Sinclair::Comparable` and declare which attributes are used for `==`.
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
class Person
|
|
278
|
+
include Sinclair::Comparable
|
|
279
|
+
|
|
280
|
+
comparable_by :name
|
|
281
|
+
attr_reader :name, :age
|
|
282
|
+
|
|
283
|
+
def initialize(name:, age:)
|
|
284
|
+
@name = name
|
|
285
|
+
@age = age
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
p1 = Person.new(name: 'Alice', age: 30)
|
|
290
|
+
p2 = Person.new(name: 'Alice', age: 25)
|
|
291
|
+
|
|
292
|
+
p1 == p2 # => true (only :name is compared)
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## 6. Sinclair::Model – Quick Plain-Ruby Models
|
|
298
|
+
|
|
299
|
+
`Sinclair::Model` generates reader/writer methods, a keyword-argument
|
|
300
|
+
initializer, and equality (via `Sinclair::Comparable`) in one call.
|
|
301
|
+
|
|
302
|
+
There are two ways to define a model:
|
|
303
|
+
|
|
304
|
+
- **`initialize_with`** – called inside the class body; adds methods to the current class.
|
|
305
|
+
- **`.for`** – class method that returns a new anonymous subclass; useful when inheriting inline.
|
|
306
|
+
|
|
307
|
+
### Basic model (initialize_with)
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
class Human < Sinclair::Model
|
|
311
|
+
initialize_with :name, :age, { gender: :undefined }, **{}
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
h1 = Human.new(name: 'John', age: 22)
|
|
315
|
+
h2 = Human.new(name: 'John', age: 22)
|
|
316
|
+
|
|
317
|
+
h1.name # => 'John'
|
|
318
|
+
h1.gender # => :undefined
|
|
319
|
+
h1 == h2 # => true
|
|
320
|
+
|
|
321
|
+
h1.name = 'Jane' # setter generated by default
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Disabling writers or equality (initialize_with)
|
|
325
|
+
|
|
326
|
+
```ruby
|
|
327
|
+
class Tv < Sinclair::Model
|
|
328
|
+
initialize_with :brand, :model, writter: false, comparable: false
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
tv1 = Tv.new(brand: 'Sony', model: 'X90L')
|
|
332
|
+
tv2 = Tv.new(brand: 'Sony', model: 'X90L')
|
|
333
|
+
|
|
334
|
+
tv1 == tv2 # => false (comparable disabled)
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Using .for (inline subclassing)
|
|
338
|
+
|
|
339
|
+
```ruby
|
|
340
|
+
class Car < Sinclair::Model.for(:brand, :model, writter: false)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
car = Car.new(brand: :ford, model: :T)
|
|
344
|
+
|
|
345
|
+
car.brand # => :ford
|
|
346
|
+
car.model # => :T
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Using .for with default values
|
|
350
|
+
|
|
351
|
+
```ruby
|
|
352
|
+
class Job < Sinclair::Model.for({ state: :starting }, writter: true)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
job = Job.new
|
|
356
|
+
|
|
357
|
+
job.state # => :starting
|
|
358
|
+
job.state = :done
|
|
359
|
+
job.state # => :done
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Options accepted by both `initialize_with` and `.for`:
|
|
363
|
+
|
|
364
|
+
| Option | Default | Description |
|
|
365
|
+
|---|---|---|
|
|
366
|
+
| `writter:` | `true` | Generate setter methods |
|
|
367
|
+
| `comparable:` | `true` | Include field in `==` comparison |
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## 7. Sinclair::Caster – Type Casting
|
|
372
|
+
|
|
373
|
+
`Sinclair::Caster` provides a registry of named type casters.
|
|
374
|
+
|
|
375
|
+
```ruby
|
|
376
|
+
class MyCaster < Sinclair::Caster
|
|
377
|
+
cast_with(:upcase, :upcase)
|
|
378
|
+
cast_with(:log) { |value, base: 10| Math.log(value.to_f, base) }
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
MyCaster.cast('hello', :upcase) # => 'HELLO'
|
|
382
|
+
MyCaster.cast(100, :log) # => 2.0
|
|
383
|
+
MyCaster.cast(16, :log, base: 2) # => 4.0
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Class-based casting
|
|
387
|
+
|
|
388
|
+
```ruby
|
|
389
|
+
class TypeCaster < Sinclair::Caster
|
|
390
|
+
master_caster!
|
|
391
|
+
|
|
392
|
+
cast_with(Integer, :to_i)
|
|
393
|
+
cast_with(Float, :to_f)
|
|
394
|
+
cast_with(String, :to_s)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
TypeCaster.cast('42', Integer) # => 42
|
|
398
|
+
TypeCaster.cast(3, Float) # => 3.0
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## 8. Sinclair::Matchers – RSpec Matchers
|
|
404
|
+
|
|
405
|
+
Include `Sinclair::Matchers` in your RSpec configuration to gain matchers for
|
|
406
|
+
testing that a builder adds or changes methods.
|
|
407
|
+
|
|
408
|
+
### Setup
|
|
409
|
+
|
|
410
|
+
```ruby
|
|
411
|
+
# spec/spec_helper.rb
|
|
412
|
+
RSpec.configure do |config|
|
|
413
|
+
config.include Sinclair::Matchers
|
|
414
|
+
end
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Available matchers
|
|
418
|
+
|
|
419
|
+
```ruby
|
|
420
|
+
# Checks that build adds an instance method
|
|
421
|
+
expect { builder.build }.to add_method(:name).to(instance)
|
|
422
|
+
expect { builder.build }.to add_method(:name).to(klass)
|
|
423
|
+
|
|
424
|
+
# Checks that build adds a class method
|
|
425
|
+
expect { builder.build }.to add_class_method(:count).to(klass)
|
|
426
|
+
|
|
427
|
+
# Checks that build changes an existing instance method
|
|
428
|
+
expect { builder.build }.to change_method(:value).on(instance)
|
|
429
|
+
|
|
430
|
+
# Checks that build changes an existing class method
|
|
431
|
+
expect { builder.build }.to change_class_method(:count).on(klass)
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### Example spec
|
|
435
|
+
|
|
436
|
+
```ruby
|
|
437
|
+
RSpec.describe MyBuilder do
|
|
438
|
+
let(:klass) { Class.new }
|
|
439
|
+
let(:instance) { klass.new }
|
|
440
|
+
let(:builder) { MyBuilder.new(klass) }
|
|
441
|
+
|
|
442
|
+
it 'adds a greeting method to instances' do
|
|
443
|
+
expect { builder.build }.to add_method(:greet).to(instance)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
it 'adds a factory class method' do
|
|
447
|
+
expect { builder.build }.to add_class_method(:create).to(klass)
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## Complete Example
|
|
455
|
+
|
|
456
|
+
```ruby
|
|
457
|
+
# Combining multiple Sinclair features in one class
|
|
458
|
+
|
|
459
|
+
class ApiClient
|
|
460
|
+
extend Sinclair::Configurable
|
|
461
|
+
extend Sinclair::EnvSettable
|
|
462
|
+
include Sinclair::Comparable
|
|
463
|
+
|
|
464
|
+
# --- Configuration (set programmatically) ---
|
|
465
|
+
configurable_with :timeout, retries: 3
|
|
466
|
+
|
|
467
|
+
# --- Environment variables ---
|
|
468
|
+
settings_prefix 'API'
|
|
469
|
+
with_settings :api_key, :secret, base_url: 'https://api.example.com'
|
|
470
|
+
|
|
471
|
+
# --- Equality based on base_url ---
|
|
472
|
+
comparable_by :base_url
|
|
473
|
+
|
|
474
|
+
attr_reader :base_url
|
|
475
|
+
|
|
476
|
+
def initialize(base_url: self.class.base_url)
|
|
477
|
+
@base_url = base_url
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# Wire up at boot time
|
|
482
|
+
ENV['API_API_KEY'] = 'secret-key'
|
|
483
|
+
ApiClient.configure(timeout: 60)
|
|
484
|
+
|
|
485
|
+
client1 = ApiClient.new
|
|
486
|
+
client2 = ApiClient.new
|
|
487
|
+
|
|
488
|
+
client1 == client2 # => true
|
|
489
|
+
ApiClient.config.timeout # => 60
|
|
490
|
+
ApiClient.config.retries # => 3
|
|
491
|
+
ApiClient.api_key # => 'secret-key'
|
|
492
|
+
```
|