Skip to content

Replace OpenStruct with a lightweight Paddle::Object - #26

Merged
d34ndev merged 1 commit into
mainfrom
replace-openstruct
Sep 27, 2026
Merged

d34ndev merged 1 commit into
mainfrom
replace-openstruct

Conversation

@d34ndev

@d34ndev d34ndev commented Sep 27, 2026

Copy link
Copy Markdown
Owner

OpenStruct is no longer a default gem, and it's slow to build because it defines singleton methods on every instance. Paddle responses are deeply nested, so every response created many OpenStructs.

This replaces it with a small Paddle::Object that stores attributes in a hash and reads them through method_missing.

What stays the same

  • Dot access (obj.id), hash access (obj[:id], obj["id"]) and setters (obj.foo = )
  • nil for missing attributes
  • each_pair, respond_to?, == and the instance update method

What's new

  • to_h now converts nested objects too, so it returns plain hashes all the way down
  • to_json returns real JSON. Before, it returned an inspect string
  • as_json, dig and key?
  • Calling update on a model without an update method now raises NoMethodError rather than a confusing NameError
  • The ostruct dependency has been removed

Performance

Benchmarked on a real transaction response from the test cassettes, Ruby 4.0.7, best of 5 runs:

Benchmark OpenStruct New Speed-up
Build one transaction (2,000×) 901ms 46ms 19.5×
Build a 50-item page (50×) 1,711ms 62ms 27.8×
Build and read 5 fields (2,000×) 941ms 58ms 16.2×
Dot-access read (500k×) 30ms 150ms 0.2×
Hash-style read (500k×) 34ms 34ms 1.0×

Memory per transaction dropped from 134 KB to 6.8 KB, and allocations to build one went from 2,744 to 56. Dot-access reads are slower because they go through method_missing, but that's around 0.3µs per read.

Compatibility

Nested objects are now Paddle::Object rather than OpenStruct. Code that checks is_a?(OpenStruct) or uses OpenStruct-only methods, such as delete_field or to_h with a block, will break. A field named after a built-in public method, such as hash or class, now returns the method's result, so use obj[:hash] for those. This probably warrants a minor version bump and a changelog note.

Testing

Added 10 tests for the new object. The full suite passes (188 runs), and RuboCop is clean.

🤖 Generated with Claude Code

OpenStruct is no longer a default gem, and it's slow to build because it
defines singleton methods on every instance. Paddle responses are deeply
nested, so every response created many OpenStructs.

Paddle::Object now stores attributes in a hash and reads them through
method_missing. Dot access, hash access, setters, nil for missing
attributes, each_pair and update all work as before. to_h now converts
nested objects too, and to_json, as_json, dig and key? have been added.
Nested objects are Paddle::Object rather than OpenStruct, and the ostruct
dependency has been removed.

Building a transaction is around 20x faster and uses around 20x less
memory. Dot-access reads are slower, at around 0.3µs each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@d34ndev
d34ndev merged commit cf314d4 into main Sep 27, 2026
4 checks passed
@d34ndev
d34ndev deleted the replace-openstruct branch September 27, 2026 09:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant