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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d9505060c3b7a018a408ea37b59af41203747d280336fd5f1f6bb148f329af6
4
- data.tar.gz: 9967733ebc9f4ba73d553816a20530ff9399d3045cf0485f7412b0116924a2c8
3
+ metadata.gz: 85ecbcdf5d1d0426ff0582775a7aba2808c2222c9f4d1dabdd0af6d772c61d0d
4
+ data.tar.gz: 4b89ed144b308319c375742e9a590055730a851f447d2c7226a70fa2142d305b
5
5
  SHA512:
6
- metadata.gz: 66bf1c943c5b9c9e6facfb8c5eecd92b86de5385f0fbb701715e62329ad5e82912272182baf4dce83f7517c8aae7b340aed8ce12e0315e0aefb805db3cfc90c7
7
- data.tar.gz: e1166a305873c0e5184a9d8043166f27e1f2ecae5be57d031fea48f5d7ec0f2c79c0d8f1acefdf1db30d54ad8579ea220d6cd64be160ae5d0f6505379291d96a
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.1
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: Coverage Test Report
41
- command: cc-test-reporter after-build --exit-code $?
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.1
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.1
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
+ ```