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.
Files changed (5) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +201 -3
  3. data/lib/emu.rb +248 -12
  4. data/lib/emu/version.rb +1 -1
  5. metadata +3 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: 80e58ff8f1a11f69581ad745836d6a0af99aae8e46b51615d154f3114730c250
4
- data.tar.gz: 942c66c48155f9298b0069df9a35dfa5ea96f0895e2cc8403a5f4441e8778e57
2
+ SHA1:
3
+ metadata.gz: 6cb8dc3aaaa590096a21438a0df18dbe3e56e3b7
4
+ data.tar.gz: f39031150ff096ba68ef6dc3245bf5f69d9e1bdd
5
5
  SHA512:
6
- metadata.gz: aecb49f1cd38e186d347cdbe4991a091e4e8318f48d87bd553b1c8da7aae61277bedbab55fcbc78fc9da876b96af974f44e95783f202cc0a776313646f1e4e9f
7
- data.tar.gz: 4146a50e2062ae91ceb7a403e5510602a4725fa4c4320fa82fa6f40c41cc268bd2eb294732f3e724efd637df9767dd02b771a92b7e8b7038054eb032065fffc1
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
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/emu`. To experiment with that code, run `bin/console` for an interactive prompt.
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
- TODO: Delete this and the text above, and describe your gem
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
- TODO: Write usage instructions here
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 integer")
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 not be converted to a Boolean")
131
+ Err.new("`#{s.inspect}` can't be converted to a Boolean")
59
132
  end
60
133
  end
61
134
  end
62
135
 
63
- def self.id
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
- def self.succeed(v)
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(v)
160
+ Ok.new(value)
72
161
  end
73
162
  end
74
163
 
75
- def self.fail(e)
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(e)
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 [Object] the value to match against
88
- # @return [Emu::Decoder<Object>]
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("'#{hash}' doesn't contain key '#{key}'") unless hash.has_key?(key)
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
@@ -1,3 +1,3 @@
1
1
  module Emu
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
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.1.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: 2018-12-23 00:00:00.000000000 Z
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: 3.0.0.beta1
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