plucker 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 52616a6406a6a077c0ac1f273ca2d4c09ed4c00d3990f2cae204ad82ac838e52
4
+ data.tar.gz: f3949aeef20eec3384743cb2170f07d03a6670734cbc8a1f40893a2dac552770
5
+ SHA512:
6
+ metadata.gz: 14579f65d8d1ba9b7c37813ca743a2b2b80d8a1ac065cb7bf410f8fd4fac2d0d439aaa7b67ced685d4c30d77769cf2409eb86983244b2df8be8d8700e5ce6a1b
7
+ data.tar.gz: 804ba6ea55a925e26d3208dd589f59091bd22f0f4ae72058949bce0effa509c1e88a92aea07581803a3aaafec60d22dd32ad2936cfc9ee65010a52afe83c9d5a
data/.rubocop.yml ADDED
@@ -0,0 +1,168 @@
1
+ require:
2
+ - rubocop-factory_bot
3
+ - rubocop-minitest
4
+ - rubocop-performance
5
+ - rubocop-rake
6
+
7
+ AllCops:
8
+ TargetRubyVersion: 2.6.0
9
+ NewCops: enable
10
+
11
+ Layout/ArgumentAlignment:
12
+ EnforcedStyle: with_fixed_indentation
13
+
14
+ Layout/DotPosition:
15
+ EnforcedStyle: trailing
16
+
17
+ Layout/FirstArrayElementIndentation:
18
+ EnforcedStyle: consistent
19
+
20
+ Layout/FirstHashElementIndentation:
21
+ EnforcedStyle: consistent
22
+
23
+ Layout/HashAlignment:
24
+ EnforcedHashRocketStyle: table
25
+
26
+ # Layout/IndentationConsistency:
27
+ # EnforcedStyle: indented_internal_methods
28
+
29
+ Layout/LineLength:
30
+ Enabled: false
31
+ Max: 120
32
+
33
+ Layout/MultilineMethodCallIndentation:
34
+ EnforcedStyle: indented
35
+
36
+ Layout/MultilineOperationIndentation:
37
+ EnforcedStyle: indented
38
+
39
+ Layout/ParameterAlignment:
40
+ EnforcedStyle: with_fixed_indentation
41
+
42
+ Lint/NestedMethodDefinition:
43
+ Enabled: false
44
+
45
+ Metrics/AbcSize:
46
+ Enabled: false
47
+
48
+ Metrics/BlockLength:
49
+ Max: 100
50
+ Exclude:
51
+ - test/**/*.rb
52
+
53
+ Metrics/BlockNesting:
54
+ Max: 3
55
+
56
+ Metrics/ClassLength:
57
+ Enabled: false
58
+
59
+ Metrics/CyclomaticComplexity:
60
+ Max: 25
61
+
62
+ Metrics/MethodLength:
63
+ Max: 100
64
+ Exclude:
65
+ - test/**/*.rb
66
+
67
+ Metrics/ModuleLength:
68
+ Enabled: false
69
+
70
+ Metrics/ParameterLists:
71
+ CountKeywordArgs: false
72
+ Exclude:
73
+ - test/**/*.rb
74
+
75
+ Metrics/PerceivedComplexity:
76
+ Max: 25
77
+
78
+ Minitest/AssertInDelta:
79
+ Enabled: false
80
+
81
+ Naming/VariableNumber:
82
+ Enabled: false
83
+
84
+ Performance/Casecmp:
85
+ Enabled: false
86
+
87
+ Style/AsciiComments:
88
+ Enabled: false
89
+
90
+ # Style/BlockDelimiters:
91
+ # Enabled: false
92
+
93
+ Style/ClassVars:
94
+ Enabled: false
95
+
96
+ # Style/CombinableLoops:
97
+ # Enabled: false
98
+
99
+ # Style/ConditionalAssignment:
100
+ # Enabled: false
101
+
102
+ Style/Documentation:
103
+ Enabled: true
104
+
105
+ Style/DocumentationMethod:
106
+ Enabled: true
107
+ Exclude:
108
+ - test/**/*
109
+
110
+ Style/EmptyMethod:
111
+ EnforcedStyle: expanded
112
+
113
+ Style/FormatStringToken:
114
+ EnforcedStyle: template
115
+
116
+ # Style/GuardClause:
117
+ # Enabled: false
118
+
119
+ Style/HashSyntax:
120
+ EnforcedShorthandSyntax: either
121
+
122
+ # Style/IfUnlessModifier:
123
+ # Enabled: false
124
+
125
+ Style/Lambda:
126
+ EnforcedStyle: literal
127
+
128
+ # Style/MethodDefParentheses:
129
+ # Enabled: false
130
+
131
+ # Style/NegatedIf:
132
+ # Enabled: false
133
+
134
+ # Style/Next:
135
+ # Enabled: false
136
+
137
+ # Style/NumericPredicate:
138
+ # Enabled: false
139
+
140
+ # Style/OpenStructUse:
141
+ # Enabled: false
142
+
143
+ Style/RaiseArgs:
144
+ EnforcedStyle: compact
145
+
146
+ Style/RedundantReturn:
147
+ Enabled: false
148
+
149
+ Style/RedundantSelf:
150
+ Enabled: false
151
+
152
+ Style/RegexpLiteral:
153
+ AllowInnerSlashes: true
154
+
155
+ # Style/RescueModifier:
156
+ # Enabled: false
157
+
158
+ # Style/SafeNavigation:
159
+ # Enabled: true
160
+
161
+ Style/SingleLineMethods:
162
+ Enabled: false
163
+
164
+ Style/StringConcatenation:
165
+ Mode: conservative
166
+
167
+ # Style/SymbolArray:
168
+ # Enabled: false
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 pioz
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,174 @@
1
+ ![build](https://github.com/pioz/plucker/workflows/Ruby/badge.svg)
2
+ [![codecov](https://codecov.io/gh/pioz/plucker/graph/badge.svg?token=95G6SJXB47)](https://codecov.io/gh/pioz/plucker)
3
+
4
+ # Plucker
5
+
6
+ Plucker allows projecting records extracted from a query into an array of
7
+ specifically defined [Ruby structs](https://ruby-doc.org/current/Struct.html) for the occasion. It is an
8
+ enchanted [`pluck`](https://www.rubydoc.info/docs/rails/ActiveRecord%2FCalculations:pluck). It
9
+ takes a list of values you want to extract and throws them into a custom
10
+ array of Ruby struct.
11
+
12
+ This can make your application more efficient because it avoids loading
13
+ ActiveRecord objects and utilizes structs, which are more efficient.
14
+
15
+ ## Installation
16
+
17
+ Install the gem and add to the application's Gemfile by executing:
18
+
19
+ $ bundle add plucker
20
+
21
+ If bundler is not being used to manage dependencies, install the gem by executing:
22
+
23
+ $ gem install plucker
24
+
25
+ ## Usage
26
+
27
+ ```ruby
28
+ posts = Post.joins(:author, :comments).group(:id).plucker(:title, 'authors.name', { comments_count: 'COUNT(comments.id)' })
29
+ post = posts.first
30
+ post.title # 'How to make pizza'
31
+ post.authors_name # 'Henry'
32
+ post.comments_count # 2
33
+ post.id # NoMethodError: undefined method `id' for #<struct title="How to make pizza", authors_name="Henry", comments_count=2>
34
+ ```
35
+
36
+ ## Purpose
37
+
38
+ Let's assume we have these classes:
39
+
40
+ ```ruby
41
+ class Author < ApplicationRecord
42
+ has_many :post
43
+
44
+ validates :name, presence: true
45
+ end
46
+
47
+ class Post < ApplicationRecord
48
+ belongs_to :author
49
+
50
+ validates :title, body, presence: true
51
+
52
+ def slug
53
+ self.title.parameterize
54
+ end
55
+ end
56
+ ```
57
+
58
+ and we execute this query:
59
+
60
+ ```ruby
61
+ posts = Post.joins(:author).select('id, authors.name AS author_name')
62
+ ```
63
+
64
+ The objects in the posts array are ActiveRecord objects of type `Post`. As I
65
+ read the code, it feels natural for me to be able to do:
66
+
67
+ ```ruby
68
+ post = posts.first
69
+ post.id
70
+ post.title
71
+ post.body
72
+ post.slug
73
+ ```
74
+
75
+ Now, out of these instructions, only `post.id` works, while all the others
76
+ will result in an error because the fields were not selected. This is very
77
+ strange to me, and in a complex codebase, it can lead to confusion and
78
+ frustration.
79
+
80
+ Furthermore, I can see in the code `post.author_name` and wonder where that
81
+ method or column is defined. Obviously, I won't find the definition of that
82
+ method because it is dynamically generated by ActiveRecord. I don't like this
83
+ very much; it makes it unclear what data is present in the object.
84
+
85
+ Therefore, I have decided to write Plucker to have well-defined objects with
86
+ clear fields right from the start. I aim for lightweight and efficient
87
+ objects without creating ActiveRecord fat objects with methods that I can't
88
+ even use.
89
+
90
+ Keep in mind that you can always continue to perform queries in the standard
91
+ ActiveRecord way. With Plucker, you have a new, more efficient, and clearer
92
+ option.
93
+
94
+ ## Doc
95
+
96
+ The arguments of Plucker can be specified in in 3 different ways depending on
97
+ the requirements: as a `Symbol`, as a `String`, or as a `Hash`.
98
+
99
+ When using a symbol, the column with the corresponding name to the symbol is
100
+ selected, and the struct field will have that name:
101
+
102
+ ```ruby
103
+ post = Post.plucker(:title).last
104
+ #<struct title="How to make pizza">
105
+
106
+ post = Post.joins(:author).plucker(:title, :name).last
107
+ #<struct title="How to make pizza", name="Henry">
108
+ ```
109
+
110
+ When using the symbol `:all` or `:*`, it is interpreted as `SELECT *`
111
+ statement, selecting all columns from the specified table:
112
+
113
+ ```ruby
114
+ post = Post.plucker(:all).last
115
+ #<struct id=1, title="How to make pizza", author_id=1, created_at=Sat, 21 Oct 2023 14:24:08 UTC +00:00, updated_at=Sat, 21 Oct 2023 14:24:08 UTC +00:00>
116
+ ```
117
+
118
+ When using a string value, the name of the struct field will be generated
119
+ using the [`parameterize`](https://www.rubydoc.info/gems/activesupport/String#parameterize-instance_method)
120
+ function with an underscore as the separator:
121
+
122
+ ```ruby
123
+ post = Post.joins(:comments).plucker('posts.title', 'COUNT(comments.id)').last
124
+ #<struct posts_title="How to make pizza", count_comments_id=2>
125
+ ```
126
+
127
+ When using the string `table_name.*`, it is interpreted as `SELECT
128
+ table_name.*` statement, selecting all columns from the specified table:
129
+
130
+ ```ruby
131
+ post = Post.joins(:author).plucker(:*, 'authors.*').last
132
+ #<struct id=1, title="How to make pizza", author_id=1, created_at=Sat, 21 Oct 2023 14:24:08 UTC +00:00, updated_at=Sat, 21 Oct 2023 14:24:08 UTC +00:00, authors_id: 1, authors_name: 'Henry', authors_created_at=Sat, 21 Oct 2023 14:24:08 UTC +00:00, authors_updated_at=Sat, 21 Oct 2023 14:24:08 UTC +00:00>
133
+ ```
134
+
135
+ When using a Hash, it operates similarly to the String case, except that the
136
+ name of the struct field will be the same as the key of the Hash:
137
+
138
+ ```ruby
139
+ post = Post.joins(:comments).plucker(:title, comments_count: 'COUNT(comments.id)').last
140
+ #<struct title="How to make pizza", comments_count=2>
141
+ ```
142
+
143
+ Plucker also takes an optional block, which is passed to the struct
144
+ definition:
145
+
146
+ ```ruby
147
+ posts = Post.plucker(:title) do
148
+ def slug
149
+ self.title.parameterize
150
+ end
151
+
152
+ def as_json
153
+ super.tap do |json|
154
+ json['slug'] = self.slug
155
+ end
156
+ end
157
+ end.last
158
+ #<struct title="How to make pizza">
159
+
160
+ post.title
161
+ # 'How to make pizza'
162
+ post.slug
163
+ # 'how-to-make-pizza'
164
+ post.as_json
165
+ # {"title"=>"How to make pizza", "slug"=>"how-to-make-pizza"}
166
+ ```
167
+
168
+ ## Contributing
169
+
170
+ Bug reports and pull requests are welcome on GitHub at https://github.com/pioz/plucker.
171
+
172
+ ## License
173
+
174
+ 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,12 @@
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
+ task default: :test
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plucker
4
+ VERSION = '0.1.0'
5
+ end
data/lib/plucker.rb ADDED
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'plucker/version'
4
+ require 'active_record'
5
+
6
+ # :nodoc:
7
+ module Plucker
8
+ # :nodoc:
9
+ def self.included(base)
10
+ base.extend(ClassMethods)
11
+ end
12
+
13
+ # :nodoc:
14
+ module ClassMethods
15
+ # Plucker allows projecting records extracted from a query into an array
16
+ # of specifically defined Ruby structs for the occasion. It is an
17
+ # enchanted `pluck`. It takes a list of values you want to extract and
18
+ # throws them into a custom array of Ruby struct.
19
+ def plucker(*args, &block)
20
+ scope = current_scope || self.all
21
+ scope_table_name = scope.table.name
22
+ columns = []
23
+ alias_names = []
24
+ args.each do |value|
25
+ case value
26
+ when Symbol
27
+ if value.in?(%i[all *])
28
+ scope_table_name.classify.constantize.columns.map do |column|
29
+ columns << column.name
30
+ alias_names << column.name.to_sym
31
+ end
32
+ else
33
+ columns << value.to_s
34
+ alias_names << value
35
+ end
36
+ when String
37
+ table_name, column_name = value.split('.')
38
+ if column_name == '*'
39
+ table_name.classify.constantize.columns.map do |column|
40
+ columns << column.name
41
+ alias_names << "#{table_name}_#{column.name}".parameterize(separator: '_').to_sym
42
+ end
43
+ else
44
+ columns << Arel.sql(value)
45
+ alias_names << value.parameterize(separator: '_').to_sym
46
+ end
47
+ when Hash
48
+ value.map do |k, v|
49
+ columns << Arel.sql(v.to_s)
50
+ alias_names << k.to_sym
51
+ end
52
+ else
53
+ raise "Invalid plucker argument: '#{value.inspect}'"
54
+ end
55
+ end
56
+
57
+ struct = Struct.new(*alias_names) do
58
+ class_eval(&block) if block
59
+ end
60
+ scope.pluck(*columns).map do |record|
61
+ struct.new(*record)
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ ActiveRecord::Base.include(Plucker)
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: plucker
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - pioz
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-11-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '5.0'
27
+ description: Plucker allows projecting a query into a specifically defined struct
28
+ for the query.
29
+ email:
30
+ - epilotto@gmx.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - ".rubocop.yml"
36
+ - LICENSE.txt
37
+ - README.md
38
+ - Rakefile
39
+ - lib/plucker.rb
40
+ - lib/plucker/version.rb
41
+ homepage: https://github.com/pioz/plucker
42
+ licenses:
43
+ - MIT
44
+ metadata:
45
+ homepage_uri: https://github.com/pioz/plucker
46
+ source_code_uri: https://github.com/pioz/plucker
47
+ rubygems_mfa_required: 'true'
48
+ post_install_message:
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: 2.6.0
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubygems_version: 3.4.22
64
+ signing_key:
65
+ specification_version: 4
66
+ summary: Pluck database records in structs.
67
+ test_files: []