activerecord-pull-alpha 0.1.0 → 0.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a600203d416c9166308cc5685dfce526d73e4b5528a385de114eb7a63b4aef6b
4
- data.tar.gz: c47287ef5de25b8f086bbe04449c1167757130b2648fb3311a02d1f2bb49809f
3
+ metadata.gz: ccb0237045bd530e776a70c9e54572c9914cbb14a62c9a1e2c33ef26ca78a989
4
+ data.tar.gz: fe01b85e846a4ed2860a16c18c8ed235877098e68e460ed618a131bc360ec615
5
5
  SHA512:
6
- metadata.gz: 4764f99abb758043709052752442c3e66743d833dcb9039c4bb34c609ee20504e578155db033bb31a600c9b4032463c649a6a5c34e8d6d59c7a251d8a292acc7
7
- data.tar.gz: 1537a8dde666ecd2e9cba6c0039b5e3fed877ee09c1033900411232191abb2c6884622548f9605e8b6423e87bf7a839c46dc338ba8103d31f9c41d0db2850d9e
6
+ metadata.gz: 53416aabcb2c4e20bbb51fbc771914bb641cba6d6330cc8d8ad0de105c31495d50c5d4e54bb6ce2f15ae9fc611096ea2d7c9c9b48f7d8147493d5cbe4dc3a3ee
7
+ data.tar.gz: 215519fe1cb05dbe49ebac256de0961dc355aec22cf4c382782509b58a8f48168362d8acb1273876b118f26d3a838cb4c7762092072a9bc90b173b9b25692071
@@ -0,0 +1,38 @@
1
+ # This workflow uses actions that are not certified by GitHub.
2
+ # They are provided by a third-party and are governed by
3
+ # separate terms of service, privacy policy, and support
4
+ # documentation.
5
+ # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
+ # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
+
8
+ name: Ruby
9
+
10
+ on:
11
+ push:
12
+ branches: [ "master" ]
13
+ pull_request:
14
+ branches: [ "master" ]
15
+
16
+ permissions:
17
+ contents: read
18
+
19
+ jobs:
20
+ test:
21
+
22
+ runs-on: ubuntu-latest
23
+ strategy:
24
+ matrix:
25
+ ruby-version: ['2.6', '2.7', '3.0']
26
+
27
+ steps:
28
+ - uses: actions/checkout@v3
29
+ - name: Set up Ruby
30
+ # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
31
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
32
+ # uses: ruby/setup-ruby@v1
33
+ uses: ruby/setup-ruby@2b019609e2b0f1ea1a2bc8ca11cb82ab46ada124
34
+ with:
35
+ ruby-version: ${{ matrix.ruby-version }}
36
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
37
+ - name: Run tests
38
+ run: bundle exec rake
data/.gitignore CHANGED
@@ -10,3 +10,7 @@
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
12
  tags
13
+ /.idea/
14
+ /.DS_Store
15
+ /.clj-kondo/
16
+ /.lsp/
data/Gemfile CHANGED
@@ -4,3 +4,9 @@ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
4
 
5
5
  # Specify your gem's dependencies in activerecord-pull.gemspec
6
6
  gemspec
7
+
8
+ gem 'activerecord', require: 'active_record'
9
+ gem 'pry'
10
+ gem 'rubocop'
11
+ gem 'rubocop-rspec'
12
+ gem 'sqlite3'
data/Gemfile.lock ADDED
@@ -0,0 +1,89 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ activerecord-pull-alpha (0.1.2)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ activemodel (6.1.6.1)
10
+ activesupport (= 6.1.6.1)
11
+ activerecord (6.1.6.1)
12
+ activemodel (= 6.1.6.1)
13
+ activesupport (= 6.1.6.1)
14
+ activesupport (6.1.6.1)
15
+ concurrent-ruby (~> 1.0, >= 1.0.2)
16
+ i18n (>= 1.6, < 2)
17
+ minitest (>= 5.1)
18
+ tzinfo (~> 2.0)
19
+ zeitwerk (~> 2.3)
20
+ ast (2.4.2)
21
+ coderay (1.1.3)
22
+ concurrent-ruby (1.1.10)
23
+ diff-lcs (1.5.0)
24
+ i18n (1.12.0)
25
+ concurrent-ruby (~> 1.0)
26
+ method_source (1.0.0)
27
+ minitest (5.15.0)
28
+ parallel (1.22.1)
29
+ parser (3.1.2.0)
30
+ ast (~> 2.4.1)
31
+ pry (0.14.1)
32
+ coderay (~> 1.1)
33
+ method_source (~> 1.0)
34
+ rainbow (3.1.1)
35
+ rake (13.0.6)
36
+ regexp_parser (2.5.0)
37
+ rexml (3.2.5)
38
+ rspec (3.11.0)
39
+ rspec-core (~> 3.11.0)
40
+ rspec-expectations (~> 3.11.0)
41
+ rspec-mocks (~> 3.11.0)
42
+ rspec-core (3.11.0)
43
+ rspec-support (~> 3.11.0)
44
+ rspec-expectations (3.11.0)
45
+ diff-lcs (>= 1.2.0, < 2.0)
46
+ rspec-support (~> 3.11.0)
47
+ rspec-mocks (3.11.1)
48
+ diff-lcs (>= 1.2.0, < 2.0)
49
+ rspec-support (~> 3.11.0)
50
+ rspec-support (3.11.0)
51
+ rubocop (1.28.2)
52
+ parallel (~> 1.10)
53
+ parser (>= 3.1.0.0)
54
+ rainbow (>= 2.2.2, < 4.0)
55
+ regexp_parser (>= 1.8, < 3.0)
56
+ rexml
57
+ rubocop-ast (>= 1.17.0, < 2.0)
58
+ ruby-progressbar (~> 1.7)
59
+ unicode-display_width (>= 1.4.0, < 3.0)
60
+ rubocop-ast (1.17.0)
61
+ parser (>= 3.1.1.0)
62
+ rubocop-rspec (2.10.0)
63
+ rubocop (~> 1.19)
64
+ ruby-progressbar (1.11.0)
65
+ sqlite3 (1.4.4)
66
+ tzinfo (2.0.5)
67
+ concurrent-ruby (~> 1.0)
68
+ unicode-display_width (2.1.0)
69
+ zeitwerk (2.6.0)
70
+
71
+ PLATFORMS
72
+ -darwin-21
73
+ arm64-darwin-21
74
+ arm64-darwin-22
75
+ x86_64-linux
76
+
77
+ DEPENDENCIES
78
+ activerecord
79
+ activerecord-pull-alpha!
80
+ bundler (~> 2.2)
81
+ pry
82
+ rake (~> 13.0)
83
+ rspec (~> 3.0)
84
+ rubocop
85
+ rubocop-rspec
86
+ sqlite3
87
+
88
+ BUNDLED WITH
89
+ 2.3.8
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ [![Ruby](https://github.com/delonnewman/activerecord-pull/actions/workflows/ruby.yml/badge.svg)](https://github.com/delonnewman/activerecord-pull/actions/workflows/ruby.yml)
2
+
1
3
  # ActiveRecord::Pull::Alpha
2
4
 
3
5
  A simple query interface for pulling deeply nested data from records.
@@ -30,13 +32,18 @@ A simple query interface for pulling deeply nested data from records.
30
32
 
31
33
  ## Why?
32
34
 
33
- Modeling [trees][tree] and [ontologies][ontology] are and excercise in relational database design when using a SQL database.
34
- Unfortunalty, when dealing with deeply nested and overlapping data (e.g. medical records, social graphs)
35
+ When using an SQL database modeling [trees][tree] and [ontologies][ontology] is an exercise in relational database design.
36
+ Unfortunately, when dealing with deeply nested and *overlapping* data (e.g. medical records and social graphs)
35
37
  trees and ontologies won't do. We need to model graphs, a much more sparsely structured data model, but
36
- this leads to very complex table structures and very complex SQL queries.
38
+ this leads to very complex (or sparse) table structures and very complex SQL queries.
39
+
40
+ Alternatives to SQL databases like [Datomic][datomic], [RDF][rdf] and graph databases resolve this problem in the general case,
41
+ but sometimes those solutions are not a viable option for one reason or another. This gem attempts to provide some of the
42
+ convenience of these solutions as an extension to ActiveRecord. As a bonus it makes querying trees and ontologies simple too!
43
+
44
+ ## Pull Syntax
37
45
 
38
- This pull syntax is inspired by the pull syntax that is used in [Datomic][datomic-pull], a database that is designed to model
39
- graph-like structures. This gem plugs that syntax into ActiveRecord. As a bonus it makes querying trees and ontologies simple too!
46
+ The pull syntax is inspired by the pull syntax that is used in [Datomic][datomic-pull].
40
47
 
41
48
  ## Installation
42
49
 
@@ -62,13 +69,14 @@ Or install it yourself as:
62
69
  - [GraphQL](https://graphql.org/)
63
70
  - [Entity Attribute Value Model][eav-model]
64
71
  - [RDF][rdf]
72
+ - [A City is Not a Tree](https://www.patternlanguage.com/archive/cityisnotatree.html)
65
73
 
66
74
  ## License
67
75
 
68
76
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
69
77
 
70
78
  [datomic]: https://www.datomic.com/
71
- [datomic-pull]: https://docs.datomic.com/on-prem/pull.html
79
+ [datomic-pull]: https://docs.datomic.com/on-prem/pull.html#pull-grammar
72
80
  [eav-model]: https://en.wikipedia.org/wiki/Entity%E2%80%93attribute%E2%80%93value_model
73
81
  [rdf]: https://en.wikipedia.org/wiki/Resource_Description_Framework
74
82
  [tree]: https://en.wikipedia.org/wiki/Tree_(data_structure)
@@ -34,7 +34,7 @@ Gem::Specification.new do |spec|
34
34
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
35
35
  spec.require_paths = ["lib"]
36
36
 
37
- spec.add_development_dependency "bundler", "~> 1.17"
38
- spec.add_development_dependency "rake", "~> 10.0"
37
+ spec.add_development_dependency "bundler", "~> 2.2"
38
+ spec.add_development_dependency "rake", "~> 13.0"
39
39
  spec.add_development_dependency "rspec", "~> 3.0"
40
40
  end
@@ -1,54 +1,89 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord
2
4
  module Pull
3
5
  module Alpha
6
+ class SyntaxError < RuntimeError; end
7
+ class ReflectionError < RuntimeError; end
8
+
4
9
  module Core
5
10
  class << self
6
- def pull_many(records, q)
7
- records.map { |r| pull(r, q) }
11
+ # Apply the pull query to a collection of records.
12
+ #
13
+ # @param records [Enumerable<ActiveRecord::Base>]
14
+ # @param query [Symbol, String, Hash, Array]
15
+ #
16
+ # @return [Array<Hash>]
17
+ def pull_many(records, query)
18
+ records.map { |r| pull(r, query) }
8
19
  end
9
-
10
- def pull(record, q)
11
- case q
20
+
21
+ # Apply the pull query to the given record.
22
+ #
23
+ # @param record [ActiveRecord::Base]
24
+ # @param query [Symbol, String, Hash, Array]
25
+ #
26
+ # @return [Hash]
27
+ def pull(record, query)
28
+ case query
12
29
  when :*, '*'
13
30
  pull_all(record)
14
31
  when Symbol, String
15
- pull_symbol(record, q)
32
+ pull_symbol(record, query)
16
33
  when Hash
17
- pull_hash(record, q)
34
+ pull_hash(record, query)
18
35
  when Array
19
- q.map { |pat| pull(record, pat) }.reduce(&:merge)
36
+ # TODO: add filter_map as a core extension for older Ruby implementations
37
+ query.map { |pat| pull(record, pat) }.reduce(&:merge)
20
38
  else
21
- raise "Invalid pull syntax: #{q.inspect}"
39
+ raise SyntaxError, "invalid pull syntax: #{query.inspect}"
22
40
  end
23
41
  end
24
-
42
+
25
43
  private
26
-
44
+
45
+ EMPTY_HASH = {}.freeze
46
+ private_constant :EMPTY_HASH
47
+
48
+ R = ActiveRecord::Reflection
49
+ private_constant :R
50
+
51
+ # @param record [#[]]
52
+ # @param attr [Symbol, String]
53
+ #
54
+ # @return [ActiveRecord::Base, ActiveRecord::Relation, Object, nil]
27
55
  def resolved_value(record, attr)
28
- if !(val = record[attr]).nil?
29
- val
30
- elsif val.nil? and !(reflection = record.class.reflect_on_association(attr)).nil?
31
- model = reflection.class_name.constantize
32
- p model
33
- case reflection
34
- when ActiveRecord::Reflection::HasManyReflection, ActiveRecord::Reflection::HasAndBelongsToManyReflection
35
- model.where(reflection.foreign_key => record[record.class.primary_key])
36
- when ActiveRecord::Reflection::BelongsToReflection
37
- model.find_by(model.primary_key => record[reflection.foreign_key])
38
- else
39
- raise "Don't know how to resolve value for reflection: #{reflection.inspect}"
40
- end
56
+ val = record[attr]
57
+ return val unless val.nil?
58
+
59
+ reflection = record.class.reflect_on_association(attr)
60
+ return if reflection.nil?
61
+
62
+ model = reflection.class_name.constantize
63
+
64
+ case reflection
65
+ when R::HasManyReflection, R::HasAndBelongsToManyReflection
66
+ model.where(reflection.foreign_key => record[record.class.primary_key])
67
+ when R::BelongsToReflection
68
+ model.find_by(model.primary_key => record[reflection.foreign_key])
41
69
  else
42
- nil
70
+ raise ReflectionError, "don't know how to resolve value for reflection: #{reflection.inspect}"
43
71
  end
44
72
  end
45
-
73
+
46
74
  def ref_type?(value)
47
75
  value.is_a?(ActiveRecord::Relation) || value.is_a?(ActiveRecord::Base)
48
76
  end
49
-
77
+
78
+ # @param record [#[]]
79
+ # @param attr [Symbol, String]
80
+ #
81
+ # @return [Hash]
50
82
  def pull_symbol(record, attr)
51
- val = resolved_value(record, attr)
83
+ attr = attr.to_sym
84
+ val = resolved_value(record, attr)
85
+ return EMPTY_HASH if val.nil?
86
+
52
87
  if ref_type?(val) && val.is_a?(Enumerable)
53
88
  { attr => val.map { |v| pull_all(v) } }
54
89
  elsif ref_type?(val)
@@ -57,7 +92,11 @@ module ActiveRecord
57
92
  { attr => val }
58
93
  end
59
94
  end
60
-
95
+
96
+ # @param record [#[]]
97
+ # @param hash [Hash{Symbol, String => Symbol, String, Hash, Array}]
98
+ #
99
+ # @return [Hash]
61
100
  def pull_hash(record, hash)
62
101
  hash.reduce({}) do |h, (attr, pat)|
63
102
  val = resolved_value(record, attr)
@@ -72,25 +111,25 @@ module ActiveRecord
72
111
  end
73
112
  end
74
113
  end
75
-
76
- # TODO: this should pull all associations
114
+
115
+ # @param [ActiveRecord::Base]
116
+ #
117
+ # @return [Hash]
77
118
  def pull_all(record)
78
- names = record.class.attribute_names
119
+ names = record.class.attribute_names.map(&:to_sym)
79
120
  nested = record.class.nested_attributes_options
80
121
  attrs = pull(record, names)
81
-
82
- if nested.empty?
83
- attrs
84
- else
85
- nested.keys.reduce(attrs) do |h, assoc|
86
- assoc_val = record.send(assoc)
87
- if assoc_val.nil? || assoc_val.is_a?(Enumerable) && assoc_val.empty?
88
- h
89
- elsif assoc_val.is_a?(Enumerable)
90
- h.merge!(assoc => assoc_val.map { |rec| pull_all(rec) }.reject(&:nil?))
91
- else
92
- h.merge!(assoc => pull_all(assoc_val))
93
- end
122
+
123
+ return attrs if nested.empty?
124
+
125
+ nested.keys.reduce(attrs) do |h, assoc|
126
+ assoc_val = record.send(assoc)
127
+ if assoc_val.nil? || assoc_val.is_a?(Enumerable) && assoc_val.empty?
128
+ h
129
+ elsif assoc_val.is_a?(Enumerable)
130
+ h.merge!(assoc => assoc_val.filter_map { |rec| pull_all(rec) })
131
+ else
132
+ h.merge!(assoc => pull_all(assoc_val))
94
133
  end
95
134
  end
96
135
  end
@@ -0,0 +1,13 @@
1
+ require 'activerecord/pull/alpha/core'
2
+
3
+ module Enumerable
4
+ def pull(*pattern)
5
+ ActiveRecord::Pull::Alpha::Core.pull_many(self, pattern)
6
+ end
7
+ end
8
+
9
+ class Object
10
+ def pull(*pattern)
11
+ ActiveRecord::Pull::Alpha::Core.pull(self, pattern)
12
+ end
13
+ end
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Activerecord
2
4
  module Pull
3
5
  module Alpha
4
- VERSION = "0.1.0"
6
+ VERSION = "0.1.2"
5
7
  end
6
8
  end
7
9
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-pull-alpha
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Delon Newman
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-02-21 00:00:00.000000000 Z
11
+ date: 2023-08-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.17'
19
+ version: '2.2'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.17'
26
+ version: '2.2'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '10.0'
33
+ version: '13.0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '10.0'
40
+ version: '13.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rspec
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -52,22 +52,25 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.0'
55
- description:
55
+ description:
56
56
  email:
57
57
  - contact@delonnewman.name
58
58
  executables: []
59
59
  extensions: []
60
60
  extra_rdoc_files: []
61
61
  files:
62
+ - ".github/workflows/ruby.yml"
62
63
  - ".gitignore"
63
64
  - ".rspec"
64
65
  - Gemfile
66
+ - Gemfile.lock
65
67
  - LICENSE.txt
66
68
  - README.md
67
69
  - Rakefile
68
70
  - activerecord-pull-alpha.gemspec
69
71
  - lib/activerecord/pull/alpha.rb
70
72
  - lib/activerecord/pull/alpha/core.rb
73
+ - lib/activerecord/pull/alpha/core_ext.rb
71
74
  - lib/activerecord/pull/alpha/version.rb
72
75
  homepage: https://github.com/delonnewman/activerecord-pull
73
76
  licenses:
@@ -77,7 +80,7 @@ metadata:
77
80
  homepage_uri: https://github.com/delonnewman/activerecord-pull
78
81
  source_code_uri: https://github.com/delonnewman/activerecord-pull
79
82
  changelog_uri: https://github.com/delonnewman/activerecord-pull#changelog
80
- post_install_message:
83
+ post_install_message:
81
84
  rdoc_options: []
82
85
  require_paths:
83
86
  - lib
@@ -92,8 +95,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
92
95
  - !ruby/object:Gem::Version
93
96
  version: '0'
94
97
  requirements: []
95
- rubygems_version: 3.0.6
96
- signing_key:
98
+ rubygems_version: 3.4.17
99
+ signing_key:
97
100
  specification_version: 4
98
101
  summary: A simple query interface for pulling deeply nested data from records.
99
102
  test_files: []