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 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
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## [1.0.0] - 2026-07-27
4
+
5
+ - Initial release.
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).
@@ -0,0 +1,3 @@
1
+ class SafeOstruct
2
+ VERSION = "1.0.0".freeze
3
+ end
@@ -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: []