resonad 1.2.0 → 1.3.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 +5 -5
- data/.rspec +4 -0
- data/.ruby-version +1 -0
- data/.travis.yml +17 -5
- data/CHANGELOG.md +50 -0
- data/README.md +247 -21
- data/lib/resonad.rb +78 -16
- data/lib/resonad/version.rb +1 -1
- data/resonad.gemspec +2 -3
- metadata +11 -24
- data/Rakefile +0 -6
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 | 
            -
             | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 2 | 
            +
            SHA256:
         | 
| 3 | 
            +
              metadata.gz: 2751182e01f6cf2c97cdc99d46cb716ec1e517d52680bc75c92fd0f5c1bd693f
         | 
| 4 | 
            +
              data.tar.gz: 2d1a5a21e81f7da804fa099d16a8b3a2032472e6f89112e4284942d050087293
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 6571a2f2e4e2ae98dee8c40efbf0bccf0aed09b6bc18cce47a5fb3fc83cc77c32141b31807d9b64b2842c62dfc737025ac24234dc22a1285f0e9910dae97cced
         | 
| 7 | 
            +
              data.tar.gz: a000acabecf552daebeff7f10b1e57ab44f7fba224e8e85deb25acb7904b0c97981453931f008b68547b2fdade0e3ebec8d8b868af6757f4271a4a434f075cdb
         | 
    
        data/.rspec
    CHANGED
    
    
    
        data/.ruby-version
    ADDED
    
    | @@ -0,0 +1 @@ | |
| 1 | 
            +
            2.7
         | 
    
        data/.travis.yml
    CHANGED
    
    | @@ -1,13 +1,25 @@ | |
| 1 1 | 
             
            language: ruby
         | 
| 2 | 
            +
            script: bundle exec rspec
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            # test old rubies
         | 
| 2 5 | 
             
            rvm:
         | 
| 3 | 
            -
            - 2.2. | 
| 4 | 
            -
            - 2.3. | 
| 5 | 
            -
            - 2.4. | 
| 6 | 
            -
             | 
| 6 | 
            +
              - 2.2.10
         | 
| 7 | 
            +
              - 2.3.8
         | 
| 8 | 
            +
              - 2.4.10
         | 
| 9 | 
            +
              - 2.5.8
         | 
| 10 | 
            +
              - 2.6.6
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            # test on latest ruby
         | 
| 13 | 
            +
            matrix:
         | 
| 14 | 
            +
              include:
         | 
| 15 | 
            +
                - rvm: 2.7.1
         | 
| 16 | 
            +
                  env: LATEST_RUBY=true
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            # Rubygems deployment
         | 
| 7 19 | 
             
            deploy:
         | 
| 8 20 | 
             
              provider: rubygems
         | 
| 9 21 | 
             
              on:
         | 
| 10 22 | 
             
                tags: true
         | 
| 11 | 
            -
                 | 
| 23 | 
            +
                condition: $LATEST_RUBY = true
         | 
| 12 24 | 
             
              api_key:
         | 
| 13 25 | 
             
                secure: 0+R2SaUaKOZE0U4FwHiC/0DLepizJ1anjfvFEWk5pYVkaZl6HuaSYq50g8ff4VqXBH955Y5W7tFI8gfg4ZY9jrivHyEZT9Yoz3PFFPxcqSdVbDsV12RQt7ZjzW0HbvUhFu9nlrVACfFsLt4WFG61pyhXvewghp1p4JBp2UxeTk1kyL7U+wfWsp1RpcKiHkaLBeWJ0N87j4QkhTfcWlModLqN/19ATigvfyDjrwerkTescVmQi4TzfAiwkeAiDgUB32FkwJjbX9RfIko33CsuctuDRU/HT+RWiXcGAxdHZx4dtQpP9pAiNTVxXdIq+QAvitekIah70pdCTNKrOh3tDj3kglkUK23MqU6kdeXjmUn9r4SuzHCX0sVf16UpfnwuryDDPlG1ISY+mf7BARr/wNQWuAD4MA4kWiPGMqT/0uoysbY7dt44lkO9NV7HBbqs2shqqMtmgPgDoJ+SGTXo8LvrjuL0jHB2/MoHziiqWDKLQ9PS2Fcqp/06d5u8GcSRt7dcgo2rvwnMwRdUW6iCV0zQgzrNccWn/SsJROtgzEIkuLJYO0GIOCgNBJ0euorWqQBEEPniltwh5StSgH2bL5khdPxiI+LhsL7UDhWGiNFl5tlQp8Ob98WmmQ39ZyVAGKlJoe2rdE/IaDVfysVvQV5XzYUVRn4ZKftwubo5Zvw=
         | 
    
        data/CHANGELOG.md
    ADDED
    
    | @@ -0,0 +1,50 @@ | |
| 1 | 
            +
            # Changelog
         | 
| 2 | 
            +
            All notable changes to this project will be documented in this file.
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
         | 
| 5 | 
            +
            and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            ## [1.3.0] - 2020-07-12
         | 
| 8 | 
            +
            ### Added
         | 
| 9 | 
            +
            - more documentation
         | 
| 10 | 
            +
            - support for Ruby 2.7 pattern matching
         | 
| 11 | 
            +
            - `#otherwise` alias for `#or_else`
         | 
| 12 | 
            +
            - `#map_value` alias for `#map`
         | 
| 13 | 
            +
            - alternate constructor methods
         | 
| 14 | 
            +
            - aliases for `#on_success` and `#on_failure`
         | 
| 15 | 
            +
            - `Success` and `Failure` class constants to `Resonad::Mixin`
         | 
| 16 | 
            +
            - `Resonad::PublicMixin` for people who want the previous behaviour of
         | 
| 17 | 
            +
              `Resonad::Mixin`
         | 
| 18 | 
            +
            ### Changed
         | 
| 19 | 
            +
            - The methods provided by `Resonad::Mixin` are now private. Replace with
         | 
| 20 | 
            +
              `Resonad::PublicMixin` to revert them back to being public.
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            ## [1.2.0] - 2017-05-22
         | 
| 23 | 
            +
            ### Added
         | 
| 24 | 
            +
            - `Resonad.Success()` and `Resonad.Failure()` arguments are optional, and
         | 
| 25 | 
            +
              default to nil.
         | 
| 26 | 
            +
            - `#successful?` and `#ok?` as new aliases for `#success?`
         | 
| 27 | 
            +
            - `#failed?` and `#bad?` as new aliases for `#failure?`
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            ## [1.1.1] - 2017-05-09
         | 
| 30 | 
            +
            ### Fixed
         | 
| 31 | 
            +
            - Aliased methods `#and_then` and `#or_else` were not aliased properly.
         | 
| 32 | 
            +
             | 
| 33 | 
            +
            ## [1.1.0] - 2017-05-01
         | 
| 34 | 
            +
            ### Added
         | 
| 35 | 
            +
            - `Resonad::Mixin`
         | 
| 36 | 
            +
            ### Changed
         | 
| 37 | 
            +
            - `Resonad` is now a class. `Resonad::Success` and `Resonad::Failure` now
         | 
| 38 | 
            +
              inherit from `Resonad`.
         | 
| 39 | 
            +
             | 
| 40 | 
            +
            ## [1.0.2] - 2017-04-22
         | 
| 41 | 
            +
            ### Fixed
         | 
| 42 | 
            +
            - Typo in class names
         | 
| 43 | 
            +
             | 
| 44 | 
            +
            ## [1.0.1] - 2017-04-22
         | 
| 45 | 
            +
            ### Changed
         | 
| 46 | 
            +
            - Nothing (bumped to force a deploy)
         | 
| 47 | 
            +
             | 
| 48 | 
            +
            ## [1.0.0] - 2017-04-22
         | 
| 49 | 
            +
            ### Added
         | 
| 50 | 
            +
            - Everything (initial release)
         | 
    
        data/README.md
    CHANGED
    
    | @@ -1,15 +1,26 @@ | |
| 1 | 
            +
            [](https://travis-ci.org/tomdalling/resonad)
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            # Resonad
         | 
| 2 4 |  | 
| 3 | 
            -
             | 
| 5 | 
            +
            Lightweight, functional "result" objects that can be used instead of exceptions.
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            Read: [Result Objects - Errors Without Exceptions](https://www.rubypigeon.com/posts/result-objects-errors-without-exceptions/)
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            ## Typical Usage Example
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            Assuming each method returns a `Resonad` (Ruby 2.7 syntax):
         | 
| 4 12 |  | 
| 5 13 | 
             
            ```ruby
         | 
| 6 14 | 
             
            find_widget(widget_id)
         | 
| 7 | 
            -
              .and_then {  | 
| 8 | 
            -
              .on_success {  | 
| 9 | 
            -
              .on_failure {  | 
| 15 | 
            +
              .and_then { update_widget(_1) }
         | 
| 16 | 
            +
              .on_success { logger.info("Updated #{_1}" }
         | 
| 17 | 
            +
              .on_failure { logger.warn("Widget update failed because #{_1}") }
         | 
| 10 18 | 
             
            ```
         | 
| 11 19 |  | 
| 12 | 
            -
            Success  | 
| 20 | 
            +
            ## Success Type
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            A value that represents success. Wraps a `value` that can be any arbitrary
         | 
| 23 | 
            +
            object.
         | 
| 13 24 |  | 
| 14 25 | 
             
            ```ruby
         | 
| 15 26 | 
             
            result = Resonad.Success(5)
         | 
| @@ -19,7 +30,10 @@ result.value #=> 5 | |
| 19 30 | 
             
            result.error #=> raises an exception
         | 
| 20 31 | 
             
            ```
         | 
| 21 32 |  | 
| 22 | 
            -
            Failure  | 
| 33 | 
            +
            ## Failure Type
         | 
| 34 | 
            +
             | 
| 35 | 
            +
            A value that represents a failure. Wraps an `error` that can be any arbitrary
         | 
| 36 | 
            +
            object.
         | 
| 23 37 |  | 
| 24 38 | 
             
            ```ruby
         | 
| 25 39 | 
             
            result = Resonad.Failure(:buzz)
         | 
| @@ -29,39 +43,199 @@ result.value #=> raises an exception | |
| 29 43 | 
             
            result.error #=> :buzz
         | 
| 30 44 | 
             
            ```
         | 
| 31 45 |  | 
| 32 | 
            -
            Mapping | 
| 46 | 
            +
            ## Mapping
         | 
| 47 | 
            +
             | 
| 48 | 
            +
            Non-destructive update for the `value` of a `Success` object. Does nothing to
         | 
| 49 | 
            +
            `Failure` objects.
         | 
| 50 | 
            +
             | 
| 51 | 
            +
            The block takes the `value` as an argument, and returns the new `value`.
         | 
| 33 52 |  | 
| 34 53 | 
             
            ```ruby
         | 
| 35 54 | 
             
            result = Resonad.Success(5)
         | 
| 36 | 
            -
              .map {  | 
| 37 | 
            -
              .map {  | 
| 38 | 
            -
              .map {  | 
| 55 | 
            +
              .map { _1 + 1 }  # 5 + 1 -> 6
         | 
| 56 | 
            +
              .map { _1 + 1 }  # 6 + 1 -> 7
         | 
| 57 | 
            +
              .map { _1 + 1 }  # 7 + 1 -> 8
         | 
| 39 58 | 
             
            result.success? #=> true
         | 
| 40 59 | 
             
            result.value #=> 8
         | 
| 41 60 |  | 
| 42 61 | 
             
            result = Resonad.Failure(:buzz)
         | 
| 43 | 
            -
              .map {  | 
| 44 | 
            -
              .map {  | 
| 45 | 
            -
              .map {  | 
| 62 | 
            +
              .map { _1 + 1 }  # not run
         | 
| 63 | 
            +
              .map { _1 + 1 }  # not run
         | 
| 64 | 
            +
              .map { _1 + 1 }  # not run
         | 
| 46 65 | 
             
            result.success? #=> false
         | 
| 47 66 | 
             
            result.error #=> :buzz
         | 
| 48 67 | 
             
            ```
         | 
| 49 68 |  | 
| 50 | 
            -
             | 
| 69 | 
            +
             | 
| 70 | 
            +
            ## Aliases
         | 
| 71 | 
            +
             | 
| 72 | 
            +
            Lots of the Resonad methods have aliases.
         | 
| 73 | 
            +
             | 
| 74 | 
            +
            Personally, I can never remember if it's `success?` or `successful?` or `ok?`,
         | 
| 75 | 
            +
            so let's just do it the Ruby way and allow all of them.
         | 
| 76 | 
            +
             | 
| 77 | 
            +
            ```ruby
         | 
| 78 | 
            +
            # >>> object creation aliases (same for failure) <<<
         | 
| 79 | 
            +
            result = Resonad.Success(5)
         | 
| 80 | 
            +
            result = Resonad.success(5)  # lowercase, for those offended by capital letters
         | 
| 81 | 
            +
            result = Resonad::Success[5]  # class constructor method
         | 
| 82 | 
            +
             | 
| 83 | 
            +
            # >>> success aliases <<<
         | 
| 84 | 
            +
            result.success?  #=> true
         | 
| 85 | 
            +
            result.successful?  #=> true
         | 
| 86 | 
            +
            result.ok?  #=> true
         | 
| 87 | 
            +
             | 
| 88 | 
            +
            # >>> failure aliases <<<
         | 
| 89 | 
            +
            result.failure?  #=> false
         | 
| 90 | 
            +
            result.failed?  #=> false
         | 
| 91 | 
            +
            result.bad?  #=> false
         | 
| 92 | 
            +
             | 
| 93 | 
            +
            # >>> mapping aliases <<<
         | 
| 94 | 
            +
            result.map { _1 + 1 }  #=> Success(6)
         | 
| 95 | 
            +
            result.map_value { _1 + 1 }  #=> Success(6)
         | 
| 96 | 
            +
             | 
| 97 | 
            +
            # >>> flat mapping aliases <<<
         | 
| 98 | 
            +
            result.and_then { Resonad.Success(_1 + 1) }  #=> Success(6)
         | 
| 99 | 
            +
            result.flat_map { Resonad.Success(_1 + 1) }  #=> Success(6)
         | 
| 100 | 
            +
             | 
| 101 | 
            +
            # >>> error flat mapping aliases <<<
         | 
| 102 | 
            +
            result.or_else { Resonad.Failure(_1 + 1) }  # not run
         | 
| 103 | 
            +
            result.otherwise { Resonad.Failure(_1 + 1) }  # not run
         | 
| 104 | 
            +
            result.flat_map_error { Resonad.Success(_1 + 1) }  # not run
         | 
| 105 | 
            +
             | 
| 106 | 
            +
            # >>> conditional tap aliases <<<
         | 
| 107 | 
            +
            # pattern: (on_|if_|when_)(success_alias|failure_alias)
         | 
| 108 | 
            +
            result.on_success { puts "hi" }  # outputs "hi"
         | 
| 109 | 
            +
            result.if_success { puts "hi" }  # outputs "hi"
         | 
| 110 | 
            +
            result.when_success { puts "hi" }  # outputs "hi"
         | 
| 111 | 
            +
            result.on_ok { puts "hi" }  # outputs "hi"
         | 
| 112 | 
            +
            result.if_ok { puts "hi" }  # outputs "hi"
         | 
| 113 | 
            +
            result.when_ok { puts "hi" }  # outputs "hi"
         | 
| 114 | 
            +
            result.on_successful { puts "hi" }  # outputs "hi"
         | 
| 115 | 
            +
            result.if_successful { puts "hi" }  # outputs "hi"
         | 
| 116 | 
            +
            result.when_successful { puts "hi" }  # outputs "hi"
         | 
| 117 | 
            +
            result.on_failure { puts "hi" }  # not run
         | 
| 118 | 
            +
            result.if_failure { puts "hi" }  # not run
         | 
| 119 | 
            +
            result.when_failure { puts "hi" }  # not run
         | 
| 120 | 
            +
            result.on_bad { puts "hi" }  # not run
         | 
| 121 | 
            +
            result.if_bad { puts "hi" }  # not run
         | 
| 122 | 
            +
            result.when_bad { puts "hi" }  # not run
         | 
| 123 | 
            +
            result.on_failed { puts "hi" }  # not run
         | 
| 124 | 
            +
            result.if_failed { puts "hi" }  # not run
         | 
| 125 | 
            +
            result.when_failed { puts "hi" }  # not run
         | 
| 126 | 
            +
            ```
         | 
| 127 | 
            +
             | 
| 128 | 
            +
             | 
| 129 | 
            +
            ## Flat Mapping (a.k.a. `and_then`)
         | 
| 130 | 
            +
             | 
| 131 | 
            +
            Non-destructive update for a `Success` object. Either turns it into another
         | 
| 132 | 
            +
            `Success` (can have a different `value`), or turns it into a `Failure`. Does
         | 
| 133 | 
            +
            nothing to `Failure` objects.
         | 
| 134 | 
            +
             | 
| 135 | 
            +
            The block takes the `value` as an argument, and returns a `Resonad` (either
         | 
| 136 | 
            +
            `Success` or `Failure`). 
         | 
| 51 137 |  | 
| 52 138 | 
             
            ```ruby
         | 
| 53 139 | 
             
            result = Resonad.Success(5)
         | 
| 54 | 
            -
              .and_then {  | 
| 55 | 
            -
              .and_then {  | 
| 56 | 
            -
              .and_then {  | 
| 140 | 
            +
              .and_then { Resonad.Success(_1 + 1) }  # updates to Success(6)
         | 
| 141 | 
            +
              .and_then { Resonad.Failure("buzz #{_1}") }  # updates to Failure("buzz 6")
         | 
| 142 | 
            +
              .and_then { Resonad.Success(_1 + 1) }  # not run (because it's a failure)
         | 
| 57 143 | 
             
              .error #=> "buzz 6"
         | 
| 58 144 |  | 
| 59 | 
            -
            #  | 
| 60 | 
            -
            result
         | 
| 61 | 
            -
              .flat_map { |i| Resonad.Success(i + 1) }
         | 
| 145 | 
            +
            # also has a less-friendly but more-technically-descriptive alias: `flat_map`
         | 
| 146 | 
            +
            result.flat_map { Resonad.Success(_1 + 1) }
         | 
| 62 147 | 
             
            ```
         | 
| 63 148 |  | 
| 64 | 
            -
             | 
| 149 | 
            +
            This is different to Ruby's `#then` method added in 2.6. The block for `#then`
         | 
| 150 | 
            +
            would take a Resonad argument, regardless of whether it's `Success` or
         | 
| 151 | 
            +
            `Failure`. The block for `#and_then` takes a _`Success` object's value_, and
         | 
| 152 | 
            +
            only runs on `Success` objects, not `Failure` objects.
         | 
| 153 | 
            +
             | 
| 154 | 
            +
             | 
| 155 | 
            +
            ## Error Mapping
         | 
| 156 | 
            +
             | 
| 157 | 
            +
            Just as `Success` objects can be chained with `#map` and `#and_then`, so can
         | 
| 158 | 
            +
            `Failure` objects with `#map_error` and `#or_else`. This isn't used as often,
         | 
| 159 | 
            +
            but has a few use cases such as:
         | 
| 160 | 
            +
             | 
| 161 | 
            +
            ```ruby
         | 
| 162 | 
            +
            # Use Case: convert an error value into another error value
         | 
| 163 | 
            +
            make_http_request  #=> Failure(404)
         | 
| 164 | 
            +
              .map_error { |status_code| "HTTP #{status_code} Error" }
         | 
| 165 | 
            +
              .error #=> "HTTP 404 Error"
         | 
| 166 | 
            +
             | 
| 167 | 
            +
            # Use Case: recover from error, turning into Success
         | 
| 168 | 
            +
            load_config_file  #=> Failure(:config_file_missing)
         | 
| 169 | 
            +
              .or_else { try_recover_from(_1) }
         | 
| 170 | 
            +
              .value  #=> { :setting => 'default' }
         | 
| 171 | 
            +
             | 
| 172 | 
            +
            def try_recover_from(error)
         | 
| 173 | 
            +
              if error == :config_file_missing
         | 
| 174 | 
            +
                Resonad.Success({ setting: 'default' })
         | 
| 175 | 
            +
              else
         | 
| 176 | 
            +
                Resonad.Failure(error)
         | 
| 177 | 
            +
              end
         | 
| 178 | 
            +
            end
         | 
| 179 | 
            +
            ```
         | 
| 180 | 
            +
             | 
| 181 | 
            +
             | 
| 182 | 
            +
            ## Conditional Tap
         | 
| 183 | 
            +
             | 
| 184 | 
            +
            If you're in the middle of a long chain of methods, and you don't want to break
         | 
| 185 | 
            +
            the chain to run some kind of side effect, you can use the `#on_success` and
         | 
| 186 | 
            +
            `#on_failure` methods. These run an arbitrary block code, but do not affect the
         | 
| 187 | 
            +
            result object in any way. They work like Ruby's `#tap` method, but `Failure`
         | 
| 188 | 
            +
            objects will not run `on_success` blocks, and `Success` objects will not run
         | 
| 189 | 
            +
            `on_failure` blocks.
         | 
| 190 | 
            +
             | 
| 191 | 
            +
            ```ruby
         | 
| 192 | 
            +
            do_step_1
         | 
| 193 | 
            +
              .and_then { do_step_2(_1) }
         | 
| 194 | 
            +
              .and_then { do_step_3(_1) }
         | 
| 195 | 
            +
              .on_success { puts "Successful step 3 result: #{_1}" }
         | 
| 196 | 
            +
              .and_then { do_step_4(_1) }
         | 
| 197 | 
            +
              .and_then { do_step_5(_1) }
         | 
| 198 | 
            +
              .on_failure { puts "Uh oh! Step 5 failed: #{_1} }
         | 
| 199 | 
            +
              .and_then { do_step_6(_1) }
         | 
| 200 | 
            +
              .and_then { do_step_7(_1) }
         | 
| 201 | 
            +
            ```
         | 
| 202 | 
            +
             | 
| 203 | 
            +
            There are lots of aliases for these methods. See the "Aliases" section above.
         | 
| 204 | 
            +
             | 
| 205 | 
            +
             | 
| 206 | 
            +
            ## Pattern Matching Support
         | 
| 207 | 
            +
             | 
| 208 | 
            +
            If you are using Ruby 2.7 or later, you can pattern match on Resonad objects.
         | 
| 209 | 
            +
            For example:
         | 
| 210 | 
            +
             | 
| 211 | 
            +
            ```ruby
         | 
| 212 | 
            +
            case result
         | 
| 213 | 
            +
            in { value: }  # match any Success
         | 
| 214 | 
            +
              puts value
         | 
| 215 | 
            +
            in { error: :not_found } # match Failure(:not_found)
         | 
| 216 | 
            +
              puts "Thing not found"
         | 
| 217 | 
            +
            in { error: String => msg } # match any Failure with a String error
         | 
| 218 | 
            +
              puts "Failed to fetch thing because #{msg}"
         | 
| 219 | 
            +
            in { error: } # match any Failure
         | 
| 220 | 
            +
              raise "Unhandled error: #{error.inspect}"
         | 
| 221 | 
            +
            end
         | 
| 222 | 
            +
            ```
         | 
| 223 | 
            +
             | 
| 224 | 
            +
            `Resonad.Success(5)` deconstructs to:
         | 
| 225 | 
            +
             | 
| 226 | 
            +
             - Hash: `{ value: 5 }`
         | 
| 227 | 
            +
             - Array: `[:success, 5]`
         | 
| 228 | 
            +
             | 
| 229 | 
            +
            And `Resonad.Failure('yikes')` deconstructs to:
         | 
| 230 | 
            +
             | 
| 231 | 
            +
             - Hash: `{ error: 'yikes' }`
         | 
| 232 | 
            +
             - Array: `[:failure, 'yikes']`
         | 
| 233 | 
            +
             | 
| 234 | 
            +
             | 
| 235 | 
            +
            ## Automatic Exception Rescuing
         | 
| 236 | 
            +
             | 
| 237 | 
            +
            If no exception is raised, wraps the block's return value in `Success`. If an
         | 
| 238 | 
            +
            exception is raised, wraps the exception object in `Failure`.
         | 
| 65 239 |  | 
| 66 240 | 
             
            ```ruby
         | 
| 67 241 | 
             
            def try_divide(top, bottom)
         | 
| @@ -76,3 +250,55 @@ nope = try_divide(6, 0) | |
| 76 250 | 
             
            nope.success? #=> false
         | 
| 77 251 | 
             
            node.error #=> #<ZeroDivisionError: ZeroDivisionError>
         | 
| 78 252 | 
             
            ```
         | 
| 253 | 
            +
             | 
| 254 | 
            +
             | 
| 255 | 
            +
            ## Convenience Mixin
         | 
| 256 | 
            +
             | 
| 257 | 
            +
            If you're tired of typing "Resonad." in front of everything, you can include
         | 
| 258 | 
            +
            the `Resonad::Mixin` mixin.
         | 
| 259 | 
            +
             | 
| 260 | 
            +
            ```ruby
         | 
| 261 | 
            +
            class RobotFortuneTeller
         | 
| 262 | 
            +
              include Resonad::Mixin
         | 
| 263 | 
            +
             | 
| 264 | 
            +
              def next_fortune
         | 
| 265 | 
            +
                case rand(0..100)
         | 
| 266 | 
            +
                when 0..70
         | 
| 267 | 
            +
                  # title-case constructor from Resonad::Mixin
         | 
| 268 | 
            +
                  Success("today is auspicious")
         | 
| 269 | 
            +
                when 71..95
         | 
| 270 | 
            +
                  # lower-case constructor from Resonad::Mixin
         | 
| 271 | 
            +
                  success("ill omens abound")
         | 
| 272 | 
            +
                else
         | 
| 273 | 
            +
                  # direct access to classes from Resonad::Mixin
         | 
| 274 | 
            +
                  Failure.new("MALFUNCTION")
         | 
| 275 | 
            +
                end
         | 
| 276 | 
            +
              end
         | 
| 277 | 
            +
            end
         | 
| 278 | 
            +
            ```
         | 
| 279 | 
            +
             | 
| 280 | 
            +
            Note that `Resonad::Mixin` provides private methods, and private constants, so
         | 
| 281 | 
            +
            you can't do this:
         | 
| 282 | 
            +
             | 
| 283 | 
            +
            ```ruby
         | 
| 284 | 
            +
            RobotFortuneTeller.new.Success(5)
         | 
| 285 | 
            +
              #=> NoMethodError: private method `Success' called for #<RobotFortuneTeller:0x00007fe7fc0ff0c8>
         | 
| 286 | 
            +
             | 
| 287 | 
            +
            RobotFortuneTeller::Success
         | 
| 288 | 
            +
              #=> NameError: private constant Resonad::Mixin::Success referenced
         | 
| 289 | 
            +
            ```
         | 
| 290 | 
            +
             | 
| 291 | 
            +
            If you want the methods/constants to be public, then use `Resonad::PublicMixin`
         | 
| 292 | 
            +
            instead.
         | 
| 293 | 
            +
             | 
| 294 | 
            +
             | 
| 295 | 
            +
            ## Contributing
         | 
| 296 | 
            +
             | 
| 297 | 
            +
            Bug reports and pull requests are welcome on GitHub at:
         | 
| 298 | 
            +
            https://github.com/tomdalling/resonad
         | 
| 299 | 
            +
             | 
| 300 | 
            +
            I'm open to PRs that make the gem more convenient, or that makes calling code
         | 
| 301 | 
            +
            read better.
         | 
| 302 | 
            +
             | 
| 303 | 
            +
            Make sure your PR has full test coverage.
         | 
| 304 | 
            +
             | 
    
        data/lib/resonad.rb
    CHANGED
    
    | @@ -2,28 +2,17 @@ class Resonad | |
| 2 2 | 
             
              class NonExistentError < StandardError; end
         | 
| 3 3 | 
             
              class NonExistentValue < StandardError; end
         | 
| 4 4 |  | 
| 5 | 
            -
               | 
| 6 | 
            -
                 | 
| 5 | 
            +
              class Success < Resonad
         | 
| 6 | 
            +
                attr_accessor :value
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                def self.[](value = nil)
         | 
| 7 9 | 
             
                  if nil == value
         | 
| 8 10 | 
             
                    NIL_SUCCESS
         | 
| 9 11 | 
             
                  else
         | 
| 10 | 
            -
                     | 
| 12 | 
            +
                    new(value)
         | 
| 11 13 | 
             
                  end
         | 
| 12 14 | 
             
                end
         | 
| 13 15 |  | 
| 14 | 
            -
                def Failure(error=nil)
         | 
| 15 | 
            -
                  if nil == error
         | 
| 16 | 
            -
                    NIL_FAILURE
         | 
| 17 | 
            -
                  else
         | 
| 18 | 
            -
                    Failure.new(error)
         | 
| 19 | 
            -
                  end
         | 
| 20 | 
            -
                end
         | 
| 21 | 
            -
              end
         | 
| 22 | 
            -
              extend Mixin
         | 
| 23 | 
            -
             | 
| 24 | 
            -
              class Success < Resonad
         | 
| 25 | 
            -
                attr_accessor :value
         | 
| 26 | 
            -
             | 
| 27 16 | 
             
                def initialize(value)
         | 
| 28 17 | 
             
                  @value = value
         | 
| 29 18 | 
             
                  freeze
         | 
| @@ -66,11 +55,27 @@ class Resonad | |
| 66 55 | 
             
                def flat_map_error
         | 
| 67 56 | 
             
                  self
         | 
| 68 57 | 
             
                end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                def deconstruct
         | 
| 60 | 
            +
                  [:success, value]
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                def deconstruct_keys(_)
         | 
| 64 | 
            +
                  { value: value }
         | 
| 65 | 
            +
                end
         | 
| 69 66 | 
             
              end
         | 
| 70 67 |  | 
| 71 68 | 
             
              class Failure < Resonad
         | 
| 72 69 | 
             
                attr_accessor :error
         | 
| 73 70 |  | 
| 71 | 
            +
                def self.[](error = nil)
         | 
| 72 | 
            +
                  if nil == error
         | 
| 73 | 
            +
                    NIL_FAILURE
         | 
| 74 | 
            +
                  else
         | 
| 75 | 
            +
                    new(error)
         | 
| 76 | 
            +
                  end
         | 
| 77 | 
            +
                end
         | 
| 78 | 
            +
             | 
| 74 79 | 
             
                def initialize(error)
         | 
| 75 80 | 
             
                  @error = error
         | 
| 76 81 | 
             
                  freeze
         | 
| @@ -113,8 +118,35 @@ class Resonad | |
| 113 118 | 
             
                def flat_map_error
         | 
| 114 119 | 
             
                  yield error
         | 
| 115 120 | 
             
                end
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                def deconstruct
         | 
| 123 | 
            +
                  [:failure, error]
         | 
| 124 | 
            +
                end
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                def deconstruct_keys(_)
         | 
| 127 | 
            +
                  { error: error }
         | 
| 128 | 
            +
                end
         | 
| 116 129 | 
             
              end
         | 
| 117 130 |  | 
| 131 | 
            +
              module PublicMixin
         | 
| 132 | 
            +
                Success = ::Resonad::Success
         | 
| 133 | 
            +
                Failure = ::Resonad::Failure
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                def Success(*args); Success[*args]; end
         | 
| 136 | 
            +
                def success(*args); Success[*args]; end
         | 
| 137 | 
            +
                def Failure(*args); Failure[*args]; end
         | 
| 138 | 
            +
                def failure(*args); Failure[*args]; end
         | 
| 139 | 
            +
              end
         | 
| 140 | 
            +
             | 
| 141 | 
            +
              Mixin = PublicMixin.dup.tap do |mixin|
         | 
| 142 | 
            +
                mixin.module_eval do
         | 
| 143 | 
            +
                  private(*public_instance_methods)
         | 
| 144 | 
            +
                  private_constant(*constants)
         | 
| 145 | 
            +
                end
         | 
| 146 | 
            +
              end
         | 
| 147 | 
            +
             | 
| 148 | 
            +
              extend PublicMixin
         | 
| 149 | 
            +
             | 
| 118 150 | 
             
              def self.rescuing_from(*exception_classes)
         | 
| 119 151 | 
             
                Success(yield)
         | 
| 120 152 | 
             
              rescue Exception => e
         | 
| @@ -143,6 +175,11 @@ class Resonad | |
| 143 175 | 
             
              def failed?; failure?; end
         | 
| 144 176 | 
             
              def bad?; failure?; end
         | 
| 145 177 |  | 
| 178 | 
            +
              def map(&block)
         | 
| 179 | 
            +
                raise NotImplementedError, "should be implemented in subclass"
         | 
| 180 | 
            +
              end
         | 
| 181 | 
            +
              def map_value(&block); map(&block); end
         | 
| 182 | 
            +
             | 
| 146 183 | 
             
              def flat_map
         | 
| 147 184 | 
             
                raise NotImplementedError, "should be implemented in subclass"
         | 
| 148 185 | 
             
              end
         | 
| @@ -152,6 +189,31 @@ class Resonad | |
| 152 189 | 
             
                raise NotImplementedError, "should be implemented in subclass"
         | 
| 153 190 | 
             
              end
         | 
| 154 191 | 
             
              def or_else(&block); flat_map_error(&block); end
         | 
| 192 | 
            +
              def otherwise(&block); flat_map_error(&block); end
         | 
| 193 | 
            +
             | 
| 194 | 
            +
              def on_success(&block)
         | 
| 195 | 
            +
                raise NotImplementedError, "should be implemented in subclass"
         | 
| 196 | 
            +
              end
         | 
| 197 | 
            +
              def if_success(&block); on_success(&block); end
         | 
| 198 | 
            +
              def when_success(&block); on_success(&block); end
         | 
| 199 | 
            +
              def on_ok(&block); on_success(&block); end
         | 
| 200 | 
            +
              def if_ok(&block); on_success(&block); end
         | 
| 201 | 
            +
              def when_ok(&block); on_success(&block); end
         | 
| 202 | 
            +
              def on_successful(&block); on_success(&block); end
         | 
| 203 | 
            +
              def if_successful(&block); on_success(&block); end
         | 
| 204 | 
            +
              def when_successful(&block); on_success(&block); end
         | 
| 205 | 
            +
             | 
| 206 | 
            +
              def on_failure(&block)
         | 
| 207 | 
            +
                raise NotImplementedError, "should be implemented in subclass"
         | 
| 208 | 
            +
              end
         | 
| 209 | 
            +
              def if_failure(&block); on_failure(&block); end
         | 
| 210 | 
            +
              def when_failure(&block); on_failure(&block); end
         | 
| 211 | 
            +
              def on_bad(&block); on_failure(&block); end
         | 
| 212 | 
            +
              def if_bad(&block); on_failure(&block); end
         | 
| 213 | 
            +
              def when_bad(&block); on_failure(&block); end
         | 
| 214 | 
            +
              def on_failed(&block); on_failure(&block); end
         | 
| 215 | 
            +
              def if_failed(&block); on_failure(&block); end
         | 
| 216 | 
            +
              def when_failed(&block); on_failure(&block); end
         | 
| 155 217 |  | 
| 156 218 | 
             
              NIL_SUCCESS = Success.new(nil)
         | 
| 157 219 | 
             
              NIL_FAILURE = Failure.new(nil)
         | 
    
        data/lib/resonad/version.rb
    CHANGED
    
    
    
        data/resonad.gemspec
    CHANGED
    
    | @@ -20,8 +20,7 @@ Gem::Specification.new do |spec| | |
| 20 20 | 
             
              spec.executables   = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
         | 
| 21 21 | 
             
              spec.require_paths = ["lib"]
         | 
| 22 22 |  | 
| 23 | 
            -
              spec.add_development_dependency "bundler", " | 
| 24 | 
            -
              spec.add_development_dependency "rake", "~> 10.0"
         | 
| 23 | 
            +
              spec.add_development_dependency "bundler", ">= 1.15"
         | 
| 25 24 | 
             
              spec.add_development_dependency "rspec", "~> 3.0"
         | 
| 26 | 
            -
              spec.add_development_dependency "gem-release", "~>  | 
| 25 | 
            +
              spec.add_development_dependency "gem-release", "~> 2.1"
         | 
| 27 26 | 
             
            end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,43 +1,29 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: resonad
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 1. | 
| 4 | 
            +
              version: 1.3.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Tom Dalling
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: exe
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date:  | 
| 11 | 
            +
            date: 2020-07-12 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: bundler
         | 
| 15 15 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| 16 16 | 
             
                requirements:
         | 
| 17 | 
            -
                - - " | 
| 17 | 
            +
                - - ">="
         | 
| 18 18 | 
             
                  - !ruby/object:Gem::Version
         | 
| 19 | 
            -
                    version: '1. | 
| 19 | 
            +
                    version: '1.15'
         | 
| 20 20 | 
             
              type: :development
         | 
| 21 21 | 
             
              prerelease: false
         | 
| 22 22 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 23 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 | 
            -
                - - "~>"
         | 
| 24 | 
            +
                - - ">="
         | 
| 39 25 | 
             
                  - !ruby/object:Gem::Version
         | 
| 40 | 
            -
                    version: ' | 
| 26 | 
            +
                    version: '1.15'
         | 
| 41 27 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 42 28 | 
             
              name: rspec
         | 
| 43 29 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -58,14 +44,14 @@ dependencies: | |
| 58 44 | 
             
                requirements:
         | 
| 59 45 | 
             
                - - "~>"
         | 
| 60 46 | 
             
                  - !ruby/object:Gem::Version
         | 
| 61 | 
            -
                    version: ' | 
| 47 | 
            +
                    version: '2.1'
         | 
| 62 48 | 
             
              type: :development
         | 
| 63 49 | 
             
              prerelease: false
         | 
| 64 50 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 65 51 | 
             
                requirements:
         | 
| 66 52 | 
             
                - - "~>"
         | 
| 67 53 | 
             
                  - !ruby/object:Gem::Version
         | 
| 68 | 
            -
                    version: ' | 
| 54 | 
            +
                    version: '2.1'
         | 
| 69 55 | 
             
            description: Objects that represent success or failure
         | 
| 70 56 | 
             
            email:
         | 
| 71 57 | 
             
            - tom@tomdalling.com
         | 
| @@ -75,12 +61,13 @@ extra_rdoc_files: [] | |
| 75 61 | 
             
            files:
         | 
| 76 62 | 
             
            - ".gitignore"
         | 
| 77 63 | 
             
            - ".rspec"
         | 
| 64 | 
            +
            - ".ruby-version"
         | 
| 78 65 | 
             
            - ".travis.yml"
         | 
| 66 | 
            +
            - CHANGELOG.md
         | 
| 79 67 | 
             
            - CODE_OF_CONDUCT.md
         | 
| 80 68 | 
             
            - Gemfile
         | 
| 81 69 | 
             
            - LICENSE.txt
         | 
| 82 70 | 
             
            - README.md
         | 
| 83 | 
            -
            - Rakefile
         | 
| 84 71 | 
             
            - bin/console
         | 
| 85 72 | 
             
            - bin/setup
         | 
| 86 73 | 
             
            - lib/resonad.rb
         | 
| @@ -106,7 +93,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement | |
| 106 93 | 
             
                  version: '0'
         | 
| 107 94 | 
             
            requirements: []
         | 
| 108 95 | 
             
            rubyforge_project: 
         | 
| 109 | 
            -
            rubygems_version: 2. | 
| 96 | 
            +
            rubygems_version: 2.7.7
         | 
| 110 97 | 
             
            signing_key: 
         | 
| 111 98 | 
             
            specification_version: 4
         | 
| 112 99 | 
             
            summary: Objects that represent success or failure
         |