decoding 0.5.0 → 0.6.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 +4 -4
- data/.rubocop.yml +16 -0
- data/CHANGELOG.md +8 -0
- data/Rakefile +12 -0
- data/lib/decoding/decoders/array.rb +1 -4
- data/lib/decoding/decoders/big_decimal.rb +9 -6
- data/lib/decoding/decoders/date.rb +2 -2
- data/lib/decoding/decoders/hash.rb +13 -13
- data/lib/decoding/decoders/map.rb +1 -1
- data/lib/decoding/decoders/optional.rb +11 -3
- data/lib/decoding/decoders/time.rb +15 -11
- data/lib/decoding/decoders/uri.rb +2 -2
- data/lib/decoding/decoders.rb +1 -1
- data/lib/decoding/result.rb +24 -7
- data/lib/decoding/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3dcd003beb1f65e74c31189b632c9c290d636a20467f5fed1a8c382d1aff634b
|
|
4
|
+
data.tar.gz: 338652cc9d8ea559a31fc9b14adc14a83cdc10c5730a175a1fa3670b87435b3d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1a20f3f88d3970063dd6b3f8890ee8f0646df418e0c157fffe3081f6de6c2c3b7718cd685335c1b7f108e21f110d5823be6bdab3e37a222c2d6b4a728bcb820f
|
|
7
|
+
data.tar.gz: 31edcf5c7f5721d93308269883d271b1cd1336738a1013c18c5d21e071dddfd3de855fb3091b5d2426ddc1e57f19baca471051cbffdddf2cc47e7b157120bd1e
|
data/.rubocop.yml
CHANGED
|
@@ -25,6 +25,9 @@ Metrics/ModuleLength:
|
|
|
25
25
|
|
|
26
26
|
Metrics/AbcSize:
|
|
27
27
|
Max: 18
|
|
28
|
+
Exclude:
|
|
29
|
+
# The benchmarks build test data in long methods.
|
|
30
|
+
- benchmark/support/*.rb
|
|
28
31
|
|
|
29
32
|
Metrics/BlockLength:
|
|
30
33
|
Exclude:
|
|
@@ -34,6 +37,9 @@ Metrics/BlockLength:
|
|
|
34
37
|
|
|
35
38
|
Metrics/MethodLength:
|
|
36
39
|
Max: 17
|
|
40
|
+
Exclude:
|
|
41
|
+
# The benchmarks build test data in long methods.
|
|
42
|
+
- benchmark/support/*.rb
|
|
37
43
|
|
|
38
44
|
Style/SignalException:
|
|
39
45
|
Enabled: false
|
|
@@ -65,3 +71,13 @@ RSpec/DescribeClass:
|
|
|
65
71
|
RSpec/ExpectActual:
|
|
66
72
|
Exclude:
|
|
67
73
|
- spec/matchers/*_spec.rb
|
|
74
|
+
|
|
75
|
+
# The benchmarks print aligned tables, where plain format strings next to each
|
|
76
|
+
# other read better than annotated ones.
|
|
77
|
+
Style/FormatStringToken:
|
|
78
|
+
Exclude:
|
|
79
|
+
- benchmark/**/*.rb
|
|
80
|
+
|
|
81
|
+
Style/RedundantFormat:
|
|
82
|
+
Exclude:
|
|
83
|
+
- benchmark/**/*.rb
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.6.0]
|
|
4
|
+
|
|
5
|
+
* Decode arrays, hashes and multi-field decoders (such as `decode_hash` and `Decoding::Data`) in linear rather than quadratic time. Decoding 1,600 records is about 29 times faster.
|
|
6
|
+
* Decode valid input to `boolean`, `big_decimal`, `time`, `date`, `uri` and `unix_time` without first trying, and failing, a decoder for another type of input. Strings and numbers decode up to 3 times faster; values that are already a `Time`, `Date` or `URI` are now checked last and take a little longer.
|
|
7
|
+
* Stop decoding an array, hash or `map` at the first failure, as only that one is reported. A document whose first record is invalid fails about 90 times faster. Items after the failure are no longer decoded, so blocks given to `map` for them no longer run.
|
|
8
|
+
* Accept a block in `Result.all` to map each item to a result, stopping at the first `Err`
|
|
9
|
+
* Make `optional` remember when its decoder has failed to decode `nil`, and decode `nil` without asking it again. A `nil` now takes about 0.3 µs instead of up to 5 µs. A decoder that does decode `nil`, such as `optional(succeed(5))`, is still asked every time.
|
|
10
|
+
|
|
3
11
|
## [0.5.0]
|
|
4
12
|
|
|
5
13
|
* Add a `zone:` argument to the `time` and `unix_time` decoders, for resolving times somewhere other than the system's time zone.
|
data/Rakefile
CHANGED
|
@@ -12,3 +12,15 @@ RuboCop::RakeTask.new
|
|
|
12
12
|
YARD::Rake::YardocTask.new
|
|
13
13
|
|
|
14
14
|
task default: %i[spec rubocop yard]
|
|
15
|
+
|
|
16
|
+
BENCHMARKS = %w[realistic scaling invalid micro memory].freeze
|
|
17
|
+
|
|
18
|
+
namespace :bench do
|
|
19
|
+
BENCHMARKS.each do |name|
|
|
20
|
+
desc "Run benchmark/#{name}.rb"
|
|
21
|
+
task(name) { ruby "benchmark/#{name}.rb" }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
desc "Run all benchmarks (see benchmark/README.md)"
|
|
26
|
+
task bench: BENCHMARKS.map { "bench:#{_1}" }
|
|
@@ -19,10 +19,7 @@ module Decoding
|
|
|
19
19
|
# @return [Decoding::Result<a>]
|
|
20
20
|
def call(value)
|
|
21
21
|
if value.is_a?(::Array)
|
|
22
|
-
value
|
|
23
|
-
.each_with_index
|
|
24
|
-
.map { |v, i| @decoder.call(v).map_err { _1.push(i) } }
|
|
25
|
-
.then { all _1 }
|
|
22
|
+
all(value.each_index) { |i| @decoder.call(value[i]).map_err { _1.push(i) } }
|
|
26
23
|
else
|
|
27
24
|
err(failure("expected Array, got #{value.class}"))
|
|
28
25
|
end
|
|
@@ -6,6 +6,12 @@ module Decoding
|
|
|
6
6
|
# Decoders are composable functions for deconstructing unknown input values
|
|
7
7
|
# into known output values.
|
|
8
8
|
module Decoders
|
|
9
|
+
# Matches the values {big_decimal} converts, in one check rather than
|
|
10
|
+
# trying a decoder for each type and discarding their failures.
|
|
11
|
+
DECIMAL_SOURCES = [::BigDecimal, ::Integer, ::Float, ::String].freeze
|
|
12
|
+
DECIMAL_SOURCE = ->(value) { DECIMAL_SOURCES.any? { _1 === value } }
|
|
13
|
+
private_constant :DECIMAL_SOURCES, :DECIMAL_SOURCE
|
|
14
|
+
|
|
9
15
|
module_function
|
|
10
16
|
|
|
11
17
|
# Decode a `BigDecimal` object, or a number or string describing one.
|
|
@@ -25,12 +31,9 @@ module Decoding
|
|
|
25
31
|
# @see Decoding::Decoders::MapErr
|
|
26
32
|
def big_decimal
|
|
27
33
|
Decoders.map_err(
|
|
28
|
-
Decoders.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
Decoders.map(Decoders.any(Decoders.integer, Decoders.float, Decoders.string)) { BigDecimal(_1) }
|
|
32
|
-
)
|
|
33
|
-
) { |number| number.finite? ? Decoders.succeed(number) : Decoders.fail("not a finite number") }
|
|
34
|
+
Decoders.map(Decoders.match(DECIMAL_SOURCE)) do |value|
|
|
35
|
+
BigDecimal(value).tap { |number| raise ArgumentError, "not a finite number" unless number.finite? }
|
|
36
|
+
end
|
|
34
37
|
) { |_message, value| "expected a decimal number, got #{value.inspect}" }
|
|
35
38
|
end
|
|
36
39
|
end
|
|
@@ -32,8 +32,8 @@ module Decoding
|
|
|
32
32
|
parse, description = date_format(format)
|
|
33
33
|
Decoders.map_err(
|
|
34
34
|
Decoders.any(
|
|
35
|
-
Decoders.
|
|
36
|
-
Decoders.
|
|
35
|
+
Decoders.map(Decoders.string) { parse.call(_1) },
|
|
36
|
+
Decoders.match(::Date)
|
|
37
37
|
)
|
|
38
38
|
) { |_message, value| "expected #{description}, got #{value.inspect}" }
|
|
39
39
|
end
|
|
@@ -21,20 +21,20 @@ module Decoding
|
|
|
21
21
|
def call(value)
|
|
22
22
|
return err(failure("expected Hash, got #{value.class}")) unless value.is_a?(::Hash)
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.call(k)
|
|
29
|
-
.map_err { _1.map { |msg| "invalid key: #{msg}" }.push(k) },
|
|
24
|
+
all(value) { |k, v| decode_entry(k, v) }.map(&:to_h)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
# Decode a single key and value into a pair, leaving the value alone
|
|
30
|
+
# when the key cannot be decoded.
|
|
31
|
+
def decode_entry(key, value)
|
|
32
|
+
@key_decoder
|
|
33
|
+
.call(key)
|
|
34
|
+
.map_err { _1.map { |msg| "invalid key: #{msg}" }.push(key) }
|
|
35
|
+
.and_then do |decoded_key|
|
|
36
|
+
@value_decoder.call(value).map_err { _1.push(key) }.map { [decoded_key, _1] }
|
|
37
|
+
end
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
end
|
|
@@ -22,7 +22,7 @@ module Decoding
|
|
|
22
22
|
# @return [Decoding::Result<b>]
|
|
23
23
|
def call(value)
|
|
24
24
|
Result
|
|
25
|
-
.all(@decoders
|
|
25
|
+
.all(@decoders) { _1.call(value) }
|
|
26
26
|
.map { @block.call(*_1) }
|
|
27
27
|
rescue StandardError => e
|
|
28
28
|
err(failure("error in map block: #{e.message}"))
|
|
@@ -11,22 +11,30 @@ module Decoding
|
|
|
11
11
|
# meaning of its own. Only when it fails to do so does this decoder treat
|
|
12
12
|
# `nil` as an absent value.
|
|
13
13
|
#
|
|
14
|
+
# Most decoders cannot decode `nil`, and building the failure that says so
|
|
15
|
+
# costs more than decoding most values. Since a decoder gives the same
|
|
16
|
+
# result for the same input, this decoder remembers when the given decoder
|
|
17
|
+
# has failed to decode `nil`, and stops asking it about `nil` from then on.
|
|
18
|
+
#
|
|
14
19
|
# @see Decoding::Decoders.optional
|
|
15
20
|
class Optional < Decoder
|
|
16
21
|
# @param decoder [Decoding::Decoder<a>]
|
|
17
22
|
def initialize(decoder)
|
|
18
23
|
@decoder = decoder.to_decoder
|
|
24
|
+
@fails_on_nil = false
|
|
19
25
|
super()
|
|
20
26
|
end
|
|
21
27
|
|
|
22
28
|
# @param value [Object]
|
|
23
29
|
# @return [Decoding::Result<a, nil>]
|
|
24
30
|
def call(value)
|
|
31
|
+
return ok(nil) if value.nil? && @fails_on_nil
|
|
32
|
+
|
|
25
33
|
result = @decoder.call(value)
|
|
26
|
-
return result if result.ok?
|
|
27
|
-
return ok(nil) if value.nil?
|
|
34
|
+
return result if result.ok? || !value.nil?
|
|
28
35
|
|
|
29
|
-
|
|
36
|
+
@fails_on_nil = true
|
|
37
|
+
ok(nil)
|
|
30
38
|
end
|
|
31
39
|
end
|
|
32
40
|
end
|
|
@@ -10,6 +10,10 @@ module Decoding
|
|
|
10
10
|
# the `Time` method that parses it.
|
|
11
11
|
TIME_FORMATS = %i[iso8601 xmlschema rfc2822 rfc822 httpdate parse].freeze
|
|
12
12
|
|
|
13
|
+
# Matches the numbers {Decoding::Decoders.unix_time} reads as a timestamp.
|
|
14
|
+
TIMESTAMP_NUMBER = ->(value) { value.is_a?(::Integer) || value.is_a?(::Float) }
|
|
15
|
+
private_constant :TIMESTAMP_NUMBER
|
|
16
|
+
|
|
13
17
|
# The units {Decoding::Decoders.unix_time} can read a timestamp in, mapped
|
|
14
18
|
# to the number of them that make up a second.
|
|
15
19
|
UNIX_TIME_UNITS = { seconds: 1, milliseconds: 1000 }.freeze
|
|
@@ -47,8 +51,8 @@ module Decoding
|
|
|
47
51
|
parse, description = time_format(format, zone)
|
|
48
52
|
Decoders.map_err(
|
|
49
53
|
Decoders.any(
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
parsed_by(parse),
|
|
55
|
+
Decoders.match(::Time)
|
|
52
56
|
)
|
|
53
57
|
) { |_message, value| "expected #{description}, got #{value.inspect}" }
|
|
54
58
|
end
|
|
@@ -98,21 +102,23 @@ module Decoding
|
|
|
98
102
|
raise ArgumentError, "unknown unit: #{unit.inspect}" unless UNIX_TIME_UNITS.key?(unit)
|
|
99
103
|
|
|
100
104
|
require_parser(zone, :at, "a unix timestamp")
|
|
105
|
+
per_second = UNIX_TIME_UNITS.fetch(unit)
|
|
106
|
+
# Cheapest checks first, so the most common inputs fail the fewest
|
|
107
|
+
# decoders on their way to the one that matches.
|
|
101
108
|
Decoders.map_err(
|
|
102
109
|
Decoders.any(
|
|
110
|
+
timestamp_in(Decoders.match(TIMESTAMP_NUMBER), zone, per_second),
|
|
103
111
|
Decoders.match(::Time),
|
|
104
|
-
timestamp_in(
|
|
112
|
+
timestamp_in(Decoders.any(Decoders.parsed_integer, Decoders.parsed_float), zone, per_second)
|
|
105
113
|
)
|
|
106
114
|
) { |_message, value| "expected a unix timestamp, got #{value.inspect}" }
|
|
107
115
|
end
|
|
108
116
|
|
|
109
|
-
# Decode
|
|
117
|
+
# Decode the number given by `decoder` as a point in time.
|
|
110
118
|
#
|
|
111
119
|
# @private
|
|
112
|
-
def timestamp_in(zone, per_second)
|
|
113
|
-
Decoders.map(
|
|
114
|
-
Decoders.any(Decoders.integer, Decoders.float, Decoders.parsed_integer, Decoders.parsed_float)
|
|
115
|
-
) { zone.at(per_second == 1 ? _1 : _1 / Rational(per_second)) }
|
|
120
|
+
def timestamp_in(decoder, zone, per_second)
|
|
121
|
+
Decoders.map(decoder) { zone.at(per_second == 1 ? _1 : _1 / Rational(per_second)) }
|
|
116
122
|
end
|
|
117
123
|
|
|
118
124
|
# Decode a string with the given parser, treating a nil answer as a failure.
|
|
@@ -123,9 +129,7 @@ module Decoding
|
|
|
123
129
|
#
|
|
124
130
|
# @private
|
|
125
131
|
def parsed_by(parse)
|
|
126
|
-
Decoders.
|
|
127
|
-
value.nil? ? Decoders.fail("parsed to nothing") : Decoders.succeed(value)
|
|
128
|
-
end
|
|
132
|
+
Decoders.map(Decoders.string) { parse.call(_1) || raise(ArgumentError, "parsed to nothing") }
|
|
129
133
|
end
|
|
130
134
|
|
|
131
135
|
# Refuse a zone that cannot answer the parsing this decoder will ask of it,
|
|
@@ -18,8 +18,8 @@ module Decoding
|
|
|
18
18
|
def uri
|
|
19
19
|
Decoders.map_err(
|
|
20
20
|
Decoders.any(
|
|
21
|
-
Decoders
|
|
22
|
-
Decoders.
|
|
21
|
+
Decoders.map(Decoders.string) { ::URI.parse(_1) },
|
|
22
|
+
Decoders::Match.new(::URI::Generic)
|
|
23
23
|
)
|
|
24
24
|
) { |_msg, value| "expected a URI, got #{value.inspect}" }
|
|
25
25
|
end
|
data/lib/decoding/decoders.rb
CHANGED
|
@@ -128,7 +128,7 @@ module Decoding
|
|
|
128
128
|
# decode(boolean, true) # => Decoding::Ok(true)
|
|
129
129
|
# decode(boolean, false) # => Decoding::Ok(false)
|
|
130
130
|
# @return [Decoding::Decoder<Boolean>]
|
|
131
|
-
def boolean = map_err(
|
|
131
|
+
def boolean = map_err(enum(true, false)) { |_msg, value| "expected true or false, got #{value.class}" }
|
|
132
132
|
|
|
133
133
|
# Decode a String value into a symbol.
|
|
134
134
|
#
|
data/lib/decoding/result.rb
CHANGED
|
@@ -30,15 +30,29 @@ module Decoding
|
|
|
30
30
|
# Collapse array of result values into a single result, or return the
|
|
31
31
|
# first `Err` value.
|
|
32
32
|
#
|
|
33
|
+
# Given a block, each item is mapped to a result with the block first,
|
|
34
|
+
# one at a time, and items after the first `Err` are not mapped at all.
|
|
35
|
+
#
|
|
33
36
|
# @example
|
|
34
37
|
# all(Ok(1), Ok(2)) # => Ok([1, 2])
|
|
35
38
|
# all(Ok(1), Err("error")) # => Err("error")
|
|
36
|
-
#
|
|
37
|
-
# @
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
# all([1, 2]) { Ok(_1 * 10) } # => Ok([10, 20])
|
|
40
|
+
# @overload all(results)
|
|
41
|
+
# @param results [Enumerable<Result<a>>]
|
|
42
|
+
# @overload all(items)
|
|
43
|
+
# @param items [Enumerable<Object>]
|
|
44
|
+
# @yieldparam item [Object]
|
|
45
|
+
# @yieldreturn [Result<a>]
|
|
46
|
+
# @return [Result<Array<a>>]
|
|
47
|
+
def self.all(items)
|
|
48
|
+
values = []
|
|
49
|
+
items.each_entry do |item|
|
|
50
|
+
result = (block_given? ? yield(item) : item).to_result
|
|
51
|
+
return result if result.err?
|
|
52
|
+
|
|
53
|
+
values << result.unwrap(nil)
|
|
41
54
|
end
|
|
55
|
+
ok(values)
|
|
42
56
|
end
|
|
43
57
|
|
|
44
58
|
private_class_method :new
|
|
@@ -48,12 +62,15 @@ module Decoding
|
|
|
48
62
|
|
|
49
63
|
protected :value
|
|
50
64
|
|
|
65
|
+
# Computed when asked for rather than up front: results are rarely used as
|
|
66
|
+
# hash keys, and hashing a large decoded value for every result created
|
|
67
|
+
# made decoding a collection quadratic.
|
|
68
|
+
#
|
|
51
69
|
# @private
|
|
52
|
-
|
|
70
|
+
def hash = [self.class, value].hash
|
|
53
71
|
|
|
54
72
|
def initialize(value)
|
|
55
73
|
@value = value
|
|
56
|
-
@hash = [self.class, value].hash
|
|
57
74
|
freeze
|
|
58
75
|
end
|
|
59
76
|
|
data/lib/decoding/version.rb
CHANGED