fix 0.18.2 → 0.19

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.md +2 -2
  3. data/README.md +384 -84
  4. data/lib/fix/builder.rb +101 -0
  5. data/lib/fix/doc.rb +59 -0
  6. data/lib/fix/dsl.rb +139 -0
  7. data/lib/fix/error/invalid_specification_name.rb +12 -0
  8. data/lib/fix/error/missing_specification_block.rb +14 -0
  9. data/lib/fix/error/specification_not_found.rb +12 -0
  10. data/lib/fix/matcher.rb +76 -0
  11. data/lib/fix/requirement.rb +119 -0
  12. data/lib/fix/run.rb +88 -0
  13. data/lib/fix/set.rb +67 -0
  14. data/lib/fix.rb +85 -20
  15. data/lib/kernel.rb +49 -0
  16. metadata +41 -153
  17. data/.gitignore +0 -11
  18. data/.rubocop.yml +0 -1
  19. data/.rubocop_todo.yml +0 -25
  20. data/.travis.yml +0 -28
  21. data/.yardopts +0 -1
  22. data/CODE_OF_CONDUCT.md +0 -13
  23. data/Gemfile +0 -5
  24. data/Rakefile +0 -23
  25. data/VERSION.semver +0 -1
  26. data/bin/console +0 -8
  27. data/bin/setup +0 -6
  28. data/checksum/fix-0.0.1.pre.gem.sha512 +0 -1
  29. data/checksum/fix-0.1.0.gem.sha512 +0 -1
  30. data/checksum/fix-0.1.0.pre.gem.sha512 +0 -1
  31. data/checksum/fix-0.10.0.gem.sha512 +0 -1
  32. data/checksum/fix-0.11.0.gem.sha512 +0 -1
  33. data/checksum/fix-0.11.1.gem.sha512 +0 -1
  34. data/checksum/fix-0.12.0.gem.sha512 +0 -1
  35. data/checksum/fix-0.12.1.gem.sha512 +0 -1
  36. data/checksum/fix-0.12.2.gem.sha512 +0 -1
  37. data/checksum/fix-0.12.3.gem.sha512 +0 -1
  38. data/checksum/fix-0.13.0.gem.sha512 +0 -1
  39. data/checksum/fix-0.14.0.gem.sha512 +0 -1
  40. data/checksum/fix-0.14.1.gem.sha512 +0 -1
  41. data/checksum/fix-0.15.0.gem.sha512 +0 -1
  42. data/checksum/fix-0.15.2.gem.sha512 +0 -1
  43. data/checksum/fix-0.16.0.gem.sha512 +0 -1
  44. data/checksum/fix-0.17.0.gem.sha512 +0 -1
  45. data/checksum/fix-0.17.1.gem.sha512 +0 -1
  46. data/checksum/fix-0.17.2.gem.sha512 +0 -1
  47. data/checksum/fix-0.18.0.gem.sha512 +0 -1
  48. data/checksum/fix-0.18.1.gem.sha512 +0 -1
  49. data/checksum/fix-0.2.0.gem.sha512 +0 -1
  50. data/checksum/fix-0.3.0.gem.sha512 +0 -1
  51. data/checksum/fix-0.4.0.gem.sha512 +0 -1
  52. data/checksum/fix-0.5.0.gem.sha512 +0 -1
  53. data/checksum/fix-0.6.0.gem.sha512 +0 -1
  54. data/checksum/fix-0.6.1.gem.sha512 +0 -1
  55. data/checksum/fix-0.7.0.gem.sha512 +0 -1
  56. data/checksum/fix-0.8.0.gem.sha512 +0 -1
  57. data/checksum/fix-0.9.0.gem.sha512 +0 -1
  58. data/checksum/fix-0.9.1.gem.sha512 +0 -1
  59. data/fix.gemspec +0 -29
  60. data/lib/fix/it.rb +0 -41
  61. data/lib/fix/on.rb +0 -139
  62. data/lib/fix/report.rb +0 -120
  63. data/lib/fix/test.rb +0 -89
  64. data/pkg_checksum +0 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f3185bfc9a8b56c3a47a7efbbcbf141ea054e405bf2d4c6d0e76ceec8bc0fed2
4
- data.tar.gz: fff9e1401b1e7aff1b9214692922eb6176548f12b94265d673fc4daa311bba53
3
+ metadata.gz: 9e44836a2e867dcb513bc3daffa30c6c319e18602be1c4cf0e098d14f7ae4f9b
4
+ data.tar.gz: 70029c70b7efd078c5d4cafdcdacd1921a2c081e1637f7f2cbc867bfde839781
5
5
  SHA512:
6
- metadata.gz: 7de8da736c7d22d71a24b0d4a03fc691e34f2c33bf1394aeda3a78856b687c6e686b1b4ac548c7a52237953996fb70e35de5733da715a2b06342654ae9737fd2
7
- data.tar.gz: 1058e6255fb4423d8479eddf1d95431de023acacefe84d87825f33ed4205378b5eea2ddfcdeb3b72aaa9e921eb96df240e52a12a55427d7dba9b68227550fb02
6
+ metadata.gz: f18450b4cc887e269471c529e95816537f620e342733a3441091b01709feaf44d7f1fe46e7ea2b228f5eca154f410de0e03456f238aa642e2a35ff1d55aa5376
7
+ data.tar.gz: 3854f42a81ddece26f7e9b7d33d347caf37c885cb65ea1e17910032c279c249aefa6ff73d3b7dccfd205d5824812d72564c79d2b76cc7d0afd2e2c51ad08428e
data/LICENSE.md CHANGED
@@ -1,6 +1,6 @@
1
- The MIT License (MIT)
1
+ # The MIT License
2
2
 
3
- Copyright (c) 2014-2019 Cyril Kato
3
+ Copyright (c) 2014-2025 Cyril Kato
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,152 +1,452 @@
1
- # Fix
1
+ # Fix Framework
2
2
 
3
- [![Build Status](https://travis-ci.org/fixrb/fix.svg?branch=master)][travis]
4
- [![Code Climate](https://codeclimate.com/github/fixrb/fix/badges/gpa.svg)][codeclimate]
5
- [![Gem Version](https://badge.fury.io/rb/fix.svg)][gem]
6
- [![Inline docs](http://inch-ci.org/github/fixrb/fix.svg?branch=master)][inchpages]
7
- [![Documentation](http://img.shields.io/:yard-docs-38c800.svg)][rubydoc]
8
- [![Gitter](https://badges.gitter.im/Join%20Chat.svg)][gitter]
3
+ [![Home](https://img.shields.io/badge/Home-fixrb.dev-00af8b)](https://fixrb.dev/)
4
+ [![Version](https://img.shields.io/github/v/tag/fixrb/fix?label=Version&logo=github)](https://github.com/fixrb/fix/tags)
5
+ [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/fixrb/fix/main)
6
+ [![License](https://img.shields.io/github/license/fixrb/fix?label=License&logo=github)](https://github.com/fixrb/fix/raw/main/LICENSE.md)
9
7
 
10
- > Specing framework for Ruby.
8
+ ## Introduction
11
9
 
12
- ## Contact
13
-
14
- * Home page: https://fixrb.dev/
15
- * Bugs/issues: https://github.com/fixrb/fix/issues
16
- * Support: https://stackoverflow.com/questions/tagged/fixrb
10
+ Fix is a modern Ruby testing framework that emphasizes clear separation between specifications and examples. Unlike traditional testing frameworks, Fix focuses on creating pure specification documents that define expected behaviors without mixing in implementation details.
17
11
 
18
12
  ## Installation
19
13
 
20
- Add this line to your application's Gemfile:
14
+ ### Prerequisites
15
+
16
+ - Ruby >= 3.1.0
17
+
18
+ ### Setup
19
+
20
+ Add to your Gemfile:
21
21
 
22
22
  ```ruby
23
- gem 'fix'
23
+ gem "fix"
24
24
  ```
25
25
 
26
- And then execute:
26
+ Then execute:
27
27
 
28
- $ bundle
28
+ ```sh
29
+ bundle install
30
+ ```
29
31
 
30
- Or install it yourself as:
32
+ Or install it yourself:
31
33
 
32
- $ gem install fix
34
+ ```sh
35
+ gem install fix
36
+ ```
33
37
 
34
- ## Let's get started!
38
+ ## Core Principles
35
39
 
36
- [![asciicast](https://asciinema.org/a/29098.png)](https://asciinema.org/a/29098)
40
+ - **Specifications vs Examples**: Fix makes a clear distinction between specifications (what is expected) and examples (how it's demonstrated). This separation leads to cleaner, more maintainable test suites.
37
41
 
38
- ## Philosophy
42
+ - **Logic-Free Specifications**: Your specification documents remain pure and focused on defining behaviors, without getting cluttered by implementation logic.
39
43
 
40
- ### Simple, stupid
44
+ - **Rich Semantic Language**: Following RFC 2119 conventions, Fix uses precise language with keywords like MUST, SHOULD, and MAY to clearly define different requirement levels in specifications.
41
45
 
42
- With 174 LOC built on top of [Spectus expectation library](https://github.com/fixrb/spectus), facilities such as benchmarking and mocking are not supported. __Fix__ offers however a **consistent** DSL to focus your BDD.
46
+ - **Fast Individual Testing**: Tests execute quickly and independently, providing rapid feedback on specification compliance.
43
47
 
44
- ### True specifications
48
+ ## Framework Features
45
49
 
46
- While specs behave like **documents** which can be logic-less, their interpretation should not be questioned regardless of the version of __Fix__, preventing from software erosion. Also, Fix specs are compliant with **[RFC 2119](http://tools.ietf.org/html/rfc2119)**.
50
+ ### Property Definition with `let`
47
51
 
48
- ### Low code complexity
52
+ Define reusable properties across your specifications:
49
53
 
50
- Monkey-patching, [magic tricks and friends](http://blog.arkency.com/2013/06/are-we-abusing-at-exit/) are not included. Instead, animated by **authentic** and **unmuted** Ruby objects, unambiguous, understandable and structured specs are encouraged.
54
+ ```ruby
55
+ Fix do
56
+ let(:name) { "Bob" }
57
+ let(:age) { 42 }
51
58
 
52
- ### Test in isolation
59
+ it MUST eq name
60
+ end
61
+ ```
53
62
 
54
- Rather than a _random order_ option to help finding bugs somewhere (and sometimes luck), __Fix__ prevents from **side effects** by running each context inside a distinct **sub-process**. As it behaves like a function, no matter how many times you call it, the build status remains the same.
63
+ ### Context Creation with `with`
55
64
 
56
- ## Usage
65
+ Test behavior under different conditions:
57
66
 
58
- Given this app:
67
+ ```ruby
68
+ Fix do
69
+ with name: "Alice", role: "admin" do
70
+ it MUST be_allowed
71
+ end
72
+
73
+ with name: "Bob", role: "guest" do
74
+ it MUST_NOT be_allowed
75
+ end
76
+ end
77
+ ```
78
+
79
+ ### Method Testing with `on`
80
+
81
+ Test how objects respond to specific messages:
59
82
 
60
83
  ```ruby
61
- # duck.rb
62
- class Duck
63
- def walks
64
- 'Klop klop!'
84
+ Fix do
85
+ on :upcase do
86
+ it MUST eq "HELLO"
65
87
  end
66
88
 
67
- def swims
68
- 'Swoosh...'
89
+ on :+, 2 do
90
+ it MUST eq 42
69
91
  end
92
+ end
93
+ ```
70
94
 
71
- def quacks
72
- puts 'Quaaaaaack!'
95
+ ### Requirement Levels
96
+
97
+ Fix provides three levels of requirements, each with clear semantic meaning:
98
+
99
+ - **MUST/MUST_NOT**: Absolute requirements or prohibitions
100
+ - **SHOULD/SHOULD_NOT**: Recommended practices with valid exceptions
101
+ - **MAY**: Optional features
102
+
103
+ ```ruby
104
+ Fix do
105
+ it MUST be_valid # Required
106
+ it SHOULD be_optimized # Recommended
107
+ it MAY include_metadata # Optional
108
+ end
109
+ ```
110
+
111
+ ## Quick Start
112
+
113
+ Create your first test file:
114
+
115
+ ```ruby
116
+ # first_test.rb
117
+ require "fix"
118
+
119
+ Fix :HelloWorld do
120
+ it MUST eq "Hello, World!"
121
+ end
122
+
123
+ Fix[:HelloWorld].test { "Hello, World!" }
124
+ ```
125
+
126
+ Run it:
127
+
128
+ ```sh
129
+ ruby first_test.rb
130
+ ```
131
+
132
+ ## Real-World Examples
133
+
134
+ Fix is designed to work with real-world applications of any complexity. Here are some examples demonstrating how Fix can be used in different scenarios:
135
+
136
+ ### Example 1: User Account Management
137
+
138
+ Here's a comprehensive example showing how to specify a user account system:
139
+
140
+ ```ruby
141
+ Fix :UserAccount do
142
+ # Define reusable properties
143
+ let(:admin) { User.new(role: "admin") }
144
+ let(:guest) { User.new(role: "guest") }
145
+
146
+ # Test basic instance properties
147
+ it MUST be_an_instance_of User
148
+
149
+ # Test with different contexts
150
+ with role: "admin" do
151
+ it MUST be_admin
152
+
153
+ on :can_access?, "settings" do
154
+ it MUST be_true
155
+ end
156
+ end
157
+
158
+ with role: "guest" do
159
+ it MUST_NOT be_admin
160
+
161
+ on :can_access?, "settings" do
162
+ it MUST be_false
163
+ end
164
+ end
165
+
166
+ # Test specific methods
167
+ on :full_name do
168
+ with first_name: "John", last_name: "Doe" do
169
+ it MUST eq "John Doe"
170
+ end
171
+ end
172
+
173
+ on :update_password, "new_password" do
174
+ it MUST change(admin, :password_hash)
175
+ it MUST be_true # Return value check
73
176
  end
74
177
  end
75
178
  ```
76
179
 
77
- When you run this:
180
+ The implementation might look like this:
78
181
 
79
182
  ```ruby
80
- # duck_spec.rb
81
- require_relative 'duck'
82
- require 'fix'
183
+ class User
184
+ attr_reader :role, :password_hash
83
185
 
84
- @bird = Duck.new
186
+ def initialize(role:)
187
+ @role = role
188
+ @password_hash = nil
189
+ end
190
+
191
+ def admin?
192
+ role == "admin"
193
+ end
194
+
195
+ def can_access?(resource)
196
+ return true if admin?
197
+ false
198
+ end
199
+
200
+ def full_name
201
+ "#{@first_name} #{@last_name}"
202
+ end
203
+
204
+ def update_password(new_password)
205
+ @password_hash = Digest::SHA256.hexdigest(new_password)
206
+ true
207
+ end
208
+ end
209
+ ```
210
+
211
+ ### Example 2: Duck Specification
212
+
213
+ Here's how Fix can be used to specify a Duck class:
214
+
215
+ ```ruby
216
+ Fix :Duck do
217
+ it SHOULD be_an_instance_of :Duck
85
218
 
86
- Fix.describe @bird do
87
219
  on :swims do
88
- it { MUST eql 'Swoosh...' }
220
+ it MUST be_an_instance_of :String
221
+ it MUST eql "Swoosh..."
89
222
  end
90
223
 
91
224
  on :speaks do
92
- it { MUST raise_exception NoMethodError }
225
+ it MUST raise_exception NoMethodError
93
226
  end
94
227
 
95
228
  on :sings do
96
- it { MAY eql '♪... ♫...' }
229
+ it MAY eql "♪... ♫..."
230
+ end
231
+ end
232
+ ```
233
+
234
+ The implementation:
235
+
236
+ ```ruby
237
+ class Duck
238
+ def walks
239
+ "Klop klop!"
240
+ end
241
+
242
+ def swims
243
+ "Swoosh..."
244
+ end
245
+
246
+ def quacks
247
+ puts "Quaaaaaack!"
97
248
  end
98
249
  end
99
250
  ```
100
251
 
101
- Then the output should look like this:
252
+ Running the test:
102
253
 
103
- $ ruby duck_spec.rb
104
- ..I
254
+ ```ruby
255
+ Fix[:Duck].test { Duck.new }
256
+ ```
257
+ ## Available Matchers
258
+
259
+ Fix includes a comprehensive set of matchers through its integration with the [Matchi library](https://github.com/fixrb/matchi):
260
+
261
+ <details>
262
+ <summary><strong>Basic Comparison Matchers</strong></summary>
263
+
264
+ - `eq(expected)` - Tests equality using `eql?`
265
+ ```ruby
266
+ it MUST eq(42) # Passes if value.eql?(42)
267
+ it MUST eq("hello") # Passes if value.eql?("hello")
268
+ ```
269
+ - `eql(expected)` - Alias for eq
270
+ - `be(expected)` - Tests object identity using `equal?`
271
+ ```ruby
272
+ string = "test"
273
+ it MUST be(string) # Passes only if it's exactly the same object
274
+ ```
275
+ - `equal(expected)` - Alias for be
276
+ </details>
277
+
278
+ <details>
279
+ <summary><strong>Type Checking Matchers</strong></summary>
280
+
281
+ - `be_an_instance_of(class)` - Verifies exact class match
282
+ ```ruby
283
+ it MUST be_an_instance_of(Array) # Passes if value.instance_of?(Array)
284
+ it MUST be_an_instance_of(User) # Passes if value.instance_of?(User)
285
+ ```
286
+ - `be_a_kind_of(class)` - Checks class inheritance and module inclusion
287
+ ```ruby
288
+ it MUST be_a_kind_of(Enumerable) # Passes if value.kind_of?(Enumerable)
289
+ it MUST be_a_kind_of(Animal) # Passes if value inherits from Animal
290
+ ```
291
+ </details>
292
+
293
+ <details>
294
+ <summary><strong>Change Testing Matchers</strong></summary>
295
+
296
+ - `change(object, method)` - Base matcher for state changes
297
+ - `.by(n)` - Expects exact change by n
298
+ ```ruby
299
+ it MUST change(user, :points).by(5) # Exactly +5 points
300
+ ```
301
+ - `.by_at_least(n)` - Expects minimum change by n
302
+ ```ruby
303
+ it MUST change(counter, :value).by_at_least(10) # At least +10
304
+ ```
305
+ - `.by_at_most(n)` - Expects maximum change by n
306
+ ```ruby
307
+ it MUST change(account, :balance).by_at_most(100) # No more than +100
308
+ ```
309
+ - `.from(old).to(new)` - Expects change from old to new value
310
+ ```ruby
311
+ it MUST change(user, :status).from("pending").to("active")
312
+ ```
313
+ - `.to(new)` - Expects change to new value
314
+ ```ruby
315
+ it MUST change(post, :title).to("Updated")
316
+ ```
317
+ </details>
318
+
319
+ <details>
320
+ <summary><strong>Numeric Matchers</strong></summary>
321
+
322
+ - `be_within(delta).of(value)` - Tests if a value is within ±delta of expected value
323
+ ```ruby
324
+ it MUST be_within(0.1).of(3.14) # Passes if value is between 3.04 and 3.24
325
+ it MUST be_within(5).of(100) # Passes if value is between 95 and 105
326
+ ```
327
+ </details>
328
+
329
+ <details>
330
+ <summary><strong>Pattern Matchers</strong></summary>
331
+
332
+ - `match(regex)` - Tests string against regular expression pattern
333
+ ```ruby
334
+ it MUST match(/^\d{3}-\d{2}-\d{4}$/) # SSN format
335
+ it MUST match(/^[A-Z][a-z]+$/) # Capitalized word
336
+ ```
337
+ - `satisfy { |value| ... }` - Custom matching with block
338
+ ```ruby
339
+ it MUST satisfy { |num| num.even? && num > 0 }
340
+ it MUST satisfy { |user| user.valid? && user.active? }
341
+ ```
342
+ </details>
343
+
344
+ <details>
345
+ <summary><strong>Exception Matchers</strong></summary>
346
+
347
+ - `raise_exception(class)` - Tests if code raises specified exception
348
+ ```ruby
349
+ it MUST raise_exception(ArgumentError)
350
+ it MUST raise_exception(CustomError, "specific message")
351
+ ```
352
+ </details>
353
+
354
+ <details>
355
+ <summary><strong>State Matchers</strong></summary>
356
+
357
+ - `be_true` - Tests for true
358
+ ```ruby
359
+ it MUST be_true # Only passes for true, not truthy values
360
+ ```
361
+ - `be_false` - Tests for false
362
+ ```ruby
363
+ it MUST be_false # Only passes for false, not falsey values
364
+ ```
365
+ - `be_nil` - Tests for nil
366
+ ```ruby
367
+ it MUST be_nil # Passes only for nil
368
+ ```
369
+ </details>
370
+
371
+ <details>
372
+ <summary><strong>Dynamic Predicate Matchers</strong></summary>
373
+
374
+ - `be_*` - Dynamically matches `object.*?` method
375
+ ```ruby
376
+ it MUST be_empty # Calls empty?
377
+ it MUST be_valid # Calls valid?
378
+ it MUST be_frozen # Calls frozen?
379
+ ```
380
+ - `have_*` - Dynamically matches `object.has_*?` method
381
+ ```ruby
382
+ it MUST have_key(:id) # Calls has_key?
383
+ it MUST have_errors # Calls has_errors?
384
+ it MUST have_permission # Calls has_permission?
385
+ ```
386
+ </details>
387
+
388
+ ### Complete Example
389
+
390
+ Here's an example using various matchers together:
105
391
 
106
- 1. Info: undefined method `sings' for #<Duck:0x007fb60383b740> (NoMethodError).
392
+ ```ruby
393
+ Fix :Calculator do
394
+ it MUST be_an_instance_of Calculator
107
395
 
108
- Ran 3 tests in 0.00038 seconds
109
- 100% compliant - 1 infos, 0 failures, 0 errors
396
+ on :add, 2, 3 do
397
+ it MUST eq 5
398
+ it MUST be_within(0.001).of(5.0)
399
+ end
400
+
401
+ on :divide, 1, 0 do
402
+ it MUST raise_exception ZeroDivisionError
403
+ end
110
404
 
111
- ## Security
405
+ with numbers: [1, 2, 3] do
406
+ it MUST_NOT be_empty
407
+ it MUST satisfy { |result| result.all? { |n| n.positive? } }
408
+ end
409
+
410
+ with string_input: "123" do
411
+ on :parse do
412
+ it MUST be_a_kind_of Numeric
413
+ it MUST satisfy { |n| n > 0 }
414
+ end
415
+ end
416
+ end
417
+ ```
112
418
 
113
- As a basic form of security __Fix__ provides a set of SHA512 checksums for
114
- every Gem release. These checksums can be found in the `checksum/` directory.
115
- Although these checksums do not prevent malicious users from tampering with a
116
- built Gem they can be used for basic integrity verification purposes.
419
+ ## Why Choose Fix?
117
420
 
118
- The checksum of a file can be checked using the `sha512sum` command. For
119
- example:
421
+ Fix brings several unique advantages to Ruby testing that set it apart from traditional testing frameworks:
120
422
 
121
- $ sha512sum pkg/fix-0.1.0.gem
122
- d12d7d9c2a4fdfe075cbb7a141fa5f2195175891e4098c7e1a28c8bca655ab44fb9d67b6a2e3991d0f852026c5e4537fdf7e314575c68d1c80b3a4b1eb1c041f pkg/fix-0.1.0.gem
423
+ - **Clear Separation of Concerns**: Keep your specifications clean and your examples separate
424
+ - **Semantic Precision**: Express requirements with different levels of necessity
425
+ - **Fast Execution**: Get quick feedback on specification compliance
426
+ - **Pure Specifications**: Write specification documents that focus on behavior, not implementation
427
+ - **Rich Matcher Library**: Comprehensive set of matchers for different testing needs
428
+ - **Modern Ruby**: Takes advantage of modern Ruby features and practices
123
429
 
124
- ## Versioning
430
+ ## Get Started
125
431
 
126
- __Fix__ follows [Semantic Versioning 2.0](http://semver.org/).
432
+ Ready to write better specifications? Visit our [GitHub repository](https://github.com/fixrb/fix) to start using Fix in your Ruby projects.
127
433
 
128
- ## Contributing
434
+ ## Community & Resources
129
435
 
130
- 1. [Fork it](https://github.com/fixrb/fix/fork)
131
- 2. Create your feature branch (`git checkout -b my-new-feature`)
132
- 3. Commit your changes (`git commit -am 'Add some feature'`)
133
- 4. Push to the branch (`git push origin my-new-feature`)
134
- 5. Create a new Pull Request
436
+ - [Blog](https://fixrb.dev/) - Related articles
437
+ - [Bluesky](https://bsky.app/profile/fixrb.dev) - Latest updates and discussions
438
+ - [Documentation](https://www.rubydoc.info/gems/fix) - Comprehensive guides and API reference
439
+ - [Source Code](https://github.com/fixrb/fix) - Contribute and report issues
440
+ - [asciinema](https://asciinema.org/~fix) - Watch practical examples in action
135
441
 
136
- ## License
442
+ ## Versioning
137
443
 
138
- See `LICENSE.md` file.
444
+ __Fix__ follows [Semantic Versioning 2.0](https://semver.org/).
139
445
 
140
- [gem]: https://rubygems.org/gems/fix
141
- [travis]: https://travis-ci.org/fixrb/fix
142
- [codeclimate]: https://codeclimate.com/github/fixrb/fix
143
- [gemnasium]: https://gemnasium.com/fixrb/fix
144
- [inchpages]: http://inch-ci.org/github/fixrb/fix
145
- [rubydoc]: http://rubydoc.info/gems/fix/frames
146
- [gitter]: https://gitter.im/fixrb/fix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge
446
+ ## License
147
447
 
148
- ***
448
+ The [gem](https://rubygems.org/gems/fix) is available as open source under the terms of the [MIT License](https://github.com/fixrb/fix/raw/main/LICENSE.md).
149
449
 
150
- This project is sponsored by:
450
+ ## Sponsors
151
451
 
152
- [![Sashite](https://pbs.twimg.com/profile_images/618485028322975744/PZ9qPuI__400x400.png)](https://sashite.com/)
452
+ This project is sponsored by [Sashité](https://sashite.com/)
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "doc"
4
+ require_relative "dsl"
5
+ require_relative "set"
6
+ require_relative "error/missing_specification_block"
7
+
8
+ module Fix
9
+ # Handles the creation and setup of Fix specifications.
10
+ #
11
+ # The Builder constructs new Fix specification sets following these steps:
12
+ # 1. Creates a new specification class inheriting from DSL
13
+ # 2. Defines the specification content using the provided block
14
+ # 3. Optionally registers the named specification
15
+ # 4. Returns the built specification set
16
+ #
17
+ # @example Create a named specification
18
+ # Fix::Builder.build(:Calculator) do
19
+ # on(:add, 2, 3) { it MUST equal 5 }
20
+ # end
21
+ #
22
+ # @example Create an anonymous specification
23
+ # Fix::Builder.build do
24
+ # it MUST be_positive
25
+ # end
26
+ #
27
+ # @see Fix::Set
28
+ # @see Fix::Dsl
29
+ # @api private
30
+ class Builder
31
+ # Creates a new specification set.
32
+ #
33
+ # @param name [String, Symbol, nil] Optional name for the specification
34
+ # @yieldparam [void] Block containing specification definitions
35
+ # @yieldreturn [void]
36
+ # @return [Fix::Set] The constructed specification set
37
+ # @raise [Fix::Error::InvalidSpecificationName] If name is invalid
38
+ # @raise [Fix::Error::MissingSpecificationBlock] If no block given
39
+ def self.build(name = nil, &block)
40
+ new(name, &block).construct_set
41
+ end
42
+
43
+ # @return [String, Symbol, nil] The name of the specification
44
+ attr_reader :name
45
+
46
+ def initialize(name = nil, &block)
47
+ raise Error::MissingSpecificationBlock unless block
48
+
49
+ @name = name
50
+ @block = block
51
+ end
52
+
53
+ # Constructs and returns a new specification set.
54
+ #
55
+ # @return [Fix::Set] The constructed specification set
56
+ def construct_set
57
+ klass = create_specification
58
+ populate_specification(klass)
59
+ register_if_named(klass)
60
+ build_set(klass)
61
+ end
62
+
63
+ private
64
+
65
+ # @return [Proc] The block containing specification definitions
66
+ attr_reader :block
67
+
68
+ # Creates a new specification class with context tracking.
69
+ #
70
+ # @return [Class] A new class inheriting from Fix::Dsl with CONTEXTS initialized
71
+ def create_specification
72
+ ::Class.new(Dsl).tap do |klass|
73
+ klass.const_set(:CONTEXTS, [klass])
74
+ end
75
+ end
76
+
77
+ # Evaluates the specification block in the context of the class.
78
+ #
79
+ # @param klass [Class] The class to populate with specifications
80
+ # @return [void]
81
+ def populate_specification(klass)
82
+ klass.instance_eval(&block)
83
+ end
84
+
85
+ # Registers the specification in Fix::Doc if a name was provided.
86
+ #
87
+ # @param klass [Class] The specification class to register
88
+ # @return [void]
89
+ def register_if_named(klass)
90
+ Doc.spec_set(name, klass) if name
91
+ end
92
+
93
+ # Creates a new specification set from the populated class.
94
+ #
95
+ # @param klass [Class] The populated specification class
96
+ # @return [Fix::Set] A new specification set
97
+ def build_set(klass)
98
+ Set.new(*klass.const_get(:CONTEXTS))
99
+ end
100
+ end
101
+ end