emu 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 +5 -5
- data/README.md +201 -3
- data/lib/emu.rb +248 -12
- data/lib/emu/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6cb8dc3aaaa590096a21438a0df18dbe3e56e3b7
|
4
|
+
data.tar.gz: f39031150ff096ba68ef6dc3245bf5f69d9e1bdd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 39934b762ce3e4aece0de5d59bab80202310f715a69377bdca63e1dae52cc6032459e9830d8440fdae1165600d0a6ccdc8fcf7020f2beb1836c3fec384170fee
|
7
|
+
data.tar.gz: 9b6b79a4711062e4928fb300f11d03049ca23d7921e4e3723f8aeace80285b0ede044af81002b9e1e174a29b81c2bfad864e10cdc010744f35f4350e186cc73a
|
data/README.md
CHANGED
@@ -1,8 +1,28 @@
|
|
1
1
|
# Emu
|
2
|
+
[](https://travis-ci.org/timhabermaas/emu)
|
2
3
|
|
3
|
-
|
4
|
+
Emu is a composable decoder and type coercion library. It can be used to
|
5
|
+
transform Rails' `params`, the result of `JSON.parse` or any other input type
|
6
|
+
to objects your business logic understands.
|
4
7
|
|
5
|
-
|
8
|
+
Its design is inspired by Elm's
|
9
|
+
[`Json.Decode`](https://package.elm-lang.org/packages/elm-lang/core/5.1.1/Json-Decode)
|
10
|
+
library in particular and [parser
|
11
|
+
combinators](https://en.wikipedia.org/wiki/Parser_combinator) in general.
|
12
|
+
|
13
|
+
## What sets it apart from the billion other coercing libraries?
|
14
|
+
|
15
|
+
The three main differences are:
|
16
|
+
|
17
|
+
* `Emu` is completely composable – there's no arbitrary difference between
|
18
|
+
decoders which return objects and decoders which return simple types. All
|
19
|
+
emus are equal!
|
20
|
+
* `Emu` isn't restricted by a 1:1 relationship between input attributes and
|
21
|
+
output attributes – you can transform the input structure in any way
|
22
|
+
you desire.
|
23
|
+
* `Emu` abstains from using a DSL. Everything can be accomplished by a
|
24
|
+
combination of method definitions and variable assignments. In particular
|
25
|
+
there's no need for `Library.register_type` calls.
|
6
26
|
|
7
27
|
## Installation
|
8
28
|
|
@@ -22,7 +42,185 @@ Or install it yourself as:
|
|
22
42
|
|
23
43
|
## Usage
|
24
44
|
|
25
|
-
|
45
|
+
Here's an example converting a `Hash` with some wind speed and direction data into a single vector describing both
|
46
|
+
parameters at once.
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
require 'emu'
|
50
|
+
|
51
|
+
direction =
|
52
|
+
(Emu.match('N') > [0, -1]) |
|
53
|
+
(Emu.match('E') > [-1, 0]) |
|
54
|
+
(Emu.match('S') > [0, 1]) |
|
55
|
+
(Emu.match('W') > [1, 0])
|
56
|
+
|
57
|
+
speed = Emu.str_to_float
|
58
|
+
|
59
|
+
wind = Emu.map_n(
|
60
|
+
Emu.from_key(:direction, direction),
|
61
|
+
Emu.from_key(:speed, speed)) do |(x, y), speed|
|
62
|
+
[x * speed, y * speed]
|
63
|
+
end
|
64
|
+
|
65
|
+
params = {
|
66
|
+
direction: "W",
|
67
|
+
speed: "4.5"
|
68
|
+
}
|
69
|
+
wind.run!(params) # => [4.5, 0.0]
|
70
|
+
```
|
71
|
+
|
72
|
+
This small example highlights almost all the features of `Emu`, hence there's a lot going on. So, let's break it down:
|
73
|
+
|
74
|
+
_For a quick overview of the most common use cases, skip to [TODO](#foo)._
|
75
|
+
|
76
|
+
All methods defined on the module `Emu` return a `Emu::Decoder`. A `Emu::Decoder` is a glorified lambda which can be run at a later time using `run!`. A decoder can either succeed or fail with a `Emu::DecodeError` exception:
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
decoder = Emu.str_to_int # a decoder converting strings to integers
|
80
|
+
decoder.run!("42") # => 42
|
81
|
+
decoder.run!("foo") # => raise DecodeError, '`"foo"` is not an Integer'
|
82
|
+
```
|
83
|
+
|
84
|
+
The individual decoders defined on `Emu` can be split into two parts:
|
85
|
+
|
86
|
+
* Basic decoders, e.g. `str_to_int` which takes a String and tries to convert it into an Integer and
|
87
|
+
* Higher order decoders which take other decoders and wrap/manipulate them.
|
88
|
+
|
89
|
+
|
90
|
+
### Basic decoders
|
91
|
+
|
92
|
+
#### Primitive types (no type conversion)
|
93
|
+
|
94
|
+
* `string`
|
95
|
+
* `integer`
|
96
|
+
* `float`
|
97
|
+
* `boolean`
|
98
|
+
* `raw`
|
99
|
+
|
100
|
+
### Higher order decoders
|
101
|
+
|
102
|
+
Just like "higher order functions" describe functions which take other functions as input "higher order decoders" describe decoders which take other decoders as input.
|
103
|
+
|
104
|
+
* `fmap`
|
105
|
+
* ...
|
106
|
+
|
107
|
+
|
108
|
+
## Common Use-Cases
|
109
|
+
|
110
|
+
### Decoding a Hash
|
111
|
+
|
112
|
+
For decoding a Hash you use a combination of `from_key(x, d)` (to decode the value at key `x` using the decoder `d`) and `map_n` to combine
|
113
|
+
multiple decoders into one:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
decoder = Emu.map_n(
|
117
|
+
Emu.from_key(:x, Emu.str_to_int),
|
118
|
+
Emu.from_key(:y, Emu.str_to_int)
|
119
|
+
) do |x, y|
|
120
|
+
[x, y]
|
121
|
+
end
|
122
|
+
|
123
|
+
params = {
|
124
|
+
x: "32",
|
125
|
+
y: "2"
|
126
|
+
}
|
127
|
+
|
128
|
+
Emu.from_key(:x, Emu.str_to_int).run!(params) # => 32
|
129
|
+
decoder.run!(params) # => [32, 2]
|
130
|
+
```
|
131
|
+
|
132
|
+
This gives you full control over optional keys, how to handle `nil`-values and makes it possible to map `n` keys to `y` values.
|
133
|
+
|
134
|
+
### Building Custom Decoders
|
135
|
+
|
136
|
+
You can build any decoder you want out of a combination of `raw`, `#then`, `succeed` and `fail`. For example the following
|
137
|
+
describes a decoder which maps the input `"foo"` to `123` and fails for any other input.
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
Emu.raw.then do |input|
|
141
|
+
if input == "foo"
|
142
|
+
Emu.succeed(123)
|
143
|
+
else
|
144
|
+
Emu.fail("bla")
|
145
|
+
end
|
146
|
+
end
|
147
|
+
```
|
148
|
+
|
149
|
+
Usually you want to make use of existing decoders which handle coercing instead of building one with `raw` from scratch.
|
150
|
+
For example the decoder which converts a String to a positive integer can be expressed as follows:
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
Emu.str_to_int.then do |n|
|
154
|
+
if n > 0
|
155
|
+
Emu.succeed(n)
|
156
|
+
else
|
157
|
+
Emu.fail("#{int.inspect} must be positive")
|
158
|
+
end
|
159
|
+
end
|
160
|
+
```
|
161
|
+
|
162
|
+
### Changing decoded values
|
163
|
+
|
164
|
+
Converting 0-based indices to 1-based ones, uppercasing some string, converting from one (physical) unit to another, ... are all
|
165
|
+
reasons where you want to run some function on a decoded value. That's what `fmap` provides:
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
zero_based_index = Emu.str_to_int
|
169
|
+
one_based_index = zero_based_index.fmap { |i| i + 1}
|
170
|
+
zero_based_index.run!("12") # => 12
|
171
|
+
one_based_index.run!("12") # => 13
|
172
|
+
```
|
173
|
+
|
174
|
+
_Note: You can't change the status of a decoder from success to failure by using only `Decoder#fmap`. You need `then` for that_
|
175
|
+
|
176
|
+
### dependent decoding (bind/then)
|
177
|
+
|
178
|
+
### Decoding Recursive Structures
|
179
|
+
|
180
|
+
When decoding recursive structures we quickly run into the issue of endless recursion:
|
181
|
+
|
182
|
+
```ruby
|
183
|
+
{
|
184
|
+
name: 'Elvis Presley',
|
185
|
+
parent: {
|
186
|
+
name: 'R2D2',
|
187
|
+
parent: {
|
188
|
+
name: 'Barack Obama'
|
189
|
+
parent: nil
|
190
|
+
}
|
191
|
+
}
|
192
|
+
}
|
193
|
+
|
194
|
+
# person will be nil on the right-hand side => runtime error
|
195
|
+
person =
|
196
|
+
Emu.map_n(
|
197
|
+
Emu.from_key(:name, Emu.string),
|
198
|
+
Emu.from_key(:parent, Emu.nil | person)) do |name, parent|
|
199
|
+
Person.new(name, parent)
|
200
|
+
end
|
201
|
+
|
202
|
+
# person calls itself => infinite recursion
|
203
|
+
def person
|
204
|
+
Emu.map_n(
|
205
|
+
Emu.from_key(:name, Emu.string),
|
206
|
+
Emu.from_key(:parent, Emu.nil | person)) do |name, parent|
|
207
|
+
Person.new(name, parent)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
```
|
211
|
+
|
212
|
+
This can be solved by wrapping the recursive call in `lazy`:
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
person =
|
216
|
+
Emu.map_n(
|
217
|
+
Emu.from_key(:name, Emu.string),
|
218
|
+
Emu.from_key(:parent, Emu.nil | Emu.lazy { person })) do |name, parent|
|
219
|
+
Person.new(name, parent)
|
220
|
+
end
|
221
|
+
```
|
222
|
+
|
223
|
+
`lazy` takes a block which is only evaluated once you call `run` on the decoder. This avoids funky behavior when defining recursive decoders.
|
26
224
|
|
27
225
|
## Development
|
28
226
|
|
data/lib/emu.rb
CHANGED
@@ -18,6 +18,14 @@ module Emu
|
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
+
# Creates a decoder which converts a string to an integer. It uses ++Integer++
|
22
|
+
# for the conversion.
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# Emu.str_to_int.run!("42") # => 42
|
26
|
+
# Emu.str_to_int.run!("a") # => raise DecodeError, "`\"a\"` can't be converted to an Integer"
|
27
|
+
# Emu.str_to_int.run!(42) # => raise DecodeError, "`42` is not a String"
|
28
|
+
# @return [Emu::Decoder<Integer>]
|
21
29
|
def self.str_to_int
|
22
30
|
Decoder.new do |s|
|
23
31
|
next Err.new("`#{s.inspect}` is not a String") unless s.is_a?(String)
|
@@ -25,11 +33,38 @@ module Emu
|
|
25
33
|
begin
|
26
34
|
Ok.new(Integer(s))
|
27
35
|
rescue TypeError, ArgumentError
|
28
|
-
Err.new("`#{s.inspect}` can't be converted to an
|
36
|
+
Err.new("`#{s.inspect}` can't be converted to an Integer")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Creates a decoder which converts a string to a float. It uses ++Float++
|
42
|
+
# for the conversion.
|
43
|
+
#
|
44
|
+
# @example
|
45
|
+
# Emu.str_to_float.run!("42.2") # => 42.2
|
46
|
+
# Emu.str_to_float.run!("42") # => 42.0
|
47
|
+
# Emu.str_to_float.run!("a") # => raise DecodeError, "`\"a\"` can't be converted to a Float"
|
48
|
+
# Emu.str_to_float.run!(42) # => raise DecodeError, "`42` is not a String"
|
49
|
+
# @return [Emu::Decoder<Float>]
|
50
|
+
def self.str_to_float
|
51
|
+
Decoder.new do |s|
|
52
|
+
next Err.new("`#{s.inspect}` is not a String") unless s.is_a?(String)
|
53
|
+
|
54
|
+
begin
|
55
|
+
Ok.new(Float(s))
|
56
|
+
rescue TypeError, ArgumentError
|
57
|
+
Err.new("`#{s.inspect}` can't be converted to a Float")
|
29
58
|
end
|
30
59
|
end
|
31
60
|
end
|
32
61
|
|
62
|
+
# Creates a decoder which only accepts integers.
|
63
|
+
#
|
64
|
+
# @example
|
65
|
+
# Emu.integer.run!(2) # => 2
|
66
|
+
# Emu.integer.run!("2") # => raise DecodeError, '`"2"` is not an Integer'
|
67
|
+
# @return [Emu::Decoder<Integer>]
|
33
68
|
def self.integer
|
34
69
|
Decoder.new do |i|
|
35
70
|
next Err.new("`#{i.inspect}` is not an Integer") unless i.is_a?(Integer)
|
@@ -38,6 +73,30 @@ module Emu
|
|
38
73
|
end
|
39
74
|
end
|
40
75
|
|
76
|
+
# Creates a decoder which only accepts floats (including integers).
|
77
|
+
# Integers are converted to floats because the result type should be uniform.
|
78
|
+
#
|
79
|
+
# @example
|
80
|
+
# Emu.float.run!(2) # => 2.0
|
81
|
+
# Emu.float.run!(2.1) # => 2.1
|
82
|
+
# Emu.float.run!("2") # => raise DecodeError, '`"2"` is not a Float'
|
83
|
+
# @return [Emu::Decoder<Float>]
|
84
|
+
def self.float
|
85
|
+
Decoder.new do |i|
|
86
|
+
next Err.new("`#{i.inspect}` is not a Float") unless i.is_a?(Float) || i.is_a?(Integer)
|
87
|
+
|
88
|
+
Ok.new(i.to_f)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Creates a decoder which only accepts booleans.
|
93
|
+
#
|
94
|
+
# @example
|
95
|
+
# Emu.boolean.run!(true) # => true
|
96
|
+
# Emu.boolean.run!(false) # => false
|
97
|
+
# Emu.boolean.run!(nil) # => raise DecodeError, "`nil` is not a Boolean"
|
98
|
+
# Emu.boolean.run!(2) # => raise DecodeError, "`2` is not a Boolean"
|
99
|
+
# @return [Emu::Decoder<TrueClass|FalseClass>]
|
41
100
|
def self.boolean
|
42
101
|
Decoder.new do |b|
|
43
102
|
next Err.new("`#{b.inspect}` is not a Boolean") unless b.is_a?(TrueClass) || b.is_a?(FalseClass)
|
@@ -46,6 +105,20 @@ module Emu
|
|
46
105
|
end
|
47
106
|
end
|
48
107
|
|
108
|
+
# Creates a decoder which converts a string to a boolean (<tt>true</tt>, <tt>false</tt>) value.
|
109
|
+
#
|
110
|
+
# <tt>"0"</tt> and <tt>"false"</tt> are considered ++false++, <tt>"1"</tt> and <tt>"true"</tt> are considered ++true++.
|
111
|
+
# Trying to decode any other value will fail.
|
112
|
+
#
|
113
|
+
# @example
|
114
|
+
# Emu.str_to_bool.run!("true") # => true
|
115
|
+
# Emu.str_to_bool.run!("1") # => true
|
116
|
+
# Emu.str_to_bool.run!("false") # => false
|
117
|
+
# Emu.str_to_bool.run!("0") # => false
|
118
|
+
# Emu.str_to_bool.run!(true) # => raise DecodeError, "`true` is not a String"
|
119
|
+
# Emu.str_to_bool.run!("2") # => raise DecodeError, "`\"2\"` can't be converted to a Boolean"
|
120
|
+
#
|
121
|
+
# @return [Emu::Decoder<TrueClass|FalseClass>]
|
49
122
|
def self.str_to_bool
|
50
123
|
Decoder.new do |s|
|
51
124
|
next Err.new("`#{s.inspect}` is not a String") unless s.is_a?(String)
|
@@ -55,51 +128,191 @@ module Emu
|
|
55
128
|
elsif s == "false" || s == "0"
|
56
129
|
Ok.new(false)
|
57
130
|
else
|
58
|
-
Err.new("`#{s.inspect}` can
|
131
|
+
Err.new("`#{s.inspect}` can't be converted to a Boolean")
|
59
132
|
end
|
60
133
|
end
|
61
134
|
end
|
62
135
|
|
63
|
-
|
136
|
+
# Creates a decoder which always succeeds and yields the input.
|
137
|
+
#
|
138
|
+
# This might be useful if you don't care about the exact shape of
|
139
|
+
# of your data and don't have a need to inspect it (e.g. some binary
|
140
|
+
# data).
|
141
|
+
#
|
142
|
+
# @example
|
143
|
+
# Emu.raw.run!(true) # => true
|
144
|
+
# Emu.raw.run!("2") # => "2"
|
145
|
+
# @return [Emu::Decoder<a>]
|
146
|
+
def self.raw
|
64
147
|
Decoder.new do |s|
|
65
148
|
Ok.new(s)
|
66
149
|
end
|
67
150
|
end
|
68
151
|
|
69
|
-
|
152
|
+
# Creates a decoder which always succeeds with the provided value.
|
153
|
+
#
|
154
|
+
# @example
|
155
|
+
# Emu.succeed("foo").run!(42) # => "foo"
|
156
|
+
# @param value [a] the value the decoder evaluates to
|
157
|
+
# @return [Emu::Decoder<a>]
|
158
|
+
def self.succeed(value)
|
70
159
|
Decoder.new do |_|
|
71
|
-
Ok.new(
|
160
|
+
Ok.new(value)
|
72
161
|
end
|
73
162
|
end
|
74
163
|
|
75
|
-
|
164
|
+
# Creates a decoder which always fails with the provided message.
|
165
|
+
#
|
166
|
+
# @example
|
167
|
+
# Emu.fail("foo").run!(42) # => raise DecodeError, "foo"
|
168
|
+
# @param message [String] the error message the decoder evaluates to
|
169
|
+
# @return [Emu::Decoder<Void>]
|
170
|
+
def self.fail(message)
|
76
171
|
Decoder.new do |_|
|
77
|
-
Err.new(
|
172
|
+
Err.new(message)
|
78
173
|
end
|
79
174
|
end
|
80
175
|
|
81
176
|
# Returns a decoder which succeeds if the input value matches ++constant++.
|
177
|
+
# If the decoder succeeds it resolves to the input value.
|
82
178
|
# #== is used for comparision, no type checks are performed.
|
83
179
|
#
|
84
180
|
# @example
|
85
181
|
# Emu.match(42).run!(42) # => 42
|
86
|
-
# Emu.match(42).run!(41) # => raise DecodeError, "`41` doesn't match `42`"
|
87
|
-
# @param constant [
|
88
|
-
# @return [Emu::Decoder<
|
182
|
+
# Emu.match(42).run!(41) # => raise DecodeError, "Input `41` doesn't match expected value `42`"
|
183
|
+
# @param constant [a] the value to match against
|
184
|
+
# @return [Emu::Decoder<a>]
|
89
185
|
def self.match(constant)
|
90
186
|
Decoder.new do |s|
|
91
|
-
s == constant ? Ok.new(s) : Err.new("`#{s.inspect}` doesn't match `#{constant.inspect}`")
|
187
|
+
s == constant ? Ok.new(s) : Err.new("Input `#{s.inspect}` doesn't match expected value `#{constant.inspect}`")
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Creates a decoder which only accepts `nil` values.
|
192
|
+
#
|
193
|
+
# @example
|
194
|
+
# Emu.nil.run!(nil) # => nil
|
195
|
+
# Emu.nil.run!(42) # => raise DecodeError, "`42` isn't `nil`"
|
196
|
+
# @return [Emu::Decoder<NilClass>]
|
197
|
+
def self.nil
|
198
|
+
Decoder.new do |s|
|
199
|
+
s.nil? ? Ok.new(s) : Err.new("`#{s.inspect}` isn't `nil`")
|
92
200
|
end
|
93
201
|
end
|
94
202
|
|
203
|
+
# Creates a decoder which extracts the value of a hash map according to the given key.
|
204
|
+
#
|
205
|
+
# @example
|
206
|
+
# Emu.from_key(:a, Emu.str_to_int).run!({a: "42"}) # => 42
|
207
|
+
# Emu.from_key(:a, Emu.str_to_int).run!({a: "a"}) # => raise DecodeError, '`"a"` can't be converted to an integer'
|
208
|
+
# Emu.from_key(:a, Emu.str_to_int).run!({b: "42"}) # => raise DecodeError, '`{:b=>"42"}` doesn't contain key `:a`'
|
209
|
+
#
|
210
|
+
# @param key [a] the key of the hash map
|
211
|
+
# @param decoder [Emu::Decoder<b>] the decoder to apply to the value at key ++key++
|
212
|
+
# @return [Emu::Decoder<b>]
|
95
213
|
def self.from_key(key, decoder)
|
96
214
|
Decoder.new do |hash|
|
97
|
-
next Err.new("
|
215
|
+
next Err.new("`#{hash.inspect}` is not a Hash") unless hash.respond_to?(:has_key?) && hash.respond_to?(:fetch)
|
216
|
+
next Err.new("`#{hash.inspect}` doesn't contain key `#{key.inspect}`") unless hash.has_key?(key)
|
98
217
|
|
99
218
|
decoder.run(hash.fetch(key))
|
100
219
|
end
|
101
220
|
end
|
102
221
|
|
222
|
+
# Creates a decoder which extracts the value of a hash map according to the
|
223
|
+
# given key. If the key cannot be found ++nil++ will be returned.
|
224
|
+
#
|
225
|
+
# Note: If a key can be found, but the value decoder fails
|
226
|
+
# ++from_key_or_nil++ will fail as well. This is usually what you want,
|
227
|
+
# because this indicates bad data you don't know how to handle.
|
228
|
+
#
|
229
|
+
# @example
|
230
|
+
# Emu.from_key_or_nil(:a, Emu.str_to_int).run!({a: "42"}) # => 42
|
231
|
+
# Emu.from_key_or_nil(:a, Emu.str_to_int).run!({a: "a"}) # => raise DecodeError, '`"a"` can't be converted to an integer'
|
232
|
+
# Emu.from_key_or_nil(:a, Emu.str_to_int).run!({b: "42"}) # => nil
|
233
|
+
#
|
234
|
+
# @param key [a] the key of the hash map
|
235
|
+
# @param decoder [Emu::Decoder<b>] the decoder to apply to the value at key ++key++
|
236
|
+
# @return [Emu::Decoder<b, NilClass>]
|
237
|
+
def self.from_key_or_nil(key, decoder)
|
238
|
+
Decoder.new do |hash|
|
239
|
+
next Err.new("`#{hash.inspect}` is not a Hash") unless hash.respond_to?(:has_key?) && hash.respond_to?(:fetch)
|
240
|
+
if hash.has_key?(key)
|
241
|
+
decoder.run(hash.fetch(key))
|
242
|
+
else
|
243
|
+
Ok.new(nil)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# Creates a decoder which extracts the value of an array at the given index.
|
249
|
+
#
|
250
|
+
# @example
|
251
|
+
# Emu.at_index(0, Emu.str_to_int).run!(["42"]) # => 42
|
252
|
+
# Emu.at_index(0, Emu.str_to_int).run!(["a"]) # => raise DecodeError, '`"a"` can't be converted to an integer'
|
253
|
+
# Emu.at_index(1, Emu.str_to_int).run!(["42"]) # => raise DecodeError, '`["42"]` doesn't contain index `1`'
|
254
|
+
#
|
255
|
+
# @param index [Integer] the key of the hash map
|
256
|
+
# @param decoder [Emu::Decoder<b>] the decoder to apply to the value at index ++index++
|
257
|
+
# @return [Emu::Decoder<b>]
|
258
|
+
def self.at_index(index, decoder)
|
259
|
+
Decoder.new do |array|
|
260
|
+
next Err.new("`#{array.inspect}` doesn't contain index `#{index.inspect}`") if index >= array.length
|
261
|
+
|
262
|
+
decoder.run(array[index])
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# Creates a decoder which decodes the values of an array and returns the decoded array.
|
267
|
+
#
|
268
|
+
# @example
|
269
|
+
# Emu.array(Emu.str_to_int).run!(["42", "43"]) # => [42, 43]
|
270
|
+
# Emu.array(Emu.str_to_int).run!("42") # => raise DecodeError, "`"a"` is not an Array"
|
271
|
+
# Emu.array(Emu.str_to_int).run!(["a"]) # => raise DecodeError, '`"a"` can't be converted to an Integer'
|
272
|
+
#
|
273
|
+
# @param decoder [Emu::Decoder<b>] the decoder to apply to all values of the array
|
274
|
+
# @return [Emu::Decoder<b>]
|
275
|
+
def self.array(decoder)
|
276
|
+
Decoder.new do |array|
|
277
|
+
next Err.new("`#{array.inspect}` is not an Array") unless array.is_a?(Array)
|
278
|
+
|
279
|
+
result = []
|
280
|
+
|
281
|
+
i = 0
|
282
|
+
error_found = nil
|
283
|
+
while i < array.length && !error_found
|
284
|
+
r = decoder.run(array[i])
|
285
|
+
if r.error?
|
286
|
+
error_found = r
|
287
|
+
else
|
288
|
+
result << r.unwrap
|
289
|
+
end
|
290
|
+
i += 1
|
291
|
+
end
|
292
|
+
|
293
|
+
if error_found
|
294
|
+
error_found
|
295
|
+
else
|
296
|
+
Ok.new(result)
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
# Builds a decoder out of ++n++ decoders and maps a function over the result
|
302
|
+
# of the passed in decoders. For the block to be called all decoders must succeed.
|
303
|
+
#
|
304
|
+
# @example
|
305
|
+
# d = Emu.map_n(Emu.string, Emu.str_to_int) do |string, integer|
|
306
|
+
# string * integer
|
307
|
+
# end
|
308
|
+
#
|
309
|
+
# d.run!("3") # => "333"
|
310
|
+
# d.run!("a") # => raise DecodeError, '`"a"` can't be converted to an Integer'
|
311
|
+
#
|
312
|
+
# @param decoders [Array<Decoder>] the decoders to map over
|
313
|
+
# @yield [a, b, c, ...] Passes the result of all decoders to the block
|
314
|
+
# @yieldreturn [z] the value the decoder should evaluate to
|
315
|
+
# @return [Emu::Decoder<z>]
|
103
316
|
def self.map_n(*decoders, &block)
|
104
317
|
raise "decoder count must match argument count of provided block" unless decoders.size == block.arity
|
105
318
|
|
@@ -116,4 +329,27 @@ module Emu
|
|
116
329
|
end
|
117
330
|
end
|
118
331
|
end
|
332
|
+
|
333
|
+
# Wraps a decoder +d+ in a lazily evaluated block to avoid endless recursion when
|
334
|
+
# dealing with recursive data structures. <tt>Emu.lazy { d }.run!</tt> behaves exactly like
|
335
|
+
# +d.run!+.
|
336
|
+
#
|
337
|
+
# @example
|
338
|
+
# person =
|
339
|
+
# Emu.map_n(
|
340
|
+
# Emu.from_key(:name, Emu.string),
|
341
|
+
# Emu.from_key(:parent, Emu.nil | Emu.lazy { person })) do |name, parent|
|
342
|
+
# Person.new(name, parent)
|
343
|
+
# end
|
344
|
+
#
|
345
|
+
# person.run!({name: "foo", parent: { name: "bar", parent: nil }}) # => Person("foo", Person("bar", nil))
|
346
|
+
#
|
347
|
+
# @yieldreturn [Emu::Decoder<a>] the wrapped decoder
|
348
|
+
# @return [Emu::Decoder<a>]
|
349
|
+
def self.lazy
|
350
|
+
Decoder.new do |input|
|
351
|
+
inner_decoder = yield
|
352
|
+
inner_decoder.run(input)
|
353
|
+
end
|
354
|
+
end
|
119
355
|
end
|
data/lib/emu/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: emu
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tim Habermaas
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-03-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -62,7 +62,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
62
62
|
version: '0'
|
63
63
|
requirements: []
|
64
64
|
rubyforge_project:
|
65
|
-
rubygems_version:
|
65
|
+
rubygems_version: 2.5.2.3
|
66
66
|
signing_key:
|
67
67
|
specification_version: 4
|
68
68
|
summary: Composable decoding library in the spirit of Json.Decode from Elm
|