decoding 0.3.0 → 0.5.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.
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../matcher_helpers"
4
+
5
+ # Passes when the given decoder decodes the given input. Chain `to` to also
6
+ # assert the decoded value, or `failing_with` (optionally with `at`) to assert
7
+ # that decoding fails with a particular message. Both expected values may
8
+ # themselves be matchers.
9
+ #
10
+ # expect(decoder).to decode_value(["foo"]).to(["foo"])
11
+ # expect(decoder).to decode_value([1]).failing_with("expected String, got Integer").at(0)
12
+ # expect(decoder).not_to decode_value(true)
13
+ RSpec::Matchers.define :decode_value do |input|
14
+ include Decoding::MatcherHelpers
15
+
16
+ match do |actual|
17
+ next false unless actual.respond_to?(:call)
18
+
19
+ result = Decoding.decode(actual, input)
20
+ next matches_failure?(result) if message_expected?
21
+ next result.ok? && matches_value?(@expected_value, result.unwrap!) if value_expected?
22
+
23
+ result.ok?
24
+ end
25
+
26
+ match_when_negated do |actual|
27
+ raise ArgumentError, "use `not_to decode_value(input)` without `to` or `failing_with`" if message_expected? || value_expected?
28
+
29
+ actual.respond_to?(:call) && Decoding.decode(actual, input).err?
30
+ end
31
+
32
+ chain :to do |value|
33
+ @expected_value = value
34
+ @value_expected = true
35
+ end
36
+
37
+ chain :failing_with do |message|
38
+ @expected_message = message
39
+ @message_expected = true
40
+ end
41
+
42
+ chain :at do |*segments|
43
+ @expected_path = segments
44
+ end
45
+
46
+ failure_message do |actual|
47
+ next "expected a decoder, got #{description_of(actual)}" unless actual.respond_to?(:call)
48
+
49
+ "expected the decoder to #{description}, but #{describe_outcome(Decoding.decode(actual, input))}"
50
+ end
51
+
52
+ failure_message_when_negated do |actual|
53
+ next "expected a decoder, got #{description_of(actual)}" unless actual.respond_to?(:call)
54
+
55
+ "expected the decoder not to decode #{description_of(input)}"
56
+ end
57
+
58
+ description do
59
+ if message_expected?
60
+ next "fail decoding #{description_of(input)} with #{description_of(@expected_message)}#{render_path(expected_path)}"
61
+ end
62
+ next "decode #{description_of(input)} to #{description_of(@expected_value)}" if value_expected?
63
+
64
+ "decode #{description_of(input)}"
65
+ end
66
+
67
+ def message_expected? = @message_expected
68
+ def value_expected? = @value_expected
69
+
70
+ def matches_failure?(result)
71
+ return false unless result.err?
72
+
73
+ msg, path = describe_failure(result)
74
+ matches_value?(@expected_message, msg) && path_matches?(path)
75
+ end
76
+
77
+ def describe_outcome(result)
78
+ unless result.ok?
79
+ msg, path = describe_failure(result)
80
+ return "it failed with #{description_of(msg)}#{render_path(path)}"
81
+ end
82
+
83
+ return "it succeeded with #{description_of(result.unwrap!)}" if message_expected?
84
+
85
+ "it decoded to #{description_of(result.unwrap!)}"
86
+ end
87
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../matcher_helpers"
4
+
5
+ # Passes when the given {Decoding::Result} is an `Err` that failed with the
6
+ # expected message. Chain `at` to also assert where the failure occurred,
7
+ # naming the path segments outermost first. The expected message may itself be
8
+ # a matcher.
9
+ #
10
+ # expect(Decoding.decode(decoder, [1])).to fail_with("expected String, got Integer").at(0)
11
+ #
12
+ # Failures are recognised both as {Decoding::Failure} values and as the strings
13
+ # they turn into once {Decoding.decode} has rendered them.
14
+ RSpec::Matchers.define :fail_with do |expected|
15
+ include Decoding::MatcherHelpers
16
+
17
+ match do |actual|
18
+ next false unless actual.is_a?(Decoding::Result) && actual.err?
19
+
20
+ msg, path = describe_failure(actual)
21
+ matches_value?(expected, msg) && path_matches?(path)
22
+ end
23
+
24
+ chain :at do |*segments|
25
+ @expected_path = segments
26
+ end
27
+
28
+ failure_message do |actual|
29
+ next "expected a Decoding::Result, got #{description_of(actual)}" unless actual.is_a?(Decoding::Result)
30
+
31
+ outcome =
32
+ if actual.ok?
33
+ "it succeeded with #{description_of(actual.unwrap!)}"
34
+ else
35
+ msg, path = describe_failure(actual)
36
+ "it failed with #{description_of(msg)}#{render_path(path)}"
37
+ end
38
+ "expected the result to #{description}, but #{outcome}"
39
+ end
40
+
41
+ failure_message_when_negated do
42
+ "expected the result not to #{description}"
43
+ end
44
+
45
+ description do
46
+ "fail with #{description_of(expected)}#{render_path(expected_path)}"
47
+ end
48
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../matcher_helpers"
4
+
5
+ # Passes when the given {Decoding::Result} is an `Ok` holding the expected
6
+ # value. The expected value may itself be a matcher.
7
+ #
8
+ # expect(Decoding.decode(decoder, "foo")).to succeed_with("foo")
9
+ RSpec::Matchers.define :succeed_with do |expected|
10
+ include Decoding::MatcherHelpers
11
+
12
+ match do |actual|
13
+ actual.is_a?(Decoding::Result) && actual.ok? && matches_value?(expected, actual.unwrap!)
14
+ end
15
+
16
+ failure_message do |actual|
17
+ next "expected a Decoding::Result, got #{description_of(actual)}" unless actual.is_a?(Decoding::Result)
18
+
19
+ outcome =
20
+ if actual.ok?
21
+ "it succeeded with #{description_of(actual.unwrap!)}"
22
+ else
23
+ "it failed with #{description_of(actual.unwrap_err(nil).to_s)}"
24
+ end
25
+ "expected the result to succeed with #{description_of(expected)}, but #{outcome}"
26
+ end
27
+
28
+ failure_message_when_negated do
29
+ "expected the result not to succeed with #{description_of(expected)}"
30
+ end
31
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Matchers for asserting on decoders and their results, for applications that
4
+ # write decoders of their own.
5
+ #
6
+ # These are the matchers this gem tests itself with. Require them from your
7
+ # spec helper; they need `rspec-expectations`, which is not a dependency of
8
+ # this gem, so nothing is loaded unless you ask for it.
9
+ #
10
+ # # spec/spec_helper.rb
11
+ # require "decoding/rspec"
12
+ #
13
+ # `decode_value` describes a decoder, and is what you want most of the time: it
14
+ # runs the decoder for you, so an example says what a decoder does rather than
15
+ # how to call it.
16
+ #
17
+ # expect(my_decoder).to decode_value({ "name" => "John" }).to({ name: "John" })
18
+ # expect(my_decoder).to decode_value({}).failing_with(%(expected Hash with key "name"))
19
+ # expect(my_decoder).to decode_value(nil) # decodes; the value is not asserted
20
+ # expect(my_decoder).not_to decode_value(nil) # fails to decode
21
+ #
22
+ # An error nested in a structure is asserted with its location, outermost
23
+ # segment first, rather than by matching the rendered "Error at ." prefix:
24
+ #
25
+ # expect(field("a", field("b", string)))
26
+ # .to decode_value({ "a" => { "b" => 1 } })
27
+ # .failing_with("expected String, got Integer").at("a", "b")
28
+ #
29
+ # `succeed_with` and `fail_with` describe a result you already have, for when
30
+ # making the call is part of what the example is testing:
31
+ #
32
+ # expect(Decoding.decode(my_decoder, input)).to succeed_with({ name: "John" })
33
+ # expect(Decoding.decode(my_decoder, input)).to fail_with("expected String, got Integer").at("name")
34
+ #
35
+ # Expected values are compared strictly, so `1` does not match `1.0` and a
36
+ # decoder answering with the wrong type cannot pass. Pass a matcher where that
37
+ # is too strict:
38
+ #
39
+ # expect(unix_time).to decode_value("1595674680.5").to(an_object_having_attributes(usec: 500_000))
40
+ require_relative "../decoding"
41
+ require_relative "matcher_helpers"
42
+ require_relative "rspec/decode_value"
43
+ require_relative "rspec/succeed_with"
44
+ require_relative "rspec/fail_with"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Decoding
4
- VERSION = "0.3.0"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/decoding.rb CHANGED
@@ -1,6 +1,7 @@
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"
5
6
  require_relative "decoding/data"
6
7
 
@@ -110,8 +111,6 @@ require_relative "decoding/data"
110
111
  # always contain a string error message. It is up to you, as a developer, to
111
112
  # decide how to deal with unsuccessful decoding.
112
113
  module Decoding
113
- class Error < StandardError; end
114
-
115
114
  module_function
116
115
 
117
116
  # Run a given `decoder` on the given input `value`.
@@ -120,4 +119,18 @@ module Decoding
120
119
  # @param value [Object]
121
120
  # @return [Decoding::Result<a>]
122
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!
123
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.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arjan van der Gaag
@@ -35,16 +35,32 @@ files:
35
35
  - lib/decoding/decoders/any.rb
36
36
  - lib/decoding/decoders/array.rb
37
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
38
41
  - lib/decoding/decoders/fail.rb
39
42
  - lib/decoding/decoders/field.rb
40
43
  - lib/decoding/decoders/hash.rb
41
44
  - lib/decoding/decoders/index.rb
45
+ - lib/decoding/decoders/lazy.rb
42
46
  - lib/decoding/decoders/map.rb
47
+ - lib/decoding/decoders/map_err.rb
43
48
  - lib/decoding/decoders/match.rb
49
+ - lib/decoding/decoders/optional.rb
50
+ - lib/decoding/decoders/optional_field.rb
44
51
  - lib/decoding/decoders/pass.rb
45
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
46
57
  - lib/decoding/failure.rb
58
+ - lib/decoding/matcher_helpers.rb
47
59
  - lib/decoding/result.rb
60
+ - lib/decoding/rspec.rb
61
+ - lib/decoding/rspec/decode_value.rb
62
+ - lib/decoding/rspec/fail_with.rb
63
+ - lib/decoding/rspec/succeed_with.rb
48
64
  - lib/decoding/version.rb
49
65
  - sig/decoding.rbs
50
66
  homepage: https://github.com/avdgaag/decoding