zx-monads 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 21a3b0d8f2b0b4653a4c2c2e3bb1f955fef4525ade867543cf5100913b74bd83
4
+ data.tar.gz: 7a4e852b48b0fec8b56e60ab734d6d1db25d355de04a99d345af326b94ac96a2
5
+ SHA512:
6
+ metadata.gz: 7b5cfdcd761036bfb7016e48e93a52405266afc45bbf96ea33a7149ba6f7873294079520e77aef9c3b9f0012c5ad0b2445eca1f69667137945a0bd8ba3d4577a
7
+ data.tar.gz: fb94ac04e0f19b4f2c3a5e6520ec1e6a8d2e47b013b78076bfa45b7114902b48aba9ca8ddb47df5ddac6f501741d2cd5801348a1bb9f295d90eb350f43517e2f
data/.editorconfig ADDED
@@ -0,0 +1,12 @@
1
+ # EditorConfig is awesome: https://EditorConfig.org
2
+
3
+ # top-most EditorConfig file
4
+ root = true
5
+
6
+ [*]
7
+ indent_style = space
8
+ indent_size = 2
9
+ end_of_line = lf
10
+ charset = utf-8
11
+ trim_trailing_whitespace = false
12
+ insert_final_newline = false
@@ -0,0 +1,28 @@
1
+ name: ci
2
+
3
+ on: [push]
4
+
5
+ permissions:
6
+ contents: read
7
+
8
+ jobs:
9
+ rspec:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ ruby-version: ['2.7', '3.0', '3.1', '3.2']
14
+
15
+ steps:
16
+ - uses: actions/checkout@v3
17
+
18
+ - name: Set up Ruby
19
+ # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
20
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
21
+ # uses: ruby/setup-ruby@v1
22
+ uses: ruby/setup-ruby@2b019609e2b0f1ea1a2bc8ca11cb82ab46ada124
23
+ with:
24
+ ruby-version: ${{ matrix.ruby-version }}
25
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
26
+
27
+ - name: Run RSpec
28
+ run: bundle exec rspec --color
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ .DS_Store
2
+ .rspec_status
3
+ .byebug_history
4
+
5
+ /.bundle/
6
+ /.yardoc
7
+ /Gemfile.lock
8
+ /_yardoc/
9
+ /coverage/
10
+ /doc/
11
+ /pkg/
12
+ /spec/reports/
13
+ /tmp/
14
+
15
+ # rspec failure tracking
16
+ .rspec_status
17
+
18
+ /.idea/
19
+ .byebug_history
20
+ .DS_store
21
+ *.gem
data/.rubocop.yml ADDED
@@ -0,0 +1,23 @@
1
+ AllCops:
2
+ DisabledByDefault: false
3
+ NewCops: enable
4
+ TargetRubyVersion: 2.7
5
+
6
+ Style/Documentation:
7
+ Enabled: false
8
+
9
+ Layout:
10
+ Enabled: false
11
+
12
+ Style/FrozenStringLiteralComment:
13
+ Enabled: true
14
+ SafeAutoCorrect: true
15
+ Exclude:
16
+ - bin/**/*
17
+ - Gemfile
18
+
19
+ Lint/MissingSuper:
20
+ Enabled: false
21
+
22
+ Metrics/BlockLength:
23
+ Enabled: false
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.7.6
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at tadeuu@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [https://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: https://contributor-covenant.org
74
+ [version]: https://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'base64'
4
+ gem 'json'
5
+ gem 'zeitwerk'
6
+
7
+ group :development, :test do
8
+ gem 'rubocop', github: 'rubocop/rubocop', require: false
9
+ end
10
+
11
+ group :test do
12
+ gem 'byebug'
13
+ gem 'rspec'
14
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2023 Thadeu Esteves Jr
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,430 @@
1
+ <p align="center">
2
+ <h1 align="center">🔃 Zx::Monads</h1>
3
+ <p align="center"><i>FP Monads for Ruby</i></p>
4
+ </p>
5
+
6
+ <p align="center">
7
+ <a href="https://rubygems.org/gems/zx-monads">
8
+ <img alt="Gem" src="https://img.shields.io/gem/v/zx-monads.svg">
9
+ </a>
10
+
11
+ <a href="https://github.com/thadeu/zx-monads/actions/workflows/ci.yml">
12
+ <img alt="Build Status" src="https://github.com/thadeu/zx-monads/actions/workflows/ci.yml/badge.svg">
13
+ </a>
14
+ </p>
15
+
16
+
17
+ ## Motivation
18
+
19
+ Because in sometimes, we need to handling a safe value for our objects. This gem simplify this work.
20
+
21
+ ## Documentation <!-- omit in toc -->
22
+
23
+ Version | Documentation
24
+ ---------- | -------------
25
+ unreleased | https://github.com/thadeu/zx-monads/blob/main/README.md
26
+
27
+ ## Table of Contents <!-- omit in toc -->
28
+ - [Installation](#installation)
29
+ - [Usage](#usage)
30
+
31
+ ## Compatibility
32
+
33
+ | kind | branch | ruby |
34
+ | -------------- | ------- | ------------------ |
35
+ | unreleased | main | >= 2.7.6, <= 3.2.x |
36
+
37
+ ## Installation
38
+
39
+ Use bundle
40
+
41
+ ```ruby
42
+ bundle add zx-monads
43
+ ```
44
+
45
+ or add this line to your application's Gemfile.
46
+
47
+ ```ruby
48
+ gem 'zx-monads'
49
+ ```
50
+
51
+ and then, require module
52
+
53
+ ```ruby
54
+ require 'zx'
55
+ ```
56
+
57
+ ## Configuration
58
+
59
+ Without configuration, because we use only Ruby. ❤️
60
+
61
+ ## Usage
62
+
63
+ How to use in my codebase?
64
+
65
+ ```rb
66
+ class Order
67
+ include Zx # include all Zx library.
68
+ end
69
+
70
+ class Order
71
+ include Zx::Maybe
72
+ end
73
+
74
+ class ProcessOrder < Zx::Steps
75
+ # Maybe included now!
76
+ end
77
+ ```
78
+
79
+ ### Available public methods
80
+
81
+ ```md
82
+ #type -> Returns maybe type
83
+ #some? -> Returns boolean
84
+ #none? -> Returns boolean
85
+ #unwrap -> Returns value unwrapped
86
+ #or(value) -> Returns unwrap or value
87
+ #>>(other) -> Forward to another Maybe
88
+ #fmap -> Create an step and wrap new value
89
+ #map(:key) -> Same the fmap, but receive another parameters
90
+ #map(&:method) -> Same the map, but respond to method
91
+ #map {} -> Same the the map, but receive an block
92
+ #map!{} -> Same the map, but change to new value
93
+ #apply!{} -> Same the map! but more legible
94
+ #dig(keys) -> Get values using keys like Hash#dig
95
+ #dig!(keys) -> Same them dig, but return unwrap
96
+ #math(some:, none:) -> Receive callables and associate them
97
+ #on_success{} -> Only when Some
98
+ #on_failure{} -> Only then None
99
+ ```
100
+
101
+ ### ZX::Maybe
102
+
103
+ ```ruby
104
+ result = Maybe[1] # or Maybe.of(1)
105
+ ```
106
+
107
+ ```ruby
108
+ result = Maybe[nil] # or Maybe.of(nil)
109
+ ```
110
+
111
+ ```ruby
112
+ result = Maybe[1].map{ _1 + 2}
113
+ # -> Some(3)
114
+ ```
115
+
116
+ ```ruby
117
+ result = Maybe[nil].map{ _1 + 2}
118
+ # -> None
119
+ ```
120
+
121
+ ```ruby
122
+ result = Maybe.of(1).or(2)
123
+ result.or(2) # 1
124
+ ```
125
+
126
+ ```ruby
127
+ result = Maybe.of(' ').or(2)
128
+ result.or(2) # 2
129
+ ```
130
+
131
+ ```ruby
132
+ result = Maybe.of(' ').or(2)
133
+ result.or(2) # 2
134
+ ```
135
+
136
+ ```ruby
137
+ order = {
138
+ shopping: {
139
+ banana: {
140
+ price: 10.0
141
+ }
142
+ }
143
+ }
144
+
145
+ price = Maybe[order]
146
+ .map { _1[:shopping] }
147
+ .map { _1[:banana] }
148
+ .map { _1[:price] }
149
+
150
+ # -> Some(10.0)
151
+
152
+ # or using #dig
153
+
154
+ price = Maybe[order].dig(:shopping, :banana, :price)
155
+ # -> Some(10.0)
156
+
157
+ price_none = Maybe[order].dig(:shopping, :banana, :price_non_exists)
158
+ # -> None
159
+
160
+ price_or = Maybe[order].dig(:shopping, :banana, :price_non_exists).or(10.0)
161
+ # -> Some(10.0)
162
+ ```
163
+
164
+ ```rb
165
+ class Response
166
+ attr_reader :body
167
+
168
+ def initialize(new_body)
169
+ @body = Maybe[new_body]
170
+ end
171
+
172
+ def change(new_body)
173
+ @body = Maybe[new_body]
174
+ end
175
+ end
176
+
177
+ response = Response.new(nil)
178
+ expect(response.body).to be_none
179
+
180
+ response.change({ status: 200 })
181
+ expect(response.body).to be_some
182
+
183
+ response_status = response.body.match(
184
+ some: ->(body) { Maybe[body].map { _1.fetch(:status) }.unwrap },
185
+ none: -> {}
186
+ )
187
+ ```
188
+
189
+ **Use case, when use to parse response stringify json**
190
+
191
+ ```rb
192
+ dump = JSON.dump({ status: { code: '300' } })
193
+
194
+ response = Response.new(dump) # It's receive an JSON stringified
195
+
196
+ module StatusCodeUnwrapModule
197
+ def self.call(body)
198
+ Maybe[body]
199
+ .map{ JSON(_1, symbolize_names: true) }
200
+ .dig(:status, :code)
201
+ .apply(&:to_i)
202
+ .unwrap
203
+ end
204
+ end
205
+
206
+ response_status = response.body.match(
207
+ some: StatusCodeUnwrapModule,
208
+ none: -> { 400 }
209
+ )
210
+
211
+ expect(response_status).to eq(300)
212
+ ```
213
+
214
+ You can use `>>` to compose many callables, like this.
215
+
216
+ ```rb
217
+ sum = ->(x) { Maybe::Some[x + 1] }
218
+
219
+ subtract = ->(x) { Maybe::Some[x - 1] }
220
+
221
+ result = Maybe[1] >> \
222
+ sum >> \
223
+ subtract
224
+
225
+ expect(result.unwrap).to eq(1)
226
+ ```
227
+
228
+ If handle None, no worries.
229
+
230
+ ```rb
231
+ sum = ->(x) { Maybe::Some[x + 1] }
232
+
233
+ subtract = ->(_) { Maybe::None.new }
234
+
235
+ result = Maybe[1] \
236
+ >> sum \
237
+ >> subtract
238
+
239
+ expect(result.unwrap).to be_nil
240
+ ```
241
+
242
+ ```rb
243
+ class Order
244
+ def self.sum(x)
245
+ Maybe[{ number: x + 1 }]
246
+ end
247
+ end
248
+
249
+ result = Order.sum(1)
250
+ .dig(:number)
251
+ .apply(&:to_i)
252
+
253
+ expect(result.unwrap).to be(2)
254
+ ```
255
+
256
+ ### Zx::Maybe::Some
257
+
258
+ ```rb
259
+ class Order
260
+ include Zx::Maybe
261
+
262
+ def self.sum(x)
263
+ new.sum(x)
264
+ end
265
+
266
+ def sum(x)
267
+ Some[{ number: x + 1 }]
268
+ end
269
+ end
270
+
271
+ result = Order.sum(1)
272
+ .dig(:number)
273
+ .apply(&:to_i)
274
+
275
+ expect(result.unwrap).to be(2)
276
+ ```
277
+
278
+ ### Zx::Maybe::None
279
+
280
+ ```rb
281
+ class Order
282
+ include Zx::Maybe
283
+
284
+ def self.sum(x)
285
+ new.sum(x)
286
+ end
287
+
288
+ def sum(x)
289
+ Try {{ number: x + ' ' }}
290
+ end
291
+ end
292
+
293
+ result = Order.sum(1)
294
+ number = result.dig(:number).apply(&:to_i)
295
+
296
+ expect(result).to be_none
297
+ expect(result).to be_a(Maybe::None)
298
+ expect(number.unwrap).to be(0) # nil.to_i == 0
299
+ ```
300
+
301
+ ### Zx::Maybe::Try
302
+
303
+ > Only included or inherited!
304
+
305
+ ```rb
306
+ class Order
307
+ include Zx::Maybe
308
+
309
+ def self.sum(x)
310
+ new.sum(x)
311
+ end
312
+
313
+ def sum(x)
314
+ Try {{ number: x + 1 }}
315
+ end
316
+ end
317
+
318
+ result = Order.sum(1)
319
+ .dig(:number)
320
+ .apply(&:to_i)
321
+
322
+ expect(result.unwrap).to be(2)
323
+ ```
324
+
325
+ With default value, in None case.
326
+
327
+ ```rb
328
+ class Order
329
+ include Zx::Maybe
330
+
331
+ def self.sum(x)
332
+ new.sum(x)
333
+ end
334
+
335
+ def sum(x)
336
+ Try(2) {{ number: x + ' ' }}
337
+ end
338
+ end
339
+
340
+ result = Order.sum(1)
341
+ .dig(:number)
342
+ .apply(&:to_i)
343
+
344
+ expect(result.unwrap).to be(2)
345
+ ```
346
+
347
+ ```rb
348
+ class Order
349
+ include Zx::Maybe
350
+
351
+ def self.sum(x)
352
+ new.sum(x)
353
+ end
354
+
355
+ def sum(x)
356
+ Try(or: 1000) {{ number: x + ' ' }}
357
+ end
358
+ end
359
+
360
+ result = Order.sum(1).dig(:number).apply(&:to_i)
361
+
362
+ expect(result.unwrap).to be(1000)
363
+ ```
364
+
365
+ ### Zx::Steps
366
+
367
+ ```rb
368
+ class OrderStep < Zx::Steps
369
+ def initialize(x = nil)
370
+ @x = x
371
+ end
372
+
373
+ step :positive?
374
+ step :apply_tax
375
+ step :divide
376
+
377
+ def positive?
378
+ return None unless total.is_a?(Integer) || total.is_a?(Float)
379
+ return None if total <= 0
380
+
381
+ Some total
382
+ end
383
+
384
+ def apply_tax
385
+ Try { @x -= (@x * 0.1) }
386
+ end
387
+
388
+ def divide
389
+ Try { @x /= 2 }
390
+ end
391
+
392
+ def total
393
+ @x
394
+ end
395
+ end
396
+ ```
397
+
398
+ ```rb
399
+ order = OrderStep.new(20)
400
+
401
+ order.call
402
+ .map { |n| n + 1 }
403
+ .on_success { |some| expect(some.unwrap).to eq(10) }
404
+ .on_failure { |none| expect(none.or(0)).to eq(0) }
405
+ ```
406
+
407
+ ```rb
408
+ order = OrderStep.new(-1)
409
+
410
+ order.call
411
+ .on_success { raise }
412
+ .on_failure { |none| expect(none.or(0)).to eq(0) }
413
+ ```
414
+
415
+ [⬆️ &nbsp;Back to Top](#table-of-contents-)
416
+
417
+ ## Development
418
+
419
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
420
+
421
+ To install this gem onto your local machine, run `bundle 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 tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
422
+
423
+ ## Contributing
424
+
425
+ Bug reports and pull requests are welcome on GitHub at https://github.com/thadeu/zx-monads. 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/thadeu/zx-monads/blob/master/CODE_OF_CONDUCT.md).
426
+
427
+
428
+ ## License
429
+
430
+ 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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+ require 'rubocop/rake_task'
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ RuboCop::RakeTask.new(:rubocop) do |t|
10
+ t.options = ['--display-cop-names']
11
+ end
12
+
13
+ task default: %i(spec rubocop)
data/bin/console ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+
5
+ require File.expand_path('lib/zx')
6
+
7
+ require 'irb'
8
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+ IFS=$'\n\t'
5
+ set -vx
6
+
7
+ bundle install
8
+
9
+ # Do any other automated setup that you need to do here
data/lib/either.rb ADDED
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Either
4
+ class ArgumentError < ArgumentError
5
+ end
6
+
7
+ class ValueError < ArgumentError
8
+ end
9
+
10
+ def self.[](...)
11
+ new(...)
12
+ end
13
+
14
+ def success?
15
+ raise NotImplementedError
16
+ end
17
+
18
+ def failure?
19
+ !success?
20
+ end
21
+
22
+ def value!
23
+ @value || raise(ValueError, 'value is empty')
24
+ end
25
+
26
+ def on_success(&block)
27
+ return self if failure?
28
+
29
+ block.call(@value)
30
+
31
+ self
32
+ end
33
+
34
+ def on_failure(&block)
35
+ return self if success?
36
+
37
+ block.call(@value)
38
+
39
+ self
40
+ end
41
+
42
+ def match(right:, left:)
43
+ case self
44
+ in Right then right.call(@value)
45
+ else left.call(@error)
46
+ end
47
+ end
48
+
49
+ class Left < Either
50
+ attr_reader :error
51
+
52
+ def initialize(error)
53
+ @error = error
54
+ end
55
+
56
+ def deconstruct
57
+ [@error]
58
+ end
59
+
60
+ def success?
61
+ false
62
+ end
63
+ end
64
+
65
+ class Right < Either
66
+ attr_reader :value
67
+
68
+ def initialize(value)
69
+ @value = value
70
+ end
71
+
72
+ def deconstruct
73
+ [@value]
74
+ end
75
+
76
+ def success?
77
+ true
78
+ end
79
+ end
80
+ end
data/lib/maybe.rb ADDED
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Maybe
4
+ attr_reader :value
5
+
6
+ IsBlank = ->(value) { value.nil? || value.to_s.strip&.empty? || !value }
7
+
8
+ def self.of(...)
9
+ new.of(...)
10
+ end
11
+
12
+ def self.[](...)
13
+ of(...)
14
+ end
15
+
16
+ def of(value)
17
+ return None.new if IsBlank[value]
18
+
19
+ Some.new(value)
20
+ rescue StandardError
21
+ None.new
22
+ end
23
+
24
+ def type
25
+ to_s.downcase.to_sym
26
+ end
27
+
28
+ def some?
29
+ type == :some
30
+ end
31
+
32
+ def none?
33
+ type == :none
34
+ end
35
+
36
+ def unwrap
37
+ @value
38
+ end
39
+
40
+ def or(value)
41
+ IsBlank[@value] ? value : @value
42
+ end
43
+
44
+ def >>(other)
45
+ self > other
46
+ end
47
+
48
+ def fmap(_)
49
+ self
50
+ end
51
+
52
+ def >(_other)
53
+ self
54
+ end
55
+
56
+ def map(arg = nil, &block)
57
+ return Maybe[block.call(@value)] if block_given?
58
+ return Maybe[arg.arity > 1 ? arg.curry.call(@value) : arg.call(@value)] if arg.respond_to?(:call)
59
+
60
+ case arg
61
+ in None then self
62
+ in Symbol | String then dig(arg)
63
+ end
64
+ rescue StandardError => e
65
+ None.new(e.message)
66
+ end
67
+ alias apply map
68
+
69
+ def map!(&block)
70
+ @value = block.call(@value)
71
+
72
+ Maybe[@value]
73
+ end
74
+
75
+ def apply!(...)
76
+ apply(...).unwrap
77
+ end
78
+
79
+ def dig(...)
80
+ Maybe[@value&.dig(...)]
81
+ end
82
+
83
+ def dig!(...)
84
+ dig(...).unwrap
85
+ end
86
+
87
+ def match(some:, none:)
88
+ case self
89
+ in Some then some.call(@value)
90
+ else none.call
91
+ end
92
+ end
93
+
94
+ def on_success(&block)
95
+ return self if none?
96
+
97
+ block.call(Some[@value])
98
+
99
+ self
100
+ end
101
+
102
+ def on_failure(&block)
103
+ return self if some?
104
+
105
+ block.call(None[@value])
106
+
107
+ self
108
+ end
109
+
110
+ class Some < Maybe
111
+ def self.[](...)
112
+ new(...)
113
+ end
114
+
115
+ def initialize(value = nil)
116
+ @value = value
117
+ end
118
+
119
+ def deconstruct
120
+ [@value]
121
+ end
122
+
123
+ def inspect
124
+ format("#<Zx::Maybe::#{self}:0x%x value=%s>", object_id, @value.inspect)
125
+ end
126
+
127
+ def to_s
128
+ 'Some'
129
+ end
130
+
131
+ def >(other)
132
+ other.respond_to?(:call) ? other.call(@value) : other
133
+ end
134
+
135
+ def fmap(&block)
136
+ Maybe[block.call(@value)]
137
+ end
138
+ end
139
+
140
+ class None < Maybe
141
+ def self.[](...)
142
+ new(...)
143
+ end
144
+
145
+ def initialize(value = nil)
146
+ @value = value
147
+ end
148
+
149
+ def deconstruct
150
+ [nil]
151
+ end
152
+
153
+ def inspect
154
+ format("#<Zx::Maybe::#{self}:0x%x value=%s>", object_id, @value.inspect)
155
+ end
156
+
157
+ def to_s
158
+ 'None'
159
+ end
160
+
161
+ def map
162
+ self
163
+ end
164
+ end
165
+ end
data/lib/zx/either.rb ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ module Zx
3
+ module Either
4
+ module Eitherable
5
+ Left = ->(*kwargs) { ::Either::Left.new(*kwargs) }
6
+ Right = ->(*kwargs) { ::Either::Right.new(*kwargs) }
7
+
8
+ def Right(*kwargs)
9
+ ::Either::Right.new(*kwargs)
10
+ end
11
+
12
+ def Left(*kwargs)
13
+ ::Either::Left.new(*kwargs)
14
+ end
15
+
16
+ def Try(default = nil, options = {})
17
+ Right yield
18
+ rescue StandardError => e
19
+ Left default || options.fetch(:or, nil)
20
+ end
21
+ end
22
+
23
+ def self.included(klass)
24
+ klass.include(Eitherable)
25
+ end
26
+ end
27
+
28
+ include Either
29
+ end
data/lib/zx/maybe.rb ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+ module Zx
3
+ module Maybe
4
+ module Maybeable
5
+ None = ->(*kwargs) { ::Maybe::None.new(*kwargs) }
6
+ Some = ->(*kwargs) { ::Maybe::Some.new(*kwargs) }
7
+ Maybe = ->(*kwargs) { ::Maybe.of(*kwargs) }
8
+
9
+ def Maybe(*kwargs)
10
+ ::Maybe.of(*kwargs)
11
+ end
12
+
13
+ def Some(*kwargs)
14
+ ::Maybe::Some.new(*kwargs)
15
+ end
16
+
17
+ def None(*kwargs)
18
+ ::Maybe::None.new(*kwargs)
19
+ end
20
+
21
+ def Try(default = nil, options = {})
22
+ Some yield
23
+ rescue StandardError => e
24
+ None[default || options.fetch(:or, nil)]
25
+ end
26
+ end
27
+
28
+ def self.included(klass)
29
+ klass.include(Maybeable)
30
+ klass.extend(Maybeable)
31
+ end
32
+ end
33
+ end
data/lib/zx/steps.rb ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zx
4
+ class Steps
5
+ include Maybe
6
+
7
+ class << self
8
+ def step(step)
9
+ steps << step
10
+ end
11
+
12
+ def steps
13
+ @steps ||= []
14
+ end
15
+ end
16
+
17
+ def call
18
+ list = self.class.steps
19
+ list.reduce(Some()) { |result, step| result >> send(step) }
20
+ end
21
+ end
22
+ end
data/lib/zx/version.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zx
4
+ VERSION = '0.0.1'
5
+ end
data/lib/zx.rb ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zx
4
+ def self.included(klass)
5
+ klass.include(Maybe::Maybeable)
6
+ klass.extend(Maybe::Maybeable)
7
+ end
8
+ end
9
+
10
+ require 'zeitwerk'
11
+
12
+ loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
13
+ loader.setup
14
+ loader.eager_load
data/zx-monads.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require 'zx/version'
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = 'zx-monads'
10
+ spec.version = Zx::VERSION
11
+ spec.authors = ['Thadeu Esteves']
12
+ spec.email = ['tadeuu@gmail.com']
13
+ spec.summary = 'FP Monads for Ruby'
14
+ spec.description = 'Use Either or Maybe to handle errors in your code'
15
+ spec.homepage = 'https://github.com/thadeu/zx-monads'
16
+ spec.license = 'MIT'
17
+
18
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
+ f.match(%r{^(test|spec|features)/})
20
+ end
21
+
22
+ spec.required_ruby_version = '>= 2.7.6'
23
+ spec.require_paths = ['lib']
24
+
25
+ spec.add_development_dependency 'bundler', '>= 1.14'
26
+ spec.add_development_dependency 'rake', '>= 10.0'
27
+ spec.add_development_dependency 'rspec', '>= 3.0'
28
+ spec.add_development_dependency 'rubocop', '>= 0.70'
29
+ spec.metadata['rubygems_mfa_required'] = 'true'
30
+ end
metadata ADDED
@@ -0,0 +1,120 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: zx-monads
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Thadeu Esteves
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-09-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '1.14'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '1.14'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0.70'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0.70'
69
+ description: Use Either or Maybe to handle errors in your code
70
+ email:
71
+ - tadeuu@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".editorconfig"
77
+ - ".github/workflows/ci.yml"
78
+ - ".gitignore"
79
+ - ".rubocop.yml"
80
+ - ".ruby-version"
81
+ - CODE_OF_CONDUCT.md
82
+ - Gemfile
83
+ - LICENSE
84
+ - README.md
85
+ - Rakefile
86
+ - bin/console
87
+ - bin/setup
88
+ - lib/either.rb
89
+ - lib/maybe.rb
90
+ - lib/zx.rb
91
+ - lib/zx/either.rb
92
+ - lib/zx/maybe.rb
93
+ - lib/zx/steps.rb
94
+ - lib/zx/version.rb
95
+ - zx-monads.gemspec
96
+ homepage: https://github.com/thadeu/zx-monads
97
+ licenses:
98
+ - MIT
99
+ metadata:
100
+ rubygems_mfa_required: 'true'
101
+ post_install_message:
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: 2.7.6
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements: []
116
+ rubygems_version: 3.1.6
117
+ signing_key:
118
+ specification_version: 4
119
+ summary: FP Monads for Ruby
120
+ test_files: []