Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v4

- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v3
5 changes: 2 additions & 3 deletions .github/workflows/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,17 @@ on:
branches: [master]
pull_request:
branches: [master]
types: [opened, reopened]

jobs:
test:

runs-on: ubuntu-latest
strategy:
matrix:
ruby-version: ['2.7', '3.0', '3.1']
ruby-version: ['3.3', '3.4', '4.0']

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
Expand Down
3 changes: 2 additions & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
AllCops:
TargetRubyVersion: 2.7
TargetRubyVersion: 3.3
NewCops: enable

Style/StringLiterals:
Enabled: true
Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
## [Unreleased]

## [0.5.0] - 2026-07-03
### Breaking
- Symbol hash keys are now canonicalized to strings internally. This means:
- `Struct#to_h`, `#to_hash`, and `#inspect` return string keys even when the input hash used symbols.
- Hashes containing both a symbol and string key for the same logical property (e.g. `{ foo: 1, "foo" => 2 }`) will keep only one value after canonicalization.
- Marshalled data from previous versions that used symbol keys will load with string keys.
- Raised minimum Ruby version to 3.3.
- `StrictStruct#to_h` / `#to_hash` now return a shallow copy instead of the internal mutable hash.

### Fixed
- Canonicalize symbol keys to strings internally so symbol/string key collisions are handled consistently and cached nested structs have stable identity.
- README typos (`ResouceStruct`) and GitHub URL placeholder.
- Removed unused `bindir` / `executables` configuration from the gemspec.
- Fixed GitHub Actions pull_request trigger so pushes to PR branches (including force pushes) re-run CI.

### Changed
- Updated RuboCop and CI to current Ruby versions (3.3, 3.4, 4.0) and action versions.

## [0.4.0] - 2022-01-09
### Feature
- Better support for `ArgumentError` on `FlexStruct`
Expand Down
5 changes: 3 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ source "https://rubygems.org"
# Specify your gem's dependencies in resource-struct.gemspec
gemspec

gem "rake", "~> 13.0"
gem "irb"
gem "rake", "~> 13.2"

gem "rspec", "~> 3.0"

gem "rubocop", "~> 1.21"
gem "rubocop", "~> 1.75"
gem "rubocop-rake"
gem "rubocop-rspec"
112 changes: 75 additions & 37 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,61 +1,99 @@
PATH
remote: .
specs:
resource-struct (0.4.0)
resource-struct (0.5.0)

GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
diff-lcs (1.4.4)
parallel (1.21.0)
parser (3.0.3.2)
ast (2.4.3)
diff-lcs (1.6.2)
erb (6.0.4)
io-console (0.8.2)
irb (1.18.0)
pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
json (2.20.0)
language_server-protocol (3.17.0.6)
lint_roller (1.1.0)
logger (1.7.0)
parallel (2.1.0)
parser (3.3.11.1)
ast (~> 2.4.1)
rainbow (3.0.0)
rake (13.0.6)
regexp_parser (2.2.0)
rexml (3.3.9)
rspec (3.10.0)
rspec-core (~> 3.10.0)
rspec-expectations (~> 3.10.0)
rspec-mocks (~> 3.10.0)
rspec-core (3.10.1)
rspec-support (~> 3.10.0)
rspec-expectations (3.10.1)
racc
pp (0.6.4)
prettyprint
prettyprint (0.2.0)
prism (1.9.0)
racc (1.8.1)
rainbow (3.1.1)
rake (13.4.2)
rbs (4.0.3)
logger
prism (>= 1.6.0)
tsort
rdoc (8.0.0)
erb
prism (>= 1.6.0)
rbs (>= 4.0.0)
tsort
regexp_parser (2.12.0)
reline (0.6.3)
io-console (~> 0.5)
rspec (3.13.2)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0)
rspec-core (3.13.6)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.10.0)
rspec-mocks (3.10.2)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.8)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.10.0)
rspec-support (3.10.3)
rubocop (1.23.0)
parallel (~> 1.10)
parser (>= 3.0.0.0)
rspec-support (~> 3.13.0)
rspec-support (3.13.7)
rubocop (1.88.1)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (>= 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml
rubocop-ast (>= 1.12.0, < 2.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.49.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.15.0)
parser (>= 3.0.1.1)
rubocop-rake (0.6.0)
rubocop (~> 1.0)
rubocop-rspec (2.6.0)
rubocop (~> 1.19)
ruby-progressbar (1.11.0)
unicode-display_width (2.1.0)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.49.1)
parser (>= 3.3.7.2)
prism (~> 1.7)
rubocop-rake (0.7.1)
lint_roller (~> 1.1)
rubocop (>= 1.72.1)
rubocop-rspec (3.10.2)
lint_roller (~> 1.1)
regexp_parser (>= 2.0)
rubocop (~> 1.86, >= 1.86.2)
ruby-progressbar (1.13.0)
tsort (0.2.0)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.2.0)

PLATFORMS
arm64-darwin-25
x86_64-darwin-18
x86_64-darwin-19
x86_64-linux

DEPENDENCIES
rake (~> 13.0)
irb
rake (~> 13.2)
resource-struct!
rspec (~> 3.0)
rubocop (~> 1.21)
rubocop (~> 1.75)
rubocop-rake
rubocop-rspec

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ This is a gem for working with JSON resources from a network source with indiffe
Instead of overriding Hash implementation, this wraps a Hash with indifferent access (by symbol or string keys).
This makes it fast at runtime, while still providing the necessary lookup method of choice.

There are two types `ResouceStruct::StrictStruct` and `ResourceStruct::FlexStruct`.
There are two types `ResourceStruct::StrictStruct` and `ResourceStruct::FlexStruct`.

`ResourceStruct::StrictStruct` provides a way of wrapping a Hash such that accesses to invalid keys will raise an exception through the method lookup method; it also is immutable.

`ResouceStruct::FlexStruct` provides a way of wrapping a Hash such that it returns nil instead of raising an exception when the key is not present in the hash.
`ResourceStruct::FlexStruct` provides a way of wrapping a Hash such that it returns nil instead of raising an exception when the key is not present in the hash.

## Installation

Expand Down Expand Up @@ -71,7 +71,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To

## Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/resource-struct.
Bug reports and pull requests are welcome on GitHub at https://github.com/AlexRiedler/resource-struct.

## License

Expand Down
81 changes: 47 additions & 34 deletions lib/resource_struct/extensions/indifferent_lookup.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,35 +13,35 @@ module IndifferentLookup
def_delegators :@hash, :to_h, :to_hash, :to_s, :as_json, :to_json

def initialize(hash = {})
@hash = hash || {}
@ro_struct = {}
hash = {} if hash.nil?
raise ::ArgumentError, "first argument must be a Hash, found #{hash.class.name}" unless hash.is_a?(Hash)

raise ::ArgumentError, "first argument must be a Hash, found #{@hash.class.name}" unless @hash.is_a?(Hash)
@hash = ___canonicalize_hash(hash)
@ro_struct = {}
end

def inspect
"#{self.class.name}<#{@hash.inspect}>"
end

def ==(other)
other.is_a?(Hash) && ___all_keys_equal(other) ||
(other.is_a?(LooseStruct) || other.is_a?(FirmStruct)) &&
___all_keys_equal(other.instance_variable_get(:@hash))
other_hash = case other
when Hash then other
when LooseStruct, FirmStruct then other.instance_variable_get(:@hash)
else return false
end

___all_keys_equal(other_hash)
end

def dig(key, *sub_keys)
ckey = ___convert_key(key)

result =
if @ro_struct.key?(ckey)
@ro_struct[ckey]
elsif @hash.key?(key)
@ro_struct[ckey] = ___convert_value(@hash[key])
elsif key.is_a?(String) && @hash.key?(key.to_sym)
@ro_struct[ckey] = ___convert_value(@hash[key.to_sym])
elsif key.is_a?(Symbol) && @hash.key?(key.to_s)
@ro_struct[ckey] = ___convert_value(@hash[key.to_s])
end
result = if @ro_struct.key?(ckey)
@ro_struct[ckey]
elsif @hash.key?(ckey)
@ro_struct[ckey] = ___convert_value(@hash[ckey])
end

return result if sub_keys.empty?

Expand All @@ -54,18 +54,35 @@ def dig(key, *sub_keys)
alias [] dig

def marshal_dump
{
data: @hash
}
{ data: @hash }
end

def marshal_load(obj)
@ro_struct = {}
@hash = obj[:data]
@hash = ___canonicalize_hash(obj[:data] || {})
end

private

def ___canonicalize_hash(hash)
hash.each_with_object({}) do |(key, value), memo|
memo[___convert_key(key)] = ___canonicalize_value(value)
end
end

def ___canonicalize_value(value)
case value
when LooseStruct, FirmStruct
value.instance_variable_get(:@hash)
when ::Array
value.map { |v| ___canonicalize_value(v) }
when Hash
___canonicalize_hash(value)
else
value
end
end

def ___convert_value(value)
case value
when ::Array
Expand All @@ -78,9 +95,7 @@ def ___convert_value(value)
end

def ___key?(key)
@hash.key?(key) ||
@hash.key?(___convert_key(key)) ||
key.is_a?(String) && @hash.key?(key.to_sym)
@hash.key?(___convert_key(key))
end

def ___convert_key(key)
Expand All @@ -90,17 +105,15 @@ def ___convert_key(key)
def ___all_keys_equal(other)
return false unless @hash.count == other.count

@hash.reduce(true) do |acc, (k, _)|
value = self[k]
if other.key?(k)
acc && value == other[k]
elsif k.is_a?(String)
ck = k.to_sym
acc && other.key?(ck) && value == other[ck]
else
ck = ___convert_key(k)
acc && other.key?(ck) && value == other[ck]
end
@hash.all? do |k, _|
other_value = if other.key?(k)
other[k]
elsif k.is_a?(String) && other.key?(k.to_sym)
other[k.to_sym]
else
return false
end
self[k] == other_value
end
end
end
Expand Down
13 changes: 1 addition & 12 deletions lib/resource_struct/flex_struct.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,7 @@ class FlexStruct
def []=(key, value)
ckey = ___convert_key(key)
@ro_struct.delete(ckey)

value = value.instance_variable_get(:@hash) if value.is_a?(FlexStruct) || value.is_a?(StrictStruct)

if @hash.key?(key)
@hash[key] = value
elsif key.is_a?(String) && @hash.key?(key.to_sym)
@hash[key.to_sym] = value
elsif key.is_a?(Symbol) && @hash.key?(key.to_s)
@hash[key.to_s] = value
else
@hash[key] = value
end
@hash[ckey] = ___canonicalize_value(value)
end

def method_missing(name, *args)
Expand Down
Loading
Loading