emu 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/timhabermaas/emu.svg?branch=master)](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
|