llull 0.2.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +371 -0
- data/Rakefile +12 -0
- data/lib/llull/pattern_matching.rb +32 -0
- data/lib/llull/result.rb +322 -0
- data/lib/llull/version.rb +5 -0
- data/lib/llull.rb +10 -0
- data/sig/llull.rbs +93 -0
- metadata +126 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8e0e20df9c2ad6bc3b999fdab13ce83dd7b31cf9c1faa5e64ab57386be60f948
|
|
4
|
+
data.tar.gz: 80fbc31fb798cfa5ade0b83d07d1f1fdfa2f459c597972d5de515ac0c3fdbfa4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c49f21eea1c1714c6db2a1b875bdae5a050a58f37c3586d83e73cad0c811ba64abe5c7a0c34be012ad4e2c0e714929fe80c45c3931f06ad991703dbe1c1fc64d
|
|
7
|
+
data.tar.gz: 746c6cf76d8e3ab28790872283cca0ed04efc2378c720774556921639cf04d219508caf5a40790809afe650f818a8be04436198272c28cff6ead1a420105624e
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Thomas Murphy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# Llull
|
|
2
|
+
|
|
3
|
+
Composable Result types for Ruby. Implements functional programming patterns inspired by [railway-oriented programming](https://fsharpforfunandprofit.com/rop/). Named after [Ramon Llull](https://en.wikipedia.org/wiki/Ramon_Llull), whose _ars combinatoria_ explored the systematic combination of concepts.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Replace nested conditionals and exception handling:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
def create_order(params)
|
|
11
|
+
return { error: "Invalid params" } unless valid_params?(params)
|
|
12
|
+
|
|
13
|
+
user = User.find(params[:user_id])
|
|
14
|
+
return { error: "User not found" } unless user
|
|
15
|
+
|
|
16
|
+
begin
|
|
17
|
+
order = Order.create!(params.merge(user: user))
|
|
18
|
+
{ success: true, order: order }
|
|
19
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
20
|
+
{ error: e.message }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
With composable operations:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
def create_order(params)
|
|
29
|
+
validate_params(params)
|
|
30
|
+
.and_then { |valid_params| find_user(valid_params) }
|
|
31
|
+
.and_then { |user, params| create_order_record(user, params) }
|
|
32
|
+
.tap_success { |order| send_confirmation(order) }
|
|
33
|
+
end
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
Add to your Gemfile:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
gem 'llull'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Then run:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
bundle install
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
Include `Llull` in your classes to access the `Success()` and `Failure()` constructors:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
class MyService
|
|
56
|
+
include Llull
|
|
57
|
+
|
|
58
|
+
def call(input)
|
|
59
|
+
Success(input)
|
|
60
|
+
.and_then { |data| process_data(data) }
|
|
61
|
+
.map { |result| format_result(result) }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Basic Result Operations
|
|
67
|
+
|
|
68
|
+
#### Creating Results
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
# Success with a value
|
|
72
|
+
success = Success("hello world")
|
|
73
|
+
success.success? # => true
|
|
74
|
+
success.value # => "hello world"
|
|
75
|
+
|
|
76
|
+
# Failure with error type and data
|
|
77
|
+
failure = Failure(:validation, "Email is required")
|
|
78
|
+
failure.failure? # => true
|
|
79
|
+
failure.error_type # => :validation
|
|
80
|
+
failure.error_data # => "Email is required"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### Transforming Values
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
# map: transform successful values
|
|
87
|
+
Success(10).map { |x| x * 2 } # => Success(20)
|
|
88
|
+
Failure(:error, "oops").map { |x| x * 2 } # => Failure(:error, "oops")
|
|
89
|
+
|
|
90
|
+
# and_then: chain operations that return Results
|
|
91
|
+
Success("hello")
|
|
92
|
+
.and_then { |str| Success(str.upcase) }
|
|
93
|
+
.and_then { |str| Success("#{str}!") }
|
|
94
|
+
# => Success("HELLO!")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
#### Error Recovery
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
# or_else: recover with another Result-returning operation
|
|
101
|
+
result = Failure(:primary_failed, "error")
|
|
102
|
+
.or_else { |type, data| Success("backup plan") }
|
|
103
|
+
# => Success("backup plan")
|
|
104
|
+
|
|
105
|
+
# recover: convert failure to success value
|
|
106
|
+
result = Failure(:not_found, "User missing")
|
|
107
|
+
.recover { |type, data| "Default User" }
|
|
108
|
+
# => Success("Default User")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### Unwrapping Values
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
# Unsafe unwrapping (raises on failure)
|
|
115
|
+
Success("hello").unwrap! # => "hello"
|
|
116
|
+
Failure(:error, "oops").unwrap! # => raises Llull::ResultError
|
|
117
|
+
|
|
118
|
+
# Safe unwrapping with default
|
|
119
|
+
Success("hello").unwrap_or("default") # => "hello"
|
|
120
|
+
Failure(:error, "oops").unwrap_or("default") # => "default"
|
|
121
|
+
|
|
122
|
+
# Dynamic default based on error
|
|
123
|
+
Failure(:not_found, "User").value_or { |type, data| "Missing #{data}" }
|
|
124
|
+
# => "Missing User"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Rails Service Objects
|
|
128
|
+
|
|
129
|
+
Complete example:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
class UserRegistrationService
|
|
133
|
+
include Llull
|
|
134
|
+
|
|
135
|
+
def call(params)
|
|
136
|
+
validate_params(params)
|
|
137
|
+
.and_then { |valid_params| check_email_uniqueness(valid_params) }
|
|
138
|
+
.and_then { |valid_params| create_user(valid_params) }
|
|
139
|
+
.tap_success { |user| send_welcome_email(user) }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
def validate_params(params)
|
|
145
|
+
errors = []
|
|
146
|
+
errors << "Email is required" if params[:email].blank?
|
|
147
|
+
errors << "Invalid email format" unless params[:email]&.include?("@")
|
|
148
|
+
errors << "Password too short" if params[:password]&.length.to_i < 8
|
|
149
|
+
|
|
150
|
+
errors.empty? ? Success(params) : Failure(:validation, errors)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def check_email_uniqueness(params)
|
|
154
|
+
existing = User.find_by(email: params[:email])
|
|
155
|
+
existing ? Failure(:duplicate_email, "Email already taken") : Success(params)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def create_user(params)
|
|
159
|
+
user = User.new(params)
|
|
160
|
+
user.save ? Success(user) : Failure(:creation_failed, user.errors.full_messages)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def send_welcome_email(user)
|
|
164
|
+
WelcomeMailer.deliver_later(user)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
#### Controller Integration
|
|
170
|
+
|
|
171
|
+
Controller usage:
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
class UsersController < ApplicationController
|
|
175
|
+
def create
|
|
176
|
+
UserRegistrationService.new.call(user_params)
|
|
177
|
+
.on_success { |user| redirect_to user, notice: "Welcome!" }
|
|
178
|
+
.on_failure(:validation) { |errors|
|
|
179
|
+
flash.now[:errors] = errors
|
|
180
|
+
render :new, status: :unprocessable_entity
|
|
181
|
+
}
|
|
182
|
+
.on_failure(:duplicate_email) { |message|
|
|
183
|
+
flash.now[:error] = message
|
|
184
|
+
render :new, status: :conflict
|
|
185
|
+
}
|
|
186
|
+
.on_failure { |type, data|
|
|
187
|
+
flash[:error] = "Registration failed"
|
|
188
|
+
redirect_to signup_path
|
|
189
|
+
}
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Pattern Matching (Ruby 3+)
|
|
195
|
+
|
|
196
|
+
Ruby 3+ pattern matching support:
|
|
197
|
+
|
|
198
|
+
```ruby
|
|
199
|
+
result = UserService.new.call(params)
|
|
200
|
+
|
|
201
|
+
case result
|
|
202
|
+
in [:success, user]
|
|
203
|
+
puts "Created user: #{user.email}"
|
|
204
|
+
in [:failure, :validation, errors]
|
|
205
|
+
puts "Validation failed: #{errors.join(', ')}"
|
|
206
|
+
in [:failure, error_type, data]
|
|
207
|
+
puts "Other error: #{error_type}"
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Or with hash patterns
|
|
211
|
+
case result
|
|
212
|
+
in { success: true, value: user }
|
|
213
|
+
puts "Success: #{user.email}"
|
|
214
|
+
in { success: false, error_type: :validation }
|
|
215
|
+
puts "Validation error"
|
|
216
|
+
end
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Advanced Pattern Matching DSL
|
|
220
|
+
|
|
221
|
+
Complex error handling:
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
PaymentService.new.call(order, payment_method).match do |m|
|
|
225
|
+
m.success { |payment| redirect_to_confirmation(payment) }
|
|
226
|
+
m.failure(:insufficient_funds) { |data| offer_payment_plan }
|
|
227
|
+
m.failure(:invalid_card) { |data| ask_for_different_card }
|
|
228
|
+
m.failure(:service_unavailable) { |data| try_again_later }
|
|
229
|
+
m.failure { |type, data| log_unexpected_error(type, data) }
|
|
230
|
+
end
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Side Effects
|
|
234
|
+
|
|
235
|
+
Execute side effects without affecting the Result chain:
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
CreateOrderService.new.call(params)
|
|
239
|
+
.tap_success { |order| analytics.track("order_created", order.id) }
|
|
240
|
+
.tap_failure { |type, data| logger.error("Order failed: #{type}") }
|
|
241
|
+
.and_then { |order| PaymentService.new.call(order) }
|
|
242
|
+
.tap_success { |payment| send_receipt(payment) }
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Collection Operations
|
|
246
|
+
|
|
247
|
+
Multiple Results:
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
# Process multiple items, fail fast on first error
|
|
251
|
+
user_data = [
|
|
252
|
+
{ email: "user1@example.com", name: "Alice" },
|
|
253
|
+
{ email: "user2@example.com", name: "Bob" },
|
|
254
|
+
{ email: "user3@example.com", name: "Carol" }
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
results = user_data.map { |data| UserService.new.call(data) }
|
|
258
|
+
all_users = collect(results) # Success([user1, user2, user3]) or first Failure
|
|
259
|
+
|
|
260
|
+
# Transform a collection with Results
|
|
261
|
+
result = traverse(user_emails) do |email|
|
|
262
|
+
user = User.find_by(email: email)
|
|
263
|
+
user ? Success(user) : Failure(:not_found, email)
|
|
264
|
+
end
|
|
265
|
+
# => Success([user1, user2, ...]) or first Failure
|
|
266
|
+
|
|
267
|
+
# Convert Result containing array into array of Results
|
|
268
|
+
result = Success([1, 2, 3])
|
|
269
|
+
individual_results = sequence(result)
|
|
270
|
+
# => [Success(1), Success(2), Success(3)]
|
|
271
|
+
|
|
272
|
+
failure_result = Failure(:error, "failed")
|
|
273
|
+
sequence(failure_result)
|
|
274
|
+
# => [Failure(:error, "failed")]
|
|
275
|
+
|
|
276
|
+
# Partition mixed Results into successes and failures
|
|
277
|
+
mixed_results = [
|
|
278
|
+
Success("good1"),
|
|
279
|
+
Failure(:error, "bad1"),
|
|
280
|
+
Success("good2")
|
|
281
|
+
]
|
|
282
|
+
successes, failures = partition(mixed_results)
|
|
283
|
+
# successes => ["good1", "good2"]
|
|
284
|
+
# failures => [[:error, "bad1"]]
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Error Recovery
|
|
288
|
+
|
|
289
|
+
Multiple fallback strategies:
|
|
290
|
+
|
|
291
|
+
```ruby
|
|
292
|
+
PrimaryPaymentService.new.call(order)
|
|
293
|
+
.or_else { |type, data| BackupPaymentService.new.call(order) }
|
|
294
|
+
.or_else { |type, data| ManualReviewService.new.call(order) }
|
|
295
|
+
.recover { |type, data|
|
|
296
|
+
# Last resort: create pending order
|
|
297
|
+
order.update!(status: 'pending_payment')
|
|
298
|
+
order
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Integration with ActiveRecord
|
|
303
|
+
|
|
304
|
+
```ruby
|
|
305
|
+
class Order
|
|
306
|
+
def self.create_with_validation(params)
|
|
307
|
+
order = new(params)
|
|
308
|
+
|
|
309
|
+
if order.valid?
|
|
310
|
+
order.save ? Llull::Success(order) : Llull::Failure(:save_failed, order.errors)
|
|
311
|
+
else
|
|
312
|
+
Llull::Failure(:validation, order.errors.full_messages)
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Usage
|
|
318
|
+
Order.create_with_validation(params)
|
|
319
|
+
.and_then { |order| PaymentService.new.call(order) }
|
|
320
|
+
.and_then { |payment| ShippingService.new.call(payment.order) }
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## API Reference
|
|
324
|
+
|
|
325
|
+
### Result Methods
|
|
326
|
+
|
|
327
|
+
- `success?` / `failure?` - Check result status
|
|
328
|
+
- `value` / `error_type` / `error_data` - Access result data
|
|
329
|
+
- `map { |value| ... }` - Transform successful values
|
|
330
|
+
- `and_then { |value| ... }` - Chain Result-returning operations
|
|
331
|
+
- `or_else { |type, data| ... }` - Recover with Result-returning operation
|
|
332
|
+
- `recover { |type, data| ... }` - Convert failure to success value
|
|
333
|
+
- `unwrap!` - Extract value (raises on failure)
|
|
334
|
+
- `unwrap_or(default)` - Extract value with default
|
|
335
|
+
- `value_or { |type, data| ... }` - Extract value with dynamic default
|
|
336
|
+
- `tap_success { |value| ... }` - Side effect on success
|
|
337
|
+
- `tap_failure { |type, data| ... }` - Side effect on failure
|
|
338
|
+
- `on_success { |value| ... }` - Rails-friendly success handler
|
|
339
|
+
- `on_failure(type = nil) { |type, data| ... }` - Rails-friendly failure handler
|
|
340
|
+
|
|
341
|
+
### Module Methods
|
|
342
|
+
|
|
343
|
+
- `Success(value)` - Create successful Result
|
|
344
|
+
- `Failure(type, data = nil)` - Create failed Result
|
|
345
|
+
- `collect(results)` - Combine multiple Results
|
|
346
|
+
- `traverse(collection) { |item| ... }` - Map collection to Results and collect
|
|
347
|
+
- `sequence(result)` - Convert Result containing array to array of Results
|
|
348
|
+
- `partition(results)` - Split array of Results into [successes, failures]
|
|
349
|
+
|
|
350
|
+
### Pattern Matching Support
|
|
351
|
+
|
|
352
|
+
Results work with Ruby 3+ pattern matching via `deconstruct` and `deconstruct_keys`.
|
|
353
|
+
|
|
354
|
+
## Requirements
|
|
355
|
+
|
|
356
|
+
- Ruby 3.2+
|
|
357
|
+
- No external dependencies
|
|
358
|
+
|
|
359
|
+
## Development
|
|
360
|
+
|
|
361
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
362
|
+
|
|
363
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
364
|
+
|
|
365
|
+
## Contributing
|
|
366
|
+
|
|
367
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/llull.
|
|
368
|
+
|
|
369
|
+
## License
|
|
370
|
+
|
|
371
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Llull
|
|
4
|
+
# Pattern matching functionality for Result instances
|
|
5
|
+
module PatternMatching
|
|
6
|
+
# Pattern matching support for Ruby 3+ (array-style)
|
|
7
|
+
# @return [Array] deconstructed result for pattern matching
|
|
8
|
+
def deconstruct
|
|
9
|
+
success? ? [:success, value] : [:failure, error_type, error_data]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Pattern matching support for Ruby 3+ (hash-style)
|
|
13
|
+
# @param keys [Array<Symbol>] requested keys for pattern matching
|
|
14
|
+
# @return [Hash] deconstructed result for hash pattern matching
|
|
15
|
+
def deconstruct_keys(_keys)
|
|
16
|
+
if success?
|
|
17
|
+
{ success: true, value: value }
|
|
18
|
+
else
|
|
19
|
+
{ success: false, error_type: error_type, error_data: error_data }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Advanced pattern matching with DSL
|
|
24
|
+
# @param block [Proc] block that receives a Matcher for defining patterns
|
|
25
|
+
# @return [Object] the result of executing the matched handler
|
|
26
|
+
def match(&block)
|
|
27
|
+
matcher = Matcher.new(self)
|
|
28
|
+
block.call(matcher)
|
|
29
|
+
matcher.execute
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/llull/result.rb
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "pattern_matching"
|
|
4
|
+
|
|
5
|
+
# Llull provides composable Result types for Ruby applications.
|
|
6
|
+
# Inspired by functional programming and railway-oriented programming patterns.
|
|
7
|
+
module Llull
|
|
8
|
+
# Result represents the result of an operation that can either succeed or fail.
|
|
9
|
+
# It provides a functional programming approach to error handling without exceptions.
|
|
10
|
+
class Result
|
|
11
|
+
attr_reader :value, :error_type, :error_data
|
|
12
|
+
|
|
13
|
+
# @param success [Boolean] whether this is a success or failure
|
|
14
|
+
# @param value [Object] the successful value (nil for failures)
|
|
15
|
+
# @param error_type [Symbol] the type/category of error (nil for successes)
|
|
16
|
+
# @param error_data [Object] additional error data (nil for successes)
|
|
17
|
+
def initialize(success, value, error_type = nil, error_data = nil)
|
|
18
|
+
@success = success
|
|
19
|
+
@value = value
|
|
20
|
+
@error_type = error_type
|
|
21
|
+
@error_data = error_data
|
|
22
|
+
freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @return [Boolean] true if this is a successful result
|
|
26
|
+
def success?
|
|
27
|
+
@success
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [Boolean] true if this is a failed result
|
|
31
|
+
def failure?
|
|
32
|
+
!@success
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Transform the value if successful, return self if failed
|
|
36
|
+
# @param block [Proc] transformation to apply to the value
|
|
37
|
+
# @return [Result] new Result with transformed value or original failure
|
|
38
|
+
def map(&block)
|
|
39
|
+
return self if failure?
|
|
40
|
+
|
|
41
|
+
Llull::Success(block.call(value))
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
Llull::Failure(:exception, e)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Chain operations that return Results
|
|
47
|
+
# @param block [Proc] operation that returns a Result
|
|
48
|
+
# @return [Result] the result of the operation or original failure
|
|
49
|
+
def and_then(&block)
|
|
50
|
+
return self if failure?
|
|
51
|
+
|
|
52
|
+
begin
|
|
53
|
+
result = block.call(value)
|
|
54
|
+
raise TypeError, "Block must return a Result" unless result.is_a?(Result)
|
|
55
|
+
|
|
56
|
+
result
|
|
57
|
+
rescue TypeError
|
|
58
|
+
raise # Re-raise TypeError so it's not caught by the general rescue below
|
|
59
|
+
rescue StandardError => e
|
|
60
|
+
Llull::Failure(:exception, e)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
alias flat_map and_then
|
|
65
|
+
alias bind and_then
|
|
66
|
+
|
|
67
|
+
# Recover from failure by chaining another Result-returning operation
|
|
68
|
+
# @param block [Proc] operation that takes (error_type, error_data) and returns a Result
|
|
69
|
+
# @return [Result] the result of recovery operation or original success
|
|
70
|
+
def or_else(&block)
|
|
71
|
+
return self if success?
|
|
72
|
+
|
|
73
|
+
result = block.call(error_type, error_data)
|
|
74
|
+
raise TypeError, "Block must return a Result" unless result.is_a?(Result)
|
|
75
|
+
|
|
76
|
+
result
|
|
77
|
+
rescue StandardError => e
|
|
78
|
+
Llull::Failure(:exception, e)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Recover from failure by transforming error into a success value
|
|
82
|
+
# @param block [Proc] operation that takes (error_type, error_data) and returns a value
|
|
83
|
+
# @return [Result] Success with transformed value or original success
|
|
84
|
+
def recover(&block)
|
|
85
|
+
return self if success?
|
|
86
|
+
|
|
87
|
+
Llull::Success(block.call(error_type, error_data))
|
|
88
|
+
rescue StandardError => e
|
|
89
|
+
Llull::Failure(:exception, e)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Extract the value, raising an exception if this is a failure
|
|
93
|
+
# @return [Object] the success value
|
|
94
|
+
# @raise [ResultError] if this is a failure
|
|
95
|
+
def unwrap!
|
|
96
|
+
raise ResultError.new(error_type, error_data) if failure?
|
|
97
|
+
|
|
98
|
+
value
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Extract the value or return a default if this is a failure
|
|
102
|
+
# @param default [Object] the default value to return on failure
|
|
103
|
+
# @return [Object] the success value or the default
|
|
104
|
+
def unwrap_or(default)
|
|
105
|
+
success? ? value : default
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Extract the value or call a block with error information if this is a failure
|
|
109
|
+
# @param block [Proc] operation that takes (error_type, error_data) and returns a value
|
|
110
|
+
# @return [Object] the success value or the result of the block
|
|
111
|
+
def value_or(&block)
|
|
112
|
+
success? ? value : block.call(error_type, error_data)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Execute a side effect if successful, return self unchanged
|
|
116
|
+
# @param block [Proc] operation to execute with the success value
|
|
117
|
+
# @return [Result] self, unchanged
|
|
118
|
+
def tap_success(&block)
|
|
119
|
+
block.call(value) if success?
|
|
120
|
+
self
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Execute a side effect if failed, return self unchanged
|
|
124
|
+
# @param block [Proc] operation to execute with (error_type, error_data)
|
|
125
|
+
# @return [Result] self, unchanged
|
|
126
|
+
def tap_failure(&block)
|
|
127
|
+
block.call(error_type, error_data) if failure?
|
|
128
|
+
self
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
alias tap_error tap_failure
|
|
132
|
+
|
|
133
|
+
# Execute a block if successful, return self for chaining
|
|
134
|
+
# @param block [Proc] operation to execute with the success value
|
|
135
|
+
# @return [Result] self, unchanged
|
|
136
|
+
def on_success(&block)
|
|
137
|
+
block.call(value) if success?
|
|
138
|
+
self
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Execute a block if failed with optional error type matching, return self for chaining
|
|
142
|
+
# @param match_type [Symbol, nil] specific error type to match, or nil for any failure
|
|
143
|
+
# @param block [Proc] operation to execute with (error_type, error_data)
|
|
144
|
+
# @return [Result] self, unchanged
|
|
145
|
+
def on_failure(match_type = nil, &block)
|
|
146
|
+
block.call(error_type, error_data) if failure? && (match_type.nil? || match_type == error_type)
|
|
147
|
+
self
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Inspect method for better debugging
|
|
151
|
+
def inspect
|
|
152
|
+
if success?
|
|
153
|
+
"#<Llull::Result::Success(#{value.inspect})>"
|
|
154
|
+
else
|
|
155
|
+
"#<Llull::Result::Failure(#{error_type.inspect}, #{error_data.inspect})>"
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def to_s
|
|
160
|
+
inspect
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Equality comparison
|
|
164
|
+
def ==(other)
|
|
165
|
+
return false unless other.is_a?(Result)
|
|
166
|
+
|
|
167
|
+
success? == other.success? &&
|
|
168
|
+
value == other.value &&
|
|
169
|
+
error_type == other.error_type &&
|
|
170
|
+
error_data == other.error_data
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
alias eql? ==
|
|
174
|
+
|
|
175
|
+
def hash
|
|
176
|
+
[self.class, success?, value, error_type, error_data].hash
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
include PatternMatching
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Matcher class for the pattern matching DSL
|
|
183
|
+
class Matcher
|
|
184
|
+
def initialize(result)
|
|
185
|
+
@result = result
|
|
186
|
+
@success_handlers = []
|
|
187
|
+
@failure_handlers = {}
|
|
188
|
+
@executed = false
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Register a handler for successful results
|
|
192
|
+
# @param block [Proc] handler that receives the success value
|
|
193
|
+
# @return [Matcher] self for chaining
|
|
194
|
+
def success(&block)
|
|
195
|
+
@success_handlers << block
|
|
196
|
+
self
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Register a handler for failed results
|
|
200
|
+
# @param type [Symbol, nil] specific error type to match, or nil for catch-all
|
|
201
|
+
# @param block [Proc] handler that receives (error_type, error_data) for catch-all
|
|
202
|
+
# or just (error_data) for specific type matches
|
|
203
|
+
# @return [Matcher] self for chaining
|
|
204
|
+
def failure(type = nil, &block)
|
|
205
|
+
if type.nil?
|
|
206
|
+
@failure_handlers[:catch_all] = block
|
|
207
|
+
else
|
|
208
|
+
@failure_handlers[type] = block
|
|
209
|
+
end
|
|
210
|
+
self
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Execute the appropriate handler based on the result
|
|
214
|
+
# @return [Object] the result of executing the matched handler
|
|
215
|
+
def execute
|
|
216
|
+
return if @executed
|
|
217
|
+
|
|
218
|
+
@executed = true
|
|
219
|
+
|
|
220
|
+
if @result.success?
|
|
221
|
+
@success_handlers.each { |handler| handler.call(@result.value) }
|
|
222
|
+
elsif @failure_handlers.key?(@result.error_type)
|
|
223
|
+
# Try specific error type handler first
|
|
224
|
+
@failure_handlers[@result.error_type].call(@result.error_data)
|
|
225
|
+
elsif @failure_handlers.key?(:catch_all)
|
|
226
|
+
@failure_handlers[:catch_all].call(@result.error_type, @result.error_data)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Custom exception for Result errors
|
|
232
|
+
class ResultError < StandardError
|
|
233
|
+
attr_reader :error_type, :error_data
|
|
234
|
+
|
|
235
|
+
def initialize(error_type, error_data)
|
|
236
|
+
@error_type = error_type
|
|
237
|
+
@error_data = error_data
|
|
238
|
+
super("Result failure: #{error_type} - #{error_data}")
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Constructor functions for creating Results
|
|
243
|
+
module_function
|
|
244
|
+
|
|
245
|
+
# Create a successful Result
|
|
246
|
+
# @param value [Object] the successful value
|
|
247
|
+
# @return [Result] a successful Result containing the value
|
|
248
|
+
def Success(value)
|
|
249
|
+
Result.new(true, value)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Create a failed Result
|
|
253
|
+
# @param error_type [Symbol] the type/category of error
|
|
254
|
+
# @param error_data [Object] additional error information
|
|
255
|
+
# @return [Result] a failed Result containing the error information
|
|
256
|
+
def Failure(error_type, error_data = nil)
|
|
257
|
+
Result.new(false, nil, error_type, error_data)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Collect multiple Results into a single Result
|
|
261
|
+
# Returns Success with array of all values if all Results are successful,
|
|
262
|
+
# otherwise returns the first Failure encountered
|
|
263
|
+
# @param results [Array<Result>] array of Results to collect
|
|
264
|
+
# @return [Result] Success with array of values or first Failure
|
|
265
|
+
def collect(results)
|
|
266
|
+
results.each do |result|
|
|
267
|
+
raise TypeError, "All elements must be Results" unless result.is_a?(Result)
|
|
268
|
+
return result if result.failure?
|
|
269
|
+
end
|
|
270
|
+
Llull::Success(results.map(&:value))
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
alias all collect
|
|
274
|
+
|
|
275
|
+
# Transform a collection by applying a function that returns Results
|
|
276
|
+
# Similar to collect but maps over input and collects the results
|
|
277
|
+
# @param collection [Array] input collection to transform
|
|
278
|
+
# @param block [Proc] function that takes an element and returns a Result
|
|
279
|
+
# @return [Result] Success with array of transformed values or first Failure
|
|
280
|
+
def traverse(collection, &)
|
|
281
|
+
results = collection.map(&)
|
|
282
|
+
collect(results)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Convert a Result containing an array into an array of Results
|
|
286
|
+
# @param result [Result] Result containing an array value
|
|
287
|
+
# @return [Array<Result>] array of individual Results
|
|
288
|
+
def sequence(result)
|
|
289
|
+
raise TypeError, "Argument must be a Result" unless result.is_a?(Result)
|
|
290
|
+
|
|
291
|
+
if result.success?
|
|
292
|
+
raise TypeError, "Result value must be an Array" unless result.value.is_a?(Array)
|
|
293
|
+
|
|
294
|
+
result.value.map { |item| Llull::Success(item) }
|
|
295
|
+
else
|
|
296
|
+
[result] # Return array containing the single failure
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Partition an array of Results into successes and failures
|
|
301
|
+
# @param results [Array<Result>] array of Results to partition
|
|
302
|
+
# @return [Array] tuple of [success_values, failure_tuples]
|
|
303
|
+
# where success_values is Array of successful values
|
|
304
|
+
# and failure_tuples is Array of [error_type, error_data] pairs
|
|
305
|
+
def partition(results)
|
|
306
|
+
validate_results_array(results)
|
|
307
|
+
|
|
308
|
+
successes, failures = results.partition(&:success?)
|
|
309
|
+
[successes.map(&:value), failures.map { |f| [f.error_type, f.error_data] }]
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
private
|
|
313
|
+
|
|
314
|
+
# Validate that all elements in array are Results
|
|
315
|
+
# @param results [Array] array to validate
|
|
316
|
+
# @raise [TypeError] if any element is not a Result
|
|
317
|
+
def validate_results_array(results)
|
|
318
|
+
results.each do |result|
|
|
319
|
+
raise TypeError, "All elements must be Results" unless result.is_a?(Result)
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
data/lib/llull.rb
ADDED
data/sig/llull.rbs
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
module Llull
|
|
2
|
+
VERSION: String
|
|
3
|
+
|
|
4
|
+
class Result[out Value, out ErrorData]
|
|
5
|
+
@success: bool
|
|
6
|
+
@value: Value?
|
|
7
|
+
@error_type: Symbol?
|
|
8
|
+
@error_data: ErrorData?
|
|
9
|
+
|
|
10
|
+
def initialize: (bool success, Value? value, Symbol? error_type, ErrorData? error_data) -> void
|
|
11
|
+
|
|
12
|
+
def success?: () -> bool
|
|
13
|
+
def failure?: () -> bool
|
|
14
|
+
def value: () -> Value?
|
|
15
|
+
def error_type: () -> Symbol?
|
|
16
|
+
def error_data: () -> ErrorData?
|
|
17
|
+
|
|
18
|
+
# Monadic operations
|
|
19
|
+
def map: [NewValue] () { (Value) -> NewValue } -> Result[NewValue, ErrorData]
|
|
20
|
+
def and_then: [NewValue] () { (Value) -> Result[NewValue, ErrorData] } -> Result[NewValue, ErrorData]
|
|
21
|
+
def flat_map: [NewValue] () { (Value) -> Result[NewValue, ErrorData] } -> Result[NewValue, ErrorData]
|
|
22
|
+
def bind: [NewValue] () { (Value) -> Result[NewValue, ErrorData] } -> Result[NewValue, ErrorData]
|
|
23
|
+
|
|
24
|
+
# Recovery operations
|
|
25
|
+
def or_else: [NewValue] () { (Symbol, ErrorData) -> Result[NewValue, ErrorData] } -> Result[NewValue | Value, ErrorData]
|
|
26
|
+
def recover: [NewValue] () { (Symbol, ErrorData) -> NewValue } -> Result[NewValue | Value, ErrorData]
|
|
27
|
+
|
|
28
|
+
# Unwrapping
|
|
29
|
+
def unwrap!: () -> Value
|
|
30
|
+
def unwrap_or: [DefaultValue] (DefaultValue default) -> (Value | DefaultValue)
|
|
31
|
+
def value_or: [DefaultValue] () { (Symbol, ErrorData) -> DefaultValue } -> (Value | DefaultValue)
|
|
32
|
+
|
|
33
|
+
# Side effects
|
|
34
|
+
def tap_success: () { (Value) -> untyped } -> self
|
|
35
|
+
def tap_failure: () { (Symbol, ErrorData) -> untyped } -> self
|
|
36
|
+
def tap_error: () { (Symbol, ErrorData) -> untyped } -> self
|
|
37
|
+
|
|
38
|
+
# Rails-friendly helpers
|
|
39
|
+
def on_success: () { (Value) -> untyped } -> self
|
|
40
|
+
def on_failure: (?Symbol? match_type) { (Symbol, ErrorData) -> untyped } -> self
|
|
41
|
+
|
|
42
|
+
# Pattern matching support
|
|
43
|
+
def deconstruct: () -> [:success, Value] | [:failure, Symbol, ErrorData]
|
|
44
|
+
def deconstruct_keys: (Array[Symbol] keys) -> { success: bool, value: Value?, error_type: Symbol?, error_data: ErrorData? }
|
|
45
|
+
|
|
46
|
+
# Matching DSL
|
|
47
|
+
def match: [ReturnType] () { (Matcher[Value, ErrorData]) -> ReturnType } -> ReturnType
|
|
48
|
+
|
|
49
|
+
# Equality and inspection
|
|
50
|
+
def ==: (untyped other) -> bool
|
|
51
|
+
def eql?: (untyped other) -> bool
|
|
52
|
+
def hash: () -> Integer
|
|
53
|
+
def inspect: () -> String
|
|
54
|
+
def to_s: () -> String
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class Matcher[Value, ErrorData]
|
|
58
|
+
def initialize: (Result[Value, ErrorData] result) -> void
|
|
59
|
+
def success: () { (Value) -> untyped } -> self
|
|
60
|
+
def failure: (?Symbol? type) { (Symbol, ErrorData) -> untyped } | { (ErrorData) -> untyped } -> self
|
|
61
|
+
def execute: () -> untyped
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class ResultError < StandardError
|
|
65
|
+
attr_reader error_type: Symbol
|
|
66
|
+
attr_reader error_data: untyped
|
|
67
|
+
|
|
68
|
+
def initialize: (Symbol error_type, untyped error_data) -> void
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Constructor functions
|
|
72
|
+
def self.Success: [Value] (Value value) -> Result[Value, untyped]
|
|
73
|
+
def self.Failure: [ErrorData] (Symbol error_type, ?ErrorData? error_data) -> Result[untyped, ErrorData]
|
|
74
|
+
|
|
75
|
+
# Collection operations
|
|
76
|
+
def self.collect: [Value, ErrorData] (Array[Result[Value, ErrorData]] results) -> Result[Array[Value], ErrorData]
|
|
77
|
+
def self.all: [Value, ErrorData] (Array[Result[Value, ErrorData]] results) -> Result[Array[Value], ErrorData]
|
|
78
|
+
def self.traverse: [Input, Value, ErrorData] (Array[Input] collection) { (Input) -> Result[Value, ErrorData] } -> Result[Array[Value], ErrorData]
|
|
79
|
+
def self.sequence: [Value, ErrorData] (Result[Array[Value], ErrorData] result) -> Array[Result[Value, ErrorData]]
|
|
80
|
+
def self.partition: [Value, ErrorData] (Array[Result[Value, ErrorData]] results) -> [Array[Value], Array[[Symbol, ErrorData]]]
|
|
81
|
+
|
|
82
|
+
# Module functions available when including Llull
|
|
83
|
+
def Success: [Value] (Value value) -> Result[Value, untyped]
|
|
84
|
+
def Failure: [ErrorData] (Symbol error_type, ?ErrorData? error_data) -> Result[untyped, ErrorData]
|
|
85
|
+
def collect: [Value, ErrorData] (Array[Result[Value, ErrorData]] results) -> Result[Array[Value], ErrorData]
|
|
86
|
+
def all: [Value, ErrorData] (Array[Result[Value, ErrorData]] results) -> Result[Array[Value], ErrorData]
|
|
87
|
+
def traverse: [Input, Value, ErrorData] (Array[Input] collection) { (Input) -> Result[Value, ErrorData] } -> Result[Array[Value], ErrorData]
|
|
88
|
+
def sequence: [Value, ErrorData] (Result[Array[Value], ErrorData] result) -> Array[Result[Value, ErrorData]]
|
|
89
|
+
def partition: [Value, ErrorData] (Array[Result[Value, ErrorData]] results) -> [Array[Value], Array[[Symbol, ErrorData]]]
|
|
90
|
+
|
|
91
|
+
class Error < StandardError
|
|
92
|
+
end
|
|
93
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: llull
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Thomas Murphy
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-12-29 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rake
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '13.0'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '13.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rspec
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rubocop
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '1.21'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '1.21'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rubocop-rake
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0.6'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0.6'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rubocop-rspec
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '3.0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '3.0'
|
|
83
|
+
description: Inspired by railway-oriented programming, Llull provides chainable operations
|
|
84
|
+
that make complex business logic clear and maintainable.
|
|
85
|
+
email:
|
|
86
|
+
- thomaspmurphy@proton.me
|
|
87
|
+
executables: []
|
|
88
|
+
extensions: []
|
|
89
|
+
extra_rdoc_files: []
|
|
90
|
+
files:
|
|
91
|
+
- LICENSE.txt
|
|
92
|
+
- README.md
|
|
93
|
+
- Rakefile
|
|
94
|
+
- lib/llull.rb
|
|
95
|
+
- lib/llull/pattern_matching.rb
|
|
96
|
+
- lib/llull/result.rb
|
|
97
|
+
- lib/llull/version.rb
|
|
98
|
+
- sig/llull.rbs
|
|
99
|
+
homepage: https://github.com/thomaspmurphy/llull
|
|
100
|
+
licenses:
|
|
101
|
+
- MIT
|
|
102
|
+
metadata:
|
|
103
|
+
allowed_push_host: https://rubygems.org
|
|
104
|
+
homepage_uri: https://github.com/thomaspmurphy/llull
|
|
105
|
+
source_code_uri: https://github.com/thomaspmurphy/llull
|
|
106
|
+
rubygems_mfa_required: 'true'
|
|
107
|
+
post_install_message:
|
|
108
|
+
rdoc_options: []
|
|
109
|
+
require_paths:
|
|
110
|
+
- lib
|
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - ">="
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: 3.2.0
|
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
117
|
+
requirements:
|
|
118
|
+
- - ">="
|
|
119
|
+
- !ruby/object:Gem::Version
|
|
120
|
+
version: '0'
|
|
121
|
+
requirements: []
|
|
122
|
+
rubygems_version: 3.4.10
|
|
123
|
+
signing_key:
|
|
124
|
+
specification_version: 4
|
|
125
|
+
summary: Composable result types for Ruby.
|
|
126
|
+
test_files: []
|