decoding 0.2.3 → 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 +4 -4
- data/.claude/hooks/allow-rspec.sh +18 -0
- data/.claude/settings.json +39 -0
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +8 -1
- data/CLAUDE.md +79 -0
- data/lib/decoding/decoders/and_then.rb +2 -0
- data/lib/decoding/decoders/array.rb +1 -1
- data/lib/decoding/decoders/at.rb +1 -1
- data/lib/decoding/decoders/fail.rb +34 -0
- data/lib/decoding/decoders/field.rb +2 -2
- data/lib/decoding/decoders/hash.rb +14 -16
- data/lib/decoding/decoders/index.rb +3 -3
- data/lib/decoding/decoders/map.rb +2 -0
- data/lib/decoding/decoders/pass.rb +1 -1
- data/lib/decoding/decoders/succeed.rb +29 -0
- data/lib/decoding/decoders.rb +5 -3
- data/lib/decoding/failure.rb +7 -5
- data/lib/decoding/version.rb +1 -1
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b61483d249ba9fc5939de6d7b1aadf7ecc6c2ea4fceb65f927447a4d3828bcfa
|
|
4
|
+
data.tar.gz: beab3f51cec564c1bdf2bd7f40ffbc4f97280e7197e14b9e8cd5a980b3756366
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
-
## [0.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
|
|
4
11
|
|
|
5
12
|
* Implement `Decoding::Result#deconstruct` to support pattern matching on result values.
|
|
6
13
|
|
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
|
data/lib/decoding/decoders/at.rb
CHANGED
|
@@ -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
|
|
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 [
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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 {
|
|
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
|
|
@@ -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
|
-
#
|
|
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
|
data/lib/decoding/decoders.rb
CHANGED
|
@@ -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
|
|
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) =
|
|
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) =
|
|
118
|
+
def fail(value) = Decoders::Fail.new(value)
|
|
117
119
|
|
|
118
120
|
# A decoder that returns the input value, unaltered.
|
|
119
121
|
#
|
data/lib/decoding/failure.rb
CHANGED
|
@@ -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
|
-
# @
|
|
13
|
-
|
|
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
|
|
31
|
-
self
|
|
33
|
+
self.class.new(@msg, @path + [segment])
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
def to_s
|
data/lib/decoding/version.rb
CHANGED
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.
|
|
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:
|