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 +4 -4
- data/.envrc +1 -1
- data/.rubocop.yml +28 -1
- data/CONTRIBUTING.md +67 -0
- data/README.md +180 -18
- data/Rakefile +14 -1
- data/flake.lock +26 -4
- data/flake.nix +84 -19
- data/gem-groups.json +1 -0
- data/lib/errgonomic/core_ext/array.rb +38 -0
- data/lib/errgonomic/core_ext/bool.rb +68 -0
- data/lib/errgonomic/core_ext/hash.rb +36 -0
- data/lib/errgonomic/option.rb +204 -30
- data/lib/errgonomic/optional_array.rb +146 -0
- data/lib/errgonomic/optional_dig.rb +33 -0
- data/lib/errgonomic/optional_hash.rb +147 -0
- data/lib/errgonomic/presence.rb +4 -2
- data/lib/errgonomic/rails/active_record_delegate_optional.rb +6 -0
- data/lib/errgonomic/rails/active_record_optional.rb +40 -8
- data/lib/errgonomic/rails.rb +3 -1
- data/lib/errgonomic/result.rb +117 -6
- data/lib/errgonomic/type.rb +2 -0
- data/lib/errgonomic/version.rb +1 -1
- data/lib/errgonomic.rb +19 -0
- metadata +10 -3
- data/gemset.nix +0 -1084
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1b7d451a7e98b347baa9556bf72fd22d3825278bf03afddb9a4fdb0e7c325b03
|
|
4
|
+
data.tar.gz: b336d798fc24356fc0a340ac24d5c562d10d242d5507a71444daa881831ecb27
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3967b76933831abb7750f253f345a8845c4bd00a172db427d99cd22c5fd3f41d9d18b36a7a44f4f915b04acca72009f52e4927ec8dfa60d98bea80eda82e1a2c
|
|
7
|
+
data.tar.gz: 2a7ef8d3aa5dbd1e9777e8a653a48c5d3eafbf1b8514e26a483e3056b5139dce643ace35542062512da59b2fa0c9d4b5ab915754e83e747196dabe65f6af91f8
|
data/.envrc
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
watch_file
|
|
1
|
+
watch_file Gemfile Gemfile.lock
|
|
2
2
|
use flake
|
data/.rubocop.yml
CHANGED
|
@@ -1 +1,28 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
52
|
+
[].present_or_raise!("foo")
|
|
33
53
|
# => foo (Errgonomic::NotPresentError)
|
|
34
54
|
```
|
|
35
55
|
|
|
36
|
-
|
|
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
|
-
|
|
58
|
+
### Type assertions
|
|
39
59
|
|
|
40
|
-
|
|
60
|
+
The same pattern applies to runtime type expectations:
|
|
41
61
|
|
|
42
|
-
```
|
|
43
|
-
|
|
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
|
-
|
|
79
|
+
### Option
|
|
47
80
|
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
227
|
+
nix develop -c rake yard:doctest
|
|
66
228
|
```
|
|
67
229
|
|
|
68
|
-
Run
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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":
|
|
6
|
-
"narHash": "sha256-
|
|
26
|
+
"lastModified": 1784432872,
|
|
27
|
+
"narHash": "sha256-n3gKTBIV4ZA5VQpUakffBe3KGu4+mhPoA34rrqS0GkA=",
|
|
7
28
|
"owner": "nixos",
|
|
8
29
|
"repo": "nixpkgs",
|
|
9
|
-
"rev": "
|
|
30
|
+
"rev": "fd1462031fdee08f65fd0b4c6b64e22239a77870",
|
|
10
31
|
"type": "github"
|
|
11
32
|
},
|
|
12
33
|
"original": {
|
|
13
34
|
"owner": "nixos",
|
|
14
|
-
"ref": "nixos-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
+
gems = gemEnvFor pkgs;
|
|
34
110
|
in
|
|
35
111
|
{
|
|
36
112
|
default = pkgs.mkShell {
|
|
37
113
|
buildInputs = [
|
|
38
|
-
ruby
|
|
39
|
-
|
|
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
|