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.
- checksums.yaml +4 -4
- data/.rubocop.yml +12 -0
- data/CHANGELOG.md +27 -0
- data/CLAUDE.md +6 -0
- data/README.md +230 -3
- data/lib/decoding/decoders/any.rb +5 -1
- data/lib/decoding/decoders/array.rb +1 -1
- data/lib/decoding/decoders/big_decimal.rb +37 -0
- data/lib/decoding/decoders/date.rb +57 -0
- data/lib/decoding/decoders/enum.rb +47 -0
- data/lib/decoding/decoders/field.rb +2 -2
- data/lib/decoding/decoders/hash.rb +2 -2
- data/lib/decoding/decoders/index.rb +1 -4
- data/lib/decoding/decoders/lazy.rb +31 -0
- data/lib/decoding/decoders/map_err.rb +31 -0
- data/lib/decoding/decoders/match.rb +1 -1
- data/lib/decoding/decoders/optional.rb +33 -0
- data/lib/decoding/decoders/optional_field.rb +35 -0
- data/lib/decoding/decoders/succeed.rb +8 -3
- data/lib/decoding/decoders/time.rb +143 -0
- data/lib/decoding/decoders/uri.rb +27 -0
- data/lib/decoding/decoders.rb +149 -5
- data/lib/decoding/env.rb +70 -0
- data/lib/decoding/error.rb +6 -0
- data/lib/decoding/failure.rb +42 -4
- data/lib/decoding/matcher_helpers.rb +50 -0
- data/lib/decoding/result.rb +3 -1
- data/lib/decoding/rspec/decode_value.rb +87 -0
- data/lib/decoding/rspec/fail_with.rb +48 -0
- data/lib/decoding/rspec/succeed_with.rb +31 -0
- data/lib/decoding/rspec.rb +44 -0
- data/lib/decoding/version.rb +1 -1
- data/lib/decoding.rb +15 -2
- metadata +17 -1
|
@@ -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"
|
data/lib/decoding/version.rb
CHANGED
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.
|
|
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
|