hierarchable 0.1.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: c927f016abf308f6c0128f2162908afccb89f55b95495b3499ecfa6a6acdec56
4
+ data.tar.gz: 85724a8ec5e256a5251241f7dc5ef8539c2a041bf683834a6ac0081f02efd922
5
+ SHA512:
6
+ metadata.gz: 28f8b977db00b49eba24c634bcd19381fa631b519c1c371a4b8fdba15b9ac7475ce638a8e0f87c46f28caf1247abc783de1359d999f6f8343bd1d181f48548f4
7
+ data.tar.gz: 7515ea80776d9b60856f91374cedf3d50bfec3f32d572e06bdada94a72a64d64f940ce52b7fd56b1e625ab467885553d6b077d612ad49862f51106e27a6ea4de
@@ -0,0 +1,116 @@
1
+ version: 2.1
2
+
3
+ # ----------------------------------------------------------------------------
4
+ #
5
+ # REUSABLE CUSTOM DEFINITIONS & COMMANDS
6
+ #
7
+ # ----------------------------------------------------------------------------
8
+
9
+ commands:
10
+ attach-dependencies:
11
+ steps:
12
+ - checkout
13
+ - run:
14
+ name: Set up bundler
15
+ command: |
16
+ gem install bundler:2.3.26
17
+ - run:
18
+ name: Bundle Install
19
+ command: |
20
+ bundle config set --local clean 'true'
21
+ bundle config set --local deployment 'true'
22
+ bundle check || bundle install --jobs=4 --retry=3
23
+ - attach_workspace:
24
+ at: .
25
+
26
+ save-results:
27
+ steps:
28
+ - store_test_results:
29
+ path: test/reports
30
+ - store_artifacts:
31
+ name: "Store artifacts: test reports"
32
+ path: reports
33
+ destination: reports
34
+
35
+ # ----------------------------------------------------------------------------
36
+ #
37
+ # JOB DEFINITIONS
38
+ #
39
+ # ----------------------------------------------------------------------------
40
+
41
+ jobs:
42
+ #
43
+ # QUALITY: Make sure that the code is safe, secure, and clean.
44
+ #
45
+ quality:
46
+ resource_class: small
47
+
48
+ docker:
49
+ - image: cimg/ruby:3.1.3
50
+
51
+ steps:
52
+ # --------- SETUP ---------
53
+
54
+ - attach-dependencies
55
+
56
+ # --------- QUALITY CHECKS ---------
57
+
58
+ - run:
59
+ name: Ruby Audit
60
+ command: bundle exec ruby-audit check
61
+ - run:
62
+ name: Bundle Audit
63
+ command: bundle exec bundle-audit check --update
64
+ - run:
65
+ name: Rubocop
66
+ command: bundle exec rubocop
67
+
68
+ # --------- SAVE RESULTS ---------
69
+
70
+ - save-results
71
+
72
+ test:
73
+ resource_class: small
74
+
75
+ docker:
76
+ - image: cimg/ruby:3.1.3
77
+
78
+ steps:
79
+ # --------- SETUP ---------
80
+
81
+ - attach-dependencies
82
+
83
+ # --------- RUN TESTS ---------
84
+
85
+ - run:
86
+ name: Run tests
87
+ command: bundle exec rake test
88
+
89
+ # --------- SAVE RESULTS ---------
90
+
91
+ - save-results
92
+
93
+
94
+ # ----------------------------------------------------------------------------
95
+ #
96
+ # WORKFLOW DEFINITIONS
97
+ #
98
+ # ----------------------------------------------------------------------------
99
+
100
+ workflows:
101
+ version: 2
102
+ commit:
103
+ jobs:
104
+ - quality
105
+ - test
106
+ nightly:
107
+ jobs:
108
+ - quality
109
+ - test
110
+ triggers:
111
+ - schedule:
112
+ cron: "0 2 * * *"
113
+ filters:
114
+ branches:
115
+ only:
116
+ - main
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
data/.rubocop.yml ADDED
@@ -0,0 +1,58 @@
1
+ require:
2
+ - 'rubocop-performance'
3
+ - 'rubocop-rails'
4
+ - 'rubocop-minitest'
5
+ - 'rubocop-rake'
6
+
7
+ AllCops:
8
+ NewCops: enable
9
+ # Don't run rubocop on these files/directories
10
+ Exclude:
11
+ - '**/templates/**/*'
12
+ - '**/vendor/**/*'
13
+ - 'actionpack/lib/action_dispatch/journey/parser.rb'
14
+ - 'lib/templates/**/*'
15
+ - 'db/**/*'
16
+ - 'config/**/*'
17
+ - 'vendor/**/*'
18
+ - 'bin/**/*'
19
+ - 'node_modules/**/*'
20
+
21
+ Layout/LineLength:
22
+ Max: 80
23
+ Exclude:
24
+ - 'lib/hierarchable/hierarchable.rb'
25
+
26
+ Metrics/AbcSize:
27
+ Max: 30
28
+ Exclude:
29
+ - 'test/**/*'
30
+
31
+ Metrics/BlockLength:
32
+ Max: 40
33
+ Exclude:
34
+ - 'hierarchable.gemspec'
35
+ - 'lib/hierarchable/hierarchable.rb'
36
+ - 'test/**/*'
37
+
38
+ Metrics/ClassLength:
39
+ Max: 150
40
+ Exclude:
41
+ - 'test/**/*'
42
+
43
+ Metrics/MethodLength:
44
+ Max: 35
45
+ Exclude:
46
+ - 'test/**/*'
47
+
48
+ Metrics/ModuleLength:
49
+ Max: 100
50
+ Exclude:
51
+ - 'lib/hierarchable/hierarchable.rb'
52
+
53
+ Minitest/MultipleAssertions:
54
+ Max: 10
55
+
56
+ Rails/RefuteMethods:
57
+ Exclude:
58
+ - 'test/**/*'
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ hierarchable
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-3.1.3
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in hierarchable.gemspec
6
+ gemspec
7
+
8
+ gem 'rake', '~> 13.0'
9
+
10
+ gem 'minitest', '~> 5.0'
11
+
12
+ gem 'rubocop', '~> 1.21'
data/Gemfile.lock ADDED
@@ -0,0 +1,94 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ hierarchable (0.1.0)
5
+ activerecord (> 4.2.0)
6
+ activesupport (> 4.2.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activemodel (7.0.4)
12
+ activesupport (= 7.0.4)
13
+ activerecord (7.0.4)
14
+ activemodel (= 7.0.4)
15
+ activesupport (= 7.0.4)
16
+ activesupport (7.0.4)
17
+ concurrent-ruby (~> 1.0, >= 1.0.2)
18
+ i18n (>= 1.6, < 2)
19
+ minitest (>= 5.1)
20
+ tzinfo (~> 2.0)
21
+ ast (2.4.2)
22
+ bundler-audit (0.9.1)
23
+ bundler (>= 1.2.0, < 3)
24
+ thor (~> 1.0)
25
+ concurrent-ruby (1.1.10)
26
+ i18n (1.12.0)
27
+ concurrent-ruby (~> 1.0)
28
+ json (2.6.3)
29
+ minitest (5.16.3)
30
+ parallel (1.22.1)
31
+ parser (3.1.3.0)
32
+ ast (~> 2.4.1)
33
+ rack (3.0.2)
34
+ rainbow (3.1.1)
35
+ rake (13.0.6)
36
+ regexp_parser (2.6.1)
37
+ rexml (3.2.5)
38
+ rubocop (1.40.0)
39
+ json (~> 2.3)
40
+ parallel (~> 1.10)
41
+ parser (>= 3.1.2.1)
42
+ rainbow (>= 2.2.2, < 4.0)
43
+ regexp_parser (>= 1.8, < 3.0)
44
+ rexml (>= 3.2.5, < 4.0)
45
+ rubocop-ast (>= 1.23.0, < 2.0)
46
+ ruby-progressbar (~> 1.7)
47
+ unicode-display_width (>= 1.4.0, < 3.0)
48
+ rubocop-ast (1.24.0)
49
+ parser (>= 3.1.1.0)
50
+ rubocop-minitest (0.25.0)
51
+ rubocop (>= 0.90, < 2.0)
52
+ rubocop-performance (1.15.1)
53
+ rubocop (>= 1.7.0, < 2.0)
54
+ rubocop-ast (>= 0.4.0)
55
+ rubocop-rails (2.17.3)
56
+ activesupport (>= 4.2.0)
57
+ rack (>= 1.1)
58
+ rubocop (>= 1.33.0, < 2.0)
59
+ rubocop-rake (0.6.0)
60
+ rubocop (~> 1.0)
61
+ ruby-progressbar (1.11.0)
62
+ ruby_audit (2.1.0)
63
+ bundler-audit (~> 0.9.0)
64
+ sqlite3 (1.5.4-x86_64-darwin)
65
+ sqlite3 (1.5.4-x86_64-linux)
66
+ temping (4.0.0)
67
+ activerecord (>= 5.2, < 7.1)
68
+ activesupport (>= 5.2, < 7.1)
69
+ thor (1.2.1)
70
+ tzinfo (2.0.5)
71
+ concurrent-ruby (~> 1.0)
72
+ unicode-display_width (2.3.0)
73
+
74
+ PLATFORMS
75
+ x86_64-darwin-22
76
+ x86_64-linux
77
+
78
+ DEPENDENCIES
79
+ bundler (~> 2.3)
80
+ bundler-audit
81
+ hierarchable!
82
+ minitest (~> 5.0)
83
+ rake (~> 13.0)
84
+ rubocop (~> 1.21)
85
+ rubocop-minitest
86
+ rubocop-performance
87
+ rubocop-rails
88
+ rubocop-rake
89
+ ruby_audit
90
+ sqlite3
91
+ temping (~> 4.0)
92
+
93
+ BUNDLED WITH
94
+ 2.3.26
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 TODO: Write your name
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,176 @@
1
+ # Hierarchable
2
+
3
+ [![CircleCI](https://dl.circleci.com/status-badge/img/gh/prschmid/hierarchable/tree/main.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/prschmid/hierarchable/tree/main)
4
+
5
+ Cross model hierarchical (parent, child, sibling) relationship between ActiveRecord models.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'hierarchable'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install hierarchable
22
+
23
+ Once the gem is installed, you will need to make sure that your models have the correct columns.
24
+
25
+ * hierarchy_root: The root node in the hierarchy hierarchy (polymorphic)
26
+ * hierarchy_parent: The parent of the current object (polymorphic)
27
+ * hierarchy_ancestors_path: The string representation of all ancestors of
28
+ the current object (string).
29
+
30
+ Assuming that you are using UUIDs for your IDs, this can be done by adding the following to the model(s) for which you wish to have hierarchy information:
31
+
32
+ ```
33
+ t.references :hierarchy_root,
34
+ polymorphic: true,
35
+ null: true,
36
+ type: :uuid,
37
+ index: true
38
+ t.references :hierarchy_parent,
39
+ polymorphic: true,
40
+ null: true,
41
+ type: :uuid,
42
+ index: true
43
+ t.string :hierarchy_ancestors_path, index: true
44
+ ```
45
+
46
+ The `hierarchy_ancestors_path` column does contain all of the information that is in the `hierarchy_root` and `hierarchy_parent` columns, but those two columns are created for more efficient querying as the direct parent and the root are the most frequent parts of the hierarchy that are needed.
47
+
48
+ ## Usage
49
+
50
+ We will describe the usage using a simplistic Project and Task analogy where we assume that a Project can have many tasks. Given a class `Project` we can set it up as follows
51
+
52
+ ```ruby
53
+ class Project
54
+ include Hierarchable
55
+ hierarchable
56
+ end
57
+ ```
58
+
59
+ This will set up the `Project` as the root of the hierarchy. This means that when we query for its root or parent, it will return "self". I.e.
60
+
61
+ ```ruby
62
+ project = Project.create!
63
+
64
+ # These will be true (assuming the the ID of the project is the UUID xxxxxxxx-...)
65
+ project.hierarchy_root == project
66
+ project.hierarchy_parent == project
67
+ project.hierarchy_ancestors_path == 'Project|xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
68
+ ```
69
+
70
+ Now that we have a project configured, we can add tasks that have projects as a parent.
71
+
72
+ ```ruby
73
+ class Task
74
+ include Hierarchable
75
+ hierarchable parent_source: :project
76
+
77
+ belongs_to :project
78
+ end
79
+ ```
80
+
81
+ This will configure the hierarchy to look at the project association and use that to compute the parent. So now when we instantiate a task, the parent and root of that task will be the project.
82
+
83
+ ```ruby
84
+ project = Project.create!
85
+ task = Task.create!(project: project)
86
+
87
+ # These will be true
88
+ task.hierarchy_root == project
89
+ task.hierarchy_parent == project
90
+ project.hierarchy_ancestors_path == 'Project|xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
91
+ task.hierarchy_root == project
92
+ task.hierarchy_parent == project
93
+ task.hierarchy_ancestors_path == 'Project|xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/Task|yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy'
94
+ ```
95
+
96
+ Now, let's assume that our tasks can also have other Tasks as subtasks. Once we do that, we need to ensure that the parent of a subtask is the task and not the project. For this we, can do something like the following:
97
+
98
+ ```ruby
99
+ class Task
100
+ include Hierarchable
101
+ hierarchable parent_source: ->(object) {
102
+ obj.parent_task.present? ? :parent_task : :project
103
+ }
104
+
105
+ belongs_to :project
106
+ belongs_to :parent_task,
107
+ class_name: 'Task',
108
+ optional: true
109
+ has_many :sub_tasks,
110
+ class_name: 'Task',
111
+ foreign_key: :parent_task
112
+ inverse_of: :parent_task,
113
+ dependent: :destroy
114
+ end
115
+ ```
116
+
117
+ What we have done here is configured the source attribute for the hierarchy computation to be `:parent_task` if the task has a `parent_task` set (i.e. it's a subtask), or use `:project` if one is not set (i.e. it's a top level task).
118
+
119
+ ```ruby
120
+ project = Project.create!
121
+ task = Task.create!(project: project)
122
+ sub_task = Task.create!(project: project, parent_task: task)
123
+
124
+ # These will be true
125
+ task.hierarchy_root == project
126
+ task.hierarchy_parent == project
127
+ project.hierarchy_ancestors_path == 'Project|xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
128
+ task.hierarchy_root == project
129
+ task.hierarchy_parent == project
130
+ task.hierarchy_ancestors_path == 'Project|xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/Task|yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy'
131
+ sub_task.hierarchy_root == project
132
+ sub_task.hierarchy_parent == task
133
+ sub_task.hierarchy_ancestors_path == 'Project|xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/Task|yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy/Task|zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz'
134
+ ```
135
+
136
+ ### Configuring the separators
137
+
138
+ By default the separators to use for the path and records are `/` and `|` respectively. This means that a hierarchy path will look something like
139
+
140
+ ```
141
+ <record1_type>|<record1_id>/<record2_type>|<record2_id>
142
+ ```
143
+
144
+ In the event you need to modify the separators used to build the path, you can pass in your desired separators:
145
+
146
+ ```ruby
147
+ class SomeObject
148
+ include Hierarchable
149
+ hierarchable parent_source: :parent_object,
150
+ path_separator: '@@',
151
+ record_separator: '++'
152
+
153
+ belongs_to :parent_object
154
+ end
155
+ ```
156
+
157
+ Assuming that you set the separators like that for all models, then your path will look something like
158
+ ```
159
+ <record1_type>++<record1_id>@@<record2_type>++<record2_id>
160
+ ```
161
+
162
+ CAUTION: When setting custom path and/or record separators, do not use any characters that are likely to be in class/module names such as -, _, :, etc. Otherwise it will not be possible to determine the objects in the path.
163
+
164
+ # Development
165
+
166
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
167
+
168
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
169
+
170
+ ## Contributing
171
+
172
+ Bug reports and pull requests are welcome on GitHub at https://github.com/prschmid/hierarchable.
173
+
174
+ ## License
175
+
176
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << 'test'
8
+ t.libs << 'lib'
9
+ t.test_files = FileList['test/**/*_test.rb']
10
+ end
11
+
12
+ require 'rubocop/rake_task'
13
+
14
+ RuboCop::RakeTask.new
15
+
16
+ task default: %i[test rubocop]
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "hierarchable"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'hierarchable/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'hierarchable'
9
+ spec.version = Hierarchable::VERSION
10
+ spec.authors = ['Patrick R. Schmid']
11
+ spec.email = ['prschmid@gmail.com']
12
+
13
+ spec.summary = 'Cross model hierarchical (parent, child, sibling) ' \
14
+ 'relationship between ActiveRecord models.'
15
+ spec.description = 'Cross model hierarchical (parent, child, sibling) ' \
16
+ 'relationship between ActiveRecord models.'
17
+ spec.homepage = 'https://github.com/prschmid/hierarchable'
18
+ spec.license = 'MIT'
19
+
20
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the
21
+ # 'allowed_push_host' to allow pushing to a single host or delete this
22
+ # section to allow pushing to any host.
23
+ if spec.respond_to?(:metadata)
24
+ spec.metadata['homepage_uri'] = spec.homepage
25
+ spec.metadata['source_code_uri'] = 'https://github.com/prschmid/hierarchable'
26
+ spec.metadata['changelog_uri'] = 'https://github.com/prschmid/hierarchable'
27
+ else
28
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
29
+ 'public gem pushes.'
30
+ end
31
+
32
+ # Specify which files should be added to the gem when it is released.
33
+ # The `git ls-files -z` loads the files in the RubyGem that have been added
34
+ # into git.
35
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
36
+ `git ls-files -z`.split("\x0").reject do |f|
37
+ f.match(%r{^(test|spec|features)/})
38
+ end
39
+ end
40
+ spec.bindir = 'exe'
41
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
42
+ spec.require_paths = ['lib']
43
+
44
+ spec.required_ruby_version = '>= 3.1'
45
+
46
+ spec.add_development_dependency('bundler', '~> 2.3')
47
+ spec.add_development_dependency('bundler-audit', '>= 0')
48
+ spec.add_development_dependency('minitest', '~> 5.0')
49
+ spec.add_development_dependency('rake', '~> 12.3')
50
+ spec.add_development_dependency('rubocop', '>= 0')
51
+ spec.add_development_dependency('rubocop-minitest', '>= 0')
52
+ spec.add_development_dependency('rubocop-performance', '>= 0')
53
+ spec.add_development_dependency('rubocop-rails', '>= 0')
54
+ spec.add_development_dependency('rubocop-rake', '>= 0')
55
+ spec.add_development_dependency('ruby_audit', '>= 0')
56
+ spec.add_development_dependency('sqlite3', '>= 0')
57
+ spec.add_development_dependency('temping', '~> 4.0')
58
+ spec.add_runtime_dependency('activerecord', '> 4.2.0')
59
+ spec.add_runtime_dependency('activesupport', '> 4.2.0')
60
+ spec.metadata['rubygems_mfa_required'] = 'true'
61
+ end
@@ -0,0 +1,405 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ # All objects that want to make use of this concern must have three columns
6
+ # defined:
7
+ # hierarchy_root: The root node in the hierarchy hierarchy (polymorphic)
8
+ # hierarchy_parent: The parent of the current object (polymorphic)
9
+ # hierarchy_ancestors_path: The string representation of all ancestors of
10
+ # the current object (string).
11
+ # The `hierarchy_ancestors_path` column does contain all of the information
12
+ # that is in the `hierarchy_root` and `hierarchy_parent` columns, but those two
13
+ # columns are created for more efficient querying as the direct parent and the
14
+ # root are the most frequent parts of the hierarchy that are needed.
15
+ #
16
+ # To set the attribute that should be used as the parent, one needs to set the
17
+ # `parent_source` value when including this in a model.
18
+ #
19
+ # class A
20
+ # include Hierarchable
21
+ # hierarchable parent_source: :some_column
22
+ # end
23
+ #
24
+ # If some model doesn't have a parent (e.g. it's the root of the hierarchy)
25
+ # then the `parent_source` can be ommited or explicitly set to `nil`
26
+ #
27
+ # class B
28
+ # include Hierarchable
29
+ # hierarchable parent_source: nil
30
+ # end
31
+ #
32
+ # class AlternateB
33
+ # include Hierarchable
34
+ # hierarchable
35
+ # end
36
+ #
37
+ # There are times when the parent is dependent on the state of an object. For
38
+ # example, let's assume that a Project can have tasks, and that tasks can also
39
+ # have tasks (sub tasks). Assuming that the Task model has both `project`
40
+ # and `parent_task` attributes, we could define the parent source dynamically
41
+ # as follows:
42
+ #
43
+ # class Task
44
+ # include Hierarchable
45
+ # hierarchable parent_source: ->(obj) {
46
+ # obj.parent_task.present ? :parent_task : :project
47
+ # }
48
+ # end
49
+ #
50
+ # By default the separators to use for the path and records are / and |
51
+ # respectively. This means that a hierarchy path will look something like
52
+ #
53
+ # <record1_type>|<record1_id>/<record2_type>|<record2_id>
54
+ #
55
+ # A user can change this default behavior by setting the `path_separator` and/or
56
+ # `record_seprator` option when including this in a class. For example:
57
+ #
58
+ # class Foo
59
+ # include Hierarchable
60
+ # hierachable path_separator: '##', record_separator: '@@'
61
+ # end
62
+ #
63
+ # CAUTION: When setting custom path and/or record separators, do not use any
64
+ # characters that are likely to be in class/module names such as -, _, :, etc.
65
+ module Hierarchable
66
+ extend ActiveSupport::Concern
67
+
68
+ HIERARCHABLE_DEFAULT_PATH_SEPARATOR = '/'
69
+ HIERARCHABLE_DEFAULT_RECORD_SEPARATOR = '|'
70
+
71
+ class_methods do
72
+ def hierarchable(opts = {})
73
+ class_attribute :hierarchable_config
74
+
75
+ # Save the configuration
76
+ self.hierarchable_config = {
77
+ parent_source: opts.fetch(:parent_source, nil),
78
+ path_separator: opts.fetch(
79
+ :path_separator, HIERARCHABLE_DEFAULT_PATH_SEPARATOR
80
+ ),
81
+ record_separator: opts.fetch(
82
+ :record_separator, HIERARCHABLE_DEFAULT_RECORD_SEPARATOR
83
+ )
84
+ }
85
+
86
+ belongs_to :hierarchy_root, polymorphic: true, optional: true
87
+
88
+ belongs_to :hierarchy_parent, polymorphic: true, optional: true
89
+ alias_method :hierarchy_parent_relationship, :hierarchy_parent
90
+
91
+ # Set the parent of the current object. This needs to happen first as
92
+ # setting the hierarchy_root and the hierarchy_ancestors_path depends on
93
+ # having the hierarchy_parent set first.
94
+ before_save :set_hierarchy_parent
95
+
96
+ # Based on the hierarchy_parent that is set, set the root. This will take
97
+ # the hierarchy_root of the hierarchy_parent.
98
+ before_save :set_hierarchy_root
99
+
100
+ # If an object gets moved, we need to ensure that then
101
+ # hierarchy_ancestors_path is updated to ensure that it stays accurate
102
+ before_save :update_dirty_hierarchy_ancestors_path,
103
+ unless: :new_record?,
104
+ if: :hierarchy_parent_changed?
105
+
106
+ before_create :set_hierarchy_ancestors_path
107
+
108
+ scope :descendants_of,
109
+ lambda { |object|
110
+ where(
111
+ 'hierarchy_ancestors_path LIKE :hierarchy_ancestors_path',
112
+ hierarchy_ancestors_path: "#{object.hierarchy_full_path}%"
113
+ )
114
+ }
115
+
116
+ scope :siblings_of,
117
+ lambda { |object|
118
+ where(
119
+ 'hierarchy_parent_type=:parent_type AND hierarchy_parent_id=:parent_id',
120
+ parent_type: object.hierarchy_parent.class.name,
121
+ parent_id: object.hierarchy_parent.id
122
+ )
123
+ }
124
+
125
+ include InstanceMethods
126
+ end
127
+ end
128
+
129
+ # Instance methods to include
130
+ module InstanceMethods
131
+ def hierarchy_parent(raw: false)
132
+ return hierarchy_parent_relationship if raw
133
+
134
+ # Depending on whether or not the object has been saved or not, we need
135
+ # to be smart as to how we try to get the parent. If it's saved, then
136
+ # the `hierarchy_parent` attribute in the model will be set and so we
137
+ # can use the `belongs_to` relationship to get the parent. However,
138
+ # if the parent has changed or the object has yet to be saved, we can't
139
+ # use the relationship to get the parent as the value will not have been
140
+ # set properly yet in the model (since it's a `before_save` hook).
141
+ use_relationship = if persisted?
142
+ !hierarchy_parent_changed?
143
+ else
144
+ false
145
+ end
146
+
147
+ if use_relationship
148
+ hierarchy_parent_relationship
149
+ else
150
+ source = hierarchy_parent_source
151
+ source.nil? ? nil : send(source)
152
+ end
153
+ end
154
+
155
+ # Return the attribute name that links this object to its parent.
156
+ #
157
+ # This should return the name of the attribute/relation/etc either as a
158
+ # string or symbol.
159
+ #
160
+ # For example, if this is a Task, then the hierarchy_parent_source is
161
+ # likely the attribute that references the Project this task belongs to.
162
+ # If the method returns nil (the default behavior), the assumption is that
163
+ # this object is the root of the hierarchy.
164
+ def hierarchy_parent_source
165
+ source = hierarchable_config[:parent_source]
166
+ return nil unless source
167
+
168
+ source.respond_to?(:call) ? source.call(self) : source
169
+ end
170
+
171
+ # Return the string representation of the current object in the format when
172
+ # used as part of a hierarchy.
173
+ #
174
+ # If this is a new record (i.e. not saved yet), this will return "", and
175
+ # will return the string representation of the format once it is saved.
176
+ def to_hierarchy_ancestors_path_format
177
+ return '' if new_record?
178
+
179
+ to_hierarchy_format(self)
180
+ end
181
+
182
+ # Return the full hierarchy path from the root to this object.
183
+ #
184
+ # Unlike the hierarchy_ancestors_path which DOES NOT include the current
185
+ # object in the path, this path contains both the ancestors path AND
186
+ # the current object.
187
+ def hierarchy_full_path
188
+ return '' if new_record? ||
189
+ !respond_to?(:hierarchy_ancestors_path)
190
+
191
+ if hierarchy_ancestors_path.present?
192
+ format('%<path>s%<sep>s%<current>s',
193
+ path: hierarchy_ancestors_path,
194
+ sep: hierarchable_config[:path_separator],
195
+ current: to_hierarchy_ancestors_path_format)
196
+ else
197
+ to_hierarchy_ancestors_path_format
198
+ end
199
+ end
200
+
201
+ # Return hierarchy path for given list of objects
202
+
203
+ def hierarchy_path_for(objects)
204
+ return '' if objects.blank?
205
+
206
+ objects.map do |obj|
207
+ to_hierarchy_format(obj)
208
+ end.join(hierarchable_config[:path_separator])
209
+ end
210
+
211
+ def to_hierarchy_format(object)
212
+ "#{object.class}#{hierarchable_config[:record_separator]}#{object.id}"
213
+ end
214
+
215
+ # Return the full hierarchy path from the root to this object as objects.
216
+ #
217
+ # Unlike the hierarchy_full_path that returns a string of the path,
218
+ # this returns a list of items. The pattern of the returned list will be
219
+ #
220
+ # [Class, Object, Class, Object, ...]
221
+ #
222
+ # Where the Class is the class of the object coming right after it. This
223
+ # representation is useful when creating a breadcrumb and we want to
224
+ # have both all the ancestors (like in the ancestors method), but also
225
+ # the collections (classes), so that we can build up a nice path with
226
+ # links.
227
+ def hierarchy_full_path_reified
228
+ return '' if new_record? ||
229
+ !respond_to?(:hierarchy_ancestors_path)
230
+
231
+ path = []
232
+ hierarchy_full_path.split(hierarchable_config[:path_separator])
233
+ .each do |record|
234
+ ancestor_class_name, ancestor_id = record.split(
235
+ hierarchable_config[:record_separator]
236
+ )
237
+ ancestor_class = ancestor_class_name.safe_constantize
238
+ path << ancestor_class
239
+ path << ancestor_class.find(ancestor_id)
240
+ end
241
+ path
242
+ end
243
+
244
+ # Get ancestors of the same type for an object.
245
+ #
246
+ # For a given object type, return all ancestors that have the same type.
247
+ # Note, since ancestors may be of different types, this may skip parts
248
+ # of the hierarchy if the particular ancestor happens to be of a different
249
+ # type.
250
+ def ancestors
251
+ return [] if !respond_to?(:hierarchy_ancestors_path) ||
252
+ hierarchy_ancestors_path.blank?
253
+
254
+ a = hierarchy_ancestors_path.split(
255
+ hierarchable_config[:path_separator]
256
+ ).map do |ancestor|
257
+ ancestor_class, ancestor_id = ancestor.split(
258
+ hierarchable_config[:record_separator]
259
+ )
260
+
261
+ if ancestor_class == self.class.name
262
+ ancestor_class.safe_constantize.find(ancestor_id)
263
+ end
264
+ end
265
+ a.compact
266
+ end
267
+
268
+ # Return the list of all ancestor objects for the current object
269
+ #
270
+ # Using the `hierarchy_ancestors_path`, this will iteratively get all
271
+ # ancestor objects and return them as a list.
272
+ #
273
+ # As there may be ancestors of different types, this is not a single query
274
+ # and may return things of many different types. E.g. if we have a Project,
275
+ # Task, and a Comment, the ancestors of a coment may be the Task and the
276
+ # Project.
277
+ def all_ancestors
278
+ return [] if !respond_to?(:hierarchy_ancestors_path) ||
279
+ hierarchy_ancestors_path.blank?
280
+
281
+ hierarchy_ancestors_path.split(
282
+ hierarchable_config[:path_separator]
283
+ ).map do |ancestor|
284
+ ancestor_class, ancestor_id = ancestor.split(
285
+ hierarchable_config[:record_separator]
286
+ )
287
+ ancestor_class.safe_constantize.find(ancestor_id)
288
+ end
289
+ end
290
+
291
+ # Get siblings of the same type for an object.
292
+ #
293
+ # For a given object type, return all siblings. Note, this DOES NOT return
294
+ # siblings of different types and those need to be queried separetly.
295
+ # equivalent to c.hierarchy_parent.children
296
+ #
297
+ # Params:
298
+ # +include_self+:: Whether or not to include self in the list.
299
+ # Default is true
300
+ def siblings(include_self: true)
301
+ # The method should always return relation, not an Array sometimes and
302
+ # Relation the other
303
+ return self.class.none unless respond_to?(:hierarchy_parent_id)
304
+
305
+ query = self.class.where(
306
+ hierarchy_parent_type: public_send(:hierarchy_parent_type),
307
+ hierarchy_parent_id: public_send(:hierarchy_parent_id)
308
+ )
309
+ query = query.where.not(id:) unless include_self
310
+ query
311
+ end
312
+
313
+ # Get all siblings of this object regardless of object type.
314
+ #
315
+ # This has yet to be implemented and would likely require a separate join
316
+ # table that has all of the data across all tables linked to the particular
317
+ # parent. I.e. a simple table that has parent, child in it that we could
318
+ # use to query.
319
+ #
320
+ # Params:
321
+ # +include_self+:: Whether or not to include self in the list.
322
+ # Default is true
323
+ def all_siblings
324
+ raise NotImplementedError
325
+ end
326
+
327
+ protected
328
+
329
+ # Set the hierarchy_parent of the current object.
330
+ #
331
+ # This will look at the `hierarchy_parent_source` and take the value that
332
+ # is returned by that method and use it to set the parent. If the parent
333
+ # is set to `nil`, the assumption is that this object is then the root of
334
+ # the hierarchy.
335
+ def set_hierarchy_parent
336
+ return unless respond_to?(:hierarchy_parent_id)
337
+ return if hierarchy_parent_source.blank?
338
+
339
+ self.hierarchy_parent = public_send(hierarchy_parent_source)
340
+ end
341
+
342
+ # Set the hierarchy_root of the current object.
343
+ #
344
+ # This will look at the `hierarchy_parent` and take the `hierarchy_root` of
345
+ # that object. Since this looks at the `hierarchy_parent`, it is imperative
346
+ # that the `hierarchy_parent` is set before this method is called.
347
+ def set_hierarchy_root
348
+ return unless respond_to?(:hierarchy_root_id) &&
349
+ respond_to?(:hierarchy_parent_id)
350
+
351
+ parent = hierarchy_parent
352
+ self.hierarchy_root = if parent.respond_to?(:hierarchy_root)
353
+ if parent.hierarchy_root.nil?
354
+ parent
355
+ else
356
+ parent.hierarchy_root
357
+ end
358
+ else
359
+ parent
360
+ end
361
+ end
362
+
363
+ # Set the hierarchy_ancestors_path of the current object.
364
+ #
365
+ # Based on the hierarchy_parent, this will append the necessary information
366
+ # to update the hierarchy_ancestors_path
367
+ def set_hierarchy_ancestors_path
368
+ return unless respond_to?(:hierarchy_ancestors_path)
369
+ return unless respond_to?(:hierarchy_parent)
370
+
371
+ parent = hierarchy_parent
372
+ self.hierarchy_ancestors_path = \
373
+ if parent.nil? || !parent.respond_to?(:hierarchy_ancestors_path)
374
+ nil
375
+ elsif parent.hierarchy_ancestors_path.blank?
376
+ parent.to_hierarchy_ancestors_path_format
377
+ else
378
+ format('%<path>s%<sep>s%<current>s',
379
+ path: parent.hierarchy_ancestors_path,
380
+ sep: hierarchable_config[:path_separator],
381
+ current: parent.to_hierarchy_ancestors_path_format)
382
+ end
383
+ end
384
+
385
+ # Check to see if the hierarchy_parent has changed
386
+ #
387
+ # This will take the `hierarchy_parent_source` and check to see if the
388
+ # current objects's value for the ID corresponding to the
389
+ # `hierarchy_parent_source` has been updated.
390
+ def hierarchy_parent_changed?
391
+ # FIXME: We need to figure out how to deal with updating the
392
+ # object_hierarchy_ancestry_path, object_hierarchy_full_path, etc.,
393
+ if hierarchy_parent_source.present?
394
+ public_send("#{hierarchy_parent_source}_id_changed?")
395
+ else
396
+ false
397
+ end
398
+ end
399
+
400
+ # Update the hierarchy_ancestors_path if the hierarchy has changed.
401
+ def update_dirty_hierarchy_ancestors_path
402
+ set_hierarchy_ancestors_path
403
+ end
404
+ end
405
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hierarchable
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hierarchable/version'
4
+ require File.join(File.dirname(__FILE__), 'hierarchable', 'hierarchable')
5
+
6
+ ActiveRecord::Base.instance_eval { include Hierarchable }
7
+ if defined?(Rails) && Rails.version.to_i < 4
8
+ raise 'This version of hierarchable requires Rails 4 or higher'
9
+ end
metadata ADDED
@@ -0,0 +1,261 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hierarchable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Patrick R. Schmid
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-12-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler-audit
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '12.3'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '12.3'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop-minitest
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop-performance
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop-rails
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop-rake
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: ruby_audit
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: sqlite3
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: temping
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '4.0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '4.0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: activerecord
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">"
186
+ - !ruby/object:Gem::Version
187
+ version: 4.2.0
188
+ type: :runtime
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">"
193
+ - !ruby/object:Gem::Version
194
+ version: 4.2.0
195
+ - !ruby/object:Gem::Dependency
196
+ name: activesupport
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">"
200
+ - !ruby/object:Gem::Version
201
+ version: 4.2.0
202
+ type: :runtime
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">"
207
+ - !ruby/object:Gem::Version
208
+ version: 4.2.0
209
+ description: Cross model hierarchical (parent, child, sibling) relationship between
210
+ ActiveRecord models.
211
+ email:
212
+ - prschmid@gmail.com
213
+ executables: []
214
+ extensions: []
215
+ extra_rdoc_files: []
216
+ files:
217
+ - ".circleci/config.yml"
218
+ - ".gitignore"
219
+ - ".rubocop.yml"
220
+ - ".ruby-gemset"
221
+ - ".ruby-version"
222
+ - Gemfile
223
+ - Gemfile.lock
224
+ - LICENSE.txt
225
+ - README.md
226
+ - Rakefile
227
+ - bin/console
228
+ - bin/setup
229
+ - hierarchable.gemspec
230
+ - lib/hierarchable.rb
231
+ - lib/hierarchable/hierarchable.rb
232
+ - lib/hierarchable/version.rb
233
+ homepage: https://github.com/prschmid/hierarchable
234
+ licenses:
235
+ - MIT
236
+ metadata:
237
+ homepage_uri: https://github.com/prschmid/hierarchable
238
+ source_code_uri: https://github.com/prschmid/hierarchable
239
+ changelog_uri: https://github.com/prschmid/hierarchable
240
+ rubygems_mfa_required: 'true'
241
+ post_install_message:
242
+ rdoc_options: []
243
+ require_paths:
244
+ - lib
245
+ required_ruby_version: !ruby/object:Gem::Requirement
246
+ requirements:
247
+ - - ">="
248
+ - !ruby/object:Gem::Version
249
+ version: '3.1'
250
+ required_rubygems_version: !ruby/object:Gem::Requirement
251
+ requirements:
252
+ - - ">="
253
+ - !ruby/object:Gem::Version
254
+ version: '0'
255
+ requirements: []
256
+ rubygems_version: 3.3.26
257
+ signing_key:
258
+ specification_version: 4
259
+ summary: Cross model hierarchical (parent, child, sibling) relationship between ActiveRecord
260
+ models.
261
+ test_files: []