Module: Decoding::Decoders

Defined in:
lib/decoding/decoders.rb,
lib/decoding/decoders/at.rb,
lib/decoding/decoders/any.rb,
lib/decoding/decoders/map.rb,
lib/decoding/decoders/uri.rb,
lib/decoding/decoders/date.rb,
lib/decoding/decoders/enum.rb,
lib/decoding/decoders/fail.rb,
lib/decoding/decoders/hash.rb,
lib/decoding/decoders/lazy.rb,
lib/decoding/decoders/pass.rb,
lib/decoding/decoders/time.rb,
lib/decoding/decoders/array.rb,
lib/decoding/decoders/field.rb,
lib/decoding/decoders/index.rb,
lib/decoding/decoders/match.rb,
lib/decoding/decoders/map_err.rb,
lib/decoding/decoders/succeed.rb,
lib/decoding/decoders/and_then.rb,
lib/decoding/decoders/optional.rb,
lib/decoding/decoders/big_decimal.rb,
lib/decoding/decoders/optional_field.rb

Overview

Decoders are composable functions for deconstructing unknown input values into known output values.

Defined Under Namespace

Classes: AndThen, Any, Array, At, Enum, Fail, Field, Hash, Index, Lazy, Map, MapErr, Match, Optional, OptionalField, Pass, Succeed

Constant Summary collapse

DATE_FORMATS =

The standard formats date can parse, each named after the Date method that parses it.

%i[iso8601 xmlschema rfc2822 rfc822 rfc3339 httpdate jisx0301 parse].freeze
TIME_FORMATS =

The standard formats time can parse, each named after the Time method that parses it.

%i[iso8601 xmlschema rfc2822 rfc822 httpdate parse].freeze
UNIX_TIME_UNITS =

The units unix_time can read a timestamp in, mapped to the number of them that make up a second.

{ seconds: 1, milliseconds: 1000 }.freeze

Simple decoders collapse

Parsing decoders collapse

Utility decoders collapse

Compound decoders collapse

Class Method Summary collapse

Class Method Details

.and_then(deocder) {|value| ... } ⇒ Decoding::Decoder<b>

Create a decoder that depends on a previously decoded value.

Examples:

decoder = and_then(field("version", integer)) do |version|
  if version == 1
    field("name", string)
  else
    field("fullName", string)
  end
end
decode(decoder, { "version" => 1, "name" => "john" })
# => Decoding::Ok("john")
decode(decoder, { "version" => 2, "fullName" => "john" })
# => Decoding::Ok("john")

Parameters:

Yield Parameters:

  • value (a)

Yield Returns:

Returns:

See Also:



387
# File 'lib/decoding/decoders.rb', line 387

def and_then(...) = Decoders::AndThen.new(...)

.any(decoder, *decoders) ⇒ Decoding::Decoder<a>

Decode a value by trying many different decoders in order, using the first matching result -- or a failure when none of the given decoders succeed.

Examples:

decode(any(string, integer), 12) # => Decoding::Ok(12)
decode(any(string, integer), '12') # => Decoding::Ok('12')

Parameters:

Returns:

See Also:



262
# File 'lib/decoding/decoders.rb', line 262

def any(...) = Decoders::Any.new(...)

.array(decoder) ⇒ Decoding::Decoder<Array<a>>

Decode an array of values using a given decoder.

Examples:

decode(array(integer), [1, 2, 3]) # => Decoding::Ok([1, 2, 3])

Parameters:

Returns:

See Also:



318
# File 'lib/decoding/decoders.rb', line 318

def array(...) = Decoders::Array.new(...)

.at(*fields, decoder) ⇒ Decoding::Decoder<a>

Decode deeply-nested fields.

Examples:

decoder = at('a', 'b', 'c', string)
decode(decoder, { "a" => { "b" => { "c" => "d" } } })
# => Decoding::Ok("d")
decode(decoder, { "a" => { "b" => "d" } })
# => Decoding::Err("Error at .a.b: expected Hash, got String")

Parameters:

Returns:

See Also:



422
423
424
# File 'lib/decoding/decoders.rb', line 422

def at(...)
  Decoders::At.new(...)
end

.big_decimal ⇒ Decoding::Decoder<BigDecimal>

Decode a BigDecimal object, or a number or string describing one.

Only finite numbers are accepted: NaN and Infinity are errors, as a decimal is usually reached for when a value has to be exact.

Note this decoder needs the bigdecimal gem, which is no longer part of Ruby's default gems. Add it to your Gemfile to use this decoder.

Examples:

decode(big_decimal, "1.23") # => Decoding::Ok(BigDecimal("1.23"))
decode(big_decimal, 42) # => Decoding::Ok(BigDecimal("42"))
decode(big_decimal, "abc")
# => Decoding::Err(%(expected a decimal number, got "abc"))

Returns:

See Also:



26
27
28
29
30
31
32
33
34
35
# File 'lib/decoding/decoders/big_decimal.rb', line 26

def big_decimal
  Decoders.map_err(
    Decoders.and_then(
      Decoders.any(
        Decoders.match(::BigDecimal),
        Decoders.map(Decoders.any(Decoders.integer, Decoders.float, Decoders.string)) { BigDecimal(_1) }
      )
    ) { |number| number.finite? ? Decoders.succeed(number) : Decoders.fail("not a finite number") }
  ) { |_message, value| "expected a decimal number, got #{value.inspect}" }
end

.boolean ⇒ Decoding::Decoder<Boolean>

Decode a boolean value (either true or false).

Examples:

decode(boolean, true) # => Decoding::Ok(true)
decode(boolean, false) # => Decoding::Ok(false)

Returns:



131
# File 'lib/decoding/decoders.rb', line 131

def boolean = map_err(any(self.true, self.false)) { |_msg, value| "expected true or false, got #{value.class}" }

.date(format) ⇒ Decoding::Decoder<Date>

Decode a Date object, or a string describing a date in a given format.

The format is either the name of one of DATE_FORMATS, or a string with a strptime pattern. Note that the :parse format is lenient: it fills in any components the input value leaves out from the current date.

Examples:

decode(date(:iso8601), "2020-01-01")
# => Decoding::Ok(#<Date: 2020-01-01>)
decode(date("%Y|%m"), "nope")
# => Decoding::Err("expected a date matching \"%Y|%m\", got \"nope\"")

Parameters:

  • format (Symbol, String)

Returns:

Raises:

  • (ArgumentError) —

    when the format is not a known name or a pattern. This is raised when the decoder is built, not when it is used.

See Also:



31
32
33
34
35
36
37
38
39
# File 'lib/decoding/decoders/date.rb', line 31

def date(format)
  parse, description = date_format(format)
  Decoders.map_err(
    Decoders.any(
      Decoders.match(::Date),
      Decoders.map(Decoders.string) { parse.call(_1) }
    )
  ) { |_message, value| "expected #{description}, got #{value.inspect}" }
end

.decode_hash(decoders) ⇒ Object

Decode a value into a hash using multiple decoders.

This is a shortcut for:

deocde(map(field("id", integer), field("name", string)) { |id, name|
{ id:, name: }
}, { "id" => 1, "name" => "John" })
# => Decoding::Ok({ id: 1, name: "John" })

Examples:

decode(decode_hash(
  id: field("id", integer)
), { "id" => 1 })
# => Decode::Ok({ id: 1 })

Returns:

  • Decoding::Decoder



359
360
361
362
363
364
365
# File 'lib/decoding/decoders.rb', line 359

def decode_hash(decoders)
  return succeed({}) if decoders.empty?

  map(*decoders.values) do |*values|
    decoders.keys.zip(values).to_h
  end
end

.enum(value, *values) ⇒ Decoding::Decoder<Object> .enum(values) ⇒ Decoding::Decoder<Object>

Decode a value that must be one of a fixed set of values.

The values are compared for equality, and can be given either as separate arguments or as a single array.

Examples:

decode(enum("active", "archived"), "active") # => Decoding::Ok("active")
decode(enum("active", "archived"), "nope")
# => Decoding::Err(%(expected one of "active", "archived", got "nope"))

Overloads:

Returns:

Raises:

  • (ArgumentError) —

    when no values are given, or one is repeated.

See Also:



67
# File 'lib/decoding/decoders.rb', line 67

def enum(...) = Decoders::Enum.new(...)

.fail(value) ⇒ Decoding::Decoder<String>

A decoder that always fails with the given value.

Examples:

decode(fail("oh no"), "foo") # => Decoding::Err("oh no")

Returns:



194
# File 'lib/decoding/decoders.rb', line 194

def fail(value) = Decoders::Fail.new(value)

.false ⇒ Decoding::Decoder<FalseClass>

Decode a false value.

Examples:

decode(Decoders.false, false) # => Decoding::Ok(false)

Returns:

See Also:



123
# File 'lib/decoding/decoders.rb', line 123

def false = Decoders::Match.new(FalseClass)

.field(key, decoder) ⇒ Decoding::Decoder<a>

Decode a value from a given key in a hash.

Examples:

decode(field('id', integer), { 'id' => 5 }) # => Decoding::Ok(5)

Parameters:

Returns:

See Also:



288
# File 'lib/decoding/decoders.rb', line 288

def field(...) = Decoders::Field.new(...)

.float ⇒ Decoding::Decoder<Float>

Decode any float value.

Examples:

decode(float, 0.5) # => Decoding::Ok(0.5)

Returns:

See Also:



90
# File 'lib/decoding/decoders.rb', line 90

def float = Decoders::Match.new(Float)

.hash(key_decoder, value_decoder) ⇒ Decoding::Decoder<Hash<a, b>>

Decode a Hash with arbitrary contents using two decoders for the keys and the pairs.

Examples:

decode(hash(string, integer), { 'john' => 1 })
# => Decoding::Ok({ 'john' => 1 })

Parameters:

Returns:

See Also:



342
# File 'lib/decoding/decoders.rb', line 342

def hash(...) = Decoders::Hash.new(...)

.index(integer, decoder) ⇒ Decoding::Decoder<a>

Decode an array element by index using a given decoder.

Examples:

decode(index(0, integer), [1, 2, 3]) # => Decoding::Ok(1)

Parameters:

Returns:

See Also:



329
# File 'lib/decoding/decoders.rb', line 329

def index(...) = Decoders::Index.new(...)

.integer ⇒ Decoding::Decoder<Integer>

Decode any integer value.

Examples:

decode(integer, 1) # => Decoding::Ok(1)

Returns:

See Also:



82
# File 'lib/decoding/decoders.rb', line 82

def integer = Decoders::Match.new(Integer)

.lazy ⇒ Decoding::Decoder<a>

Create a decoder that is only built when it is used.

This makes recursive decoders possible: without it, a decoder that refers to itself would recurse endlessly while being built.

Examples:

def tree
  decode_hash(
    name: field("name", string),
    children: field("children", array(lazy { tree }))
  )
end
decode(tree, { "name" => "a", "children" => [] })
# => Decoding::Ok({ name: "a", children: [] })

Yield Returns:

Returns:

See Also:



407
# File 'lib/decoding/decoders.rb', line 407

def lazy(...) = Decoders::Lazy.new(...)

.map(decoder, *decoders) {|value| ... } ⇒ Decoding::Decoder<b>

Decode a value with the given decoder and, if successful, apply a block to the decoded result.

Given multiple decoders, apply them all to the same value and, if all succeeded, create a single output value from them.

Examples:

map over a single value

decode(map(string, &:upcase), "foo") # => Decoding::Ok("FOO")

map over multiple values

decode(
  map(
    field("id", integer),
    field("name", string)
  ) { |id, name| [id, name] },
  { "id" => 1, "name" => "john" }
)
# => [1, "john"]

Parameters:

Yield Parameters:

  • value (a)

Yield Returns:

  • (b)

Returns:

See Also:



228
# File 'lib/decoding/decoders.rb', line 228

def map(...) = Decoders::Map.new(...)

.map_err(decoder) {|msg, value| ... } ⇒ Decoding::Decoder<a>

Decode a value with the given decoder and, if it failed, replace its error message with the result of the given block.

This is useful for giving a compound decoder a single, fitting error message rather than exposing the messages of the decoders it is built from. The location of the original error, if any, is retained.

Examples:

decoder = map_err(any(string, integer)) { |_msg, value|
  "expected a string or integer, got #{value.inspect}"
}
decode(decoder, nil)
# => Decoding::Err("expected a string or integer, got nil")

Parameters:

Yield Parameters:

  • msg (String) —

    the original error message

  • value (Object) —

    the value being decoded

Yield Returns:

  • (String)

Returns:

See Also:



250
# File 'lib/decoding/decoders.rb', line 250

def map_err(...) = Decoders::MapErr.new(...)

.match(pattern) ⇒ Decoding::Decoder<Object>

Decode any value matching the given pattern, using the === operator. This works with anything that can be used in a case statement, such as classes, ranges and regular expressions.

Examples:

decode(match(Symbol), :foo) # => Decoding::Ok(:foo)
decode(match(1..5), 3) # => Decoding::Ok(3)

Parameters:

  • pattern (#===)

Returns:

See Also:



48
# File 'lib/decoding/decoders.rb', line 48

def match(pattern) = Decoders::Match.new(pattern)

.nil ⇒ Decoding::Decoder<NilClass>

Decode a nil value.

Examples:

decode(Decoders.nil, nil) # => Decoding::Ok(nil)

Returns:

See Also:



107
# File 'lib/decoding/decoders.rb', line 107

def nil = Decoders::Match.new(NilClass)

.numeric ⇒ Decoding::Decoder<Numeric>

Decode any numeric value (includes both integers and floats).

Examples:

decode(numeric, 1) # => Decoding::Ok(1)
decode(numeric, 1.5) # => Decoding::Ok(1.5)

Returns:

See Also:



99
# File 'lib/decoding/decoders.rb', line 99

def numeric = Decoders::Match.new(Numeric)

.optional(decoder) ⇒ Decoding::Decoder<a, nil>

Decode a value that may or may not be nil.

The given decoder gets to decode a nil value first, so it can give it a meaning of its own. Only when it fails to do so is nil treated as an absent value.

Examples:

decode(string, "foo") # => Decoding::Ok("foo")
decode(string, nil) # => Decoding::Ok(nil)

Parameters:

Returns:

See Also:



277
# File 'lib/decoding/decoders.rb', line 277

def optional(...) = Decoders::Optional.new(...)

.optional_field(key, decoder, default: nil) ⇒ Decoding::Decoder<a>

Decode a value from a key that may be absent from a hash.

When the key is absent, the given default is used. When it is present, its value must still decode: a key holding a value of the wrong type is an error rather than a reason to fall back to the default.

Examples:

decoder = optional_field("count", integer, default: 0)
decode(decoder, {}) # => Decoding::Ok(0)
decode(decoder, { "count" => 5 }) # => Decoding::Ok(5)
decode(decoder, { "count" => "x" })
# => Decoding::Err("Error at .count: expected Integer, got String")

Parameters:

Returns:

See Also:



308
# File 'lib/decoding/decoders.rb', line 308

def optional_field(...) = Decoders::OptionalField.new(...)

.original ⇒ Decoding::Decoder<Object>

A decoder that returns the input value, unaltered.

Examples:

decode(original, [1, 2]) # => Decoding::Ok([1, 2])

Returns:



201
# File 'lib/decoding/decoders.rb', line 201

def original = Decoders::Pass.new

.parsed_boolean ⇒ Decoding::Decoder<Boolean>

Decode the string "true" or "false" into the matching boolean.

Only those two values are accepted: anything else, such as "1" or "yes", is an error rather than a guess at what was meant.

Examples:

decode(parsed_boolean, "true") # => Decoding::Ok(true)
decode(parsed_boolean, "1") # => Decoding::Err(%(expected "true" or "false", got "1"))

Returns:



174
175
176
177
178
# File 'lib/decoding/decoders.rb', line 174

def parsed_boolean
  map_err(map(enum("true", "false")) { _1 == "true" }) do |_message, value|
    %(expected "true" or "false", got #{value.inspect})
  end
end

.parsed_float ⇒ Decoding::Decoder<Float>

Decode a string describing a number into a float.

Examples:

decode(parsed_float, "1.5") # => Decoding::Ok(1.5)
decode(parsed_float, "abc") # => Decoding::Err(%(expected a number, got "abc"))

Returns:



161
162
163
# File 'lib/decoding/decoders.rb', line 161

def parsed_float
  map_err(map(string) { Float(_1) }) { |_message, value| "expected a number, got #{value.inspect}" }
end

.parsed_integer ⇒ Decoding::Decoder<Integer>

Decode a string describing an integer into that integer.

The string is always read as a decimal number, so "08" decodes to 8 and "0x1f" is not a valid integer.

Examples:

decode(parsed_integer, "8080") # => Decoding::Ok(8080)
decode(parsed_integer, "abc") # => Decoding::Err(%(expected an integer, got "abc"))

Returns:



151
152
153
# File 'lib/decoding/decoders.rb', line 151

def parsed_integer
  map_err(map(string) { Integer(_1, 10) }) { |_message, value| "expected an integer, got #{value.inspect}" }
end

.regexp(regex) ⇒ Decoding::Decoder<String>

Decode any string value that matches a regular expression.

Parameters:

  • regex (Regexp, String)

Returns:

See Also:



74
# File 'lib/decoding/decoders.rb', line 74

def regexp(regex) = Decoders::Match.new(Regexp.new(regex))

.string ⇒ Decoding::Decoder<String>

Decode any string value.

Examples:

decode(string, "foo") # => Decoding::Ok("foo")

Returns:

See Also:



36
# File 'lib/decoding/decoders.rb', line 36

def string = Decoders::Match.new(String)

.succeed(value) ⇒ Decoding::Decoder<String>

A decoder that always succeeds with the given value.

Examples:

decode(succeed(5), "foo") # => Decoding::Ok(5)

Returns:



187
# File 'lib/decoding/decoders.rb', line 187

def succeed(value) = Decoders::Succeed.new(value)

.symbol ⇒ Decoding::Decoder<Symbol>

Decode a String value into a symbol.

Examples:

decode(symbol, "foo") # => Decoding::Ok(:foo)

Returns:



138
# File 'lib/decoding/decoders.rb', line 138

def symbol = map(string, &:to_sym)

.time(format, zone: ::Time) ⇒ Decoding::Decoder<Time>

Decode a Time object, or a string describing a time in a given format.

The format is either the name of one of TIME_FORMATS, or a string with a strptime pattern. Note that the :parse format is lenient: it fills in any components the input value leaves out from the current time.

Times are resolved by Time itself, and so in the system's time zone, unless another zone is given. Anything answering the format you name will do: an ActiveSupport::TimeZone resolves in the application's zone, which is rarely the system's, but note it answers only iso8601, rfc3339 and parse of the names above.

Examples:

decode(time(:iso8601), "2020-01-01T10:00:00Z")
# => Decoding::Ok(2020-01-01 10:00:00 UTC)
decode(time("%Y|%m"), "nope")
# => Decoding::Err("expected a time matching \"%Y|%m\", got \"nope\"")
decode(time(:parse, zone: Time.zone), "2020-01-01 10:00:00")
# => Decoding::Ok(2020-01-01 10:00:00 +0100)

Parameters:

  • format (Symbol, String)
  • zone (Object) (defaults to: ::Time) —

    anything answering the format you name, such as Time or an ActiveSupport::TimeZone.

Returns:

Raises:

  • (ArgumentError) —

    when the format is not a known name or a pattern, or the zone cannot parse it. This is raised when the decoder is built, not when it is used.

See Also:



46
47
48
49
50
51
52
53
54
# File 'lib/decoding/decoders/time.rb', line 46

def time(format, zone: ::Time)
  parse, description = time_format(format, zone)
  Decoders.map_err(
    Decoders.any(
      Decoders.match(::Time),
      parsed_by(parse)
    )
  ) { |_message, value| "expected #{description}, got #{value.inspect}" }
end

.true ⇒ Decoding::Decoder<TrueClass>

Decode a true value.

Examples:

decode(Decoding.true, true) # => Decoding::Ok(true)

Returns:

See Also:



115
# File 'lib/decoding/decoders.rb', line 115

def true = Decoders::Match.new(TrueClass)

.unix_time(unit = :seconds, zone: ::Time) ⇒ Decoding::Decoder<Time>

Decode a unix timestamp, given as a number or as a string describing one.

Timestamps are read as a number of seconds since the epoch unless another unit is given. Note that reading a timestamp in the wrong unit is not an error but a wildly different point in time, so a source that reports milliseconds has to say so.

A timestamp names a point in time rather than a local one, so the zone only decides how the value describes itself afterwards -- which matters as soon as anything derives a date from it.

Examples:

decode(unix_time, 1_595_674_680) # => Decoding::Ok(2020-07-25 10:58:00 UTC)
decode(unix_time(:milliseconds), 1_595_674_680_123)
# => Decoding::Ok(2020-07-25 10:58:00.123 UTC)

Parameters:

  • unit (Symbol) (defaults to: :seconds) —

    one of the keys of UNIX_TIME_UNITS.

  • zone (Object) (defaults to: ::Time) —

    anything answering at, such as Time or an ActiveSupport::TimeZone.

Returns:

Raises:

  • (ArgumentError) —

    when the unit is not a known one, or the zone cannot read a timestamp. This is raised when the decoder is built, not when it is used.

See Also:



97
98
99
100
101
102
103
104
105
106
107
# File 'lib/decoding/decoders/time.rb', line 97

def unix_time(unit = :seconds, zone: ::Time)
  raise ArgumentError, "unknown unit: #{unit.inspect}" unless UNIX_TIME_UNITS.key?(unit)

  require_parser(zone, :at, "a unix timestamp")
  Decoders.map_err(
    Decoders.any(
      Decoders.match(::Time),
      timestamp_in(zone, UNIX_TIME_UNITS.fetch(unit))
    )
  ) { |_message, value| "expected a unix timestamp, got #{value.inspect}" }
end

.uri ⇒ Decoding::Decoder<URI::Generic>

Decode a URI object, or a string that can be parsed as one.

Examples:

decode(uri, "https://example.com") # => Decoding::Ok(URI("https://example.com"))
decode(uri, 123) # => Decoding::Err("expected a URI, got 123")

Returns:

See Also:



18
19
20
21
22
23
24
25
# File 'lib/decoding/decoders/uri.rb', line 18

def uri
  Decoders.map_err(
    Decoders.any(
      Decoders::Match.new(::URI::Generic),
      Decoders.map(Decoders.string) { ::URI.parse(_1) }
    )
  ) { |_msg, value| "expected a URI, got #{value.inspect}" }
end