errgonomic 0.7.0 → 0.8.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: 1490dc9d568769e95cc3d584907bf7a4bffb1af0381de3e4ce9a141736133166
4
- data.tar.gz: 75fe0c80ad6ee5067aa8cce76c78f293e22ca1019b9d3415c57688c9c64ec2b9
3
+ metadata.gz: 1b7d451a7e98b347baa9556bf72fd22d3825278bf03afddb9a4fdb0e7c325b03
4
+ data.tar.gz: b336d798fc24356fc0a340ac24d5c562d10d242d5507a71444daa881831ecb27
5
5
  SHA512:
6
- metadata.gz: 836b910b35ffd4bef2e78a7e5f72548e952a3da57f2e7642baaeeae1ba7bf93992f09b62baca76f2918836621f2dfa5780bfc56beea9c359d38d0154ae601286
7
- data.tar.gz: 1fd7d78dcebdbc6eda3f54471f5e7f41c9f2f2db718ee7885489de16d9fbdbe42bf00ae842f171a32067f187abd26e7d6d93c1cb3422d893805f6c2716b7eb67
6
+ metadata.gz: 3967b76933831abb7750f253f345a8845c4bd00a172db427d99cd22c5fd3f41d9d18b36a7a44f4f915b04acca72009f52e4927ec8dfa60d98bea80eda82e1a2c
7
+ data.tar.gz: 2a7ef8d3aa5dbd1e9777e8a653a48c5d3eafbf1b8514e26a483e3056b5139dce643ace35542062512da59b2fa0c9d4b5ab915754e83e747196dabe65f6af91f8
data/.envrc CHANGED
@@ -1,2 +1,2 @@
1
- watch_file gemset.nix
1
+ watch_file Gemfile Gemfile.lock
2
2
  use flake
data/.rubocop.yml CHANGED
@@ -1 +1,28 @@
1
- require: rubocop-yard
1
+ plugins: rubocop-yard
2
+
3
+ # Doctest expectation lines are executable spec; wrapping them would change
4
+ # the assertions. Long lines carrying a `#=>` expectation are allowed.
5
+ Layout/LineLength:
6
+ AllowedPatterns:
7
+ - '#=>'
8
+
9
+ # Some(), None(), Ok() and Err() are the library's Rust-style value
10
+ # constructors; their capitalized names are the point.
11
+ Naming/MethodName:
12
+ AllowedPatterns:
13
+ - '\A(Some|None|Ok|Err)\z'
14
+
15
+ # Option::Any and Result::Any deliberately carry the whole combinator API in
16
+ # one class each, mirroring Rust's Option and Result surface.
17
+ Metrics/ClassLength:
18
+ Exclude:
19
+ - lib/errgonomic/option.rb
20
+ - lib/errgonomic/result.rb
21
+
22
+ # core_ext vendors ActiveSupport's blank?/present? patches; the reopened core
23
+ # classes there are explained by the file header, not per class. Test
24
+ # fixtures and cases are likewise self-describing.
25
+ Style/Documentation:
26
+ Exclude:
27
+ - lib/errgonomic/core_ext/**/*
28
+ - test/**/*
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,67 @@
1
+ # Contributing
2
+
3
+ This document captures how we develop `errgonomic`: the workflow, the quality gate, and the values that decide a judgment call when the rules run out.
4
+
5
+ ## Environment
6
+
7
+ The toolchain is pinned by `flake.nix` and loaded automatically by `direnv`. A checkout with `direnv allow` already has the correct Ruby toolchain (ruby and the locked gem environment) on the path. The project is self-contained and targets multiple systems (x86_64-linux, aarch64-linux, x86_64-darwin, aarch64-darwin): do not install tools globally or reach outside the repository, except where a task explicitly calls for it (for example, reading a script in a sibling project as a reference).
8
+
9
+ Gems are provided by [gems4nix](https://github.com/omc/gems4nix), which reads `Gemfile.lock` directly — there is no `gemset.nix`. When gem dependencies change, run `bundle lock --add-checksums` (preserving the platform list — gems4nix needs the `CHECKSUMS` section and the precompiled platform variants), then `rake gems4nix:groups` to regenerate the committed `gem-groups.json` group mapping.
10
+
11
+ Nontrivial development, debugging, and testing commands live as Rake tasks in the `Rakefile` rather than being re-typed ad hoc — they should be reproducible and not churn permission prompts. `rake -T` lists what is available.
12
+
13
+ ## Development loop
14
+
15
+ We work test-first, in three beats:
16
+
17
+ 1. **Red** — write a failing test that names the behavior you intend. Run it and watch it fail for the reason you expect. A test that passes the moment you write it was not testing the new behavior.
18
+ 2. **Green** — write the least code that makes the test pass. Resist designing ahead of the test in front of you.
19
+ 3. **Refactor** — with the test green, improve the shape of the code. The test is your safety net; the behavior must not change.
20
+
21
+ Each beat ends with a compiling, formatted tree. One small conceptual change per edit; multi-concern edits get broken up. Small, reversible steps beat large speculative ones.
22
+
23
+ ## The gate — a ladder
24
+
25
+ The inner rungs run constantly; the outer rungs are slower and run when preparing to push or open a PR. Run them in order; do not skip ahead.
26
+
27
+ **Per edit, and before each commit (inner gate):**
28
+
29
+ ```sh
30
+ bundle exec rubocop # formatted & lint-clean
31
+ bundle exec yard doctest # doctests pass — most behavior is specified here
32
+ bundle exec rake test # unit tests pass (incl. the Rails integration test)
33
+ ```
34
+
35
+ A change that fails any of these is not ready. Keep formatting-only changes in their own commit so they do not obscure a behavioral diff. `bundle exec rake` runs the full suite (test + yard:doctest) in one shot.
36
+
37
+ **Before push / PR (outer gate):**
38
+
39
+ ```sh
40
+ bundle exec rake # full suite: unit tests + doctests
41
+ nix build .#errgonomic # the gem builds as a derivation; rake runs in its checkPhase
42
+ nix flake check --all-systems # builds every check on the local system, evaluates all four
43
+ ```
44
+
45
+ **After push (CI gate):** a push is done when CI is green, not when `git push` succeeds. Check whatever CI this repo runs (`gh run list`, `gh run view --log-failed`); checks take minutes, so it is fine to schedule the check as a followup and keep working — but the change is not landed until they pass. A CI failure is a regression: diagnose it from the logs, reproduce it locally where you can, and capture it as a test so it cannot recur silently. CI earns you the coverage you cannot run locally — a target your machine isn't, a matrix leg, a slower suite — for free. Here, CI (`.github/workflows/main.yml`) runs `yard doctest` and `rake test` on the latest Ruby 3.4.x on ubuntu-latest, matching the Ruby pinned by the flake.
46
+
47
+ Tests *are* the requirements: a behavior is defined by the test that asserts it. Prefer doctests where an example clarifies a function's contract — they document and test at once and cannot drift out of date without failing the build. In this repo, the YARD `@example` blocks under `lib/**/*.rb` are the primary suite.
48
+
49
+ ## Commits
50
+
51
+ - One logical change per commit. The subject names the conceptual change (not the file change); the body says why, when the why is not obvious.
52
+ - **Agents do not sign commits** unless explicitly directed. Pass `--no-gpg-sign` per commit (do not set `git config commit.gpgsign false`). Signing happens later, by a human, at review.
53
+ - Agents do not add co-author or generated-by trailers.
54
+
55
+ ## Comments and documentation
56
+
57
+ Comments explain intent and rationale — the *why* behind a non-obvious choice. They must stand on their own: a reader should understand a comment without chasing a ticket, an external document, a previous version of the code, a project plan, or (rarely, and only when it genuinely aids understanding) another file. Write for the developer who arrives a year from now with none of today's context.
58
+
59
+ ## What we value, in order
60
+
61
+ When two designs compete, prefer them in this order.
62
+
63
+ 1. **Correct.** The code states its behavior and is tested against that statement. Invalid states are made unrepresentable rather than guarded against after the fact. Runtime failures produce diagnostics that tell an operator what went wrong and what to do.
64
+ 2. **Simple.** The code reads clearly at the right level of abstraction, and is idiomatic and approachable to another developer. Fewer moving parts, fewer sources of truth.
65
+ 3. **Performant.** Done well, the first two rarely cost us speed. Do not trade clarity for micro-optimization without a measurement that demands it.
66
+
67
+ We also keep abstraction just-in-time: let the compiler and tests tell us where a seam is needed rather than pre-abstracting, and refactor when a real need surfaces.
data/README.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  Errgonomic provides some lightweight, opinionated ergonomics for error handling in Ruby. These semantics are a blend of Rails `present?` conventions, and Rust `Option` and `Result` type combinators. Without going full Option and Result. Probably.
4
4
 
5
+ ## Installation
6
+
7
+ Install the gem and add to the application's Gemfile by executing:
8
+
9
+ ```bash
10
+ bundle add errgonomic
11
+ ```
12
+
13
+ If bundler is not being used to manage dependencies, install the gem by executing:
14
+
15
+ ```bash
16
+ gem install errgonomic
17
+ ```
18
+
19
+ Errgonomic requires Ruby >= 3.0.
20
+
21
+ ## Usage
22
+
23
+ ### Presence helpers
24
+
5
25
  The `present_or` method takes what you might ordinarily write as `foo || default` with a possible nil or falsey value, and brings that to any other object that may be `blank?`.
6
26
 
7
27
  ```ruby
@@ -29,45 +49,187 @@ When constructing that fallback object may be expensive, you can provide a block
29
49
  And when all else fails, you can control the failure, by raising an exception for blank objects. This can be preferable to sending a blank object to some other downstream code that may be expecting a value, causing an ambiguous failure.
30
50
 
31
51
  ```ruby
32
- irb(main):007> [].present_or_raise("foo")
52
+ [].present_or_raise!("foo")
33
53
  # => foo (Errgonomic::NotPresentError)
34
54
  ```
35
55
 
36
- ## Installation
56
+ Each helper has a `blank_or*` counterpart for when you expect the object to be blank: `blank_or`, `blank_or_else`, `blank_or_raise!`.
37
57
 
38
- TODO: Replace `errgonomic` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
58
+ ### Type assertions
39
59
 
40
- Install the gem and add to the application's Gemfile by executing:
60
+ The same pattern applies to runtime type expectations:
41
61
 
42
- ```bash
43
- bundle add errgonomic
62
+ ```ruby
63
+ "hello".type_or_raise!(String)
64
+ # => "hello"
65
+
66
+ 123.type_or_raise!(String)
67
+ # => Expected String but got Integer (Errgonomic::TypeMismatchError)
68
+
69
+ 123.type_or(String, "default")
70
+ # => "default"
71
+
72
+ 123.type_or_else(String) { "default" }
73
+ # => "default"
74
+
75
+ "hello".not_type_or_raise!(Integer)
76
+ # => "hello"
44
77
  ```
45
78
 
46
- If bundler is not being used to manage dependencies, install the gem by executing:
79
+ ### Option
47
80
 
48
- ```bash
49
- gem install errgonomic
81
+ `Some(value)` and `None()` wrap a value that may or may not be there, with most of the Rust `Option` combinators:
82
+
83
+ ```ruby
84
+ Some(1).unwrap! # => 1
85
+ None().unwrap! # => raises Errgonomic::UnwrapError
86
+ None().unwrap_or(2) # => 2
87
+ None().unwrap_or_else { 2 } # => 2
88
+ Some(1).expect!("must be set") # => 1
89
+
90
+ Some(1).map { |x| x + 1 } # => Some(2)
91
+ Some(2).and_then { |x| Some(x + 1) } # => Some(3)
92
+ None().or(Some(1)) # => Some(1)
93
+ Some(:left).xor(None()) # => Some(:left)
94
+ Some(1).zip(Some(2)) # => Some([1, 2])
95
+ Some(1).ok_or("nope") # => Ok(1)
96
+ None().ok_or("nope") # => Err("nope")
50
97
  ```
51
98
 
52
- ## Usage
99
+ Options support pattern matching:
100
+
101
+ ```ruby
102
+ case measurement
103
+ in Errgonomic::Option::Some, value
104
+ "Measurement is #{value}"
105
+ in Errgonomic::Option::None
106
+ "Measurement is not available"
107
+ end
108
+ ```
109
+
110
+ An unhandled Option refuses to leak into your output: `to_s` and `to_json` raise `Errgonomic::SerializeError`, so you handle the inner value deliberately rather than shipping `#<Errgonomic::Option::Some...>` to a user.
111
+
112
+ `unwrap!` and `expect!` are for tests and consoles, not application code: they raise on `None`, which is exactly the ambiguous failure the type exists to prevent. Application code should always have a combinator or pattern match that handles the `None` branch explicitly; if none fits, that is a gap worth an issue rather than a reason to unwrap.
113
+
114
+ Presence follows the discriminant, as in Rust: `Some` is `present?` and `None` is `blank?`, regardless of the wrapped value. So `Some(false).present?` and `Some(nil).present?` are both `true`. If you care about the inner value's own presence, unwrap it first.
115
+
116
+ Equality is between Options only: `Some(5) == Some(5)`, but `Some(5) == 5` and `None() == nil` are `false`. That is quiet, never an error, matching how every Ruby object compares across types. Rust rejects `Some(5) == 5` at compile time; Ruby cannot, so guard the idiom in review and tests: compare against a wrapped value (`opt == Some(5)`) or test the inner value (`opt.some_and? { |v| v == 5 }`).
117
+
118
+ ### Result
53
119
 
54
- TODO: Write usage instructions here
120
+ `Ok(value)` and `Err(error)` express an operation that may fail, again with the Rust combinators:
121
+
122
+ ```ruby
123
+ Ok(1).unwrap! # => 1
124
+ Err(:nope).unwrap! # => raises Errgonomic::UnwrapError
125
+ Err(:nope).unwrap_or(2) # => 2
126
+
127
+ Ok(1).map { |x| x + 1 } # => Ok(2)
128
+ Err(:bob).map_err { |e| e.capitalize } # => Err(:Bob)
129
+ Ok(1).and_then { |x| Ok(x + 1) } # => Ok(2)
130
+ Err(:e).or_else { |e| Ok(1) } # => Ok(1)
131
+
132
+ Ok(1).ok_and?(&:odd?) # => true
133
+ Err(:a).err_and? { |_| true } # => true
134
+ ```
135
+
136
+ Results also pattern match, including against the kind of inner value:
137
+
138
+ ```ruby
139
+ case result
140
+ in Errgonomic::Result::Ok, value
141
+ "Measurement is #{value}"
142
+ in Errgonomic::Result::Err, String => msg
143
+ "Measurement failed with a message: #{msg}"
144
+ in Errgonomic::Result::Err, Exception => e
145
+ "Measurement produced an exception -- #{e.class}: #{e}"
146
+ end
147
+ ```
148
+
149
+ Like Options, unwrapped Results refuse `to_s` and `to_json`. And `Object#result?` / `Object#assert_result!` help enforce at runtime that a value is a Result.
150
+
151
+ ### Optional collections
152
+
153
+ Hash and Array gain two additive lookups each, and nothing else changes about them. `fetch_option` follows presence the way Rust's `HashMap#get` and slice `get` do: a present key or index holding `nil` is `Some(nil)`, and only a missing one is `None()`.
154
+
155
+ ```ruby
156
+ h = { color: :blue, shade: nil }
157
+ h.fetch_option(:color) # => Some(:blue)
158
+ h.fetch_option(:shade) # => Some(nil)
159
+ h.fetch_option(:smell) # => None()
160
+
161
+ [:a, nil].fetch_option(1) # => Some(nil)
162
+ [:a, nil].fetch_option(2) # => None()
163
+ ```
164
+
165
+ `into_optional` wraps the collection in `Errgonomic::OptionalHash` / `Errgonomic::OptionalArray`, a view whose lookups all return Options. The wrappers are deliberately small — `[]`, `[]=`, `dig`, presence checks, and (for arrays) `first`/`last` — and are composed around the plain collection rather than subclassing it, because a subclass sheds its custom semantics every time `select` or `transform_values` returns a plain Hash. `to_h` / `to_a` hand back a detached copy.
166
+
167
+ ```ruby
168
+ h = { person: { name: 'Ada', middle_name: nil } }.into_optional
169
+ h.dig(:person, :name) # => Some("Ada")
170
+ h.dig(:person, :middle_name) # => Some(nil) (present, holding nil)
171
+ h.dig(:person, :nickname) # => None() (absent)
172
+
173
+ [].into_optional.first # => None()
174
+ ```
175
+
176
+ `dig` checks presence at every step, so an absent path and a present `nil` stay distinguishable, which core `dig` conflates. Digging into a non-collection raises `Errgonomic::TypeMismatchError` rather than answering `None()`, in the gem's pedantic style.
177
+
178
+ ### Booleans
179
+
180
+ Booleans lift into the containers, following Rust's `bool`: `then_some`, and `ok_or`/`ok_or_else` from nightly. Rust splits the lazy form into `then`, but that name is core Ruby (`Kernel#then`), which Errgonomic will not redefine; `then_some` takes either a value or a block instead. Rust's `ok_or` returns `Result<(), E>`; Ruby has no unit type, so `Ok` carries `true`.
181
+
182
+ ```ruby
183
+ admin.then_some(:badge) # => Some(:badge) when true, None() when false
184
+ admin.then_some { badge! } # lazy variant
185
+ valid.ok_or("invalid input") # => Ok(true) / Err("invalid input")
186
+ ```
187
+
188
+ ### Pedantic runtime checks
189
+
190
+ Combinators that accept a block (`and_then`, `or_else`, ...) check at runtime that the block returned an Option or Result, raising `Errgonomic::ArgumentError` otherwise. That beats an ambiguous `undefined method` error somewhere downstream. If you would rather have the ambiguous downstream errors, you can opt out — but not quietly:
191
+
192
+ ```ruby
193
+ Errgonomic.with_ambiguous_downstream_errors do
194
+ # anything goes in here
195
+ end
196
+ ```
197
+
198
+ ### Rails integration
199
+
200
+ When `Rails::Railtie` is defined, Errgonomic installs a Railtie with two opt-in integrations for ActiveRecord:
201
+
202
+ - `include Errgonomic::Rails::ActiveRecordOptional` in a model makes its nullable attributes and `optional: true` associations return `Some(value)` or `None()` instead of a value-or-nil. This is all-or-nothing per model: every nullable column and optional association is wrapped, with no per-attribute opt-in.
203
+ - `delegate_optional :name, to: :association` (available on all models) delegates through an optional association, returning an Option instead of raising on nil.
204
+
205
+ `Object#to_option` is also available in Rails to lift any value into an Option (`nil.to_option # => None()`).
206
+
207
+ #### ActiveRecord compromises
208
+
209
+ ActiveRecord assumes things about accessors that a strict Rust Option cannot satisfy, so the integration carries four deliberate compromises. Everywhere else, treat a departure from Rust's `Option` semantics as a bug; these four are intended:
210
+
211
+ 1. `None#nil?` answers `true`, so ActiveRecord internals and ordinary `.nil?` checks treat an absent value as absent. Equality does not follow suit: `None() == nil` is still `false`.
212
+ 2. `Some` delegates `persisted?`, `marked_for_destruction?`, and `touch_later` to its record, so a `Some` can stand in for its record during persistence.
213
+ 3. Quoting is patched so an `Option` passed into `where`/`quote` is unwrapped at the SQL boundary.
214
+ 4. `SomeValidator` provides a presence-style validation for Option attributes.
215
+
216
+ The set is closed. If a future integration appears to need a fifth compromise, that is a signal ActiveRecord is pushing back somewhere unmapped, and it warrants a design discussion rather than a quiet patch.
55
217
 
56
218
  ## Development
57
219
 
58
- After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
220
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment. The repository is a self-contained Nix flake; with direnv, `direnv allow` puts the right toolchain on your path.
59
221
 
60
222
  This project encourages **red, green, refactor** when making changes. First, add or change a test that captures the desired behavior; next, run the tests to observe the failure message, confirming the test is useful; next, make the smallest code change(s) to make the test pass. Once tests pass, review your diff and look for opportunities to simplify or improve abstractions; make changes and iterate, running tests on each change to guard against regressions.
61
223
 
62
- Run the doctest suite with:
224
+ Most of the behavior above is specified as YARD doctests, so the examples in the code documentation are the test suite. Run them with:
63
225
 
64
226
  ```bash
65
- nix develop -c yard doctest
227
+ nix develop -c rake yard:doctest
66
228
  ```
67
229
 
68
- Run all tests with:
230
+ Run the full suite (unit tests plus doctests) with:
69
231
 
70
- ```
232
+ ```bash
71
233
  nix develop -c rake
72
234
  ```
73
235
 
@@ -75,7 +237,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
75
237
 
76
238
  ## Contributing
77
239
 
78
- Bug reports and pull requests are welcome on GitHub at https://github.com/omc/errgonomic. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/nz/errgonomic/blob/main/CODE_OF_CONDUCT.md).
240
+ Bug reports and pull requests are welcome on GitHub at https://github.com/omc/errgonomic. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/omc/errgonomic/blob/main/CODE_OF_CONDUCT.md).
79
241
 
80
242
  ## License
81
243
 
@@ -83,4 +245,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
83
245
 
84
246
  ## Code of Conduct
85
247
 
86
- Everyone interacting in the Errgonomic project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/nz/errgonomic/blob/main/CODE_OF_CONDUCT.md).
248
+ Everyone interacting in the Errgonomic project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/omc/errgonomic/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile CHANGED
@@ -11,7 +11,20 @@ end
11
11
  require 'yard/doctest/rake'
12
12
  YARD::Doctest::RakeTask.new do |task|
13
13
  task.doctest_opts = %w[-v]
14
- task.pattern = 'lib/**/*.rb'
14
+ # Expand here: the pattern reaches yard through a shell whose ** means *,
15
+ # which silently dropped every doctest under lib/errgonomic/*/.
16
+ task.pattern = FileList['lib/**/*.rb'].join(' ')
15
17
  end
16
18
 
17
19
  task default: %i[test yard:doctest]
20
+
21
+ namespace :gems4nix do
22
+ desc 'Regenerate gem-groups.json after Gemfile/Gemfile.lock changes'
23
+ task :groups do
24
+ require 'json'
25
+ locked = JSON.parse(`nix flake metadata --json`).dig('locks', 'nodes', 'gems4nix', 'locked')
26
+ ref = "github:#{locked['owner']}/#{locked['repo']}/#{locked['rev']}"
27
+ src = JSON.parse(`nix flake prefetch --json #{ref}`).fetch('storePath')
28
+ sh "ruby #{src}/lib/gemfile-env/gem-groups.rb > gem-groups.json"
29
+ end
30
+ end
data/flake.lock CHANGED
@@ -1,23 +1,45 @@
1
1
  {
2
2
  "nodes": {
3
+ "gems4nix": {
4
+ "inputs": {
5
+ "nixpkgs": [
6
+ "nixpkgs"
7
+ ]
8
+ },
9
+ "locked": {
10
+ "lastModified": 1784080771,
11
+ "narHash": "sha256-+Vnzrg4XMbTmFtr00krmdt5W8uKKKDVmV1B1cLDsGks=",
12
+ "owner": "omc",
13
+ "repo": "gems4nix",
14
+ "rev": "faecc855e315fc38ada09d7b60e53f1158370fd8",
15
+ "type": "github"
16
+ },
17
+ "original": {
18
+ "owner": "omc",
19
+ "ref": "nz/gemspec-directive-fix",
20
+ "repo": "gems4nix",
21
+ "type": "github"
22
+ }
23
+ },
3
24
  "nixpkgs": {
4
25
  "locked": {
5
- "lastModified": 1751741127,
6
- "narHash": "sha256-t75Shs76NgxjZSgvvZZ9qOmz5zuBE8buUaYD28BMTxg=",
26
+ "lastModified": 1784432872,
27
+ "narHash": "sha256-n3gKTBIV4ZA5VQpUakffBe3KGu4+mhPoA34rrqS0GkA=",
7
28
  "owner": "nixos",
8
29
  "repo": "nixpkgs",
9
- "rev": "29e290002bfff26af1db6f64d070698019460302",
30
+ "rev": "fd1462031fdee08f65fd0b4c6b64e22239a77870",
10
31
  "type": "github"
11
32
  },
12
33
  "original": {
13
34
  "owner": "nixos",
14
- "ref": "nixos-25.05",
35
+ "ref": "nixos-26.05",
15
36
  "repo": "nixpkgs",
16
37
  "type": "github"
17
38
  }
18
39
  },
19
40
  "root": {
20
41
  "inputs": {
42
+ "gems4nix": "gems4nix",
21
43
  "nixpkgs": "nixpkgs"
22
44
  }
23
45
  }
data/flake.nix CHANGED
@@ -2,11 +2,20 @@
2
2
  description = "Errgonomic";
3
3
 
4
4
  inputs = {
5
- nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-25.05";
5
+ nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-26.05";
6
+ gems4nix = {
7
+ url = "github:omc/gems4nix/nz/gemspec-directive-fix";
8
+ inputs.nixpkgs.follows = "nixpkgs";
9
+ };
6
10
  };
7
11
 
8
12
  outputs =
9
- inputs@{ self, nixpkgs, ... }:
13
+ {
14
+ self,
15
+ nixpkgs,
16
+ gems4nix,
17
+ ...
18
+ }:
10
19
  let
11
20
  allSystems = [
12
21
  "x86_64-linux"
@@ -14,8 +23,7 @@
14
23
  "x86_64-darwin"
15
24
  "aarch64-darwin"
16
25
  ];
17
- overlays = [
18
- ];
26
+ overlays = [ gems4nix.overlays.default ];
19
27
  forAllSystems =
20
28
  f:
21
29
  nixpkgs.lib.genAttrs allSystems (
@@ -25,33 +33,90 @@
25
33
  inherit system;
26
34
  }
27
35
  );
36
+ # lib/errgonomic/version.rb is the single source of truth for the version.
37
+ version = builtins.head (
38
+ builtins.match ".*VERSION = '([^']+)'.*" (builtins.readFile ./lib/errgonomic/version.rb)
39
+ );
40
+ # The full Gemfile.lock environment, shared by the dev shell and the
41
+ # package's check phase. The gemspec and version.rb ride along because the
42
+ # Gemfile references the gemspec, which requires version.rb.
43
+ gemEnvFor =
44
+ pkgs:
45
+ pkgs.gemfileEnv {
46
+ name = "errgonomic-gems";
47
+ gemfile = ./Gemfile;
48
+ gemfileLock = ./Gemfile.lock;
49
+ gemspec = ./errgonomic.gemspec;
50
+ extraFiles = {
51
+ "lib/errgonomic/version.rb" = ./lib/errgonomic/version.rb;
52
+ };
53
+ # Committed group mapping keeps evaluation pure (no Ruby IFD), so
54
+ # foreign systems still evaluate on one machine. Regenerate with
55
+ # `rake gems4nix:groups` after changing the Gemfile.
56
+ gemGroups = builtins.fromJSON (builtins.readFile ./gem-groups.json);
57
+ };
28
58
  in
29
59
  {
60
+ packages = forAllSystems (
61
+ { pkgs, ... }:
62
+ let
63
+ gems = gemEnvFor pkgs;
64
+ in
65
+ rec {
66
+ default = errgonomic;
67
+ errgonomic = pkgs.stdenv.mkDerivation {
68
+ pname = "errgonomic";
69
+ inherit version;
70
+ src = ./.;
71
+ nativeBuildInputs = [
72
+ pkgs.ruby
73
+ pkgs.git
74
+ ];
75
+ # The gemspec computes its file list with `git ls-files`, so give the
76
+ # sandboxed source copy a git index to enumerate.
77
+ buildPhase = ''
78
+ runHook preBuild
79
+ git init -q
80
+ git add -A
81
+ gem build errgonomic.gemspec
82
+ runHook postBuild
83
+ '';
84
+ nativeCheckInputs = [ gems ];
85
+ doCheck = true;
86
+ checkPhase = ''
87
+ runHook preCheck
88
+ export HOME="$TMPDIR"
89
+ export GEM_PATH="${gems}/${pkgs.ruby.gemPath}"
90
+ rake
91
+ runHook postCheck
92
+ '';
93
+ installPhase = ''
94
+ runHook preInstall
95
+ mkdir -p $out
96
+ cp errgonomic-${version}.gem $out/
97
+ runHook postInstall
98
+ '';
99
+ };
100
+ }
101
+ );
102
+
103
+ # Every package builds and its tests pass; `nix flake check` certifies it.
104
+ checks = self.packages;
105
+
30
106
  devShells = forAllSystems (
31
107
  { pkgs, ... }:
32
108
  let
33
- inherit (pkgs) ruby bundix;
109
+ gems = gemEnvFor pkgs;
34
110
  in
35
111
  {
36
112
  default = pkgs.mkShell {
37
113
  buildInputs = [
38
- ruby
39
- bundix
40
- (pkgs.bundlerEnv {
41
- name = "errgonomic";
42
- gemdir = ./.;
43
- extraConfigPaths = [
44
- ./errgonomic.gemspec
45
- ./lib/errgonomic/version.rb
46
- ];
47
- postInstall = ''
48
- find . >&2
49
- '';
50
- })
114
+ pkgs.ruby
115
+ gems
51
116
  ];
117
+ env.GEM_PATH = "${gems}/${pkgs.ruby.gemPath}";
52
118
  };
53
119
  }
54
120
  );
55
-
56
121
  };
57
122
  }
data/gem-groups.json ADDED
@@ -0,0 +1 @@
1
+ {"errgonomic":["default"],"yard":["development"],"yard-doctest":["development"],"activerecord":["development"],"minitest":["development"],"rails":["development"],"rake":["development"],"rspec":["development"],"rubocop":["development"],"rubocop-yard":["development"],"solargraph":["development"],"sqlite3":["development"],"concurrent-ruby":["default","development"],"drb":["development"],"prism":["development"],"activemodel":["development"],"activesupport":["development"],"base64":["development"],"bigdecimal":["development"],"connection_pool":["development"],"i18n":["development"],"json":["development"],"logger":["development"],"securerandom":["development"],"timeout":["development"],"tzinfo":["development"],"uri":["development"],"action_text-trix":["development"],"actioncable":["development"],"actionmailbox":["development"],"actionmailer":["development"],"actionpack":["development"],"actiontext":["development"],"actionview":["development"],"activejob":["development"],"activestorage":["development"],"builder":["development"],"bundler":["development"],"crass":["development"],"date":["development"],"erb":["development"],"erubi":["development"],"globalid":["development"],"io-console":["development"],"irb":["development"],"loofah":["development"],"mail":["development"],"marcel":["development"],"mini_mime":["development"],"net-imap":["development"],"net-pop":["development"],"net-protocol":["development"],"net-smtp":["development"],"nio4r":["development"],"nokogiri":["development"],"pp":["development"],"prettyprint":["development"],"psych":["development"],"racc":["development"],"rack":["development"],"rack-session":["development"],"rack-test":["development"],"rackup":["development"],"rails-dom-testing":["development"],"rails-html-sanitizer":["development"],"railties":["development"],"rdoc":["development"],"reline":["development"],"stringio":["development"],"thor":["development"],"tsort":["development"],"useragent":["development"],"websocket-driver":["development"],"websocket-extensions":["development"],"zeitwerk":["development"],"diff-lcs":["development"],"rspec-core":["development"],"rspec-expectations":["development"],"rspec-mocks":["development"],"rspec-support":["development"],"ast":["development"],"language_server-protocol":["development"],"lint_roller":["development"],"parallel":["development"],"parser":["development"],"rainbow":["development"],"regexp_parser":["development"],"rubocop-ast":["development"],"ruby-progressbar":["development"],"unicode-display_width":["development"],"unicode-emoji":["development"],"backport":["development"],"benchmark":["development"],"commander":["development"],"highline":["development"],"jaro_winkler":["development"],"kramdown":["development"],"kramdown-parser-gfm":["development"],"observer":["development"],"open3":["development"],"ostruct":["development"],"parlour":["development"],"rbs":["development"],"reverse_markdown":["development"],"rexml":["development"],"sorbet-runtime":["development"],"sord":["development"],"tilt":["development"],"yard-activesupport-concern":["development"],"yard-solargraph":["development"]}
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../option'
4
+ require_relative '../optional_array'
5
+
6
+ # Two additive lookups; no existing Array behavior changes, for the same
7
+ # reasons given in core_ext/hash.rb.
8
+ class Array
9
+ # Retrieve the element at an integer index, wrapped in an Option,
10
+ # following element presence as Rust's slice get does: an element holding
11
+ # nil is Some(nil); only an out-of-bounds index is None.
12
+ #
13
+ # @example
14
+ # a = [:a, nil]
15
+ # a.fetch_option(0) # => Some(:a)
16
+ # a.fetch_option(1) # => Some(nil)
17
+ # a.fetch_option(2) # => None()
18
+ def fetch_option(index)
19
+ unless index.is_a?(::Integer)
20
+ raise Errgonomic::TypeMismatchError,
21
+ "index must be an Integer, got #{index.class}"
22
+ end
23
+ return None() unless (-length...length).cover?(index)
24
+
25
+ Some(self[index])
26
+ end
27
+
28
+ # Wrap this array in an Errgonomic::OptionalArray view. The wrapper reads
29
+ # and writes this same array; use its to_a for a detached copy.
30
+ #
31
+ # @example
32
+ # a = [:a].into_optional
33
+ # a[0] # => Some(:a)
34
+ # a[1] # => None()
35
+ def into_optional
36
+ Errgonomic::OptionalArray.new(self)
37
+ end
38
+ end