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
Datemethod 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
Timemethod 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
-
.boolean ⇒ Decoding::Decoder<Boolean>
Decode a boolean value (either
trueorfalse). -
.enum ⇒ Decoding::Decoder<Object>
Decode a value that must be one of a fixed set of values.
-
.false ⇒ Decoding::Decoder<FalseClass>
Decode a
falsevalue. -
.float ⇒ Decoding::Decoder<Float>
Decode any float value.
-
.integer ⇒ Decoding::Decoder<Integer>
Decode any integer value.
-
.match(pattern) ⇒ Decoding::Decoder<Object>
Decode any value matching the given pattern, using the
===operator. -
.nil ⇒ Decoding::Decoder<NilClass>
Decode a
nilvalue. -
.numeric ⇒ Decoding::Decoder<Numeric>
Decode any numeric value (includes both integers and floats).
-
.regexp(regex) ⇒ Decoding::Decoder<String>
Decode any string value that matches a regular expression.
-
.string ⇒ Decoding::Decoder<String>
Decode any string value.
-
.symbol ⇒ Decoding::Decoder<Symbol>
Decode a String value into a symbol.
-
.true ⇒ Decoding::Decoder<TrueClass>
Decode a
truevalue.
Parsing decoders collapse
-
.parsed_boolean ⇒ Decoding::Decoder<Boolean>
Decode the string
"true"or"false"into the matching boolean. -
.parsed_float ⇒ Decoding::Decoder<Float>
Decode a string describing a number into a float.
-
.parsed_integer ⇒ Decoding::Decoder<Integer>
Decode a string describing an integer into that integer.
Utility decoders collapse
-
.fail(value) ⇒ Decoding::Decoder<String>
A decoder that always fails with the given value.
-
.original ⇒ Decoding::Decoder<Object>
A decoder that returns the input value, unaltered.
-
.succeed(value) ⇒ Decoding::Decoder<String>
A decoder that always succeeds with the given value.
Compound decoders collapse
-
.and_then(deocder) {|value| ... } ⇒ Decoding::Decoder<b>
Create a decoder that depends on a previously decoded value.
-
.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.
-
.array(decoder) ⇒ Decoding::Decoder<Array<a>>
Decode an array of values using a given decoder.
-
.at(*fields, decoder) ⇒ Decoding::Decoder<a>
Decode deeply-nested fields.
-
.decode_hash(decoders) ⇒ Object
Decode a value into a hash using multiple decoders.
-
.field(key, decoder) ⇒ Decoding::Decoder<a>
Decode a value from a given key in a hash.
-
.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.
-
.index(integer, decoder) ⇒ Decoding::Decoder<a>
Decode an array element by index using a given decoder.
-
.lazy ⇒ Decoding::Decoder<a>
Create a decoder that is only built when it is used.
-
.map(decoder, *decoders) {|value| ... } ⇒ Decoding::Decoder<b>
Decode a value with the given decoder and, if successful, apply a block to the decoded result.
-
.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.
-
.optional(decoder) ⇒ Decoding::Decoder<a, nil>
Decode a value that may or may not be
nil. -
.optional_field(key, decoder, default: nil) ⇒ Decoding::Decoder<a>
Decode a value from a key that may be absent from a hash.
Class Method Summary collapse
-
.big_decimal ⇒ Decoding::Decoder<BigDecimal>
Decode a
BigDecimalobject, or a number or string describing one. -
.date(format) ⇒ Decoding::Decoder<Date>
Decode a
Dateobject, or a string describing a date in a given format. -
.time(format, zone: ::Time) ⇒ Decoding::Decoder<Time>
Decode a
Timeobject, or a string describing a time in a given format. -
.unix_time(unit = :seconds, zone: ::Time) ⇒ Decoding::Decoder<Time>
Decode a unix timestamp, given as a number or as a string describing one.
-
.uri ⇒ Decoding::Decoder<URI::Generic>
Decode a URI object, or a string that can be parsed as one.
Class Method Details
.and_then(deocder) {|value| ... } ⇒ Decoding::Decoder<b>
Create a decoder that depends on a previously decoded value.
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.
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.
318 |
# File 'lib/decoding/decoders.rb', line 318 def array(...) = Decoders::Array.new(...) |
.at(*fields, decoder) ⇒ Decoding::Decoder<a>
Decode deeply-nested fields.
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.
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") } ) { |, value| "expected a decimal number, got #{value.inspect}" } end |
.boolean ⇒ Decoding::Decoder<Boolean>
Decode a boolean value (either true or false).
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.
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) } ) ) { |, 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" })
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.
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.
194 |
# File 'lib/decoding/decoders.rb', line 194 def fail(value) = Decoders::Fail.new(value) |
.false ⇒ Decoding::Decoder<FalseClass>
Decode a false value.
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.
288 |
# File 'lib/decoding/decoders.rb', line 288 def field(...) = Decoders::Field.new(...) |
.float ⇒ Decoding::Decoder<Float>
Decode any float value.
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.
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.
329 |
# File 'lib/decoding/decoders.rb', line 329 def index(...) = Decoders::Index.new(...) |
.integer ⇒ Decoding::Decoder<Integer>
Decode any integer value.
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.
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.
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.
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.
48 |
# File 'lib/decoding/decoders.rb', line 48 def match(pattern) = Decoders::Match.new(pattern) |
.nil ⇒ Decoding::Decoder<NilClass>
Decode a nil value.
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).
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.
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.
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.
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.
174 175 176 177 178 |
# File 'lib/decoding/decoders.rb', line 174 def parsed_boolean map_err(map(enum("true", "false")) { _1 == "true" }) do |, value| %(expected "true" or "false", got #{value.inspect}) end end |
.parsed_float ⇒ Decoding::Decoder<Float>
Decode a string describing a number into a float.
161 162 163 |
# File 'lib/decoding/decoders.rb', line 161 def parsed_float map_err(map(string) { Float(_1) }) { |, 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.
151 152 153 |
# File 'lib/decoding/decoders.rb', line 151 def parsed_integer map_err(map(string) { Integer(_1, 10) }) { |, value| "expected an integer, got #{value.inspect}" } end |
.regexp(regex) ⇒ Decoding::Decoder<String>
Decode any string value that matches a regular expression.
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.
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.
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.
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.
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) ) ) { |, value| "expected #{description}, got #{value.inspect}" } end |
.true ⇒ Decoding::Decoder<TrueClass>
Decode a true value.
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.
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), (zone, UNIX_TIME_UNITS.fetch(unit)) ) ) { |, 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.
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 |