decoding 0.2.6 → 0.4.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 +1 -1
- data/CHANGELOG.md +26 -0
- data/README.md +173 -3
- data/lib/decoding/data.rb +53 -0
- data/lib/decoding/decoders/any.rb +5 -1
- data/lib/decoding/decoders/array.rb +1 -1
- data/lib/decoding/decoders/big_decimal.rb +37 -0
- data/lib/decoding/decoders/date.rb +57 -0
- data/lib/decoding/decoders/enum.rb +47 -0
- data/lib/decoding/decoders/field.rb +2 -2
- data/lib/decoding/decoders/hash.rb +2 -2
- data/lib/decoding/decoders/index.rb +1 -4
- data/lib/decoding/decoders/lazy.rb +31 -0
- data/lib/decoding/decoders/map_err.rb +31 -0
- data/lib/decoding/decoders/match.rb +1 -1
- data/lib/decoding/decoders/optional.rb +33 -0
- data/lib/decoding/decoders/optional_field.rb +35 -0
- data/lib/decoding/decoders/succeed.rb +8 -3
- data/lib/decoding/decoders/time.rb +91 -0
- data/lib/decoding/decoders/uri.rb +27 -0
- data/lib/decoding/decoders.rb +151 -5
- data/lib/decoding/env.rb +70 -0
- data/lib/decoding/error.rb +6 -0
- data/lib/decoding/failure.rb +42 -4
- data/lib/decoding/result.rb +3 -1
- data/lib/decoding/version.rb +1 -1
- data/lib/decoding.rb +16 -2
- metadata +13 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0af816a4ee858fb01b7affa44921ddb05b38199a2fbdee33f9439342d8c5dee7
|
|
4
|
+
data.tar.gz: 18511340412ca51970844209ed9d878a2f46b32ffec15f1c9862c97a540c8921
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 044227d25ff8af0830443a267b86455de69ba14c72fa37bed1b5c84080d5f1156aeb533cceb46d5e8cce7b57a6d2a55a85729fb652b36bb747dc6b9f704ef6ef
|
|
7
|
+
data.tar.gz: f957565332b96ca5fa607fb12ecebfc0bc3379beb6aa154760b70f14f92f59b4c8fbb39e235621e020473f49d68e20fe7909c8a0f6bcef1e091cd7d6f02b6a99
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.4.0]
|
|
4
|
+
|
|
5
|
+
* Add `uri` decoder for URI objects and strings that can be parsed as one, available after `require "decoding/decoders/uri"`
|
|
6
|
+
* Add `map_err` decoder for replacing the error message of a failed decoder
|
|
7
|
+
* Add `Decoding.decode!` for decoding a value, raising `Decoding::UnwrapError` when decoding fails
|
|
8
|
+
* Add `lazy` decoder, making recursive decoders possible
|
|
9
|
+
* Add `match` decoder for matching a value against any pattern using `===`
|
|
10
|
+
* Add `enum` decoder for a value that must be one of a fixed set of values
|
|
11
|
+
* Add `optional_field` decoder for a key that may be absent from a hash
|
|
12
|
+
* Add `time` and `date` decoders, available after `require "decoding/decoders/time"` and `require "decoding/decoders/date"`
|
|
13
|
+
* Add `big_decimal` decoder, available after `require "decoding/decoders/big_decimal"`
|
|
14
|
+
* Add `unix_time` decoder for timestamps, alongside the `time` decoder
|
|
15
|
+
* Add `parsed_integer`, `parsed_float` and `parsed_boolean` decoders for reading typed values out of strings
|
|
16
|
+
* Add `Decoding.env` for decoding a single environment variable, available after `require "decoding/env"`
|
|
17
|
+
* Raise `Decoding::UnwrapError` as a `Decoding::Error`
|
|
18
|
+
* Remove the unused `Decoding::Decoders::Index::Err` constant
|
|
19
|
+
* Report the location of errors nested inside a `hash` decoder as a path, like `field` and `array` do, rather than flattening it into the error message.
|
|
20
|
+
* Give the `boolean` decoder its own error message, rather than reporting the failures of the decoders it is built from
|
|
21
|
+
* Use consistent phrasing for the error messages of the `match`, `field`, `array` and `index` decoders
|
|
22
|
+
* Report the failure of the given decoder from the `optional` decoder, rather than also reporting that the value was not `nil`
|
|
23
|
+
* Report the location shared by all failures of an `any` decoder once, instead of repeating it in every collected message
|
|
24
|
+
|
|
25
|
+
## [0.3.0]
|
|
26
|
+
|
|
27
|
+
* Add `Decoding::Data` for creating decodable data classes
|
|
28
|
+
|
|
3
29
|
## [0.2.6]
|
|
4
30
|
|
|
5
31
|
* Add `Decoding::Result#unwrap!`
|
data/README.md
CHANGED
|
@@ -31,7 +31,7 @@ Assume the response body, parsed as JSON, results in a value like this:
|
|
|
31
31
|
"orderID" => "7EBWXB5",
|
|
32
32
|
"orderDate" => "1595674680",
|
|
33
33
|
"estimatedDeliveryDate" => "1596365935",
|
|
34
|
-
"deliveryDate" =>
|
|
34
|
+
"deliveryDate" => nil,
|
|
35
35
|
"delayed" => false,
|
|
36
36
|
"status" => {
|
|
37
37
|
"orderPlaced" => true,
|
|
@@ -54,8 +54,8 @@ time_decoder = D.map(D.string) { Time.at(_1.to_i) }
|
|
|
54
54
|
order_decoder = D.map(
|
|
55
55
|
D.field("orderID", D.string),
|
|
56
56
|
D.field("orderDate", time_decoder),
|
|
57
|
-
D.hash(D.string, D.boolean)
|
|
58
|
-
) { Order.new(*args) }
|
|
57
|
+
D.field("status", D.hash(D.string, D.boolean))
|
|
58
|
+
) { |*args| Order.new(*args) }
|
|
59
59
|
|
|
60
60
|
Decoding.decode(order_decoder, body)
|
|
61
61
|
# => Decoding::Ok(#<data Order
|
|
@@ -84,6 +84,8 @@ You can use the base decoders along with `map` to write more complex decoder. Fo
|
|
|
84
84
|
time_decoder = D.map(D.string) { Time.at(_1.to_i) }
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
(For unix timestamps specifically there is a `unix_time` decoder, described under optional decoders below; the point here is that `map` lets you build one yourself.)
|
|
88
|
+
|
|
87
89
|
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
90
|
|
|
89
91
|
```ruby
|
|
@@ -92,6 +94,31 @@ Decoding.decode(string_or_integer, 1) # => Decoding::Ok(1)
|
|
|
92
94
|
Decoding.decode(string_or_integer, '1') # => Decoding::Ok('1')
|
|
93
95
|
```
|
|
94
96
|
|
|
97
|
+
A value that might be absent is decoded with `optional`. It hands the value to the given decoder first, so that decoder can give `nil` a meaning of its own, and only falls back to `nil` when the decoder cannot handle it:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
optional_name = D.optional(D.string)
|
|
101
|
+
Decoding.decode(optional_name, "John") # => Decoding::Ok("John")
|
|
102
|
+
Decoding.decode(optional_name, nil) # => Decoding::Ok(nil)
|
|
103
|
+
Decoding.decode(optional_name, 123) # => Decoding::Err("expected String, got Integer")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Note how the failure is the one reported by the given decoder: since you asked for an optional string, being told the value was not `nil` either adds nothing.
|
|
107
|
+
|
|
108
|
+
Decoders that refer to themselves need `lazy`, which defers building the decoder until there is a value to decode. Without it, building the decoder would recurse endlessly:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
def tree
|
|
112
|
+
D.decode_hash(
|
|
113
|
+
name: D.field("name", D.string),
|
|
114
|
+
children: D.field("children", D.array(D.lazy { tree }))
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
Decoding.decode(tree, { "name" => "a", "children" => [{ "name" => "b", "children" => [] }] })
|
|
119
|
+
# => Decoding::Ok({ name: "a", children: [{ name: "b", children: [] }] })
|
|
120
|
+
```
|
|
121
|
+
|
|
95
122
|
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
123
|
|
|
97
124
|
```ruby
|
|
@@ -115,6 +142,49 @@ Decoding.decode(multiple_version_decoder, "version" => "2", "fullName" => "Paul"
|
|
|
115
142
|
|
|
116
143
|
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
144
|
|
|
145
|
+
When you would rather not handle failure explicitly, `decode!` returns the decoded value itself and raises `Decoding::UnwrapError` when decoding fails:
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
Decoding.decode!(D.string, "foo") # => "foo"
|
|
149
|
+
Decoding.decode!(D.field("name", D.string), { "name" => 123 })
|
|
150
|
+
# raises Decoding::UnwrapError: Error at .name: expected String, got Integer
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Error messages
|
|
154
|
+
|
|
155
|
+
Decoders that reach into a value -- `field`, `at`, `array`, `index` and `hash` -- record where in the input the error occurred, so a failure deep inside a nested structure still tells you how to find it:
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
Decoding.decode(D.at("a", "b", D.string), { "a" => { "b" => 1 } })
|
|
159
|
+
# => Decoding::Err("Error at .a.b: expected String, got Integer")
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
A decoder composed of other decoders reports the failures of the decoders it is built from, which is not always what a caller needs to know:
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
id = D.any(D.integer, D.map(D.string, &:to_i))
|
|
166
|
+
Decoding.decode(id, true)
|
|
167
|
+
# => Decoding::Err("None of the decoders matched:\n - expected Integer, got TrueClass\n - expected String, got TrueClass")
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Use `map_err` to give such a decoder a single error message of its own. The block receives the original message and the value being decoded, and returns the message to use instead:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
id = D.map_err(D.any(D.integer, D.map(D.string, &:to_i))) do |_message, value|
|
|
174
|
+
"expected an ID, got #{value.inspect}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
Decoding.decode(id, true)
|
|
178
|
+
# => Decoding::Err("expected an ID, got true")
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Replacing the message does not discard where the error occurred, so a decoder built this way still composes:
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
Decoding.decode(D.field("id", id), { "id" => true })
|
|
185
|
+
# => Decoding::Err("Error at .id: expected an ID, got true")
|
|
186
|
+
```
|
|
187
|
+
|
|
118
188
|
## Available decoders
|
|
119
189
|
|
|
120
190
|
The following decoders are included:
|
|
@@ -129,23 +199,123 @@ The following decoders are included:
|
|
|
129
199
|
* `false`
|
|
130
200
|
* `boolean`
|
|
131
201
|
* `symbol`
|
|
202
|
+
* `parsed_integer`
|
|
203
|
+
* `parsed_float`
|
|
204
|
+
* `parsed_boolean`
|
|
132
205
|
* `regexp`
|
|
206
|
+
* `match`
|
|
207
|
+
* `enum`
|
|
133
208
|
* Utility decoders
|
|
134
209
|
* `succeed`
|
|
135
210
|
* `fail`
|
|
136
211
|
* `original`
|
|
137
212
|
* `map`
|
|
213
|
+
* `map_err`
|
|
138
214
|
* `decode_hash`
|
|
139
215
|
* `and_then`
|
|
216
|
+
* `lazy`
|
|
140
217
|
* Compound decoders
|
|
141
218
|
* `any`
|
|
142
219
|
* `optional`
|
|
143
220
|
* `field`
|
|
221
|
+
* `optional_field`
|
|
144
222
|
* `array`
|
|
145
223
|
* `index`
|
|
146
224
|
* `hash`
|
|
147
225
|
* `at`
|
|
148
226
|
|
|
227
|
+
### A note on `include`
|
|
228
|
+
|
|
229
|
+
Refer to the decoders through a short alias, as the examples above do:
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
D = Decoding::Decoders
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Do not `include Decoding::Decoders`. Several decoders are named after methods every object already has: `hash` would override `Object#hash`, leaving instances unusable as hash keys, and `fail` would shadow `Kernel#fail`, so raising an exception would silently build a decoder instead. In specs, `match` would shadow RSpec's `match` matcher. The decoders `nil`, `true` and `false` are unreachable as bare words in any case, since those are keywords.
|
|
236
|
+
|
|
237
|
+
### Optional decoders
|
|
238
|
+
|
|
239
|
+
Some decoders depend on parts of the standard library that not every application needs, so they are not loaded by default. Require them explicitly to make them available:
|
|
240
|
+
|
|
241
|
+
* `uri` -- decodes a `URI` object, or a string that can be parsed as one:
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
require "decoding/decoders/uri"
|
|
245
|
+
|
|
246
|
+
Decoding.decode(D.uri, "https://example.com") # => Decoding::Ok(URI("https://example.com"))
|
|
247
|
+
Decoding.decode(D.uri, 123) # => Decoding::Err("expected a URI, got 123")
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
* `time` and `date` -- decode a `Time` or `Date` object, or a string in a given format. The format is either the name of a standard format or a `strptime` pattern:
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
require "decoding/decoders/time"
|
|
254
|
+
require "decoding/decoders/date"
|
|
255
|
+
|
|
256
|
+
Decoding.decode(D.time(:iso8601), "2020-01-01T10:00:00Z") # => Decoding::Ok(2020-01-01 10:00:00 UTC)
|
|
257
|
+
Decoding.decode(D.date("%Y|%m"), "2020|01") # => Decoding::Ok(#<Date: 2020-01-01>)
|
|
258
|
+
Decoding.decode(D.date(:iso8601), "3rd Feb") # => Decoding::Err("expected a date in iso8601 format, got \"3rd Feb\"")
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`time` accepts `:iso8601`, `:xmlschema`, `:rfc2822`, `:rfc822`, `:httpdate` and `:parse`; `date` also accepts `:rfc3339` and `:jisx0301`. The same file provides `unix_time`, which reads a number, or a string describing one, as a number of seconds since the epoch:
|
|
262
|
+
|
|
263
|
+
```ruby
|
|
264
|
+
Decoding.decode(D.unix_time, 1_595_674_680) # => Decoding::Ok(2020-07-25 10:58:00 UTC)
|
|
265
|
+
Decoding.decode(D.unix_time, "1595674680") # => Decoding::Ok(2020-07-25 10:58:00 UTC)
|
|
266
|
+
Decoding.decode(D.unix_time(:milliseconds), 1_595_674_680_123) # => Decoding::Ok(2020-07-25 10:58:00.123 UTC)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
A timestamp read in the wrong unit is not an error but a wildly different point in time, so a source reporting milliseconds has to say so. There is deliberately no default: `:parse` is lenient and fills in whatever the input leaves out from the current time, so it has to be asked for by name.
|
|
270
|
+
|
|
271
|
+
* `big_decimal` -- decode a `BigDecimal` object, or a number or string describing one. Only finite numbers are accepted:
|
|
272
|
+
|
|
273
|
+
```ruby
|
|
274
|
+
require "decoding/decoders/big_decimal"
|
|
275
|
+
|
|
276
|
+
Decoding.decode(D.big_decimal, "1.23") # => Decoding::Ok(BigDecimal("1.23"))
|
|
277
|
+
Decoding.decode(D.big_decimal, 42) # => Decoding::Ok(BigDecimal("42"))
|
|
278
|
+
Decoding.decode(D.big_decimal, "abc") # => Decoding::Err("expected a decimal number, got \"abc\"")
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
This decoder needs the `bigdecimal` gem, which is no longer one of Ruby's default gems. Add `gem "bigdecimal"` to your Gemfile to use it.
|
|
282
|
+
|
|
283
|
+
## Reading configuration from the environment
|
|
284
|
+
|
|
285
|
+
Environment variables are always strings, and an application that is misconfigured should refuse to boot rather than fail later. `Decoding.env` reads a single variable, decodes it, and raises when it is missing or its value does not make sense:
|
|
286
|
+
|
|
287
|
+
```ruby
|
|
288
|
+
require "decoding/env"
|
|
289
|
+
|
|
290
|
+
Decoding.env("DATABASE_URL") # => "postgres://localhost/app"
|
|
291
|
+
Decoding.env("PORT", :integer) # => 8080
|
|
292
|
+
Decoding.env("DEBUG", :boolean) # => true
|
|
293
|
+
Decoding.env("PORT", :integer, default: 3000) # => 3000 when PORT is not set
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
The named types are `:string` (the default), `:symbol`, `:integer`, `:float` and `:boolean`. Any decoder is accepted too, which is how you read the types that live behind their own require:
|
|
297
|
+
|
|
298
|
+
```ruby
|
|
299
|
+
require "decoding/decoders/uri"
|
|
300
|
+
|
|
301
|
+
Decoding.env("DATABASE_URL", D.uri) # => #<URI::Generic postgres://localhost/app>
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Failures name the variable, so the reason an application would not start is clear:
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
Decoding::UnwrapError: ENV["PORT"] is not set
|
|
308
|
+
Decoding::UnwrapError: ENV["PORT"]: expected an integer, got "abc"
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
A variable set to an empty string counts as set, so it still has to decode rather than quietly falling back to the default. To allow a variable to be absent without a default, give it a decoder that accepts `nil`:
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
Decoding.env("SENTRY_DSN", D.optional(D.string)) # => nil when not set
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Pass `from:` to read from somewhere other than `ENV`, which is useful in tests.
|
|
318
|
+
|
|
149
319
|
## Development
|
|
150
320
|
|
|
151
321
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "decoders"
|
|
4
|
+
|
|
5
|
+
module Decoding
|
|
6
|
+
# Wrapper around Ruby's `Data` class, providing a way to both define a new
|
|
7
|
+
# data class as usual but including a decoder to decode a value into such a
|
|
8
|
+
# data value.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# User = Decoding::Data.define(
|
|
12
|
+
# name: field("postName", string),
|
|
13
|
+
# age: field("yearsOld", integer)
|
|
14
|
+
# ) do
|
|
15
|
+
# def greet
|
|
16
|
+
# "Hello, my name is #{name} and I am #{age} years old."
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
# payload = { "postName" => "John", "yearsOld" => 30 }
|
|
20
|
+
# Decoding.decode(User.decoder, payload) => Decoding::Ok(user)
|
|
21
|
+
# user.greet # => "Hello, my name is John and I am 30 years old."
|
|
22
|
+
# @return [Class]
|
|
23
|
+
module Data
|
|
24
|
+
# @param attributes [Hash<Symbol, Decoding::Decoder>] A hash of attribute
|
|
25
|
+
# names and decoders
|
|
26
|
+
# @see ::Data.define
|
|
27
|
+
# @return [Class] A new data class with a `decoder` method.
|
|
28
|
+
def self.define(attributes, &)
|
|
29
|
+
# First ensure we have a hash of attribute names and decoders
|
|
30
|
+
attributes = attributes.to_h do |key, value|
|
|
31
|
+
[key.to_sym, value.to_decoder]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Define the data class as usual
|
|
35
|
+
data_class = ::Data.define(*attributes.keys, &)
|
|
36
|
+
|
|
37
|
+
# Add a special `decode` method to use the provided decoders to decode a
|
|
38
|
+
# value as hash and build a new instance with it
|
|
39
|
+
data_class.define_singleton_method(:decoder) do
|
|
40
|
+
Decoding::Decoders.map(
|
|
41
|
+
Decoding::Decoders.decode_hash(attributes)
|
|
42
|
+
) { |attrs| new(**attrs) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Adapt to `to_decoder` protocol
|
|
46
|
+
data_class.define_singleton_method(:to_decoder) { decoder }
|
|
47
|
+
|
|
48
|
+
# Return the data class so regular assignment like
|
|
49
|
+
# `User = ::Data.define(...)` works.
|
|
50
|
+
data_class
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -31,7 +31,11 @@ module Decoding
|
|
|
31
31
|
|
|
32
32
|
failures << result.unwrap_err(nil)
|
|
33
33
|
end
|
|
34
|
-
err(
|
|
34
|
+
err(
|
|
35
|
+
failures.first.combine(failures.drop(1)) do |messages|
|
|
36
|
+
"None of the decoders matched:\n#{messages.map { " - #{_1}" }.join("\n")}"
|
|
37
|
+
end
|
|
38
|
+
)
|
|
35
39
|
end
|
|
36
40
|
end
|
|
37
41
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
|
|
5
|
+
module Decoding
|
|
6
|
+
# Decoders are composable functions for deconstructing unknown input values
|
|
7
|
+
# into known output values.
|
|
8
|
+
module Decoders
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# Decode a `BigDecimal` object, or a number or string describing one.
|
|
12
|
+
#
|
|
13
|
+
# Only finite numbers are accepted: `NaN` and `Infinity` are errors, as a
|
|
14
|
+
# decimal is usually reached for when a value has to be exact.
|
|
15
|
+
#
|
|
16
|
+
# Note this decoder needs the `bigdecimal` gem, which is no longer part of
|
|
17
|
+
# Ruby's default gems. Add it to your Gemfile to use this decoder.
|
|
18
|
+
#
|
|
19
|
+
# @example
|
|
20
|
+
# decode(big_decimal, "1.23") # => Decoding::Ok(BigDecimal("1.23"))
|
|
21
|
+
# decode(big_decimal, 42) # => Decoding::Ok(BigDecimal("42"))
|
|
22
|
+
# decode(big_decimal, "abc")
|
|
23
|
+
# # => Decoding::Err(%(expected a decimal number, got "abc"))
|
|
24
|
+
# @return [Decoding::Decoder<BigDecimal>]
|
|
25
|
+
# @see Decoding::Decoders::MapErr
|
|
26
|
+
def big_decimal
|
|
27
|
+
Decoders.map_err(
|
|
28
|
+
Decoders.and_then(
|
|
29
|
+
Decoders.any(
|
|
30
|
+
Decoders.match(::BigDecimal),
|
|
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
|
+
) { |_message, value| "expected a decimal number, got #{value.inspect}" }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module Decoding
|
|
6
|
+
# Decoders are composable functions for deconstructing unknown input values
|
|
7
|
+
# into known output values.
|
|
8
|
+
module Decoders
|
|
9
|
+
# The standard formats {Decoding::Decoders.date} can parse, each named after
|
|
10
|
+
# the `Date` method that parses it.
|
|
11
|
+
DATE_FORMATS = %i[iso8601 xmlschema rfc2822 rfc822 rfc3339 httpdate jisx0301 parse].freeze
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# Decode a `Date` object, or a string describing a date in a given format.
|
|
16
|
+
#
|
|
17
|
+
# The format is either the name of one of {DATE_FORMATS}, or a string with a
|
|
18
|
+
# `strptime` pattern. Note that the `:parse` format is lenient: it fills in
|
|
19
|
+
# any components the input value leaves out from the current date.
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# decode(date(:iso8601), "2020-01-01")
|
|
23
|
+
# # => Decoding::Ok(#<Date: 2020-01-01>)
|
|
24
|
+
# decode(date("%Y|%m"), "nope")
|
|
25
|
+
# # => Decoding::Err("expected a date matching \"%Y|%m\", got \"nope\"")
|
|
26
|
+
# @param format [Symbol, String]
|
|
27
|
+
# @raise [ArgumentError] when the format is not a known name or a pattern.
|
|
28
|
+
# This is raised when the decoder is built, not when it is used.
|
|
29
|
+
# @return [Decoding::Decoder<Date>]
|
|
30
|
+
# @see Decoding::Decoders::MapErr
|
|
31
|
+
def date(format)
|
|
32
|
+
parse, description = date_format(format)
|
|
33
|
+
Decoders.map_err(
|
|
34
|
+
Decoders.any(
|
|
35
|
+
Decoders.match(::Date),
|
|
36
|
+
Decoders.map(Decoders.string) { parse.call(_1) }
|
|
37
|
+
)
|
|
38
|
+
) { |_message, value| "expected #{description}, got #{value.inspect}" }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @private
|
|
42
|
+
def date_format(format)
|
|
43
|
+
case format
|
|
44
|
+
when ::String
|
|
45
|
+
[->(str) { ::Date.strptime(str, format) }, "a date matching #{format.inspect}"]
|
|
46
|
+
when ::Symbol
|
|
47
|
+
raise ArgumentError, "unknown date format: #{format.inspect}" unless DATE_FORMATS.include?(format)
|
|
48
|
+
|
|
49
|
+
[->(str) { ::Date.public_send(format, str) }, format == :parse ? "a date" : "a date in #{format} format"]
|
|
50
|
+
else
|
|
51
|
+
raise ArgumentError, "expected a date format name or a strptime pattern, got #{format.inspect}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private_class_method :date_format
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../decoder"
|
|
4
|
+
|
|
5
|
+
module Decoding
|
|
6
|
+
module Decoders
|
|
7
|
+
# A decoder for a value that must be one of a fixed set of values.
|
|
8
|
+
#
|
|
9
|
+
# Values are compared for equality, not with the `===` operator the {Match}
|
|
10
|
+
# decoder uses, so the values are read as themselves rather than as
|
|
11
|
+
# patterns.
|
|
12
|
+
#
|
|
13
|
+
# @see Decoding::Decoders.enum
|
|
14
|
+
class Enum < Decoder
|
|
15
|
+
# @overload initialize(value, *values)
|
|
16
|
+
# @param value [Object]
|
|
17
|
+
# @param values [Object]
|
|
18
|
+
# @overload initialize(values)
|
|
19
|
+
# @param values [Array<Object>] the values as a single array
|
|
20
|
+
# @raise [ArgumentError] when no values are given, or one is repeated.
|
|
21
|
+
def initialize(value, *values)
|
|
22
|
+
@values = collect(value, values)
|
|
23
|
+
super()
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param value [Object]
|
|
27
|
+
# @return [Decoding::Result<Object>]
|
|
28
|
+
def call(value)
|
|
29
|
+
return ok(value) if @values.include?(value)
|
|
30
|
+
|
|
31
|
+
err(failure("expected one of #{@values.map(&:inspect).join(", ")}, got #{value.inspect}"))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def collect(value, values)
|
|
37
|
+
values = (values.empty? && value.is_a?(::Array) ? value.dup : [value, *values]).freeze
|
|
38
|
+
raise ArgumentError, "expected at least one value to decode" if values.empty?
|
|
39
|
+
|
|
40
|
+
duplicates = values.tally.select { |_, count| count > 1 }.keys
|
|
41
|
+
raise ArgumentError, "duplicate values: #{duplicates.map(&:inspect).join(", ")}" if duplicates.any?
|
|
42
|
+
|
|
43
|
+
values
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -23,10 +23,10 @@ module Decoding
|
|
|
23
23
|
if value.key?(@key)
|
|
24
24
|
@decoder.call(value.fetch(@key)).map_err { _1.push(@key) }
|
|
25
25
|
else
|
|
26
|
-
err(failure("expected
|
|
26
|
+
err(failure("expected Hash with key #{@key.inspect}"))
|
|
27
27
|
end
|
|
28
28
|
else
|
|
29
|
-
err(failure("expected
|
|
29
|
+
err(failure("expected Hash, got #{value.class}"))
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
end
|
|
@@ -26,11 +26,11 @@ module Decoding
|
|
|
26
26
|
[
|
|
27
27
|
@key_decoder
|
|
28
28
|
.call(k)
|
|
29
|
-
.map_err { |
|
|
29
|
+
.map_err { _1.map { |msg| "invalid key: #{msg}" }.push(k) },
|
|
30
30
|
|
|
31
31
|
@value_decoder
|
|
32
32
|
.call(v)
|
|
33
|
-
.map_err {
|
|
33
|
+
.map_err { _1.push(k) }
|
|
34
34
|
]
|
|
35
35
|
)
|
|
36
36
|
end
|
|
@@ -9,9 +9,6 @@ module Decoding
|
|
|
9
9
|
#
|
|
10
10
|
# @see Decoding::Decoders.index
|
|
11
11
|
class Index < Decoder
|
|
12
|
-
# @private
|
|
13
|
-
Err = Result.err("error decoding array: index is out of bounds")
|
|
14
|
-
|
|
15
12
|
# @param index [Integer]
|
|
16
13
|
# @param decoder [Decoding::Decoder<Object>]
|
|
17
14
|
def initialize(index, decoder)
|
|
@@ -23,7 +20,7 @@ module Decoding
|
|
|
23
20
|
# @param value [Object]
|
|
24
21
|
# @return [Decoding::Decoder<Object>]
|
|
25
22
|
def call(value)
|
|
26
|
-
return err(failure("expected
|
|
23
|
+
return err(failure("expected Array, got #{value.class}")) unless value.is_a?(::Array)
|
|
27
24
|
|
|
28
25
|
@decoder
|
|
29
26
|
.call(value.fetch(@index))
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../decoder"
|
|
4
|
+
|
|
5
|
+
module Decoding
|
|
6
|
+
module Decoders
|
|
7
|
+
# A decoder that builds the decoder it delegates to on first use, rather
|
|
8
|
+
# than when it is created.
|
|
9
|
+
#
|
|
10
|
+
# This is what makes recursive decoders possible: a decoder for a tree can
|
|
11
|
+
# refer to itself, since the reference is only resolved once there is a
|
|
12
|
+
# value to decode.
|
|
13
|
+
#
|
|
14
|
+
# @see Decoding::Decoders.lazy
|
|
15
|
+
class Lazy < Decoder
|
|
16
|
+
# @yieldreturn [Decoding::Decoder<a>]
|
|
17
|
+
def initialize(&block)
|
|
18
|
+
@block = block
|
|
19
|
+
super()
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @param value [Object]
|
|
23
|
+
# @return [Decoding::Result<a>]
|
|
24
|
+
def call(value) = decoder.call(value)
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def decoder = @decoder ||= @block.call.to_decoder
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../decoder"
|
|
4
|
+
|
|
5
|
+
module Decoding
|
|
6
|
+
module Decoders
|
|
7
|
+
# A decoder that replaces the error message of a decoder that failed,
|
|
8
|
+
# leaving successful results untouched.
|
|
9
|
+
#
|
|
10
|
+
# @see Decoding::Decoders.map_err
|
|
11
|
+
class MapErr < Decoder
|
|
12
|
+
# @param decoder [Decoding::Decoder<a>]
|
|
13
|
+
# @yieldparam msg [String]
|
|
14
|
+
# @yieldparam value [Object]
|
|
15
|
+
# @yieldreturn [String]
|
|
16
|
+
def initialize(decoder, &block)
|
|
17
|
+
@decoder = decoder.to_decoder
|
|
18
|
+
@block = block
|
|
19
|
+
super()
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @param value [Object]
|
|
23
|
+
# @return [Decoding::Result<a>]
|
|
24
|
+
def call(value)
|
|
25
|
+
@decoder.call(value).map_err { |f| f.map { @block.call(_1, value) } }
|
|
26
|
+
rescue StandardError => e
|
|
27
|
+
err(failure("error in map_err block: #{e.message}"))
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -21,7 +21,7 @@ module Decoding
|
|
|
21
21
|
elsif @pattern.is_a?(Class)
|
|
22
22
|
err(failure("expected #{@pattern}, got #{value.class}"))
|
|
23
23
|
else
|
|
24
|
-
err(failure("expected value matching #{@pattern.inspect}, got
|
|
24
|
+
err(failure("expected value matching #{@pattern.inspect}, got #{value.inspect}"))
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../decoder"
|
|
4
|
+
|
|
5
|
+
module Decoding
|
|
6
|
+
module Decoders
|
|
7
|
+
# A decoder for a value that may or may not be `nil`, decoding any other
|
|
8
|
+
# value with a given decoder.
|
|
9
|
+
#
|
|
10
|
+
# The given decoder gets to decode a `nil` value first, so it can give it a
|
|
11
|
+
# meaning of its own. Only when it fails to do so does this decoder treat
|
|
12
|
+
# `nil` as an absent value.
|
|
13
|
+
#
|
|
14
|
+
# @see Decoding::Decoders.optional
|
|
15
|
+
class Optional < Decoder
|
|
16
|
+
# @param decoder [Decoding::Decoder<a>]
|
|
17
|
+
def initialize(decoder)
|
|
18
|
+
@decoder = decoder.to_decoder
|
|
19
|
+
super()
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @param value [Object]
|
|
23
|
+
# @return [Decoding::Result<a, nil>]
|
|
24
|
+
def call(value)
|
|
25
|
+
result = @decoder.call(value)
|
|
26
|
+
return result if result.ok?
|
|
27
|
+
return ok(nil) if value.nil?
|
|
28
|
+
|
|
29
|
+
result
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../decoder"
|
|
4
|
+
require_relative "field"
|
|
5
|
+
|
|
6
|
+
module Decoding
|
|
7
|
+
module Decoders
|
|
8
|
+
# Decode a value from a key that may be absent from a hash.
|
|
9
|
+
#
|
|
10
|
+
# This differs from wrapping a {Field} decoder in an {Optional} decoder,
|
|
11
|
+
# which describes a key that is present but may hold `nil`. Here the key
|
|
12
|
+
# itself may be missing; when it is present, its value must still decode.
|
|
13
|
+
#
|
|
14
|
+
# @see Decoding::Decoders.optional_field
|
|
15
|
+
class OptionalField < Decoder
|
|
16
|
+
# @param key [Object]
|
|
17
|
+
# @param decoder [Decoding::Decoder<a>]
|
|
18
|
+
# @param default [Object] used when the key is absent.
|
|
19
|
+
def initialize(key, decoder, default: nil)
|
|
20
|
+
@key = String(key)
|
|
21
|
+
@field = Field.new(@key, decoder)
|
|
22
|
+
@default = default
|
|
23
|
+
super()
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param value [Object]
|
|
27
|
+
# @return [Decoding::Result<a>]
|
|
28
|
+
def call(value)
|
|
29
|
+
return ok(@default) if value.is_a?(::Hash) && !value.key?(@key)
|
|
30
|
+
|
|
31
|
+
@field.call(value)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -11,9 +11,14 @@ module Decoding
|
|
|
11
11
|
# @example Always return a fixed value
|
|
12
12
|
# decode(Succeed.new(5), "anything") # => Decoding::Ok(5)
|
|
13
13
|
#
|
|
14
|
-
# @example Use
|
|
15
|
-
#
|
|
16
|
-
#
|
|
14
|
+
# @example Use in conditional decoding
|
|
15
|
+
# and_then(field("kind", string)) do |kind|
|
|
16
|
+
# kind == "none" ? Succeed.new(nil) : field("value", integer)
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# Note this is not the way to give a key a default value: combining it with
|
|
20
|
+
# {Any} would also swallow the failure of a key that is present but holds a
|
|
21
|
+
# value of the wrong type. Use {OptionalField} for that instead.
|
|
17
22
|
class Succeed < Decoder
|
|
18
23
|
# @param value [Object] the value to always return
|
|
19
24
|
def initialize(value)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Decoding
|
|
6
|
+
# Decoders are composable functions for deconstructing unknown input values
|
|
7
|
+
# into known output values.
|
|
8
|
+
module Decoders
|
|
9
|
+
# The standard formats {Decoding::Decoders.time} can parse, each named after
|
|
10
|
+
# the `Time` method that parses it.
|
|
11
|
+
TIME_FORMATS = %i[iso8601 xmlschema rfc2822 rfc822 httpdate parse].freeze
|
|
12
|
+
|
|
13
|
+
# The units {Decoding::Decoders.unix_time} can read a timestamp in, mapped
|
|
14
|
+
# to the number of them that make up a second.
|
|
15
|
+
UNIX_TIME_UNITS = { seconds: 1, milliseconds: 1000 }.freeze
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# Decode a `Time` object, or a string describing a time in a given format.
|
|
20
|
+
#
|
|
21
|
+
# The format is either the name of one of {TIME_FORMATS}, or a string with a
|
|
22
|
+
# `strptime` pattern. Note that the `:parse` format is lenient: it fills in
|
|
23
|
+
# any components the input value leaves out from the current time.
|
|
24
|
+
#
|
|
25
|
+
# @example
|
|
26
|
+
# decode(time(:iso8601), "2020-01-01T10:00:00Z")
|
|
27
|
+
# # => Decoding::Ok(2020-01-01 10:00:00 UTC)
|
|
28
|
+
# decode(time("%Y|%m"), "nope")
|
|
29
|
+
# # => Decoding::Err("expected a time matching \"%Y|%m\", got \"nope\"")
|
|
30
|
+
# @param format [Symbol, String]
|
|
31
|
+
# @raise [ArgumentError] when the format is not a known name or a pattern.
|
|
32
|
+
# This is raised when the decoder is built, not when it is used.
|
|
33
|
+
# @return [Decoding::Decoder<Time>]
|
|
34
|
+
# @see Decoding::Decoders::MapErr
|
|
35
|
+
def time(format)
|
|
36
|
+
parse, description = time_format(format)
|
|
37
|
+
Decoders.map_err(
|
|
38
|
+
Decoders.any(
|
|
39
|
+
Decoders.match(::Time),
|
|
40
|
+
Decoders.map(Decoders.string) { parse.call(_1) }
|
|
41
|
+
)
|
|
42
|
+
) { |_message, value| "expected #{description}, got #{value.inspect}" }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @private
|
|
46
|
+
def time_format(format)
|
|
47
|
+
case format
|
|
48
|
+
when ::String
|
|
49
|
+
[->(str) { ::Time.strptime(str, format) }, "a time matching #{format.inspect}"]
|
|
50
|
+
when ::Symbol
|
|
51
|
+
raise ArgumentError, "unknown time format: #{format.inspect}" unless TIME_FORMATS.include?(format)
|
|
52
|
+
|
|
53
|
+
[->(str) { ::Time.public_send(format, str) }, format == :parse ? "a time" : "a time in #{format} format"]
|
|
54
|
+
else
|
|
55
|
+
raise ArgumentError, "expected a time format name or a strptime pattern, got #{format.inspect}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Decode a unix timestamp, given as a number or as a string describing one.
|
|
60
|
+
#
|
|
61
|
+
# Timestamps are read as a number of seconds since the epoch unless another
|
|
62
|
+
# unit is given. Note that reading a timestamp in the wrong unit is not an
|
|
63
|
+
# error but a wildly different point in time, so a source that reports
|
|
64
|
+
# milliseconds has to say so.
|
|
65
|
+
#
|
|
66
|
+
# @example
|
|
67
|
+
# decode(unix_time, 1_595_674_680) # => Decoding::Ok(2020-07-25 10:58:00 UTC)
|
|
68
|
+
# decode(unix_time(:milliseconds), 1_595_674_680_123)
|
|
69
|
+
# # => Decoding::Ok(2020-07-25 10:58:00.123 UTC)
|
|
70
|
+
# @param unit [Symbol] one of the keys of {UNIX_TIME_UNITS}.
|
|
71
|
+
# @raise [ArgumentError] when the unit is not a known one. This is raised
|
|
72
|
+
# when the decoder is built, not when it is used.
|
|
73
|
+
# @return [Decoding::Decoder<Time>]
|
|
74
|
+
# @see Decoding::Decoders::MapErr
|
|
75
|
+
def unix_time(unit = :seconds)
|
|
76
|
+
raise ArgumentError, "unknown unit: #{unit.inspect}" unless UNIX_TIME_UNITS.key?(unit)
|
|
77
|
+
|
|
78
|
+
per_second = UNIX_TIME_UNITS.fetch(unit)
|
|
79
|
+
Decoders.map_err(
|
|
80
|
+
Decoders.any(
|
|
81
|
+
Decoders.match(::Time),
|
|
82
|
+
Decoders.map(
|
|
83
|
+
Decoders.any(Decoders.integer, Decoders.float, Decoders.parsed_integer, Decoders.parsed_float)
|
|
84
|
+
) { ::Time.at(per_second == 1 ? _1 : _1 / Rational(per_second)) }
|
|
85
|
+
)
|
|
86
|
+
) { |_message, value| "expected a unix timestamp, got #{value.inspect}" }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private_class_method :time_format
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Decoding
|
|
6
|
+
# Decoders are composable functions for deconstructing unknown input values
|
|
7
|
+
# into known output values.
|
|
8
|
+
module Decoders
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# Decode a URI object, or a string that can be parsed as one.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# decode(uri, "https://example.com") # => Decoding::Ok(URI("https://example.com"))
|
|
15
|
+
# decode(uri, 123) # => Decoding::Err("expected a URI, got 123")
|
|
16
|
+
# @return [Decoding::Decoder<URI::Generic>]
|
|
17
|
+
# @see Decoding::Decoders::MapErr
|
|
18
|
+
def uri
|
|
19
|
+
Decoders.map_err(
|
|
20
|
+
Decoders.any(
|
|
21
|
+
Decoders::Match.new(::URI::Generic),
|
|
22
|
+
Decoders.map(Decoders.string) { ::URI.parse(_1) }
|
|
23
|
+
)
|
|
24
|
+
) { |_msg, value| "expected a URI, got #{value.inspect}" }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/decoding/decoders.rb
CHANGED
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "decoders/match"
|
|
4
4
|
require_relative "decoders/map"
|
|
5
|
+
require_relative "decoders/map_err"
|
|
6
|
+
require_relative "decoders/optional"
|
|
7
|
+
require_relative "decoders/lazy"
|
|
8
|
+
require_relative "decoders/enum"
|
|
5
9
|
require_relative "decoders/any"
|
|
6
10
|
require_relative "decoders/field"
|
|
11
|
+
require_relative "decoders/optional_field"
|
|
7
12
|
require_relative "decoders/array"
|
|
8
13
|
require_relative "decoders/index"
|
|
9
14
|
require_relative "decoders/hash"
|
|
@@ -30,6 +35,37 @@ module Decoding
|
|
|
30
35
|
# @see Decoding::Decoders::Match
|
|
31
36
|
def string = Decoders::Match.new(String)
|
|
32
37
|
|
|
38
|
+
# Decode any value matching the given pattern, using the `===` operator.
|
|
39
|
+
# This works with anything that can be used in a `case` statement, such as
|
|
40
|
+
# classes, ranges and regular expressions.
|
|
41
|
+
#
|
|
42
|
+
# @example
|
|
43
|
+
# decode(match(Symbol), :foo) # => Decoding::Ok(:foo)
|
|
44
|
+
# decode(match(1..5), 3) # => Decoding::Ok(3)
|
|
45
|
+
# @param pattern [#===]
|
|
46
|
+
# @return [Decoding::Decoder<Object>]
|
|
47
|
+
# @see Decoding::Decoders::Match
|
|
48
|
+
def match(pattern) = Decoders::Match.new(pattern)
|
|
49
|
+
|
|
50
|
+
# Decode a value that must be one of a fixed set of values.
|
|
51
|
+
#
|
|
52
|
+
# The values are compared for equality, and can be given either as separate
|
|
53
|
+
# arguments or as a single array.
|
|
54
|
+
#
|
|
55
|
+
# @example
|
|
56
|
+
# decode(enum("active", "archived"), "active") # => Decoding::Ok("active")
|
|
57
|
+
# decode(enum("active", "archived"), "nope")
|
|
58
|
+
# # => Decoding::Err(%(expected one of "active", "archived", got "nope"))
|
|
59
|
+
# @overload enum(value, *values)
|
|
60
|
+
# @param value [Object]
|
|
61
|
+
# @param values [Object]
|
|
62
|
+
# @overload enum(values)
|
|
63
|
+
# @param values [Array<Object>]
|
|
64
|
+
# @raise [ArgumentError] when no values are given, or one is repeated.
|
|
65
|
+
# @return [Decoding::Decoder<Object>]
|
|
66
|
+
# @see Decoding::Decoders::Enum
|
|
67
|
+
def enum(...) = Decoders::Enum.new(...)
|
|
68
|
+
|
|
33
69
|
# Decode any string value that matches a regular expression.
|
|
34
70
|
#
|
|
35
71
|
# @param regex [Regexp, String]
|
|
@@ -92,7 +128,7 @@ module Decoding
|
|
|
92
128
|
# decode(boolean, true) # => Decoding::Ok(true)
|
|
93
129
|
# decode(boolean, false) # => Decoding::Ok(false)
|
|
94
130
|
# @return [Decoding::Decoder<Boolean>]
|
|
95
|
-
def boolean = any(self.true, self.false)
|
|
131
|
+
def boolean = map_err(any(self.true, self.false)) { |_msg, value| "expected true or false, got #{value.class}" }
|
|
96
132
|
|
|
97
133
|
# Decode a String value into a symbol.
|
|
98
134
|
#
|
|
@@ -101,6 +137,46 @@ module Decoding
|
|
|
101
137
|
# @return [Decoding::Decoder<Symbol>]
|
|
102
138
|
def symbol = map(string, &:to_sym)
|
|
103
139
|
|
|
140
|
+
# @!group Parsing decoders
|
|
141
|
+
|
|
142
|
+
# Decode a string describing an integer into that integer.
|
|
143
|
+
#
|
|
144
|
+
# The string is always read as a decimal number, so `"08"` decodes to `8`
|
|
145
|
+
# and `"0x1f"` is not a valid integer.
|
|
146
|
+
#
|
|
147
|
+
# @example
|
|
148
|
+
# decode(parsed_integer, "8080") # => Decoding::Ok(8080)
|
|
149
|
+
# decode(parsed_integer, "abc") # => Decoding::Err(%(expected an integer, got "abc"))
|
|
150
|
+
# @return [Decoding::Decoder<Integer>]
|
|
151
|
+
def parsed_integer
|
|
152
|
+
map_err(map(string) { Integer(_1, 10) }) { |_message, value| "expected an integer, got #{value.inspect}" }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Decode a string describing a number into a float.
|
|
156
|
+
#
|
|
157
|
+
# @example
|
|
158
|
+
# decode(parsed_float, "1.5") # => Decoding::Ok(1.5)
|
|
159
|
+
# decode(parsed_float, "abc") # => Decoding::Err(%(expected a number, got "abc"))
|
|
160
|
+
# @return [Decoding::Decoder<Float>]
|
|
161
|
+
def parsed_float
|
|
162
|
+
map_err(map(string) { Float(_1) }) { |_message, value| "expected a number, got #{value.inspect}" }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Decode the string `"true"` or `"false"` into the matching boolean.
|
|
166
|
+
#
|
|
167
|
+
# Only those two values are accepted: anything else, such as `"1"` or
|
|
168
|
+
# `"yes"`, is an error rather than a guess at what was meant.
|
|
169
|
+
#
|
|
170
|
+
# @example
|
|
171
|
+
# decode(parsed_boolean, "true") # => Decoding::Ok(true)
|
|
172
|
+
# decode(parsed_boolean, "1") # => Decoding::Err(%(expected "true" or "false", got "1"))
|
|
173
|
+
# @return [Decoding::Decoder<Boolean>]
|
|
174
|
+
def parsed_boolean
|
|
175
|
+
map_err(map(enum("true", "false")) { _1 == "true" }) do |_message, value|
|
|
176
|
+
%(expected "true" or "false", got #{value.inspect})
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
104
180
|
# @!group Utility decoders
|
|
105
181
|
|
|
106
182
|
# A decoder that always succeeds with the given value.
|
|
@@ -151,6 +227,28 @@ module Decoding
|
|
|
151
227
|
# @see Decoding::Decoders::Map
|
|
152
228
|
def map(...) = Decoders::Map.new(...)
|
|
153
229
|
|
|
230
|
+
# Decode a value with the given decoder and, if it failed, replace its
|
|
231
|
+
# error message with the result of the given block.
|
|
232
|
+
#
|
|
233
|
+
# This is useful for giving a compound decoder a single, fitting error
|
|
234
|
+
# message rather than exposing the messages of the decoders it is built
|
|
235
|
+
# from. The location of the original error, if any, is retained.
|
|
236
|
+
#
|
|
237
|
+
# @example
|
|
238
|
+
# decoder = map_err(any(string, integer)) { |_msg, value|
|
|
239
|
+
# "expected a string or integer, got #{value.inspect}"
|
|
240
|
+
# }
|
|
241
|
+
# decode(decoder, nil)
|
|
242
|
+
# # => Decoding::Err("expected a string or integer, got nil")
|
|
243
|
+
# @overload map_err(decoder)
|
|
244
|
+
# @param decoder [Decoding::Decoder<a>]
|
|
245
|
+
# @yieldparam msg [String] the original error message
|
|
246
|
+
# @yieldparam value [Object] the value being decoded
|
|
247
|
+
# @yieldreturn [String]
|
|
248
|
+
# @return [Decoding::Decoder<a>]
|
|
249
|
+
# @see Decoding::Decoders::MapErr
|
|
250
|
+
def map_err(...) = Decoders::MapErr.new(...)
|
|
251
|
+
|
|
154
252
|
# Decode a value by trying many different decoders in order, using the first
|
|
155
253
|
# matching result -- or a failure when none of the given decoders succeed.
|
|
156
254
|
#
|
|
@@ -168,9 +266,15 @@ module Decoding
|
|
|
168
266
|
# @example
|
|
169
267
|
# decode(string, "foo") # => Decoding::Ok("foo")
|
|
170
268
|
# decode(string, nil) # => Decoding::Ok(nil)
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
|
|
269
|
+
# The given decoder gets to decode a `nil` value first, so it can give it a
|
|
270
|
+
# meaning of its own. Only when it fails to do so is `nil` treated as an
|
|
271
|
+
# absent value.
|
|
272
|
+
#
|
|
273
|
+
# @overload optional(decoder)
|
|
274
|
+
# @param decoder [Decoding::Decoder<a>]
|
|
275
|
+
# @return [Decoding::Decoder<a, nil>]
|
|
276
|
+
# @see Decoding::Decoders::Optional
|
|
277
|
+
def optional(...) = Decoders::Optional.new(...)
|
|
174
278
|
|
|
175
279
|
# Decode a value from a given key in a hash.
|
|
176
280
|
#
|
|
@@ -183,6 +287,26 @@ module Decoding
|
|
|
183
287
|
# @see Decoding::Decoders::Field
|
|
184
288
|
def field(...) = Decoders::Field.new(...)
|
|
185
289
|
|
|
290
|
+
# Decode a value from a key that may be absent from a hash.
|
|
291
|
+
#
|
|
292
|
+
# When the key is absent, the given default is used. When it is present, its
|
|
293
|
+
# value must still decode: a key holding a value of the wrong type is an
|
|
294
|
+
# error rather than a reason to fall back to the default.
|
|
295
|
+
#
|
|
296
|
+
# @example
|
|
297
|
+
# decoder = optional_field("count", integer, default: 0)
|
|
298
|
+
# decode(decoder, {}) # => Decoding::Ok(0)
|
|
299
|
+
# decode(decoder, { "count" => 5 }) # => Decoding::Ok(5)
|
|
300
|
+
# decode(decoder, { "count" => "x" })
|
|
301
|
+
# # => Decoding::Err("Error at .count: expected Integer, got String")
|
|
302
|
+
# @overload optional_field(key, decoder, default: nil)
|
|
303
|
+
# @param key [Object]
|
|
304
|
+
# @param decoder [Decoding::Decoder<a>]
|
|
305
|
+
# @param default [Object]
|
|
306
|
+
# @return [Decoding::Decoder<a>]
|
|
307
|
+
# @see Decoding::Decoders::OptionalField
|
|
308
|
+
def optional_field(...) = Decoders::OptionalField.new(...)
|
|
309
|
+
|
|
186
310
|
# Decode an array of values using a given decoder.
|
|
187
311
|
#
|
|
188
312
|
# @example
|
|
@@ -233,6 +357,8 @@ module Decoding
|
|
|
233
357
|
# # => Decode::Ok({ id: 1 })
|
|
234
358
|
# @return Decoding::Decoder
|
|
235
359
|
def decode_hash(decoders)
|
|
360
|
+
return succeed({}) if decoders.empty?
|
|
361
|
+
|
|
236
362
|
map(*decoders.values) do |*values|
|
|
237
363
|
decoders.keys.zip(values).to_h
|
|
238
364
|
end
|
|
@@ -260,6 +386,26 @@ module Decoding
|
|
|
260
386
|
# @see Decoding::Decoders::AndThen
|
|
261
387
|
def and_then(...) = Decoders::AndThen.new(...)
|
|
262
388
|
|
|
389
|
+
# Create a decoder that is only built when it is used.
|
|
390
|
+
#
|
|
391
|
+
# This makes recursive decoders possible: without it, a decoder that refers
|
|
392
|
+
# to itself would recurse endlessly while being built.
|
|
393
|
+
#
|
|
394
|
+
# @example
|
|
395
|
+
# def tree
|
|
396
|
+
# decode_hash(
|
|
397
|
+
# name: field("name", string),
|
|
398
|
+
# children: field("children", array(lazy { tree }))
|
|
399
|
+
# )
|
|
400
|
+
# end
|
|
401
|
+
# decode(tree, { "name" => "a", "children" => [] })
|
|
402
|
+
# # => Decoding::Ok({ name: "a", children: [] })
|
|
403
|
+
# @overload lazy
|
|
404
|
+
# @yieldreturn [Decoding::Decoder<a>]
|
|
405
|
+
# @return [Decoding::Decoder<a>]
|
|
406
|
+
# @see Decoding::Decoders::Lazy
|
|
407
|
+
def lazy(...) = Decoders::Lazy.new(...)
|
|
408
|
+
|
|
263
409
|
# Decode deeply-nested fields.
|
|
264
410
|
#
|
|
265
411
|
# @example
|
|
@@ -267,7 +413,7 @@ module Decoding
|
|
|
267
413
|
# decode(decoder, { "a" => { "b" => { "c" => "d" } } })
|
|
268
414
|
# # => Decoding::Ok("d")
|
|
269
415
|
# decode(decoder, { "a" => { "b" => "d" } })
|
|
270
|
-
# # => Decoding::Err("Error at .a.b: expected
|
|
416
|
+
# # => Decoding::Err("Error at .a.b: expected Hash, got String")
|
|
271
417
|
# @overload at(*fields, decoder)
|
|
272
418
|
# @param fields [String]
|
|
273
419
|
# @param decoder [Decoding::Decoder<a>]
|
data/lib/decoding/env.rb
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../decoding"
|
|
4
|
+
|
|
5
|
+
# Decoding is a library to help transform unknown external data into neat values
|
|
6
|
+
# with known shapes.
|
|
7
|
+
module Decoding
|
|
8
|
+
# The types {Decoding.env} accepts by name, mapped to the decoder that reads
|
|
9
|
+
# them.
|
|
10
|
+
#
|
|
11
|
+
# Environment variables are always strings, so these are all decoders that
|
|
12
|
+
# parse a string. Pass a decoder directly for anything else.
|
|
13
|
+
ENV_TYPES = {
|
|
14
|
+
string: :string,
|
|
15
|
+
symbol: :symbol,
|
|
16
|
+
integer: :parsed_integer,
|
|
17
|
+
float: :parsed_float,
|
|
18
|
+
boolean: :parsed_boolean
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
# Marks the absence of a default value, so that `nil` can be used as one.
|
|
22
|
+
NO_DEFAULT = Object.new.freeze
|
|
23
|
+
private_constant :NO_DEFAULT
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# Read a single environment variable, decoding its value.
|
|
28
|
+
#
|
|
29
|
+
# This is meant for configuration read at boot time, such as a Rails
|
|
30
|
+
# initializer: it returns the decoded value itself and raises when the
|
|
31
|
+
# variable is missing or its value cannot be decoded, rather than letting a
|
|
32
|
+
# misconfigured application start.
|
|
33
|
+
#
|
|
34
|
+
# @example
|
|
35
|
+
# Decoding.env("DATABASE_URL") # => "postgres://localhost/app"
|
|
36
|
+
# Decoding.env("PORT", :integer) # => 8080
|
|
37
|
+
# Decoding.env("PORT", :integer, default: 3000) # => 3000 when unset
|
|
38
|
+
# Decoding.env("DATABASE_URL", Decoders.uri) # => #<URI::Generic ...>
|
|
39
|
+
# @param name [String] the name of the environment variable.
|
|
40
|
+
# @param type [Symbol, Decoding::Decoder] one of the keys of {ENV_TYPES}, or
|
|
41
|
+
# any decoder to run against the value.
|
|
42
|
+
# @param default [Object] returned, undecoded, when the variable is not set.
|
|
43
|
+
# @param from [#key?, #fetch] where to read the variable from.
|
|
44
|
+
# @raise [ArgumentError] when the type is not a known name or a decoder.
|
|
45
|
+
# @raise [Decoding::UnwrapError] when the variable is missing or its value
|
|
46
|
+
# cannot be decoded.
|
|
47
|
+
# @return [Object]
|
|
48
|
+
def env(name, type = :string, default: NO_DEFAULT, from: ENV)
|
|
49
|
+
decoder = env_decoder(type)
|
|
50
|
+
return default if !from.key?(name) && !default.equal?(NO_DEFAULT)
|
|
51
|
+
|
|
52
|
+
decode!(
|
|
53
|
+
Decoders.map_err(decoder) do |message, value|
|
|
54
|
+
value.nil? ? "ENV[#{name.inspect}] is not set" : "ENV[#{name.inspect}]: #{message}"
|
|
55
|
+
end,
|
|
56
|
+
from.fetch(name, nil)
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @private
|
|
61
|
+
def env_decoder(type)
|
|
62
|
+
return type.to_decoder if type.respond_to?(:to_decoder)
|
|
63
|
+
raise ArgumentError, "expected a type name or a decoder, got #{type.inspect}" unless type.is_a?(Symbol)
|
|
64
|
+
raise ArgumentError, "unknown type: #{type.inspect}" unless ENV_TYPES.key?(type)
|
|
65
|
+
|
|
66
|
+
Decoders.public_send(ENV_TYPES.fetch(type))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private_class_method :env_decoder
|
|
70
|
+
end
|
data/lib/decoding/failure.rb
CHANGED
|
@@ -9,6 +9,17 @@ module Decoding
|
|
|
9
9
|
# error, the `array` decoder can push `3` to the stack to indicate that
|
|
10
10
|
# happened at index 3 in its input value.
|
|
11
11
|
class Failure
|
|
12
|
+
# The error message, without the location it occurred at.
|
|
13
|
+
#
|
|
14
|
+
# @return [String]
|
|
15
|
+
attr_reader :msg
|
|
16
|
+
|
|
17
|
+
# The stack of segments describing where the error occurred, innermost
|
|
18
|
+
# first.
|
|
19
|
+
#
|
|
20
|
+
# @return [Array]
|
|
21
|
+
attr_reader :path
|
|
22
|
+
|
|
12
23
|
# @param msg [String]
|
|
13
24
|
# @param path [Array] Internal parameter for creating copies with updated paths
|
|
14
25
|
def initialize(msg, path = [])
|
|
@@ -33,6 +44,37 @@ module Decoding
|
|
|
33
44
|
self.class.new(@msg, @path + [segment])
|
|
34
45
|
end
|
|
35
46
|
|
|
47
|
+
# Create a copy of this failure with a transformed error message, retaining
|
|
48
|
+
# the current stack of errors.
|
|
49
|
+
#
|
|
50
|
+
# This is useful for decoders that want to replace the error message of a
|
|
51
|
+
# nested decoder with something more fitting, without losing the location
|
|
52
|
+
# of the error.
|
|
53
|
+
#
|
|
54
|
+
# @yieldparam msg [String]
|
|
55
|
+
# @yieldreturn [String]
|
|
56
|
+
# @return [Decoding::Failure]
|
|
57
|
+
def map = self.class.new(yield(@msg), @path)
|
|
58
|
+
|
|
59
|
+
# Combine this failure with others into a single failure, using the given
|
|
60
|
+
# block to build a single message from all of their messages.
|
|
61
|
+
#
|
|
62
|
+
# When all failures occurred at the same location, that location is kept for
|
|
63
|
+
# the combined failure and left out of the individual messages, since the
|
|
64
|
+
# combined failure already describes it. Otherwise each message describes
|
|
65
|
+
# its own location.
|
|
66
|
+
#
|
|
67
|
+
# @param others [Array<Decoding::Failure>]
|
|
68
|
+
# @yieldparam messages [Array<String>]
|
|
69
|
+
# @yieldreturn [String]
|
|
70
|
+
# @return [Decoding::Failure]
|
|
71
|
+
def combine(others)
|
|
72
|
+
failures = [self, *others]
|
|
73
|
+
return self.class.new(yield(failures.map(&:to_s))) unless failures.map(&:path).uniq.size == 1
|
|
74
|
+
|
|
75
|
+
self.class.new(yield(failures.map(&:msg)), @path)
|
|
76
|
+
end
|
|
77
|
+
|
|
36
78
|
def to_s
|
|
37
79
|
if @path.any?
|
|
38
80
|
"Error at .#{@path.reverse.join(".")}: #{@msg}"
|
|
@@ -40,9 +82,5 @@ module Decoding
|
|
|
40
82
|
@msg
|
|
41
83
|
end
|
|
42
84
|
end
|
|
43
|
-
|
|
44
|
-
protected
|
|
45
|
-
|
|
46
|
-
attr_reader :msg, :path
|
|
47
85
|
end
|
|
48
86
|
end
|
data/lib/decoding/result.rb
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "error"
|
|
4
|
+
|
|
3
5
|
module Decoding
|
|
4
6
|
# Raised when calling {Result#unwrap!} on an `Err` value.
|
|
5
|
-
class UnwrapError <
|
|
7
|
+
class UnwrapError < Error; end
|
|
6
8
|
|
|
7
9
|
# A result represent the outcome of some computation that can succeed or fail.
|
|
8
10
|
# The results are represented with two subclasses of `Result`: `Ok` and `Err`.
|
data/lib/decoding/version.rb
CHANGED
data/lib/decoding.rb
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "decoding/version"
|
|
4
|
+
require_relative "decoding/error"
|
|
4
5
|
require_relative "decoding/decoders"
|
|
6
|
+
require_relative "decoding/data"
|
|
5
7
|
|
|
6
8
|
# Decoding is a library to help transform unknown external data into neat values
|
|
7
9
|
# with known shapes. Consider calling an HTTP API: you might pull in whatever
|
|
@@ -109,8 +111,6 @@ require_relative "decoding/decoders"
|
|
|
109
111
|
# always contain a string error message. It is up to you, as a developer, to
|
|
110
112
|
# decide how to deal with unsuccessful decoding.
|
|
111
113
|
module Decoding
|
|
112
|
-
class Error < StandardError; end
|
|
113
|
-
|
|
114
114
|
module_function
|
|
115
115
|
|
|
116
116
|
# Run a given `decoder` on the given input `value`.
|
|
@@ -119,4 +119,18 @@ module Decoding
|
|
|
119
119
|
# @param value [Object]
|
|
120
120
|
# @return [Decoding::Result<a>]
|
|
121
121
|
def decode(decoder, value) = decoder.call(value).map_err(&:to_s)
|
|
122
|
+
|
|
123
|
+
# Run a given `decoder` on the given input `value`, returning the decoded
|
|
124
|
+
# value or raising an error when decoding failed.
|
|
125
|
+
#
|
|
126
|
+
# @example
|
|
127
|
+
# decode!(string, "foo") # => "foo"
|
|
128
|
+
# decode!(string, 123) # raises Decoding::UnwrapError
|
|
129
|
+
# @overload decode!(decoder, value)
|
|
130
|
+
# @param decoder [Decoding::Decoder<a>]
|
|
131
|
+
# @param value [Object]
|
|
132
|
+
# @raise [Decoding::UnwrapError] when decoding failed.
|
|
133
|
+
# @return [Object]
|
|
134
|
+
# @see Decoding::Result#unwrap!
|
|
135
|
+
def decode!(...) = decode(...).unwrap!
|
|
122
136
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: decoding
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Arjan van der Gaag
|
|
@@ -28,20 +28,32 @@ files:
|
|
|
28
28
|
- README.md
|
|
29
29
|
- Rakefile
|
|
30
30
|
- lib/decoding.rb
|
|
31
|
+
- lib/decoding/data.rb
|
|
31
32
|
- lib/decoding/decoder.rb
|
|
32
33
|
- lib/decoding/decoders.rb
|
|
33
34
|
- lib/decoding/decoders/and_then.rb
|
|
34
35
|
- lib/decoding/decoders/any.rb
|
|
35
36
|
- lib/decoding/decoders/array.rb
|
|
36
37
|
- lib/decoding/decoders/at.rb
|
|
38
|
+
- lib/decoding/decoders/big_decimal.rb
|
|
39
|
+
- lib/decoding/decoders/date.rb
|
|
40
|
+
- lib/decoding/decoders/enum.rb
|
|
37
41
|
- lib/decoding/decoders/fail.rb
|
|
38
42
|
- lib/decoding/decoders/field.rb
|
|
39
43
|
- lib/decoding/decoders/hash.rb
|
|
40
44
|
- lib/decoding/decoders/index.rb
|
|
45
|
+
- lib/decoding/decoders/lazy.rb
|
|
41
46
|
- lib/decoding/decoders/map.rb
|
|
47
|
+
- lib/decoding/decoders/map_err.rb
|
|
42
48
|
- lib/decoding/decoders/match.rb
|
|
49
|
+
- lib/decoding/decoders/optional.rb
|
|
50
|
+
- lib/decoding/decoders/optional_field.rb
|
|
43
51
|
- lib/decoding/decoders/pass.rb
|
|
44
52
|
- lib/decoding/decoders/succeed.rb
|
|
53
|
+
- lib/decoding/decoders/time.rb
|
|
54
|
+
- lib/decoding/decoders/uri.rb
|
|
55
|
+
- lib/decoding/env.rb
|
|
56
|
+
- lib/decoding/error.rb
|
|
45
57
|
- lib/decoding/failure.rb
|
|
46
58
|
- lib/decoding/result.rb
|
|
47
59
|
- lib/decoding/version.rb
|