decoding 0.2.2 → 0.2.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b3b7725a14c0738d4860f46509eb8e2ce781f6298bb9253797c0dad84f769564
4
- data.tar.gz: '07785a90e96fe8a56bcbb2986d0cdd61aca6e75541f084e1a5c7be5799261912'
3
+ metadata.gz: b61483d249ba9fc5939de6d7b1aadf7ecc6c2ea4fceb65f927447a4d3828bcfa
4
+ data.tar.gz: beab3f51cec564c1bdf2bd7f40ffbc4f97280e7197e14b9e8cd5a980b3756366
5
5
  SHA512:
6
- metadata.gz: 4c253b152f0f82f95fb3a5a77c84acf741686c776492418967b5a9ca11800f1296351af373126f9d1efaeba8dd2418d3bbbdbdc1ad88e77c314489442b9af1a6
7
- data.tar.gz: ddba1656e7e7cec32423d0c1e6ada7494972b620666e1dc3daa70b7bf5c1210a0ad50911778b74e8c06d2f4befbe3f644c44bf38b05c8b97a266fdf3d0afd49f
6
+ metadata.gz: fcc99943040df23b080b84d32b0a6e8755dd2fd19c4a62befdd49970c843f0e28d96c1df885bb7c42538cc2e2b65f89d71c9ebb4f0f1da983d726bc95de39002
7
+ data.tar.gz: 8e53f19908fbccadb6068b851d96188d583267787631ce0c6207d61d2e2542716f44012c7ce0996b1146026ec07a764ad716c6b15d1b1c09974d3bf0994057d0
@@ -0,0 +1,18 @@
1
+ #!/bin/bash
2
+ # Auto-allow RSpec commands
3
+
4
+ # Read JSON from stdin
5
+ input=$(cat)
6
+
7
+ # Extract command from JSON (handle mcp__acp__Bash tool)
8
+ command=$(echo "$input" | grep -o '"command":"[^"]*"' | head -1 | cut -d'"' -f4)
9
+
10
+ # Check if it's an RSpec command
11
+ if [[ "$command" =~ (^|[[:space:]])bundle[[:space:]]+exec[[:space:]]+rspec || "$command" =~ (^|[[:space:]])rspec ]]; then
12
+ # Auto-allow RSpec commands
13
+ echo '{"permissionDecision":"allow","permissionDecisionReason":"RSpec commands are safe in this repo"}' >&2
14
+ exit 0
15
+ fi
16
+
17
+ # For other commands, no special handling
18
+ exit 0
@@ -0,0 +1,39 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "mcp__acp__Bash",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/allow-rspec.sh",
10
+ "timeout": 5
11
+ }
12
+ ]
13
+ }
14
+ ],
15
+ "SessionStart": [
16
+ {
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "echo '## Current Git Branch' && git branch --show-current && echo '\n## Pending Changes' && git status --short",
21
+ "timeout": 5
22
+ }
23
+ ]
24
+ }
25
+ ],
26
+ "PostToolUse": [
27
+ {
28
+ "matcher": "mcp__acp__Edit|mcp__acp__Write",
29
+ "hooks": [
30
+ {
31
+ "type": "command",
32
+ "command": "bundle exec rubocop -a \"$CLAUDE_TOOL_RESULT_file_path\" 2>&1 || true",
33
+ "timeout": 10
34
+ }
35
+ ]
36
+ }
37
+ ]
38
+ }
39
+ }
data/.rubocop.yml CHANGED
@@ -23,6 +23,9 @@ Metrics/ModuleLength:
23
23
  Exclude:
24
24
  - spec/**/*_spec.rb
25
25
 
26
+ Metrics/AbcSize:
27
+ Max: 18
28
+
26
29
  Metrics/BlockLength:
27
30
  Exclude:
28
31
  - spec/**/*_spec.rb
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.4]
4
+
5
+ * Make `succeed` and `fail` proper, composable decoders
6
+ * Ensure different decoders use the same error values
7
+ * Turn exceptions in decoder blocks into errors
8
+ * Use immutable failure values as errors
9
+
10
+ ## [0.2.3] - 2025-10-25
11
+
12
+ * Implement `Decoding::Result#deconstruct` to support pattern matching on result values.
13
+
3
14
  ## [0.2.2] - 2025-10-25
4
15
 
5
16
  * Added `Result#unwrap_err`
data/CLAUDE.md ADDED
@@ -0,0 +1,79 @@
1
+ # Decoding Gem - Instructions for Agentic Coding
2
+
3
+ ## Project Overview
4
+
5
+ Ruby gem for decoding dynamic/external data into known structures. Functional-style decoder composition pattern. Min Ruby 3.3.
6
+
7
+ ## Code Style & Standards
8
+
9
+ **Must follow:**
10
+ - Rubocop with rubocop-rspec, rubocop-performance, rubocop-rake
11
+ - Double quotes for strings
12
+ - Max line length: 140
13
+ - Frozen string literals
14
+ - Run `rake` (runs specs, rubocop, yard)
15
+
16
+ **Testing:**
17
+ - RSpec with SimpleCov
18
+ - Min coverage: 100% line, 100% branch
19
+ - Test file: `spec/<module>/<class>_spec.rb`
20
+ - Run with `COVERAGE=true bundle exec rspec`
21
+
22
+ ## Architecture
23
+
24
+ **Core concepts:**
25
+ - Decoders are callables (`decoder.call(value)` → `Result`)
26
+ - `Result` has `Ok` and `Err` subclasses
27
+ - Immutable `Failure` class for error tracking
28
+ - Decoder composition via `map`, `and_then`, `any`
29
+ - Module `Decoding::Decoders` contains all decoders
30
+
31
+ **Key files:**
32
+ - `lib/decoding/decoder.rb` - decoder protocol
33
+ - `lib/decoding/result.rb` - result type
34
+ - `lib/decoding/failure.rb` - error tracking
35
+ - `lib/decoding/decoders.rb` - all decoder implementations
36
+ - `lib/decoding/decoders/*.rb` - individual decoders
37
+
38
+ ## Common Tasks
39
+
40
+ **Add new decoder:**
41
+ 1. Create `lib/decoding/decoders/<name>.rb` with callable returning Result
42
+ 2. Add to `Decoders` module in `lib/decoding/decoders.rb`
43
+ 3. Add spec in `spec/decoding/decoders/<name>_spec.rb`
44
+ 4. Test success/failure cases
45
+ 5. Run `rake` to verify
46
+
47
+ **Modify existing decoder:**
48
+ 1. Read current implementation and spec first
49
+ 2. Keep backward compatibility unless breaking change justified
50
+ 3. Update specs for new behavior
51
+ 4. Verify coverage remains at 100%
52
+
53
+ **Run tests:**
54
+ - All: `COVERAGE=true bundle exec rspec`
55
+ - Single file: `bundle exec rspec spec/path/to/spec.rb`
56
+ - CI tests Ruby 3.3, 3.4, 4.0
57
+
58
+ **Code quality:**
59
+ - Run `bundle exec rubocop` (auto-fix: `-a` or `-A`)
60
+ - Generate docs: `bundle exec yard`
61
+ - Check docs: `bundle exec yard server`
62
+
63
+ ## Guidelines
64
+
65
+ - Decoders compose via functional patterns, avoid mutation
66
+ - Error messages should be descriptive
67
+ - Use `frozen_string_literal: true`
68
+ - Follow existing patterns in `lib/decoding/decoders/*.rb`
69
+ - Keep `Failure` immutable
70
+ - YARD docs for public methods
71
+ - Test edge cases (nil, empty, wrong types)
72
+
73
+ ## Don't
74
+
75
+ - Break immutability of Result/Failure
76
+ - Add dependencies without justification
77
+ - Skip specs for new decoders
78
+ - Ignore Rubocop violations
79
+ - Add features without tests
@@ -24,6 +24,8 @@ module Decoding
24
24
  @decoder.call(value).and_then do |decoded_value|
25
25
  @block.call(decoded_value).call(value)
26
26
  end
27
+ rescue StandardError => e
28
+ err(failure("error in and_then block: #{e.message}"))
27
29
  end
28
30
  end
29
31
  end
@@ -24,7 +24,7 @@ module Decoding
24
24
  .map { |v, i| @decoder.call(v).map_err { _1.push(i) } }
25
25
  .then { all _1 }
26
26
  else
27
- err("expected an Array, got: #{value.class}")
27
+ err(failure("expected an Array, got: #{value.class}"))
28
28
  end
29
29
  end
30
30
  end
@@ -17,7 +17,7 @@ module Decoding
17
17
  end
18
18
 
19
19
  # @param value [Object]
20
- # @return [Deceoding::Result<a>]
20
+ # @return [Decoding::Result<a>]
21
21
  def call(value)
22
22
  first, *rest = @keys.reverse
23
23
  nested_decoder = rest.reduce(Decoders::Field.new(first, @decoder)) do |acc, k|
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../decoder"
4
+
5
+ module Decoding
6
+ module Decoders
7
+ # A decoder that always fails with a predetermined error message, ignoring
8
+ # any input. This is useful for signaling errors in conditional decoder
9
+ # composition, such as inside {Decoders::AndThen}.
10
+ #
11
+ # @example Always fail with a message
12
+ # decode(Fail.new("unsupported"), "anything")
13
+ # # => Decoding::Err("unsupported")
14
+ #
15
+ # @example Use in conditional decoding
16
+ # and_then(field("version", integer)) do |version|
17
+ # case version
18
+ # when 1 then field("name", string)
19
+ # else Fail.new("unsupported version: #{version}")
20
+ # end
21
+ # end
22
+ class Fail < Decoder
23
+ # @param message [String] the error message to always return
24
+ def initialize(message)
25
+ @message = message
26
+ super()
27
+ end
28
+
29
+ # @param _value [Object] ignored input
30
+ # @return [Decoding::Result<Object>]
31
+ def call(_value) = err(failure(@message))
32
+ end
33
+ end
34
+ end
@@ -11,13 +11,13 @@ module Decoding
11
11
  # @param key [Object]
12
12
  # @param decoder [Decoding::Decoder<a>]
13
13
  def initialize(key, decoder)
14
- @key = key.to_str
14
+ @key = String(key)
15
15
  @decoder = decoder.to_decoder
16
16
  super()
17
17
  end
18
18
 
19
19
  # @param value [Object]
20
- # @return [Deceoding::Result<a>]
20
+ # @return [Decoding::Result<a>]
21
21
  def call(value)
22
22
  if value.is_a?(::Hash)
23
23
  if value.key?(@key)
@@ -19,24 +19,22 @@ module Decoding
19
19
  # @param value [Object]
20
20
  # @return [Decoding::Result<Hash<a, b>>]
21
21
  def call(value)
22
- if value.is_a?(::Hash)
23
- key_value_pairs = value.map do |k, v|
24
- all(
25
- [
26
- @key_decoder
27
- .call(k)
28
- .map_err { |e| failure("error decoding key #{k.inspect}: #{e}") },
22
+ return err(failure("expected Hash, got #{value.class}")) unless value.is_a?(::Hash)
29
23
 
30
- @value_decoder
31
- .call(v)
32
- .map_err { |e| failure("error decoding value for key #{k.inspect}: #{e}") }
33
- ]
34
- )
35
- end
36
- all(key_value_pairs).map(&:to_h)
37
- else
38
- err("expected Hash, got #{value.class}")
24
+ key_value_pairs = value.map do |k, v|
25
+ all(
26
+ [
27
+ @key_decoder
28
+ .call(k)
29
+ .map_err { |e| failure("error decoding key #{k.inspect}: #{e}") },
30
+
31
+ @value_decoder
32
+ .call(v)
33
+ .map_err { |e| failure("error decoding value for key #{k.inspect}: #{e}") }
34
+ ]
35
+ )
39
36
  end
37
+ all(key_value_pairs).map(&:to_h)
40
38
  end
41
39
  end
42
40
  end
@@ -23,13 +23,13 @@ module Decoding
23
23
  # @param value [Object]
24
24
  # @return [Decoding::Decoder<Object>]
25
25
  def call(value)
26
- return err("expected an Array, got: #{value.class}") unless value.is_a?(::Array)
26
+ return err(failure("expected an Array, got: #{value.class}")) unless value.is_a?(::Array)
27
27
 
28
28
  @decoder
29
29
  .call(value.fetch(@index))
30
- .map_err { "error decoding array item #{@index}: #{_1}" }
30
+ .map_err { _1.push(@index) }
31
31
  rescue IndexError => e
32
- err("error decoding array: #{e}")
32
+ err(failure("error decoding array: #{e}"))
33
33
  end
34
34
  end
35
35
  end
@@ -24,6 +24,8 @@ module Decoding
24
24
  Result
25
25
  .all(@decoders.map { _1.call(value) })
26
26
  .map { @block.call(*_1) }
27
+ rescue StandardError => e
28
+ err(failure("error in map block: #{e.message}"))
27
29
  end
28
30
  end
29
31
  end
@@ -5,7 +5,7 @@ module Decoding
5
5
  # Decoder that returns the original value as-is. You are not likely to need
6
6
  # this that often, as it kind of defeats the point of decoding -- but it
7
7
  # might be useful to inspect the original input value for logging or
8
- # debguging purposes.
8
+ # debugging purposes.
9
9
  class Pass < Decoder
10
10
  def call(value) = ok(value)
11
11
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../decoder"
4
+
5
+ module Decoding
6
+ module Decoders
7
+ # A decoder that always succeeds with a predetermined value, ignoring any
8
+ # input. This is useful for providing default values or as a building block
9
+ # in decoder composition.
10
+ #
11
+ # @example Always return a fixed value
12
+ # decode(Succeed.new(5), "anything") # => Decoding::Ok(5)
13
+ #
14
+ # @example Use with field to provide defaults
15
+ # decode(any(field("x", integer), Succeed.new(0)), {})
16
+ # # => Decoding::Ok(0)
17
+ class Succeed < Decoder
18
+ # @param value [Object] the value to always return
19
+ def initialize(value)
20
+ @value = value
21
+ super()
22
+ end
23
+
24
+ # @param _value [Object] ignored input
25
+ # @return [Decoding::Result<Object>]
26
+ def call(_value) = ok(@value)
27
+ end
28
+ end
29
+ end
@@ -10,6 +10,8 @@ require_relative "decoders/hash"
10
10
  require_relative "decoders/and_then"
11
11
  require_relative "decoders/at"
12
12
  require_relative "decoders/pass"
13
+ require_relative "decoders/succeed"
14
+ require_relative "decoders/fail"
13
15
  require_relative "result"
14
16
 
15
17
  module Decoding
@@ -30,7 +32,7 @@ module Decoding
30
32
 
31
33
  # Decode any string value that matches a regular expression.
32
34
  #
33
- # @param re [Regexp, String]
35
+ # @param regex [Regexp, String]
34
36
  # @return [Decoding::Decoder<String>]
35
37
  # @see Decoding::Decoders::Match
36
38
  def regexp(regex) = Decoders::Match.new(Regexp.new(regex))
@@ -106,14 +108,14 @@ module Decoding
106
108
  # @example
107
109
  # decode(succeed(5), "foo") # => Decoding::Ok(5)
108
110
  # @return [Decoding::Decoder<String>]
109
- def succeed(value) = ->(_) { Result.ok(value) }
111
+ def succeed(value) = Decoders::Succeed.new(value)
110
112
 
111
113
  # A decoder that always fails with the given value.
112
114
  #
113
115
  # @example
114
116
  # decode(fail("oh no"), "foo") # => Decoding::Err("oh no")
115
117
  # @return [Decoding::Decoder<String>]
116
- def fail(value) = ->(_) { Result.err(Decoding::Failure.new(value)) }
118
+ def fail(value) = Decoders::Fail.new(value)
117
119
 
118
120
  # A decoder that returns the input value, unaltered.
119
121
  #
@@ -9,10 +9,12 @@ module Decoding
9
9
  # error, the `array` decoder can push `3` to the stack to indicate that
10
10
  # happened at index 3 in its input value.
11
11
  class Failure
12
- # @paramn msg [String]
13
- def initialize(msg)
12
+ # @param msg [String]
13
+ # @param path [Array] Internal parameter for creating copies with updated paths
14
+ def initialize(msg, path = [])
14
15
  @msg = msg
15
- @path = []
16
+ @path = path.dup.freeze
17
+ freeze
16
18
  end
17
19
 
18
20
  def eql?(other)
@@ -23,12 +25,12 @@ module Decoding
23
25
  alias == eql?
24
26
 
25
27
  # Add segments to the stack of errors.
28
+ # Returns a new Failure instance with the updated path.
26
29
  #
27
30
  # @param segment [String]
28
31
  # @return [Decoding::Failure]
29
32
  def push(segment)
30
- @path << segment
31
- self
33
+ self.class.new(@msg, @path + [segment])
32
34
  end
33
35
 
34
36
  def to_s
@@ -52,6 +52,10 @@ module Decoding
52
52
  freeze
53
53
  end
54
54
 
55
+ def deconstruct
56
+ [value]
57
+ end
58
+
55
59
  def eql?(other)
56
60
  other.is_a?(self.class) && value == other.value
57
61
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Decoding
4
- VERSION = "0.2.2"
4
+ VERSION = "0.2.4"
5
5
  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.2.2
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arjan van der Gaag
@@ -15,11 +15,14 @@ executables: []
15
15
  extensions: []
16
16
  extra_rdoc_files: []
17
17
  files:
18
+ - ".claude/hooks/allow-rspec.sh"
19
+ - ".claude/settings.json"
18
20
  - ".rspec"
19
21
  - ".rubocop.yml"
20
22
  - ".tool-versions"
21
23
  - ".yardopts"
22
24
  - CHANGELOG.md
25
+ - CLAUDE.md
23
26
  - CODE_OF_CONDUCT.md
24
27
  - LICENSE.txt
25
28
  - README.md
@@ -31,12 +34,14 @@ files:
31
34
  - lib/decoding/decoders/any.rb
32
35
  - lib/decoding/decoders/array.rb
33
36
  - lib/decoding/decoders/at.rb
37
+ - lib/decoding/decoders/fail.rb
34
38
  - lib/decoding/decoders/field.rb
35
39
  - lib/decoding/decoders/hash.rb
36
40
  - lib/decoding/decoders/index.rb
37
41
  - lib/decoding/decoders/map.rb
38
42
  - lib/decoding/decoders/match.rb
39
43
  - lib/decoding/decoders/pass.rb
44
+ - lib/decoding/decoders/succeed.rb
40
45
  - lib/decoding/failure.rb
41
46
  - lib/decoding/result.rb
42
47
  - lib/decoding/version.rb
@@ -48,6 +53,9 @@ metadata:
48
53
  allowed_push_host: https://rubygems.org
49
54
  homepage_uri: https://github.com/avdgaag/decoding
50
55
  source_code_uri: https://github.com/avdgaag/decoding
56
+ changelog_uri: https://github.com/avdgaag/decoding/blob/main/CHANGELOG.md
57
+ bug_tracker_uri: https://github.com/avdgaag/decoding/issues
58
+ documentation_uri: https://rubydoc.info/gems/decoding
51
59
  rubygems_mfa_required: 'true'
52
60
  rdoc_options: []
53
61
  require_paths: