emu 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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