Module: Emu

Defined in:
lib/emu.rb,
lib/emu/result.rb,
lib/emu/decoder.rb,
lib/emu/version.rb

Defined Under Namespace

Classes: DecodeError, Decoder, Err, Ok

Constant Summary collapse

VERSION =
"0.2.0"

Class Method Summary collapse

Class Method Details

.array(decoder) ⇒ Emu::Decoder<b>

Creates a decoder which decodes the values of an array and returns the decoded array.

Examples:

Emu.array(Emu.str_to_int).run!(["42", "43"]) # => [42, 43]
Emu.array(Emu.str_to_int).run!("42") # => raise DecodeError, "`"a"` is not an Array"
Emu.array(Emu.str_to_int).run!(["a"]) # => raise DecodeError, '`"a"` can't be converted to an Integer'

Parameters:

  • decoder (Emu::Decoder<b>)

    the decoder to apply to all values of the array

Returns:



275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/emu.rb', line 275

def self.array(decoder)
  Decoder.new do |array|
    next Err.new("`#{array.inspect}` is not an Array") unless array.is_a?(Array)

    result = []

    i = 0
    error_found = nil
    while i < array.length && !error_found
      r = decoder.run(array[i])
      if r.error?
        error_found = r
      else
        result << r.unwrap
      end
      i += 1
    end

    if error_found
      error_found
    else
      Ok.new(result)
    end
  end
end

.at_index(index, decoder) ⇒ Emu::Decoder<b>

Creates a decoder which extracts the value of an array at the given index.

Examples:

Emu.at_index(0, Emu.str_to_int).run!(["42"]) # => 42
Emu.at_index(0, Emu.str_to_int).run!(["a"]) # => raise DecodeError, '`"a"` can't be converted to an integer'
Emu.at_index(1, Emu.str_to_int).run!(["42"]) # => raise DecodeError, '`["42"]` doesn't contain index `1`'

Parameters:

  • index (Integer)

    the key of the hash map

  • decoder (Emu::Decoder<b>)

    the decoder to apply to the value at index index

Returns:



258
259
260
261
262
263
264
# File 'lib/emu.rb', line 258

def self.at_index(index, decoder)
  Decoder.new do |array|
    next Err.new("`#{array.inspect}` doesn't contain index `#{index.inspect}`") if index >= array.length

    decoder.run(array[index])
  end
end

.booleanEmu::Decoder<TrueClass|FalseClass>

Creates a decoder which only accepts booleans.

Examples:

Emu.boolean.run!(true) # => true
Emu.boolean.run!(false) # => false
Emu.boolean.run!(nil) # => raise DecodeError, "`nil` is not a Boolean"
Emu.boolean.run!(2) # => raise DecodeError, "`2` is not a Boolean"

Returns:



100
101
102
103
104
105
106
# File 'lib/emu.rb', line 100

def self.boolean
  Decoder.new do |b|
    next Err.new("`#{b.inspect}` is not a Boolean") unless b.is_a?(TrueClass) || b.is_a?(FalseClass)

    Ok.new(b)
  end
end

.fail(message) ⇒ Emu::Decoder<Void>

Creates a decoder which always fails with the provided message.

Examples:

Emu.fail("foo").run!(42) # => raise DecodeError, "foo"

Parameters:

  • message (String)

    the error message the decoder evaluates to

Returns:



170
171
172
173
174
# File 'lib/emu.rb', line 170

def self.fail(message)
  Decoder.new do |_|
    Err.new(message)
  end
end

.floatEmu::Decoder<Float>

Creates a decoder which only accepts floats (including integers). Integers are converted to floats because the result type should be uniform.

Examples:

Emu.float.run!(2) # => 2.0
Emu.float.run!(2.1) # => 2.1
Emu.float.run!("2") # => raise DecodeError, '`"2"` is not a Float'

Returns:



84
85
86
87
88
89
90
# File 'lib/emu.rb', line 84

def self.float
  Decoder.new do |i|
    next Err.new("`#{i.inspect}` is not a Float") unless i.is_a?(Float) || i.is_a?(Integer)

    Ok.new(i.to_f)
  end
end

.from_key(key, decoder) ⇒ Emu::Decoder<b>

Creates a decoder which extracts the value of a hash map according to the given key.

Examples:

Emu.from_key(:a, Emu.str_to_int).run!({a: "42"}) # => 42
Emu.from_key(:a, Emu.str_to_int).run!({a: "a"}) # => raise DecodeError, '`"a"` can't be converted to an integer'
Emu.from_key(:a, Emu.str_to_int).run!({b: "42"}) # => raise DecodeError, '`{:b=>"42"}` doesn't contain key `:a`'

Parameters:

  • key (a)

    the key of the hash map

  • decoder (Emu::Decoder<b>)

    the decoder to apply to the value at key key

Returns:



213
214
215
216
217
218
219
220
# File 'lib/emu.rb', line 213

def self.from_key(key, decoder)
  Decoder.new do |hash|
    next Err.new("`#{hash.inspect}` is not a Hash") unless hash.respond_to?(:has_key?) && hash.respond_to?(:fetch)
    next Err.new("`#{hash.inspect}` doesn't contain key `#{key.inspect}`") unless hash.has_key?(key)

    decoder.run(hash.fetch(key))
  end
end

.from_key_or_nil(key, decoder) ⇒ Emu::Decoder<b, NilClass>

Creates a decoder which extracts the value of a hash map according to the given key. If the key cannot be found nil will be returned.

Note: If a key can be found, but the value decoder fails from_key_or_nil will fail as well. This is usually what you want, because this indicates bad data you don’t know how to handle.

Examples:

Emu.from_key_or_nil(:a, Emu.str_to_int).run!({a: "42"}) # => 42
Emu.from_key_or_nil(:a, Emu.str_to_int).run!({a: "a"}) # => raise DecodeError, '`"a"` can't be converted to an integer'
Emu.from_key_or_nil(:a, Emu.str_to_int).run!({b: "42"}) # => nil

Parameters:

  • key (a)

    the key of the hash map

  • decoder (Emu::Decoder<b>)

    the decoder to apply to the value at key key

Returns:



237
238
239
240
241
242
243
244
245
246
# File 'lib/emu.rb', line 237

def self.from_key_or_nil(key, decoder)
  Decoder.new do |hash|
    next Err.new("`#{hash.inspect}` is not a Hash") unless hash.respond_to?(:has_key?) && hash.respond_to?(:fetch)
    if hash.has_key?(key)
      decoder.run(hash.fetch(key))
    else
      Ok.new(nil)
    end
  end
end

.integerEmu::Decoder<Integer>

Creates a decoder which only accepts integers.

Examples:

Emu.integer.run!(2) # => 2
Emu.integer.run!("2") # => raise DecodeError, '`"2"` is not an Integer'

Returns:



68
69
70
71
72
73
74
# File 'lib/emu.rb', line 68

def self.integer
  Decoder.new do |i|
    next Err.new("`#{i.inspect}` is not an Integer") unless i.is_a?(Integer)

    Ok.new(i)
  end
end

.lazyEmu::Decoder<a>

Wraps a decoder d in a lazily evaluated block to avoid endless recursion when dealing with recursive data structures. Emu.lazy { d }.run! behaves exactly like d.run!.

Examples:

person =
  Emu.map_n(
    Emu.from_key(:name, Emu.string),
    Emu.from_key(:parent, Emu.nil | Emu.lazy { person })) do |name, parent|
      Person.new(name, parent)
  end

person.run!({name: "foo", parent: { name: "bar", parent: nil }}) # => Person("foo", Person("bar", nil))

Yield Returns:

Returns:



349
350
351
352
353
354
# File 'lib/emu.rb', line 349

def self.lazy
  Decoder.new do |input|
    inner_decoder = yield
    inner_decoder.run(input)
  end
end

.map_n(*decoders) {|a, b, c, ...| ... } ⇒ Emu::Decoder<z>

Builds a decoder out of n decoders and maps a function over the result of the passed in decoders. For the block to be called all decoders must succeed.

Examples:

d = Emu.map_n(Emu.string, Emu.str_to_int) do |string, integer|
  string * integer
end

d.run!("3") # => "333"
d.run!("a") # => raise DecodeError, '`"a"` can't be converted to an Integer'

Parameters:

  • decoders (Array<Decoder>)

    the decoders to map over

Yields:

  • (a, b, c, ...)

    Passes the result of all decoders to the block

Yield Returns:

  • (z)

    the value the decoder should evaluate to

Returns:



316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/emu.rb', line 316

def self.map_n(*decoders, &block)
  raise "decoder count must match argument count of provided block" unless decoders.size == block.arity

  Decoder.new do |input|
    results = decoders.map do |c|
      c.run(input)
    end

    first_error = results.find(&:error?)
    if first_error
      first_error
    else
      Ok.new(block.call(*results.map(&:unwrap)))
    end
  end
end

.match(constant) ⇒ Emu::Decoder<a>

Returns a decoder which succeeds if the input value matches constant. If the decoder succeeds it resolves to the input value. #== is used for comparision, no type checks are performed.

Examples:

Emu.match(42).run!(42) # => 42
Emu.match(42).run!(41) # => raise DecodeError, "Input `41` doesn't match expected value `42`"

Parameters:

  • constant (a)

    the value to match against

Returns:



185
186
187
188
189
# File 'lib/emu.rb', line 185

def self.match(constant)
  Decoder.new do |s|
    s == constant ? Ok.new(s) : Err.new("Input `#{s.inspect}` doesn't match expected value `#{constant.inspect}`")
  end
end

.nilEmu::Decoder<NilClass>

Creates a decoder which only accepts ‘nil` values.

Examples:

Emu.nil.run!(nil) # => nil
Emu.nil.run!(42) # => raise DecodeError, "`42` isn't `nil`"

Returns:



197
198
199
200
201
# File 'lib/emu.rb', line 197

def self.nil
  Decoder.new do |s|
    s.nil? ? Ok.new(s) : Err.new("`#{s.inspect}` isn't `nil`")
  end
end

.rawEmu::Decoder<a>

Creates a decoder which always succeeds and yields the input.

This might be useful if you don’t care about the exact shape of of your data and don’t have a need to inspect it (e.g. some binary data).

Examples:

Emu.raw.run!(true) # => true
Emu.raw.run!("2") # => "2"

Returns:



146
147
148
149
150
# File 'lib/emu.rb', line 146

def self.raw
  Decoder.new do |s|
    Ok.new(s)
  end
end

.str_to_boolEmu::Decoder<TrueClass|FalseClass>

Creates a decoder which converts a string to a boolean (true, false) value.

"0" and "false" are considered false, "1" and "true" are considered true. Trying to decode any other value will fail.

Examples:

Emu.str_to_bool.run!("true") # => true
Emu.str_to_bool.run!("1") # => true
Emu.str_to_bool.run!("false") # => false
Emu.str_to_bool.run!("0") # => false
Emu.str_to_bool.run!(true) # => raise DecodeError, "`true` is not a String"
Emu.str_to_bool.run!("2") # => raise DecodeError, "`\"2\"` can't be converted to a Boolean"

Returns:



122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/emu.rb', line 122

def self.str_to_bool
  Decoder.new do |s|
    next Err.new("`#{s.inspect}` is not a String") unless s.is_a?(String)

    if s == "true" || s == "1"
      Ok.new(true)
    elsif s == "false" || s == "0"
      Ok.new(false)
    else
      Err.new("`#{s.inspect}` can't be converted to a Boolean")
    end
  end
end

.str_to_floatEmu::Decoder<Float>

Creates a decoder which converts a string to a float. It uses Float for the conversion.

Examples:

Emu.str_to_float.run!("42.2") # => 42.2
Emu.str_to_float.run!("42") # => 42.0
Emu.str_to_float.run!("a") # => raise DecodeError, "`\"a\"` can't be converted to a Float"
Emu.str_to_float.run!(42) # => raise DecodeError, "`42` is not a String"

Returns:



50
51
52
53
54
55
56
57
58
59
60
# File 'lib/emu.rb', line 50

def self.str_to_float
  Decoder.new do |s|
    next Err.new("`#{s.inspect}` is not a String") unless s.is_a?(String)

    begin
      Ok.new(Float(s))
    rescue TypeError, ArgumentError
      Err.new("`#{s.inspect}` can't be converted to a Float")
    end
  end
end

.str_to_intEmu::Decoder<Integer>

Creates a decoder which converts a string to an integer. It uses Integer for the conversion.

Examples:

Emu.str_to_int.run!("42") # => 42
Emu.str_to_int.run!("a") # => raise DecodeError, "`\"a\"` can't be converted to an Integer"
Emu.str_to_int.run!(42) # => raise DecodeError, "`42` is not a String"

Returns:



29
30
31
32
33
34
35
36
37
38
39
# File 'lib/emu.rb', line 29

def self.str_to_int
  Decoder.new do |s|
    next Err.new("`#{s.inspect}` is not a String") unless s.is_a?(String)

    begin
      Ok.new(Integer(s))
    rescue TypeError, ArgumentError
      Err.new("`#{s.inspect}` can't be converted to an Integer")
    end
  end
end

.stringEmu::Decoder<String>

Creates a decoder which only accepts strings.

Examples:

Emu.string.run!("2") # => "2"
Emu.string.run!(2) # => raise DecodeError, "`2` is not a String"

Returns:



13
14
15
16
17
18
19
# File 'lib/emu.rb', line 13

def self.string
  Decoder.new do |s|
    next Err.new("`#{s.inspect}` is not a String") unless s.is_a?(String)

    Ok.new(s)
  end
end

.succeed(value) ⇒ Emu::Decoder<a>

Creates a decoder which always succeeds with the provided value.

Examples:

Emu.succeed("foo").run!(42) # => "foo"

Parameters:

  • value (a)

    the value the decoder evaluates to

Returns:



158
159
160
161
162
# File 'lib/emu.rb', line 158

def self.succeed(value)
  Decoder.new do |_|
    Ok.new(value)
  end
end