decoding 0.1.0 → 0.2.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/.tool-versions +1 -1
- data/CHANGELOG.md +8 -0
- data/README.md +119 -96
- data/lib/decoding/decoder.rb +6 -0
- data/lib/decoding/decoders/any.rb +2 -4
- data/lib/decoding/decoders/array.rb +1 -1
- data/lib/decoding/decoders/at.rb +30 -0
- data/lib/decoding/decoders/field.rb +1 -1
- data/lib/decoding/decoders/hash.rb +2 -2
- data/lib/decoding/decoders/match.rb +2 -2
- data/lib/decoding/decoders/pass.rb +9 -0
- data/lib/decoding/decoders.rb +55 -1
- data/lib/decoding/failure.rb +43 -0
- data/lib/decoding/version.rb +1 -1
- data/lib/decoding.rb +1 -1
- metadata +6 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3f462705ab2cc82507ed66b66274dd6f9167ade6b3bab3e8d12dc329f8f564fb
|
|
4
|
+
data.tar.gz: 2a16abba19d6b9f8cb68d7db943e55627c439925b40090f96dc3c8b6dc27e65e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 918dae4e293d0585afd44aa092fd95a295d3ed5153fd9a5f4acf041a551fdc97a8ca14f030f038c7507f0b7de799b6cb6076dcbb3bac8b18f024e45ed05fd068
|
|
7
|
+
data.tar.gz: 818550ca01578f9ba869bf54bb875f962ac22ee179f12c23e7565ab4923e733aaa629325561c73f5c1659c290857200b5dd1bb3b3d4eafad00e445c8ff291385
|
data/.tool-versions
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
ruby 3.
|
|
1
|
+
ruby 3.4.7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2025-10-25
|
|
4
|
+
|
|
5
|
+
* Added `decode_hash`, `regexp` and `original` decoders
|
|
6
|
+
* Fixed incorrect `Decoding::Failure` value comparisons
|
|
7
|
+
* Fixed some decoders incorrectly returning `String` values rather than
|
|
8
|
+
`Decoding::Failure` values
|
|
9
|
+
* Bumped target Ruby version to 3.4.7
|
|
10
|
+
|
|
3
11
|
## [0.1.0] - 2024-05-08
|
|
4
12
|
|
|
5
13
|
- Initial release
|
data/README.md
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
# Decoding
|
|
2
2
|
|
|
3
|
-
Decoding is a library to help transform unknown external data into neat values
|
|
4
|
-
with known shapes.
|
|
3
|
+
Decoding is a library to help transform unknown external data into neat values with known shapes.
|
|
5
4
|
|
|
6
5
|
## Installation
|
|
7
6
|
|
|
8
|
-
TODO: Replace `decoding` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
|
9
|
-
|
|
10
7
|
Install the gem and add to the application's Gemfile by executing:
|
|
11
8
|
|
|
12
9
|
$ bundle add decoding
|
|
@@ -17,111 +14,137 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
|
17
14
|
|
|
18
15
|
## Usage
|
|
19
16
|
|
|
20
|
-
Decoding is a library to help transform unknown external data into neat values
|
|
21
|
-
with known shapes. Consider calling an HTTP API: you might pull in whatever
|
|
22
|
-
value. After passing it through decoder, you will have a value with a known
|
|
23
|
-
shape -- or a sensible error message.
|
|
17
|
+
Decoding is a library to help transform unknown external data into neat values with known shapes. Consider calling an HTTP API: you might pull in whatever value. After passing it through decoder, you will have a value with a known shape -- or a sensible error message.
|
|
24
18
|
|
|
25
19
|
For example, call an API to get some JSON value:
|
|
26
20
|
|
|
27
|
-
|
|
21
|
+
```ruby
|
|
22
|
+
body = JSON.parse(Net::HTTP.get("https://api.placeholderjson.dev/shipments/7EBWXB5"))
|
|
23
|
+
```
|
|
28
24
|
|
|
29
|
-
How do you safely work with `body`? If parsing the response body as JSON has
|
|
30
|
-
worked, you know you have some kind of Ruby value -- but you're not sure of
|
|
31
|
-
its structure. This can lead to cryptic error messages far removing of making
|
|
32
|
-
this HTTP call where values are of unexpected types, hashes turn out not to
|
|
33
|
-
have certain keys or the nesting of data is different from what you expected.
|
|
25
|
+
How do you safely work with `body`? If parsing the response body as JSON has worked, you know you have some kind of Ruby value -- but you're not sure of its structure. This can lead to cryptic error messages far removing of making this HTTP call where values are of unexpected types, hashes turn out not to have certain keys or the nesting of data is different from what you expected.
|
|
34
26
|
|
|
35
27
|
Assume the response body, parsed as JSON, results in a value like this:
|
|
36
28
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
when reality does not match our expectations.
|
|
29
|
+
```ruby
|
|
30
|
+
{
|
|
31
|
+
"orderID" => "7EBWXB5",
|
|
32
|
+
"orderDate" => "1595674680",
|
|
33
|
+
"estimatedDeliveryDate" => "1596365935",
|
|
34
|
+
"deliveryDate" => null,
|
|
35
|
+
"delayed" => false,
|
|
36
|
+
"status" => {
|
|
37
|
+
"orderPlaced" => true,
|
|
38
|
+
"orderShipped" => true,
|
|
39
|
+
"outForDelivery" => true,
|
|
40
|
+
"orderDelivered" => false
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
We can use decoders to extract exactly those pieces from this payload that we need, making assertions along the way of what the data looks like and generating helpful errors when reality does not match our expectations.
|
|
54
46
|
|
|
55
47
|
For example, we could parse the above payload like so:
|
|
56
48
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
composed together into new, more complex decoders.
|
|
76
|
-
|
|
77
|
-
A decoder is, in essence, a function that returns a result based on an input value. Consider
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
extract a `time_decoder` from the example above:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
input using that value:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
49
|
+
```ruby
|
|
50
|
+
Order = Data.define(:id, :date, :status)
|
|
51
|
+
D = Decoding::Decoders
|
|
52
|
+
|
|
53
|
+
time_decoder = D.map(D.string) { Time.at(_1.to_i) }
|
|
54
|
+
order_decoder = D.map(
|
|
55
|
+
D.field("orderID", D.string),
|
|
56
|
+
D.field("orderDate", time_decoder),
|
|
57
|
+
D.hash(D.string, D.boolean)
|
|
58
|
+
) { Order.new(*args) }
|
|
59
|
+
|
|
60
|
+
Decoding.decode(order_decoder, body)
|
|
61
|
+
# => Decoding::Ok(#<data Order
|
|
62
|
+
id: '7EBWXB5',
|
|
63
|
+
date: 2020-07-25 12:58:00 +0200,
|
|
64
|
+
status: {"orderPlaced"=>true,"orderShipped"=>true,"outForDelivery"=>true,"orderDelivered"=>false}>)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Decoders take an input value and generate an output value from it. There are decoders for basic Ruby types, compound types such as arrays and hashes, decoders for trying out various decoders and, finally, there is the `map` decoder for decoding one or more output values from a given input value and applying a transformation to them with a block. All these decoders can be composed together into new, more complex decoders.
|
|
68
|
+
|
|
69
|
+
A decoder is, in essence, a function that returns a result based on an input value. Consider how, roughly, the `string` decoder is implemented:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
string_decoder = ->(input_value) do
|
|
73
|
+
if input_value.is_a?(String)
|
|
74
|
+
Decoding::Result.ok(input_value)
|
|
75
|
+
else
|
|
76
|
+
Decoding::Result.err("expected String, got #{input_value.class}")
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
You can use the base decoders along with `map` to write more complex decoder. For example, you could extract a `time_decoder` from the example above:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
time_decoder = D.map(D.string) { Time.at(_1.to_i) }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
When the shape of the incoming data is unknown, you can try out various decoders in a row to find the first that succeeds using `any`:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
string_or_integer = D.any(D.string, D.integer)
|
|
91
|
+
Decoding.decode(string_or_integer, 1) # => Decoding::Ok(1)
|
|
92
|
+
Decoding.decode(string_or_integer, '1') # => Decoding::Ok('1')
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
You can also base one decoder on a previously decoded value. For example, a payload might contain a version number describing its format. Use `and_then` to decode one value and then construct a new decoder to run against the same input using that value:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
multiple_version_decoder = D.and_then(D.field("version", D.string)) do |version|
|
|
99
|
+
if version == "1"
|
|
100
|
+
D.field("name", D.string)
|
|
101
|
+
else
|
|
102
|
+
D.field("fullName", D.string)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
```
|
|
112
106
|
|
|
113
107
|
Now, you have a decoder that can work inputs using format version 1 and 2:
|
|
114
108
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
failed. The `Ok` values contain the decoded result, while the `Err` values
|
|
123
|
-
|
|
124
|
-
|
|
109
|
+
```ruby
|
|
110
|
+
Decoding.decode(multiple_version_decoder, "version" => "1", "name" => "John")
|
|
111
|
+
# => "John"
|
|
112
|
+
Decoding.decode(multiple_version_decoder, "version" => "2", "fullName" => "Paul")
|
|
113
|
+
# => "Paul"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The return values of decoding are `Decoding::Result` values, which come in `Ok` and `Err` subclasses. These describe how the decoding either succeeded or failed. The `Ok` values contain the decoded result, while the `Err` values always contain a string error message. It is up to you, as a developer, to decide how to deal with unsuccessful decoding.
|
|
117
|
+
|
|
118
|
+
## Available decoders
|
|
119
|
+
|
|
120
|
+
The following decoders are included:
|
|
121
|
+
|
|
122
|
+
* Basic types
|
|
123
|
+
* `string`
|
|
124
|
+
* `integer`
|
|
125
|
+
* `float`
|
|
126
|
+
* `numeric`
|
|
127
|
+
* `nil`
|
|
128
|
+
* `true`
|
|
129
|
+
* `false`
|
|
130
|
+
* `boolean`
|
|
131
|
+
* `symbol`
|
|
132
|
+
* `regexp`
|
|
133
|
+
* Utility decoders
|
|
134
|
+
* `succeed`
|
|
135
|
+
* `fail`
|
|
136
|
+
* `original`
|
|
137
|
+
* `map`
|
|
138
|
+
* `decode_hash`
|
|
139
|
+
* `and_then`
|
|
140
|
+
* Compound decoders
|
|
141
|
+
* `any`
|
|
142
|
+
* `optional`
|
|
143
|
+
* `field`
|
|
144
|
+
* `array`
|
|
145
|
+
* `index`
|
|
146
|
+
* `hash`
|
|
147
|
+
* `at`
|
|
125
148
|
|
|
126
149
|
## Development
|
|
127
150
|
|
data/lib/decoding/decoder.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "forwardable"
|
|
4
4
|
require_relative "result"
|
|
5
|
+
require_relative "failure"
|
|
5
6
|
|
|
6
7
|
module Decoding
|
|
7
8
|
# A decoder is a callable object that reads any input value and returns an
|
|
@@ -10,6 +11,7 @@ module Decoding
|
|
|
10
11
|
# @abstract
|
|
11
12
|
class Decoder
|
|
12
13
|
extend Forwardable
|
|
14
|
+
|
|
13
15
|
def_delegators "Decoding::Result", :all, :ok, :err
|
|
14
16
|
|
|
15
17
|
# @param value [Object]
|
|
@@ -18,5 +20,9 @@ module Decoding
|
|
|
18
20
|
|
|
19
21
|
# @return [Decoding::Decoder<a>]
|
|
20
22
|
def to_decoder = self
|
|
23
|
+
|
|
24
|
+
# @param str [String]
|
|
25
|
+
# @return [Decoding::Failure]
|
|
26
|
+
def failure(str) = Decoding::Failure.new(str)
|
|
21
27
|
end
|
|
22
28
|
end
|
|
@@ -10,9 +10,6 @@ module Decoding
|
|
|
10
10
|
#
|
|
11
11
|
# @see Decoding::Decoders.any
|
|
12
12
|
class Any < Decoder
|
|
13
|
-
# @private
|
|
14
|
-
Err = Result.err("None of the decoders matched")
|
|
15
|
-
|
|
16
13
|
# @param decoder [Decoding::Decoder<Object>]
|
|
17
14
|
# @param decoders [Decoding::Decoder<Object>]
|
|
18
15
|
def initialize(decoder, *decoders)
|
|
@@ -23,7 +20,8 @@ module Decoding
|
|
|
23
20
|
# @param value [Object]
|
|
24
21
|
# @return [Decoding::Result<Object>]
|
|
25
22
|
def call(value)
|
|
26
|
-
|
|
23
|
+
err_proc = -> { err(failure("None of the decoders matched")) }
|
|
24
|
+
@decoders.lazy.map { _1.call(value) }.find(err_proc, &:ok?)
|
|
27
25
|
end
|
|
28
26
|
end
|
|
29
27
|
end
|
|
@@ -21,7 +21,7 @@ module Decoding
|
|
|
21
21
|
if value.is_a?(::Array)
|
|
22
22
|
value
|
|
23
23
|
.each_with_index
|
|
24
|
-
.map { |v, i| @decoder.call(v).map_err {
|
|
24
|
+
.map { |v, i| @decoder.call(v).map_err { _1.push(i) } }
|
|
25
25
|
.then { all _1 }
|
|
26
26
|
else
|
|
27
27
|
err("expected an Array, got: #{value.class}")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../decoder"
|
|
4
|
+
|
|
5
|
+
module Decoding
|
|
6
|
+
module Decoders
|
|
7
|
+
# Decode a value in deeply nested hashes.
|
|
8
|
+
#
|
|
9
|
+
# @see Decoding::Decoders.field
|
|
10
|
+
class At < Decoder
|
|
11
|
+
# @param keys [Array<Object>]
|
|
12
|
+
# @param decoder [Decoding::Decoder<a>]
|
|
13
|
+
def initialize(*keys, decoder)
|
|
14
|
+
@keys = keys.to_a
|
|
15
|
+
@decoder = decoder.to_decoder
|
|
16
|
+
super()
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param value [Object]
|
|
20
|
+
# @return [Deceoding::Result<a>]
|
|
21
|
+
def call(value)
|
|
22
|
+
first, *rest = @keys.reverse
|
|
23
|
+
nested_decoder = rest.reduce(Decoders::Field.new(first, @decoder)) do |acc, k|
|
|
24
|
+
Decoders::Field.new(k, acc)
|
|
25
|
+
end
|
|
26
|
+
nested_decoder.call(value)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -25,11 +25,11 @@ module Decoding
|
|
|
25
25
|
[
|
|
26
26
|
@key_decoder
|
|
27
27
|
.call(k)
|
|
28
|
-
.map_err { |e| "error decoding key #{k.inspect}: #{e}" },
|
|
28
|
+
.map_err { |e| failure("error decoding key #{k.inspect}: #{e}") },
|
|
29
29
|
|
|
30
30
|
@value_decoder
|
|
31
31
|
.call(v)
|
|
32
|
-
.map_err { |e| "error decoding value for key #{k.inspect}: #{e}" }
|
|
32
|
+
.map_err { |e| failure("error decoding value for key #{k.inspect}: #{e}") }
|
|
33
33
|
]
|
|
34
34
|
)
|
|
35
35
|
end
|
|
@@ -19,9 +19,9 @@ module Decoding
|
|
|
19
19
|
if @pattern === value
|
|
20
20
|
ok(value)
|
|
21
21
|
elsif @pattern.is_a?(Class)
|
|
22
|
-
err("expected #{@pattern}, got #{value.class}")
|
|
22
|
+
err(failure("expected #{@pattern}, got #{value.class}"))
|
|
23
23
|
else
|
|
24
|
-
err("expected value matching #{@pattern.inspect}, got: #{value.inspect}")
|
|
24
|
+
err(failure("expected value matching #{@pattern.inspect}, got: #{value.inspect}"))
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
end
|
data/lib/decoding/decoders.rb
CHANGED
|
@@ -8,6 +8,8 @@ require_relative "decoders/array"
|
|
|
8
8
|
require_relative "decoders/index"
|
|
9
9
|
require_relative "decoders/hash"
|
|
10
10
|
require_relative "decoders/and_then"
|
|
11
|
+
require_relative "decoders/at"
|
|
12
|
+
require_relative "decoders/pass"
|
|
11
13
|
require_relative "result"
|
|
12
14
|
|
|
13
15
|
module Decoding
|
|
@@ -26,6 +28,13 @@ module Decoding
|
|
|
26
28
|
# @see Decoding::Decoders::Match
|
|
27
29
|
def string = Decoders::Match.new(String)
|
|
28
30
|
|
|
31
|
+
# Decode any string value that matches a regular expression.
|
|
32
|
+
#
|
|
33
|
+
# @param re [Regexp, String]
|
|
34
|
+
# @return [Decoding::Decoder<String>]
|
|
35
|
+
# @see Decoding::Decoders::Match
|
|
36
|
+
def regexp(re) = Decoders::Match.new(Regexp.new(re))
|
|
37
|
+
|
|
29
38
|
# Decode any integer value.
|
|
30
39
|
#
|
|
31
40
|
# @example
|
|
@@ -104,7 +113,14 @@ module Decoding
|
|
|
104
113
|
# @example
|
|
105
114
|
# decode(fail("oh no"), "foo") # => Decoding::Err("oh no")
|
|
106
115
|
# @return [Decoding::Decoder<String>]
|
|
107
|
-
def fail(value) = ->(_) { Result.err(value) }
|
|
116
|
+
def fail(value) = ->(_) { Result.err(Decoding::Failure.new(value)) }
|
|
117
|
+
|
|
118
|
+
# A decoder that returns the input value, unaltered.
|
|
119
|
+
#
|
|
120
|
+
# @example
|
|
121
|
+
# decode(original, [1, 2]) # => Decoding::Ok([1, 2])
|
|
122
|
+
# @return [Decoding::Decoder<Object>]
|
|
123
|
+
def original = Decoders::Pass.new
|
|
108
124
|
|
|
109
125
|
# @!group Compound decoders
|
|
110
126
|
|
|
@@ -199,6 +215,27 @@ module Decoding
|
|
|
199
215
|
# @see Decoding::Decoders::Hash
|
|
200
216
|
def hash(...) = Decoders::Hash.new(...)
|
|
201
217
|
|
|
218
|
+
# Decode a value into a hash using multiple decoders.
|
|
219
|
+
#
|
|
220
|
+
# This is a shortcut for:
|
|
221
|
+
#
|
|
222
|
+
# deocde(map(field("id", integer), field("name", string)) { |id, name|
|
|
223
|
+
# { id:, name: }
|
|
224
|
+
# }, { "id" => 1, "name" => "John" })
|
|
225
|
+
# # => Decoding::Ok({ id: 1, name: "John" })
|
|
226
|
+
#
|
|
227
|
+
# @example
|
|
228
|
+
# decode(decode_hash(
|
|
229
|
+
# id: field("id", integer)
|
|
230
|
+
# ), { "id" => 1 })
|
|
231
|
+
# # => Decode::Ok({ id: 1 })
|
|
232
|
+
# @return Decoding::Decoder
|
|
233
|
+
def decode_hash(decoders)
|
|
234
|
+
map(*decoders.values) do |*values|
|
|
235
|
+
decoders.keys.zip(values).to_h
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
202
239
|
# Create a decoder that depends on a previously decoded value.
|
|
203
240
|
#
|
|
204
241
|
# @example
|
|
@@ -220,5 +257,22 @@ module Decoding
|
|
|
220
257
|
# @return [Decoding::Decoder<b>]
|
|
221
258
|
# @see Decoding::Decoders::AndThen
|
|
222
259
|
def and_then(...) = Decoders::AndThen.new(...)
|
|
260
|
+
|
|
261
|
+
# Decode deeply-nested fields.
|
|
262
|
+
#
|
|
263
|
+
# @example
|
|
264
|
+
# decoder = at('a', 'b', 'c', string)
|
|
265
|
+
# decode(decoder, { "a" => { "b" => { "c" => "d" } } })
|
|
266
|
+
# # => Decoding::Ok("d")
|
|
267
|
+
# decode(decoder, { "a" => { "b" => "d" } })
|
|
268
|
+
# # => Decoding::Err("Error at .a.b: expected a Hash, got String")
|
|
269
|
+
# @overload at(*fields, decoder)
|
|
270
|
+
# @param fields [String]
|
|
271
|
+
# @param decoder [Decoding::Decoder<a>]
|
|
272
|
+
# @return [Decoding::Decoder<a>]
|
|
273
|
+
# @see Decoding::Decoders::At
|
|
274
|
+
def at(...)
|
|
275
|
+
Decoders::At.new(...)
|
|
276
|
+
end
|
|
223
277
|
end
|
|
224
278
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decoding
|
|
4
|
+
class Failure
|
|
5
|
+
protected attr_reader :msg
|
|
6
|
+
protected attr_reader :path
|
|
7
|
+
|
|
8
|
+
# @paramn msg [String]
|
|
9
|
+
def initialize(msg)
|
|
10
|
+
@msg = msg
|
|
11
|
+
@path = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def eql?(other)
|
|
15
|
+
other.is_a?(self.class) &&
|
|
16
|
+
msg == other.msg &&
|
|
17
|
+
path == other.path
|
|
18
|
+
end
|
|
19
|
+
alias == eql?
|
|
20
|
+
|
|
21
|
+
# Add segments to the stack of errors.
|
|
22
|
+
#
|
|
23
|
+
# This is useful to create clearer error messages when using compound
|
|
24
|
+
# decoders, such as `array(string)`. If the `string` decoder fails with an
|
|
25
|
+
# error, the `array` decoder can push `3` to the stack to indicate that
|
|
26
|
+
# happened at index 3 in its input value.
|
|
27
|
+
#
|
|
28
|
+
# @param segment [String]
|
|
29
|
+
# @return [Decoding::Failure]
|
|
30
|
+
def push(segment)
|
|
31
|
+
@path << segment
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_s
|
|
36
|
+
if @path.any?
|
|
37
|
+
"Error at .#{@path.reverse.join(".")}: #{@msg}"
|
|
38
|
+
else
|
|
39
|
+
@msg
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/decoding/version.rb
CHANGED
data/lib/decoding.rb
CHANGED
metadata
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: decoding
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Arjan van der Gaag
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies: []
|
|
13
|
-
description:
|
|
14
12
|
email:
|
|
15
13
|
- arjan@arjanvandergaag.nl
|
|
16
14
|
executables: []
|
|
@@ -32,11 +30,14 @@ files:
|
|
|
32
30
|
- lib/decoding/decoders/and_then.rb
|
|
33
31
|
- lib/decoding/decoders/any.rb
|
|
34
32
|
- lib/decoding/decoders/array.rb
|
|
33
|
+
- lib/decoding/decoders/at.rb
|
|
35
34
|
- lib/decoding/decoders/field.rb
|
|
36
35
|
- lib/decoding/decoders/hash.rb
|
|
37
36
|
- lib/decoding/decoders/index.rb
|
|
38
37
|
- lib/decoding/decoders/map.rb
|
|
39
38
|
- lib/decoding/decoders/match.rb
|
|
39
|
+
- lib/decoding/decoders/pass.rb
|
|
40
|
+
- lib/decoding/failure.rb
|
|
40
41
|
- lib/decoding/result.rb
|
|
41
42
|
- lib/decoding/version.rb
|
|
42
43
|
- sig/decoding.rbs
|
|
@@ -48,7 +49,6 @@ metadata:
|
|
|
48
49
|
homepage_uri: https://github.com/avdgaag/decoding
|
|
49
50
|
source_code_uri: https://github.com/avdgaag/decoding
|
|
50
51
|
rubygems_mfa_required: 'true'
|
|
51
|
-
post_install_message:
|
|
52
52
|
rdoc_options: []
|
|
53
53
|
require_paths:
|
|
54
54
|
- lib
|
|
@@ -63,8 +63,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
63
63
|
- !ruby/object:Gem::Version
|
|
64
64
|
version: '0'
|
|
65
65
|
requirements: []
|
|
66
|
-
rubygems_version: 3.
|
|
67
|
-
signing_key:
|
|
66
|
+
rubygems_version: 3.6.9
|
|
68
67
|
specification_version: 4
|
|
69
68
|
summary: Decode dynamic values into known Ruby data structures
|
|
70
69
|
test_files: []
|