safe_ostruct 1.0.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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +110 -0
- data/lib/safe_ostruct/version.rb +3 -0
- data/lib/safe_ostruct.rb +120 -0
- metadata +57 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9b350ef819ecad8545a838c5e3a1c1840fbbe7f64c8455a56a0afa304e4e1d7e
|
|
4
|
+
data.tar.gz: ff69a99e6fba10bf86c54cc360e4ae41c31add767ccd3e080eaa325a754c88b5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 68b2240672f66147033bc97efcc01b0a06747cf8b89d327236ac445fd1b72005cf40ed759a1127922134e70cb5934771fb4663d9fd4292d6910b5dae8c53a955
|
|
7
|
+
data.tar.gz: 959d453d08c5ee503883c6279f9c747dff73fe73368c3ef1c9a720356c5532b9064b17f0005b2aaf36b656fa505bbcc73d766fbbca6497b34e8cc812c9772815
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Waqas Ali
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# SafeOstruct
|
|
2
|
+
|
|
3
|
+
A lightweight, dependency-free `OpenStruct` alternative backed by a plain symbol-keyed `Hash`. Published as the `safe_ostruct` gem.
|
|
4
|
+
|
|
5
|
+
SafeOstruct gives you the ergonomics of `OpenStruct` (dynamic attributes, method-style access) without its cost: it never defines singleton methods, so instantiation stays fast and Ruby's method caches stay intact. Undefined attributes return `nil` instead of raising `NoMethodError`, which makes it a natural fit for API response objects, test doubles, and data with optional fields.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add to your Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem "safe_ostruct"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or install directly:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
gem install safe_ostruct
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
require "safe_ostruct" # `require "safe_ostruct"` works too
|
|
25
|
+
|
|
26
|
+
obj = SafeOstruct.new(name: "John Doe")
|
|
27
|
+
|
|
28
|
+
# Method-style and hash-style access, with symbols or strings
|
|
29
|
+
obj.name # => "John Doe"
|
|
30
|
+
obj[:name] # => "John Doe"
|
|
31
|
+
obj["name"] # => "John Doe"
|
|
32
|
+
|
|
33
|
+
# Add attributes dynamically
|
|
34
|
+
obj.address = "123 Main St"
|
|
35
|
+
obj[:city] = "Springfield"
|
|
36
|
+
|
|
37
|
+
# Undefined attributes return nil, never NoMethodError
|
|
38
|
+
obj.age # => nil
|
|
39
|
+
obj[:age] # => nil
|
|
40
|
+
|
|
41
|
+
# Remove attributes
|
|
42
|
+
obj.delete_field(:address)
|
|
43
|
+
obj.address # => nil
|
|
44
|
+
|
|
45
|
+
# Convert to a hash
|
|
46
|
+
obj.to_h # => { name: "John Doe", city: "Springfield" }
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
It also works as a `JSON.parse` object class for dot-notation access to parsed JSON:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
require "json"
|
|
53
|
+
|
|
54
|
+
parsed = JSON.parse('{"user": {"name": "Jane"}}', object_class: SafeOstruct)
|
|
55
|
+
parsed.user.name # => "Jane"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
And as a base class for simple result objects:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
class ResponseResult < SafeOstruct
|
|
62
|
+
def success?
|
|
63
|
+
self[:status] == "success"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Behavior notes
|
|
69
|
+
|
|
70
|
+
These behaviors are intentional and covered by the test suite. They will not change without a major version bump.
|
|
71
|
+
|
|
72
|
+
- **Every undefined method returns `nil`.** `obj.anything` returns `nil` rather than raising `NoMethodError`. This includes typos, so prefer SafeOstruct where absent-means-nil is the semantics you want, and avoid it where a typo should fail loudly.
|
|
73
|
+
- **`respond_to?` only reflects currently set attributes.** `obj.respond_to?(:missing)` is `false` even though `obj.missing` returns `nil`, and `respond_to?(:attr=)` is `false` even though assignment works. Duck-type checks via `respond_to?` will only see attributes that have been set.
|
|
74
|
+
- **`to_h` returns the live internal hash, not a copy.** Mutating the returned hash mutates the struct. Call `obj.to_h.dup` if you need an independent copy.
|
|
75
|
+
|
|
76
|
+
## Performance
|
|
77
|
+
|
|
78
|
+
SafeOstruct exists for a specific trade: it is dramatically cheaper than `OpenStruct` to create, at the cost of slower per-attribute access (every read and write goes through `method_missing`). For the common lifecycle of value objects (create once, read a handful of attributes), creation cost dominates and SafeOstruct wins comfortably. If you create an object once and read the same attribute millions of times, use `Struct` or `Data` instead.
|
|
79
|
+
|
|
80
|
+
Results from `benchmark/ips.rb` (Ruby 3.2.2, x86_64-linux, higher is better):
|
|
81
|
+
|
|
82
|
+
| Operation | Hash | Struct | SafeOstruct | OpenStruct |
|
|
83
|
+
|---|---|---|---|---|
|
|
84
|
+
| Instantiation (3 attributes) | 13.9M i/s | 10.5M i/s | 3.2M i/s | 0.16M i/s |
|
|
85
|
+
| Defined attribute read | 61.1M i/s | 57.7M i/s | 9.2M i/s | 27.9M i/s |
|
|
86
|
+
| Undefined attribute read | n/a | n/a | 9.2M i/s | 7.3M i/s |
|
|
87
|
+
| Attribute write | 41.4M i/s | n/a | 4.8M i/s | 21.5M i/s |
|
|
88
|
+
|
|
89
|
+
Highlights:
|
|
90
|
+
|
|
91
|
+
- **Instantiation is ~20x faster than OpenStruct.** OpenStruct defines singleton methods per instance, which is slow and invalidates method caches; SafeOstruct just stores a hash.
|
|
92
|
+
- **Reads of defined attributes are ~3x slower than OpenStruct** (9.2M vs 27.9M i/s), because OpenStruct's lazily defined accessor is a real method while SafeOstruct always dispatches through `method_missing`.
|
|
93
|
+
- Direct `Hash` or `Struct` access is roughly 6x faster than SafeOstruct. When attribute names are fixed and known up front, prefer those.
|
|
94
|
+
|
|
95
|
+
Run the benchmark yourself:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
bundle exec ruby benchmark/ips.rb
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Development
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
bundle install
|
|
105
|
+
bundle exec rake # runs rspec + rubocop
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
|
data/lib/safe_ostruct.rb
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
require_relative "safe_ostruct/version"
|
|
2
|
+
|
|
3
|
+
# SafeOstruct
|
|
4
|
+
#
|
|
5
|
+
# SafeOstruct is a flexible struct-like class that allows dynamic initialization of attributes
|
|
6
|
+
# using keyword arguments. It handles missing methods gracefully by returning `nil`
|
|
7
|
+
# instead of raising a `NoMethodError` for undefined attributes or methods.
|
|
8
|
+
# This class use Symbolic-Hash and its underlying data structure.
|
|
9
|
+
#
|
|
10
|
+
# Additionally, it supports dynamically adding and removing attributes. The object allows access
|
|
11
|
+
# to its attributes using both methods and hash-style key access with symbols or strings.
|
|
12
|
+
#
|
|
13
|
+
# Key features:
|
|
14
|
+
# - Define attributes dynamically with keyword arguments.
|
|
15
|
+
# - Handle missing methods by returning `nil` instead of raising `NoMethodError`.
|
|
16
|
+
# - Support both symbol and string access for attributes.
|
|
17
|
+
# - Dynamically add or update attributes using method assignment or hash-style assignment.
|
|
18
|
+
# - Remove attributes dynamically using `delete_field`.
|
|
19
|
+
#
|
|
20
|
+
# Example usage:
|
|
21
|
+
# obj = SafeOstruct.new(name: 'John Doe')
|
|
22
|
+
#
|
|
23
|
+
# # Accessing defined attributes
|
|
24
|
+
# obj.name # => 'John Doe'
|
|
25
|
+
# obj[:name] # => 'John Doe'
|
|
26
|
+
# obj['name'] # => 'John Doe'
|
|
27
|
+
#
|
|
28
|
+
# # Adding new attributes dynamically
|
|
29
|
+
# obj.address = '123 Main St'
|
|
30
|
+
# obj.address # => '123 Main St'
|
|
31
|
+
# obj[:address] # => '123 Main St'
|
|
32
|
+
# obj['address'] # => '123 Main St'
|
|
33
|
+
#
|
|
34
|
+
# # Accessing an undefined attribute returns nil
|
|
35
|
+
# obj.age # => nil
|
|
36
|
+
# obj[:age] # => nil
|
|
37
|
+
# obj['age'] # => nil
|
|
38
|
+
#
|
|
39
|
+
# # Deleting an attribute
|
|
40
|
+
# obj.delete_field(:address)
|
|
41
|
+
# obj.address # => nil
|
|
42
|
+
# obj.respond_to?(:address) # => false
|
|
43
|
+
#
|
|
44
|
+
# # Converting to a hash
|
|
45
|
+
# hash_representation = obj.to_h
|
|
46
|
+
# # hash_representation => { name: 'John Doe' }
|
|
47
|
+
#
|
|
48
|
+
# This class simplifies working with dynamic or optional attributes and allows for greater
|
|
49
|
+
# flexibility in handling data structures, especially when attribute names may not be known
|
|
50
|
+
# in advance or when optional fields are common.
|
|
51
|
+
#
|
|
52
|
+
class SafeOstruct
|
|
53
|
+
# Initializes the SafeOstruct with attributes provided as keyword arguments or a hash.
|
|
54
|
+
#
|
|
55
|
+
# @param kwargs [Hash] the attributes to initialize the struct with
|
|
56
|
+
def initialize(**kwargs)
|
|
57
|
+
@attributes = kwargs.transform_keys(&:to_sym)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Handles missing methods dynamically, including attribute assignment
|
|
61
|
+
#
|
|
62
|
+
# @param method_name [Symbol] the name of the missing method
|
|
63
|
+
# @param args [Array] the arguments passed to the missing method
|
|
64
|
+
# @param block [Proc] an optional block passed to the missing method
|
|
65
|
+
# @return [Object, nil] the value of the attribute, or nil if undefined
|
|
66
|
+
def method_missing(method_name, *args, &_block)
|
|
67
|
+
if method_name.end_with?("=")
|
|
68
|
+
# Handle dynamic assignment
|
|
69
|
+
attr_name = method_name[0...-1].to_sym
|
|
70
|
+
@attributes[attr_name] = args.first
|
|
71
|
+
else
|
|
72
|
+
# Handle dynamic retrieval, return nil if attribute does not exist
|
|
73
|
+
@attributes[method_name]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Access attributes using [] with symbol or string
|
|
78
|
+
#
|
|
79
|
+
# @param key [Symbol, String] the key for the attribute to be retrieved
|
|
80
|
+
# @return [Object, nil] the value of the attribute, or nil if undefined
|
|
81
|
+
def [](key)
|
|
82
|
+
@attributes[key.to_sym]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Set attributes using [] with symbol or string
|
|
86
|
+
#
|
|
87
|
+
# @param key [Symbol, String] the key for the attribute to be set
|
|
88
|
+
# @param value [Object] the value to assign to the attribute
|
|
89
|
+
def []=(key, value)
|
|
90
|
+
@attributes[key.to_sym] = value
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Ensures SafeOstruct responds to methods for defined and dynamically assigned attributes.
|
|
94
|
+
#
|
|
95
|
+
# @param method_name [Symbol] the name of the method being checked
|
|
96
|
+
# @param include_private [Boolean] whether to include private methods in the check
|
|
97
|
+
# @return [Boolean] true if the method is defined or dynamically assigned, otherwise false
|
|
98
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
99
|
+
@attributes.key?(method_name) || super
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Deletes an attribute from the struct.
|
|
103
|
+
#
|
|
104
|
+
# @param key [Symbol, String] the key of the attribute to delete
|
|
105
|
+
# @return [Object, nil] the value of the deleted attribute, or nil if the attribute did not exist
|
|
106
|
+
def delete_field(key)
|
|
107
|
+
@attributes.delete(key.to_sym)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Converts the attributes of the SafeOstruct into a hash.
|
|
111
|
+
#
|
|
112
|
+
# @return [Hash] a hash representation of the attributes, where keys are symbols
|
|
113
|
+
# and values are the corresponding attribute values. This method provides a way
|
|
114
|
+
# to easily access the internal state of the SafeOstruct as a standard hash,
|
|
115
|
+
# facilitating serialization and interoperability with other Ruby classes and
|
|
116
|
+
# methods that expect a hash input.
|
|
117
|
+
def to_h
|
|
118
|
+
@attributes
|
|
119
|
+
end
|
|
120
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: safe_ostruct
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Waqas Ali
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-07-27 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: SafeOstruct is a lightweight, dependency-free struct-like class backed
|
|
14
|
+
by a plain symbol-keyed Hash. It supports dynamic attribute definition via keyword
|
|
15
|
+
arguments, method-style and hash-style (symbol or string) access, and returns nil
|
|
16
|
+
instead of raising NoMethodError for undefined attributes. Unlike OpenStruct, it
|
|
17
|
+
never defines singleton methods, so instantiation stays fast and method caches stay
|
|
18
|
+
intact.
|
|
19
|
+
email:
|
|
20
|
+
- waqas@sendoso.com
|
|
21
|
+
executables: []
|
|
22
|
+
extensions: []
|
|
23
|
+
extra_rdoc_files: []
|
|
24
|
+
files:
|
|
25
|
+
- CHANGELOG.md
|
|
26
|
+
- LICENSE.txt
|
|
27
|
+
- README.md
|
|
28
|
+
- lib/safe_ostruct.rb
|
|
29
|
+
- lib/safe_ostruct/version.rb
|
|
30
|
+
homepage: https://github.com/wqsaali/safe-ostruct
|
|
31
|
+
licenses:
|
|
32
|
+
- MIT
|
|
33
|
+
metadata:
|
|
34
|
+
homepage_uri: https://github.com/wqsaali/safe-ostruct
|
|
35
|
+
source_code_uri: https://github.com/wqsaali/safe-ostruct
|
|
36
|
+
changelog_uri: https://github.com/wqsaali/safe-ostruct/blob/main/CHANGELOG.md
|
|
37
|
+
rubygems_mfa_required: 'true'
|
|
38
|
+
post_install_message:
|
|
39
|
+
rdoc_options: []
|
|
40
|
+
require_paths:
|
|
41
|
+
- lib
|
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.0'
|
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
48
|
+
requirements:
|
|
49
|
+
- - ">="
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '0'
|
|
52
|
+
requirements: []
|
|
53
|
+
rubygems_version: 3.4.10
|
|
54
|
+
signing_key:
|
|
55
|
+
specification_version: 4
|
|
56
|
+
summary: A hash-backed OpenStruct alternative that returns nil for undefined attributes
|
|
57
|
+
test_files: []
|