nested_objects 0.1.16

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: 1e46cbbe94fd34a0e69804d0ce6e1a6d3228fa5ff656db4e050c9d29c0e592fc
4
+ data.tar.gz: 0adb30d3a7ead3749df4f863efaabd2dbc8bb20332ff5e7b1d131bc86dbeb267
5
+ SHA512:
6
+ metadata.gz: 69ce1523d3959da5f968dc197d1d2375afbdf25be62c5faea10c9cb911edc637733f9172e326e0b0be8b77e24b2880625ee3f8cc659dcc15098417e366ec22e2
7
+ data.tar.gz: af925766a658fdef76b2ca260e9bdf6322afd737a35e16e56974e2ab7a1769d868fc7b0ec6c0b42d6358bd32c82e86774ecf3f16288da484dab16a8789182411
data/.commitlintrc.yml ADDED
@@ -0,0 +1,37 @@
1
+ ---
2
+ extends: '@commitlint/config-conventional'
3
+
4
+ rules:
5
+ # See: https://commitlint.js.org/reference/rules.html
6
+ #
7
+ # Rules are made up by a name and a configuration array. The configuration
8
+ # array contains:
9
+ #
10
+ # * Severity [0..2]: 0 disable rule, 1 warning if violated, or 2 error if
11
+ # violated
12
+ # * Applicability [always|never]: never inverts the rule
13
+ # * Value: value to use for this rule (if applicable)
14
+ #
15
+ # Run `npx commitlint --print-config` to see the current setting for all
16
+ # rules.
17
+ #
18
+ header-max-length: [2, always, 100] # Header can not exceed 100 chars
19
+
20
+ type-case: [2, always, lower-case] # Type must be lower case
21
+ type-empty: [2, never] # Type must not be empty
22
+
23
+ # Supported conventional commit types
24
+ type-enum: [2, always, [build, ci, chore, docs, feat, fix, perf, refactor, revert, style, test]]
25
+
26
+ scope-case: [2, always, lower-case] # Scope must be lower case
27
+
28
+ # Error if subject is one of these cases (encourages lower-case)
29
+ subject-case: [2, never, [sentence-case, start-case, pascal-case, upper-case]]
30
+ subject-empty: [2, never] # Subject must not be empty
31
+ subject-full-stop: [2, never, "."] # Subject must not end with a period
32
+
33
+ body-leading-blank: [2, always] # Body must have a blank line before it
34
+ body-max-line-length: [2, always, 100] # Body lines can not exceed 100 chars
35
+
36
+ footer-leading-blank: [2, always] # Footer must have a blank line before it
37
+ footer-max-line-length: [2, always, 100] # Footer lines can not exceed 100 chars
data/.husky/commit-msg ADDED
@@ -0,0 +1 @@
1
+ npx --no-install commitlint --edit "$1"
data/.markdownlint.yml ADDED
@@ -0,0 +1,26 @@
1
+ ---
2
+ default: true
3
+
4
+ # Unordered list indentation
5
+ MD007: { indent: 2 }
6
+
7
+ # Line length
8
+ MD013: { line_length: 90, tables: false, code_blocks: false }
9
+
10
+ # Heading duplication is allowed for non-sibling headings
11
+ MD024: { siblings_only: true }
12
+
13
+ # Do not allow the specified trailing punctuation in a header
14
+ MD026: { punctuation: ".,;:" }
15
+
16
+ # Order list items must have a prefix that increases in numerical order
17
+ MD029: { style: "ordered" }
18
+
19
+ # Lists do not need to be surrounded by blank lines
20
+ MD032: false
21
+
22
+ # Allow raw HTML in Markdown
23
+ MD033: false
24
+
25
+ # Allow emphasis to be used instead of a heading
26
+ MD036: false
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.1.16"
3
+ }
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,18 @@
1
+ ---
2
+ inherit_gem:
3
+ main_branch_shared_rubocop_config: config/rubocop.yml
4
+
5
+ Metrics/MethodLength:
6
+ Exclude:
7
+ - "spec/spec_helper.rb"
8
+ - "spec/**/*_spec.rb"
9
+
10
+ Metrics/AbcSize:
11
+ Exclude:
12
+ - "spec/spec_helper.rb"
13
+ - "spec/**/*_spec.rb"
14
+
15
+ AllCops:
16
+ # Pin this project to Ruby 3.1 in case the shared config above is
17
+ # upgraded to 3.2 or later.
18
+ TargetRubyVersion: 3.1
data/.yamllint.yml ADDED
@@ -0,0 +1,12 @@
1
+ ---
2
+ extends: default
3
+
4
+ ignore: |
5
+ node_modules
6
+
7
+ rules:
8
+ braces:
9
+ min-spaces-inside: 1
10
+ max-spaces-inside: 1
11
+ truthy:
12
+ check-keys: false
data/.yardopts ADDED
@@ -0,0 +1,8 @@
1
+ --no-private
2
+ --hide-void-return
3
+ --markup-provider=redcarpet
4
+ --markup markdown
5
+ --readme README.md
6
+ - CHANGELOG.md
7
+ - CONTRIBUTING.md
8
+ - LICENSE.txt
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## [0.1.16](https://github.com/main-branch/nested_objects/compare/v0.1.15...v0.1.16) (2025-04-27)
4
+
5
+
6
+ ### Features
7
+
8
+ * Initial version of NestedObjects ([515fc47](https://github.com/main-branch/nested_objects/commit/515fc4793ab7ad66dcf3beeff94a011978bff57f))
9
+
10
+ ## Change Log
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,191 @@
1
+ # Contributing to nested_objects
2
+
3
+ Thank you for your interest in contributing to the nested_objects project!
4
+
5
+ This document gives the guidelines for contributing to this project.
6
+ These guidelines may not fit every situation. When contributing use your best
7
+ judgement.
8
+
9
+ Propose changes to these guidelines with a pull request.
10
+
11
+ ## How to contribute to nested_objects
12
+
13
+ You can contribute in two ways:
14
+
15
+ 1. [Report an issue or make a feature request](#how-to-report-an-issue-or-make-a-feature-request)
16
+ 2. [Submit a code or documentation change](#how-to-submit-a-code-or-documentation-change)
17
+
18
+ ## How to report an issue or make a feature request
19
+
20
+ nested_objects utilizes [GitHub Issues](https://help.github.com/en/github/managing-your-work-on-github/about-issues)
21
+ for issue tracking and feature requests.
22
+
23
+ Report an issue or feature request by [creating a nested_objects Github issue](https://github.com/main-branch/nested_objects/issues/new).
24
+ Fill in the template to describe the issue or feature request the best you can.
25
+
26
+ ## How to submit a code or documentation change
27
+
28
+ There is three step process for code or documentation changes:
29
+
30
+ 1. [Commit your changes to a fork of nested_objects](#commit-changes-to-a-fork-of-nested_objects)
31
+ 2. [Create a pull request](#create-a-pull-request)
32
+ 3. [Get your pull request reviewed](#get-your-pull-request-reviewed)
33
+
34
+ ### Commit changes to a fork of nested_objects
35
+
36
+ Make your changes in a fork of the nested_objects repository.
37
+
38
+ #### Conventional commits
39
+
40
+ All commits to this repository MUST follow [the conventional commits
41
+ specification](https://www.conventionalcommits.org/en/v1.0.0/#specification). This is
42
+ so that release versions can automatically be generated based on the commit messages.
43
+ The Continuous Integration workflow will fail if a commit message is not in this
44
+ format.
45
+
46
+ The format for a conventional commit message is:
47
+
48
+ ```Text
49
+ <commit-type>[optional scope]: <description>
50
+
51
+ [optional body]
52
+
53
+ [optional footer(s)]
54
+ ```
55
+
56
+ Where commit type is one of:
57
+
58
+ * `feat:` A new feature for the end-user.
59
+ * `fix:` A bug fix for the end-user.
60
+ * `build:` Changes that affect the build system or external dependencies (e.g.,
61
+ changes to npm, webpack, Docker configurations).
62
+ * `chore:` Routine tasks, maintenance, or changes that don't modify source code or
63
+ test files (e.g., updating dependencies, configuring linters).
64
+ * `docs:` Changes to documentation only (e.g., updating README, guides, or extensive
65
+ code comments).
66
+ * `style:` Changes that do not affect the meaning or logic of the code (e.g.,
67
+ white-space, formatting, fixing linter warnings, missing semi-colons).
68
+ * `refactor:` A code change that neither fixes a bug nor adds a feature, but improves
69
+ the code structure, readability, or organization without changing its external
70
+ behavior.
71
+ * `perf:` A code change that improves performance.
72
+ * `test:` Adding missing tests or correcting existing tests; changes to the test
73
+ environment or configuration.
74
+
75
+ Backward incompatible changes (aka breaking changes) should append a `!` after the
76
+ commit type or add the footer `BREAKING CHANGE:` as shown in the following examples:
77
+
78
+ ```Text
79
+ fix!: change the interface to match the requirements
80
+ ```
81
+
82
+ ```Text
83
+ fix: change the interface to match the requirements
84
+
85
+ BREAKING CHANGE: users will need to add additional, non-default option
86
+ ```
87
+
88
+ These commit messages will have the following impact on the release version number:
89
+
90
+ * Breaking changes (of any commit type) will bump the major version component
91
+ * `feat:` commits will bump the minor version component
92
+ * `fix:` commits will bump the patch version component
93
+
94
+ Any other commit types (that are not breaking changes) will not trigger a release.
95
+
96
+ ### Create a pull request
97
+
98
+ See [this article](https://help.github.com/articles/about-pull-requests/) if you
99
+ are not familiar with GitHub Pull Requests.
100
+
101
+ Follow the instructions in the pull request template.
102
+
103
+ ### Get your pull request reviewed
104
+
105
+ Code review takes place in a GitHub pull request using the
106
+ [the Github pull request review feature](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-request-reviews).
107
+
108
+ Once your pull request is ready for review, request a review from at least one of the
109
+ [code owners](https://github.com/orgs/main-branch/teams/nested_objects-codeowners/members).
110
+
111
+ During the review process, you may need to make additional commits which would
112
+ need to be squashed. It may also be necessary to rebase to main again if other
113
+ changes are merged before your PR.
114
+
115
+ At least one approval is required from a project maintainer before your pull
116
+ request can be merged. The maintainer is responsible for ensuring that the pull
117
+ request meets [the project's coding standards](#coding-standards).
118
+
119
+ ## Coding standards
120
+
121
+ All pull requests must meet these requirements:
122
+
123
+ ### Rebase Merge Strategy
124
+
125
+ Our project requires a linear commit history, which is achieved by using a
126
+ rebase strategy to enable fast-forward merges for all Pull Requests (PRs).
127
+
128
+ * **Goal:** To integrate PRs without creating extra merge commits, ensuring a clean
129
+ and linear project history. This requires the PR branch to be eligible for a
130
+ [fast-forward
131
+ merge](https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging).
132
+ * **Commit History:** Before merging, clean up your PR's commit history. Squash
133
+ related changes into logical commits using interactive rebase (`git rebase -i`).
134
+ Aim for a minimal number of meaningful commits that represent distinct steps or
135
+ features.
136
+ * **Enable Fast-Forward:** To ensure a fast-forward merge is possible, you must
137
+ rebase your feature branch onto the latest version of the target branch (e.g.,
138
+ `main`) *before* the PR is merged.
139
+ * Example commands (assuming your remote is `origin` and the target branch is
140
+ `main`):
141
+
142
+ ```bash
143
+ # Fetch the latest changes from the remote
144
+ git fetch origin
145
+
146
+ # Rebase your current branch onto the latest main branch
147
+ git rebase origin/main
148
+ ```
149
+
150
+ * You may need to resolve conflicts during the rebase process. After successfully
151
+ rebasing, you'll likely need to force-push your branch (`git push
152
+ --force-with-lease`).
153
+
154
+ * You may need to rebase again if other PRs are merged to main.
155
+
156
+ ### Unit tests
157
+
158
+ - All changes must be accompanied by new or modified RSpec unit tests
159
+ - The entire test suite must pass when `bundle exec rake spec` is run from the
160
+ project's local working tree
161
+ - The unit test suite must maintain 100% code coverage to pass
162
+
163
+ ### Documentation
164
+
165
+ - New and updated public methods must have [YARD](https://yardoc.org/)
166
+ documentation added to them
167
+ - [The YARD Cheatsheet](https://gist.github.com/thelastinuit/5984665e6ab69d3c0a413a03602c45be)
168
+ is a good reference
169
+ - New and updated public facing features should be documented in the project's
170
+ [README.md](README.md)
171
+ - All documentation must pass `yardstick` documentation analysis
172
+ - The documentation suite must maintain 100% documentation to pass
173
+
174
+ ### Continuous Integration
175
+
176
+ - All tests must pass in the project's [Travis CI](https://travis-ci.org/main-branch/nested_objects)
177
+ build before the pull request will be merged.
178
+ - You can simulate what happens in the Travis CI build by running `bundle exec rake` in
179
+ the projects root directory.
180
+
181
+ ### Other Design Guidelines
182
+
183
+ - Use keyword args with defaults instead of an opts hash
184
+
185
+ ## Licensing
186
+
187
+ nested_objects uses [the MIT license](https://choosealicense.com/licenses/mit/) as
188
+ declared in the [LICENSE](LICENSE) file.
189
+
190
+ Licensing is very important to open source projects. It helps ensure the
191
+ software continues to be available under the terms that the author desired.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 James Couball
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,73 @@
1
+ # nested_objects gem
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/nested_objects.svg)](https://badge.fury.io/rb/nested_objects)
4
+ [![Build Status](https://github.com/main-branch/nested_objects/actions/workflows/continuous_integration.yml/badge.svg)](https://github.com/main-branch/nested_objects/actions/workflows/continuous_integration.yml)
5
+ [![Documentation](https://img.shields.io/badge/Documentation-Latest-green)](https://rubydoc.info/gems/nested_objects/)
6
+ [![Change Log](https://img.shields.io/badge/CHANGELOG-Latest-green)](https://rubydoc.info/gems/nested_objects/file/CHANGELOG.md)
7
+ [![Slack](https://img.shields.io/badge/slack-main--branch/track__open__instances-yellow.svg?logo=slack)](https://main-branch.slack.com/archives/C01CHR7TMM2)
8
+
9
+ The `NestedObjects` module provides module level methods to safely navigate and
10
+ manage a heirarchy of Ruby POROs nested using Hashes or Arrays. Think of these
11
+ nested data objects like what you would get after reading in a JSON file.
12
+
13
+ The key methods are:
14
+
15
+ * `NestedObjects.deep_copy(data)` - returns a deep copy of data including nested hash
16
+ values and array elements
17
+ * `NestedObjects.dig(data, path)` - returns the value at the given path
18
+ * `NestedObjects.bury(data, path, value)` - sets a value within the data structure at
19
+ the given path
20
+ * `NestedObjects.delete(data, path)` - deletes the Hash key or Array index at the
21
+ given path
22
+ * `NestedObjects.path?(data, path)` - returns true if the path exists in the given
23
+ data structure
24
+
25
+ These methods (prefixed with `nested_` to avoid method conflicts) can be mixed into
26
+ `Object` for ease of use:
27
+
28
+ ```Ruby
29
+ Object.include NestedObjects::Mixin
30
+
31
+ data = { 'users' => [{ 'name' => 'John Smith'}, { 'name' => 'Jane Doe' }] }
32
+
33
+ data.nested_dig(%w[users 1 name]) #=> 'Jane Doe'
34
+ ```
35
+
36
+ If the path is malformed or does not exist, a `BadPathError` will be raised.
37
+
38
+ ## Development
39
+
40
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
41
+ `bundle exec rake` to run tests, static analysis, and build the gem.
42
+
43
+ For experimentation, you can also run `bin/console` for an interactive (IRB) prompt that
44
+ automatically requires nested_objects.
45
+
46
+ ## Contributing
47
+
48
+ Bug reports and pull requests are welcome on GitHub at <https://github.com/main-branch/nested_objects>.
49
+
50
+ ### Commit message guidelines
51
+
52
+ All commit messages must follow the [Conventional Commits
53
+ standard](https://www.conventionalcommits.org/en/v1.0.0/). This helps us maintain a
54
+ clear and structured commit history, automate versioning, and generate changelogs
55
+ effectively.
56
+
57
+ To ensure compliance, this project includes:
58
+
59
+ - A git commit-msg hook that validates your commit messages before they are accepted.
60
+
61
+ To activate the hook, you must have node installed and run `npm install`.
62
+
63
+ - A GitHub Actions workflow that will enforce the Conventional Commit standard as
64
+ part of the continuous integration pipeline.
65
+
66
+ Any commit message that does not conform to the Conventional Commits standard will
67
+ cause the workflow to fail and not allow the PR to be merged.
68
+
69
+ ### Pull request guidelines
70
+
71
+ All pull requests must be merged using rebase merges. This ensures that commit
72
+ messages from the feature branch are preserved in the release branch, keeping the
73
+ history clean and meaningful.
data/Rakefile ADDED
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc 'Run the same tasks that the CI build will run'
4
+ if RUBY_PLATFORM == 'java'
5
+ task default: %w[spec rubocop bundle:audit build]
6
+ else
7
+ task default: %w[spec rubocop yard bundle:audit build]
8
+ end
9
+
10
+ # Bundler Audit
11
+
12
+ require 'bundler/audit/task'
13
+ Bundler::Audit::Task.new
14
+
15
+ # Bundler Gem Build
16
+
17
+ require 'bundler'
18
+ require 'bundler/gem_tasks'
19
+
20
+ # Make it so that calling `rake release` just calls `rake release:rubygems_push` to
21
+ # avoid creating and pushing a new tag.
22
+
23
+ Rake::Task['release'].clear
24
+ desc 'Customized release task to avoid creating a new tag'
25
+ task release: 'release:rubygem_push'
26
+
27
+ # RSpec
28
+
29
+ require 'rspec/core/rake_task'
30
+
31
+ RSpec::Core::RakeTask.new
32
+
33
+ CLEAN << 'coverage'
34
+ CLEAN << '.rspec_status'
35
+ CLEAN << 'rspec-report.xml'
36
+
37
+ # Rubocop
38
+
39
+ require 'rubocop/rake_task'
40
+
41
+ RuboCop::RakeTask.new
42
+
43
+ # YARD
44
+
45
+ unless RUBY_PLATFORM == 'java'
46
+ # yard:build
47
+
48
+ require 'yard'
49
+
50
+ YARD::Rake::YardocTask.new('yard:build') do |t|
51
+ t.files = %w[lib/**/*.rb examples/**/*]
52
+ t.options = %w[--no-private]
53
+ t.stats_options = %w[--list-undoc]
54
+ end
55
+
56
+ CLEAN << '.yardoc'
57
+ CLEAN << 'doc'
58
+
59
+ # yard:audit
60
+
61
+ desc 'Run yardstick to show missing YARD doc elements'
62
+ task :'yard:audit' do
63
+ sh "yardstick 'lib/**/*.rb'"
64
+ end
65
+
66
+ # yard:coverage
67
+
68
+ require 'yardstick/rake/verify'
69
+
70
+ Yardstick::Rake::Verify.new(:'yard:coverage') do |verify|
71
+ verify.threshold = 100
72
+ end
73
+
74
+ # yard
75
+
76
+ desc 'Run all YARD tasks'
77
+ task yard: %i[yard:build yard:audit yard:coverage]
78
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NestedObjects
4
+ # Include this module to add methods for working with nested data structures
5
+ #
6
+ # Usually included into the Object class.
7
+ #
8
+ # @example
9
+ # require 'nested_objects/mixin'
10
+ # Object.include(NestedObjects::Mixin)
11
+ # data = { 'a' => { 'b' => [1, 2, 3] } }
12
+ # data.nested_dig(['a', 'b', '0']) #=> 1
13
+ #
14
+ # @api public
15
+ module Mixin
16
+ # (see NestedObjects.path?)
17
+ def nested_path?(path) = NestedObjects.path?(self, path)
18
+
19
+ # (see NestedObjects.dig)
20
+ def nested_dig(path) = NestedObjects.dig(self, path)
21
+
22
+ # (see NestedObjects.bury)
23
+ def nested_bury(path, value) = NestedObjects.bury(self, path, value)
24
+
25
+ # (see NestedObjects.delete)
26
+ def nested_delete(path) = NestedObjects.delete(self, path)
27
+
28
+ # (see NestedObjects.deep_copy)
29
+ def nested_deep_copy = NestedObjects.deep_copy(self)
30
+ end
31
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NestedObjects
4
+ # The last released gem version
5
+ VERSION = '0.1.16'
6
+ end
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'nested_objects/mixin'
4
+ require_relative 'nested_objects/version'
5
+
6
+ # Utilities for working with POROs arbitrarily nested with Hashes and Arrays
7
+ # @api public
8
+ module NestedObjects
9
+ # Error raised when a path is invalid or does not exist in the data
10
+ class BadPathError < StandardError; end
11
+
12
+ class << self
13
+ # Creates a deep copy of data using Marshal
14
+ #
15
+ # @example
16
+ # data = { 'a' => { 'b' => [1, 2, 3] } }
17
+ # NestedObjects.deep_copy(data) #=> { 'a' => { 'b' => [1, 2, 3] } }
18
+ #
19
+ # @param data [Object] The object to be deeply copied
20
+ # @return [Object] A new object that is a deep copy of the input `obj`
21
+ # @raise [TypeError] if the object cannot be marshaled (see Marshal documentation)
22
+ def deep_copy(data)
23
+ Marshal.load(Marshal.dump(data))
24
+ end
25
+
26
+ # Retrieves the value at the specified path in the given data
27
+ #
28
+ # @example
29
+ # data = { 'a' => { 'b' => [1, 2, 3] } }
30
+ # NestedObjects.dig(data, ['a', 'b', '0']) #=> 1
31
+ #
32
+ # @param data [Hash, Array, Object] The data containing the value to retrieve
33
+ # @param path [Array<String>] An array of keys/indices representing the path to the desired value
34
+ # @return [Object] the value at the specified path
35
+ # @raise [BadPathError] if the path is invalid or does not exist
36
+ def dig(data, path)
37
+ return data if path.empty?
38
+
39
+ raise BadPathError unless data.is_a?(Hash) || data.is_a?(Array)
40
+
41
+ if data.is_a?(Hash)
42
+ dig_into_hash(data, path)
43
+ else
44
+ dig_into_array(data, path)
45
+ end
46
+ end
47
+
48
+ # Sets a value within a nested data structure
49
+ #
50
+ # Creates intermediate Hashes along the path if they do not exist.
51
+ # Does NOT create intermediate Arrays (creates a Hash instead).
52
+ #
53
+ # @example
54
+ # data = { 'a' => { 'b' => [1, 2, 3] } }
55
+ # NestedObjects.bury(data, ['a', 'b', '0'], 42) #=> { 'a' => { 'b' => [42, 2, 3] } }
56
+ #
57
+ # @example will overwrite existing values
58
+ # data = { 'a' => { 'b' => [1, 2, 3] } }
59
+ # NestedObjects.bury(data, ['a'], 42) #=> { 'a' => 42 }
60
+ #
61
+ # @example will create intermediate Hashes
62
+ # data = {}
63
+ # NestedObjects.bury(data, ['a', 'b'], 42) #=> { 'a' => { 'b' => 42 } })
64
+ #
65
+ # @example will NOT create intermediate Arrays (creates a Hash instead)
66
+ # data = {}
67
+ # NestedObjects.bury(data, ['a', '0'], 42) #=> { 'a' => { '0' => [42, 2, 3] } }
68
+ #
69
+ # @param data [Hash, Array, Object] The structure to modify
70
+ # @param path [Array<String>] An array of keys/indices representing the path to the item to set
71
+ # @param value [Object] The value to set at the specified path
72
+ # @return [Object] The modified data structure
73
+ # @raise [BadPathError] if the path is invalid or the item does not exist
74
+ #
75
+ def bury(data, path, value)
76
+ raise BadPathError if path.empty? || !(data.is_a?(Hash) || data.is_a?(Array))
77
+
78
+ key, found = next_key(data, path)
79
+
80
+ if path.length == 1
81
+ data[key] = value
82
+ else
83
+ data[key] = {} unless found
84
+ bury(data[key], path[1..], value)
85
+ end
86
+
87
+ data
88
+ end
89
+
90
+ # Delete a key or element from nested data identified by path
91
+ #
92
+ # @example
93
+ # data = { 'a' => { 'b' => [1, 2, 3] } }
94
+ # NestedObjects.delete(data, ['a', 'b', '0']) #=> 1
95
+ # data #=> { 'a' => { 'b' => [2, 3] } }
96
+ #
97
+ # @param data [Hash, Array, Object] The structure to modify
98
+ # @param path [Array<String>] An array of keys/indices representing the path to the item to delete
99
+ # @return [Object] The value of the element that was deleted
100
+ # @raise [BadPathError] if the path is invalid or the item does not exist
101
+ #
102
+ def delete(data, path)
103
+ raise BadPathError if path.empty? || !(data.is_a?(Hash) || data.is_a?(Array))
104
+
105
+ key, found = next_key(data, path)
106
+
107
+ raise BadPathError unless found
108
+
109
+ if path.length == 1
110
+ delete_key(data, key)
111
+ else
112
+ delete(data[key], path[1..])
113
+ end
114
+ end
115
+
116
+ # Check if the path is valid for the given data structure
117
+ #
118
+ # @example
119
+ # data = { 'a' => { 'b' => [1, 2, 3] } }
120
+ # NestedObjects.path?(data, ['a', 'b', '0']) #=> true
121
+ # NestedObjects.path?(data, ['d']) #=> false
122
+ #
123
+ # @param data [Hash, Array, Object] The structure to check
124
+ # @param path [Array<String>] An array of keys/indices representing the path to check
125
+ # @return [Boolean] `true` if the path is valid, `false` otherwise
126
+ # @raise [BadPathError] if path tries to traverse an Array with a non-integer key
127
+ #
128
+ def path?(data, path)
129
+ return true if path.empty?
130
+
131
+ return false unless data.is_a?(Hash) || data.is_a?(Array)
132
+
133
+ key, found = next_key(data, path)
134
+
135
+ return false unless found
136
+
137
+ path?(data[key], path[1..])
138
+ end
139
+
140
+ private
141
+
142
+ # Traverse through a Hash via the nest path component
143
+ # @return the value at the end of the path
144
+ # @raise [BadPathError] if the path is invalid or does not exist
145
+ # @api private
146
+ def dig_into_hash(data, path)
147
+ key = path.first
148
+ raise BadPathError unless data.key?(key)
149
+
150
+ dig(data[key], path[1..])
151
+ end
152
+
153
+ # Traverse through an Array via the nest path component
154
+ # @return the value at the end of the path
155
+ # @raise [BadPathError] if the path is invalid or does not exist
156
+ # @api private
157
+ def dig_into_array(data, path)
158
+ index = key_to_index(path.first)
159
+ raise BadPathError unless !index.nil? && index >= 0 && index < data.length
160
+
161
+ dig(data[index], path[1..])
162
+ end
163
+
164
+ # Delete a Hash key or an Array element from data
165
+ # @return the value of the element that was deleted
166
+ # @api private
167
+ def delete_key(data, key)
168
+ if data.is_a?(Hash)
169
+ data.delete(key)
170
+ else
171
+ data.delete_at(key)
172
+ end
173
+ end
174
+
175
+ # The next key in the path and whether it exists in the root of the data structure
176
+ #
177
+ # The next key is a string for Hashes and an integer for Arrays
178
+ #
179
+ # @example
180
+ # next_key({ 'a' => 1 }, ['a']) #=> ['a', true]
181
+ # next_key({ 'a' => 1 }, ['b']) #=> ['b', false]
182
+ # @param data [Hash, Array, Object] The data structure to check
183
+ # @param path [Array<String>] An array of keys/indices representing the path being traversed
184
+ # @return [Array<[String, Integer], Boolean>] The next key and whether it exists
185
+ # @api private
186
+ def next_key(data, path)
187
+ if data.is_a?(Hash)
188
+ key = path.first
189
+ [key, data.key?(key)]
190
+ else
191
+ index = key_to_index(path.first)
192
+ [index, index >= 0 && index < data.length]
193
+ end
194
+ end
195
+
196
+ # Convert a String key to an Integer array index
197
+ # @param key [String] The key to convert to an index
198
+ # @return [Integer] the index of the key in the array
199
+ # @raise [BadPathError] if the key is not a String representation of an Integer
200
+ # @api private
201
+ def key_to_index(key)
202
+ Integer(key)
203
+ rescue ArgumentError
204
+ raise BadPathError
205
+ end
206
+ end
207
+ end
data/package.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "devDependencies": {
3
+ "@commitlint/cli": "^19.5.0",
4
+ "@commitlint/config-conventional": "^19.5.0",
5
+ "husky": "^9.1.0"
6
+ },
7
+ "scripts": {
8
+ "postinstall": "husky",
9
+ "prepare": "husky"
10
+ }
11
+ }
data/pre-commit ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env bash
2
+
3
+ ##
4
+ # Run this before doing a commit. To ensure this happens, after cloning this
5
+ # repository, run bin/setup from the projects root directory
6
+
7
+ echo 'Performing pre-commit checks'
8
+
9
+ # Make sure we are testing with the latest version of gems
10
+ # since this is what will get installed in the CI build
11
+ #
12
+ echo "Running 'bundle update'..."
13
+ if ! bundle update > /dev/null; then
14
+ echo 'FAIL: bundle update failed'
15
+ exit 1
16
+ fi
17
+
18
+ # Run all the tests, code/doc analysis, gem build, etc.
19
+ #
20
+ echo "Running 'bundle exec rake default'..."
21
+ if ! bundle exec rake default > /dev/null 2>&1; then
22
+ echo 'FAIL: Rake default task failed'
23
+ exit 1
24
+ fi
25
+
26
+ echo 'SUCCESS'
@@ -0,0 +1,36 @@
1
+ {
2
+ "bootstrap-sha": "c0076bab53d3eae31f3798217e281e3d51a292a3",
3
+ "packages": {
4
+ ".": {
5
+ "release-type": "ruby",
6
+ "package-name": "nested_objects",
7
+ "changelog-path": "CHANGELOG.md",
8
+ "version-file": "lib/nested_objects/version.rb",
9
+ "bump-minor-pre-major": true,
10
+ "bump-patch-for-minor-pre-major": true,
11
+ "draft": false,
12
+ "prerelease": false,
13
+ "include-component-in-tag": false,
14
+ "pull-request-title-pattern": "chore: release v${version}",
15
+ "changelog-sections": [
16
+ { "type": "feat", "section": "Features", "hidden": false },
17
+ { "type": "fix", "section": "Bug Fixes", "hidden": false },
18
+ { "type": "build", "section": "Other Changes", "hidden": false },
19
+ { "type": "chore", "section": "Other Changes", "hidden": false },
20
+ { "type": "ci", "section": "Other Changes", "hidden": false },
21
+ { "type": "docs", "section": "Other Changes", "hidden": false },
22
+ { "type": "perf", "section": "Other Changes", "hidden": false },
23
+ { "type": "refactor", "section": "Other Changes", "hidden": false },
24
+ { "type": "revert", "section": "Other Changes", "hidden": false },
25
+ { "type": "style", "section": "Other Changes", "hidden": false },
26
+ { "type": "test", "section": "Other Changes", "hidden": false }
27
+ ]
28
+ }
29
+ },
30
+ "plugins": [
31
+ {
32
+ "type": "sentence-case"
33
+ }
34
+ ],
35
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
36
+ }
metadata ADDED
@@ -0,0 +1,244 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nested_objects
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.16
5
+ platform: ruby
6
+ authors:
7
+ - James Couball
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: bundler-audit
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.9'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.9'
26
+ - !ruby/object:Gem::Dependency
27
+ name: create_github_release
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.1'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.1'
40
+ - !ruby/object:Gem::Dependency
41
+ name: main_branch_shared_rubocop_config
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.1'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.1'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rake
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '13.2'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '13.2'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rspec
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '3.13'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '3.13'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rubocop
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '1.75'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '1.75'
96
+ - !ruby/object:Gem::Dependency
97
+ name: simplecov
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '0.22'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '0.22'
110
+ - !ruby/object:Gem::Dependency
111
+ name: simplecov-json
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '0.2'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '0.2'
124
+ - !ruby/object:Gem::Dependency
125
+ name: simplecov-rspec
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '0.4'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '0.4'
138
+ - !ruby/object:Gem::Dependency
139
+ name: redcarpet
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: '3.6'
145
+ type: :development
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: '3.6'
152
+ - !ruby/object:Gem::Dependency
153
+ name: yard
154
+ requirement: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - "~>"
157
+ - !ruby/object:Gem::Version
158
+ version: '0.9'
159
+ - - ">="
160
+ - !ruby/object:Gem::Version
161
+ version: 0.9.28
162
+ type: :development
163
+ prerelease: false
164
+ version_requirements: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - "~>"
167
+ - !ruby/object:Gem::Version
168
+ version: '0.9'
169
+ - - ">="
170
+ - !ruby/object:Gem::Version
171
+ version: 0.9.28
172
+ - !ruby/object:Gem::Dependency
173
+ name: yardstick
174
+ requirement: !ruby/object:Gem::Requirement
175
+ requirements:
176
+ - - "~>"
177
+ - !ruby/object:Gem::Version
178
+ version: '0.9'
179
+ type: :development
180
+ prerelease: false
181
+ version_requirements: !ruby/object:Gem::Requirement
182
+ requirements:
183
+ - - "~>"
184
+ - !ruby/object:Gem::Version
185
+ version: '0.9'
186
+ description: |
187
+ Utility methods such as deep_copy, dig, bury, delete, and path? for working with
188
+ PORO structures arbitrarily nested with Hashes and Arrays.
189
+ email:
190
+ - jcouball@yahoo.com
191
+ executables: []
192
+ extensions: []
193
+ extra_rdoc_files: []
194
+ files:
195
+ - ".commitlintrc.yml"
196
+ - ".husky/commit-msg"
197
+ - ".markdownlint.yml"
198
+ - ".release-please-manifest.json"
199
+ - ".rspec"
200
+ - ".rubocop.yml"
201
+ - ".yamllint.yml"
202
+ - ".yardopts"
203
+ - CHANGELOG.md
204
+ - CONTRIBUTING.md
205
+ - LICENSE.txt
206
+ - README.md
207
+ - Rakefile
208
+ - lib/nested_objects.rb
209
+ - lib/nested_objects/mixin.rb
210
+ - lib/nested_objects/version.rb
211
+ - package.json
212
+ - pre-commit
213
+ - release-please-config.json
214
+ homepage: https://github.com/main-branch/nested_objects
215
+ licenses:
216
+ - MIT
217
+ metadata:
218
+ allowed_push_host: https://rubygems.org
219
+ homepage_uri: https://github.com/main-branch/nested_objects
220
+ source_code_uri: https://github.com/main-branch/nested_objects
221
+ documentation_uri: https://rubydoc.info/gems/nested_objects/0.1.16
222
+ changelog_uri: https://rubydoc.info/gems/nested_objects/0.1.16/file/CHANGELOG.md
223
+ rubygems_mfa_required: 'true'
224
+ rdoc_options: []
225
+ require_paths:
226
+ - lib
227
+ required_ruby_version: !ruby/object:Gem::Requirement
228
+ requirements:
229
+ - - ">="
230
+ - !ruby/object:Gem::Version
231
+ version: 3.1.0
232
+ required_rubygems_version: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - ">="
235
+ - !ruby/object:Gem::Version
236
+ version: '0'
237
+ requirements:
238
+ - 'Platform: Mac, Linux, or Windows'
239
+ - 'Ruby: MRI 3.1 or later, TruffleRuby 24 or later, or JRuby 9.4 or later'
240
+ rubygems_version: 3.6.7
241
+ specification_version: 4
242
+ summary: Utilities for working with PORO structures arbitrarily nested with Hashes
243
+ and Arrays
244
+ test_files: []