class ReSorcery::Decoder

Test that an object satisfies some property

A ‘Decoder` represents a piece of logic for verifying some property. A simple example would be a `Decoder` that verifies that an object `is_a?(String)`. A `ReSorcery::Result` is used to represent the result of the logic. This can be created like:

is_string =
  Decoder.new { |n| n.is_a?(String) ? ok(n) : err("Expected String, got #{n.class}") }

And then used like:

is_string.test("I'm a string") #=> ok("I'm a string")
is_string.test(:symbol) #=> err("Expected String, got Symbol")

Because returning the original object wrapped in ‘ok` is the common result when a `Decoder` passes, a shorthand in the initializer is to return `true`, and `Decoder` will wrap the object for you.

is_string =
  Decoder.new { |n| n.is_a?(String) || err("Expected String, got #{n.class}") }

A similar shorthand: Since the alternative is always something wrapped in ‘err`, if anything but `true` or a `Result` is returned, it will be wrapped in `err`.

is_string =
  Decoder.new { |n| n.is_a?(String) || "Expected String, got #{n.class}" }

All three of these implementations of ‘is_string` are equivalent.

Note that this library has strong opinions against using ‘nil`, so `nil` will never pass `Decoder#test`.

Public Class Methods

new(&block) click to toggle source
# File lib/re_sorcery/decoder.rb, line 43
def initialize(&block)
  @block = block
end

Public Instance Methods

and(&block) click to toggle source

Chain decoders like ‘and_then`, but always with a specific decoder

# File lib/re_sorcery/decoder.rb, line 108
def and(&block)
  and_then { Decoder.new(&block) }
end
and_then(decoder = nil, &block) click to toggle source

Chain decoders

The second decoder can be chosen based on the (successful) result of the first decoder.

The block must return a ‘Decoder`.

# File lib/re_sorcery/decoder.rb, line 81
def and_then(decoder = nil, &block)
  ArgCheck['decoder', decoder, Decoder] unless decoder.nil?

  Decoder.new do |unknown|
    test(unknown).and_then do |value|
      # Don't try to re-assign `decoder` here, because it's captured from outside the Decoder
      (decoder || ArgCheck['block.call(value)', block.call(value), Decoder]).test(unknown)
    end
  end
end
assign(key, other) click to toggle source

Chain decoders like and_then, but use the chain to build an object

# File lib/re_sorcery/decoder.rb, line 113
def assign(key, other)
  ArgCheck['key', key, Symbol]
  ArgCheck['other', other, Proc, Decoder]
  other = ->(_) { other.call } if other.is_a?(Proc) && other.arity.zero?

  and_then do |a|
    ArgCheck['decoded value', a, Hash]

    decoder = other.is_a?(Decoder) ? other : ArgCheck['other.call', other.call(a), Decoder]
    decoder.map { |b| a.merge(key => b) }
  end
end
map(&block) click to toggle source

Apply some block within the context of a successful decoder

# File lib/re_sorcery/decoder.rb, line 66
def map(&block)
  Decoder.new { |unknown| test(unknown).map(&block) }
end
map_error(&block) click to toggle source

Apply some block within the context of an unsuccessful decoder

# File lib/re_sorcery/decoder.rb, line 71
def map_error(&block)
  Decoder.new { |unknown| test(unknown).map_error(&block) }
end
or_else(decoder = nil, &block) click to toggle source

If the ‘Decoder` failed, try another `Decoder`

The block must return a ‘Decoder`.

# File lib/re_sorcery/decoder.rb, line 95
def or_else(decoder = nil, &block)
  ArgCheck['decoder', decoder, Decoder] unless decoder.nil?

  Decoder.new do |unknown|
    test(unknown).or_else do |value|
      decoder ||= ArgCheck['block.call(value)', block.call(value), Decoder]

      decoder.test(unknown)
    end
  end
end
test(unknown) click to toggle source

Use the decoder to ‘test` that an `unknown` object satisfies some property

Note that ‘ok(nil)` will never be returned.

@param [unknown] unknown @return [Result]

# File lib/re_sorcery/decoder.rb, line 53
def test(unknown)
  result = @block.call(unknown)
  case result
  when Result::Ok, Result::Err
    result
  when TrueClass
    ok(unknown)
  else
    err(result)
  end.and_then { |r| r.nil? ? err("`nil` was returned on a successful test!") : ok(r) }
end