activerecord-query 0.1.1

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: fdf9b2ff6e244407f97068227e4db1514e85e4a6912178845a12123ad4f952af
4
+ data.tar.gz: d0b9acdfb57e77f20e82fe34d36b7158c9ae3c667af80807c1659d78269b5669
5
+ SHA512:
6
+ metadata.gz: '06893476dc21302c35ca1db93973abaf91dc8e1bcc58a27d4ae83a66a68e9ac7f87d2054f0d9ec31a9d76ec4a638a5b01054877d3d911561916a477d1c010740'
7
+ data.tar.gz: 83bd2598980986b424f694b06cf49acf609764030dbb226f326fc34f389143f59d77497d1fde0b7c497881b9cd5c6bcf49bc36c2976c5727bdd7da762cd19b90
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## Changes in 0.1.0
2
+
3
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in active_record_query.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
data/README.md ADDED
@@ -0,0 +1,425 @@
1
+ # ActiveRecordQuery
2
+
3
+ ![example workflow](https://github.com/marcosfelipe/activerecord-query/actions/workflows/ruby.yml/badge.svg)
4
+
5
+
6
+ ActiveRecordQuery is a DSL buit on top of [ActiveRecord](https://github.com/rails/rails/tree/main/activerecord)
7
+ to help you write complex SQL queries
8
+ in the cleanest way possible. The lib provides a base class to
9
+ build a query pattern in your object oriented project.
10
+
11
+ Quick usage sample:
12
+
13
+ ```ruby
14
+ # verbose
15
+ query = Post.joins(:author).where(Post.arel_table[:created_at].gt(Date.new(2000, 1, 2))).order(title: :asc)
16
+
17
+ # clean ;)
18
+ class Query < ActiveRecordQuery::Base
19
+ from Post
20
+ join :author
21
+ where created_at > Date.new(2000, 1, 2)
22
+ order_by title.asc, author.name.asc
23
+ end
24
+ query = Query.execute
25
+ ```
26
+
27
+ The main goal is to turn your queries (or scopes) into classes.
28
+ These classes will be written naturally like a SQL query using a ruby DSL.
29
+ The common problem with the actual design of activerecord is that
30
+ the users tend to write the query features in chain, like this:
31
+
32
+ ```ruby
33
+ Post.select(:title)
34
+ .where(title: 'A title')
35
+ .where('created_at > ?', Date.today)
36
+ .order(:title)
37
+ ```
38
+
39
+ You can refactor with scopes, I guess..
40
+
41
+ ```ruby
42
+ class Post < ActiveRecord::Base
43
+ scope :titled, -> { where(title: 'A title') }
44
+ scope :created, -> { where('created_at < ?', Date.today) }
45
+ scope :titled_created, -> { titled.created }
46
+
47
+ def self.a_query
48
+ select(:title).titled_created.order(:title)
49
+ end
50
+ end
51
+ ```
52
+
53
+ Very messy...
54
+ When arel table features comes in, it becomes even worst.
55
+
56
+ ```ruby
57
+ is_fixed = Post[:fixed].eq(true)
58
+ is_coming = Post[:coming].eq(true).and(Post[:activated_at].not_eq(nil))
59
+ Post.where(is_fixed.or(is_coming))
60
+ ```
61
+
62
+ Now, let's try the ActiveRecordQuery:
63
+
64
+ ```ruby
65
+ class PostQuery < ActiveRecordQuery::Base
66
+ from Post
67
+ where fixed == true
68
+ wor do |other|
69
+ other.where coming == true
70
+ other.where actived_at != nil
71
+ end
72
+ end
73
+ ```
74
+
75
+ ## Installation
76
+
77
+ Add this line to your application's Gemfile:
78
+
79
+ ```ruby
80
+ gem 'activerecord-query'
81
+ ```
82
+
83
+ And then execute:
84
+
85
+ $ bundle install
86
+
87
+ Or install it yourself as:
88
+
89
+ $ gem install activerecord-query
90
+
91
+ ## Usage
92
+
93
+ ActiveRecordQuery adds the ActiveRecord::Base class to your project,
94
+ so we can easily create query classes by extending it.
95
+
96
+ ### Queries
97
+ The concept is given by the notion of query pattern classes.
98
+ The suggestion is that you put these classes in app/queries folder.
99
+
100
+ ### The query setup
101
+
102
+ Consider to have a Post model:
103
+
104
+ ```ruby
105
+ class Post < ActiveRecord::Base
106
+ end
107
+ ```
108
+
109
+ Now, you have to create a classe extending `ActiveRecordQuery::Base` class,
110
+ and then add the reference for the `Post` activerecord type
111
+ on `from` method.
112
+
113
+ ```ruby
114
+ class PostQuery < ActiveRecordQuery::Base
115
+ from Post
116
+ end
117
+ ```
118
+
119
+ Once you have defined the from resource, the columns from Post will be available
120
+ as methods in the class scope.
121
+ E.g. the method `title` will be defined as a `Arel::Attributes::Attribute` object.
122
+
123
+ ```ruby
124
+ class PostQuery < ActiveRecordQuery::Base
125
+ from Post
126
+ select title # the 'title' method was defined from 'from'
127
+ end
128
+ ```
129
+
130
+ The public method `execute` will return the `ActiveRecord_Relation` object.
131
+ ```ruby
132
+ PostQuery.execute # => ActiveRecord_Relation
133
+ ```
134
+
135
+ Or you can instantiate it also:
136
+ ```ruby
137
+ PostQuery.new.execute # => ActiveRecord_Relation
138
+ ```
139
+
140
+ ### Query Features
141
+
142
+ #### Conditions
143
+ The methods `where` and `wor` are available to set the query conditions.
144
+ The argument must be a `Arel::Node` object.
145
+ To a cleaner experience, you can use the generated methods for columns
146
+ generated from the `from` method.
147
+
148
+ ```ruby
149
+ class PostQuery < ActiveRecordQuery::Base
150
+ from Post
151
+
152
+ # and operation
153
+ where title == 'something' # using title helper
154
+ where Post.arel_table[:title].eq('something') # using arel
155
+
156
+ # or operation
157
+ wor title == 'something else'
158
+ end
159
+ ```
160
+
161
+ The Arel predications are available:
162
+
163
+ ```ruby
164
+ # between
165
+ where column.between 1..10
166
+
167
+ # matches
168
+ where column.matches '%something%'
169
+
170
+ # not in all
171
+ where column.not_in_all %w[something]
172
+ ```
173
+
174
+ You can nest the conditions:
175
+ ```ruby
176
+ where column == 'c1'
177
+ wor do |nested|
178
+ nested.where column == 'c2'
179
+ nested.where other == 'c3'
180
+ nested.wor do |deep_nested|
181
+ deep_nested.where other == 'c1'
182
+ deep_nested.where column == 'c4'
183
+ end
184
+ end
185
+ ```
186
+ It Generates:
187
+ ```sql
188
+ column = "c1" or (column = "c2" and other = "c3" or (other = 'c1' and column = 'c4'))
189
+ ```
190
+
191
+ The dynamic values for conditions can be add as a symbol to reference
192
+ a method or can be a proc. The values will be evaluate when the `execute` is called.
193
+
194
+ ```ruby
195
+ class PostQuery < ActiveRecordQuery::Base
196
+ from Post
197
+ where title == :a_dynamic_method # references a method
198
+ where title == proc { 'a title' } # the value will be evaluate on execute
199
+
200
+ def a_dynamic_method
201
+ 'a title'
202
+ end
203
+ end
204
+ ```
205
+
206
+ #### Conditional where
207
+ The `where/wor` state can be conditioned by passing the option `if:`.
208
+ The value must be a symbol referencing a method.
209
+ ```ruby
210
+ class PostQuery < ActiveRecordQuery::Base
211
+ from Post
212
+ where title == 'test', if: :a_method?
213
+
214
+ def a_method?
215
+ false
216
+ end
217
+ end
218
+ ```
219
+
220
+ #### Selects
221
+ Selects can be done by passing a list of columns to the `select` method.
222
+ The args must be a list of `Arel::Attributes::Attribute`.
223
+ If no select is defined in the query, then the `*` selection will be taken.
224
+ Every call of `select` the attrs will be added to the selection.
225
+
226
+ ```ruby
227
+ class PostQuery < ActiveRecordQuery::Base
228
+ from Post
229
+
230
+ # simple select
231
+ select title, created_at
232
+
233
+ # can do a math op (it's just a arel attr)
234
+ select id + id
235
+
236
+ # plain arel attr
237
+ select Post.arel_table[:id]
238
+ end
239
+ ```
240
+
241
+ #### Order by
242
+ Much like the select method, you shall pass a list of attributes to the method `order_by`.
243
+ Every call of `order_by` the attrs will be added to the selection.
244
+
245
+ ```ruby
246
+ class PostQuery < ActiveRecordQuery::Base
247
+ from Post
248
+
249
+ # list of arel attrs
250
+ order_by title.asc, created_at.desc
251
+ end
252
+ ```
253
+
254
+ #### Limits
255
+ The `limit` method is available to define a query limit.
256
+ An integer value is the only arg acceptable.
257
+ Every time the limit method is called, the limit will be redefined.
258
+ ```ruby
259
+ class PostQuery < ActiveRecordQuery::Base
260
+ from Post
261
+ limit 10
262
+ end
263
+ ```
264
+
265
+ #### Offsets
266
+ The `offset` method is available to define a query offset.
267
+ An integer value is the only arg acceptable.
268
+ Every time the offset method is called, the offset will be redefined.
269
+ ```ruby
270
+ class PostQuery < ActiveRecordQuery::Base
271
+ from Post
272
+ offset 10
273
+ end
274
+ ```
275
+
276
+ #### Joins
277
+ The `join` method defines one/many relationships with the current resource (`from` state).
278
+ The following example has a Post and Author models, the way we define a join is the same as
279
+ defining a `joins` on activerecord (check the active record querying doc.).
280
+ Right after defined the join a new method will be available for retrieve the columns
281
+ from the new resource, the `author` method on this example. Every relationship listed in
282
+ the args will be converted to a method with the same name.
283
+ ```ruby
284
+ # models
285
+ class Post < ActiveRecord::Base
286
+ belongs_to :author
287
+ end
288
+
289
+ class Author < ActiveRecord::Base
290
+ has_many :posts
291
+ end
292
+
293
+ # query
294
+ class PostQuery < ActiveRecordQuery::Base
295
+ from Post
296
+ join :author
297
+
298
+ # the author helper will be available
299
+ where author.name == 'John'
300
+ end
301
+ ```
302
+
303
+
304
+ #### Group by
305
+ To apply a GROUP BY clause to the query, you can use the `group_by` method.
306
+ The method accepts a list of columns.
307
+ ```ruby
308
+ class PostQuery < ActiveRecordQuery::Base
309
+ from Post
310
+ group_by title
311
+ end
312
+ ```
313
+
314
+ #### Having
315
+ You can add the HAVING clause to the query by defining a `having` method.
316
+ A column condition can be done by calling the Arel predication methods like `gt`.
317
+ ```ruby
318
+ class PostQuery < ActiveRecordQuery::Base
319
+ from Post
320
+ group_by title
321
+ having id.gt 5
322
+ end
323
+ ```
324
+
325
+
326
+ ### Scopes
327
+ There are at least to ways to scope your query class. The first one is the use of
328
+ class inheritance. The second one is extract features to modules.
329
+
330
+ #### Class inheritance
331
+ You can merge queries by extending the class.
332
+ Let's say that you have a base query definition `AScopeQuery`.
333
+
334
+ ```ruby
335
+ class AScopeQuery < ApplicationQuery
336
+ from Post
337
+ where title != nil
338
+ end
339
+ ```
340
+ And then you extend this query:
341
+ ```ruby
342
+ class AQuery < AScopeQuery
343
+ where id > 5
344
+ order_by title
345
+ end
346
+ ```
347
+ The result will be the merge of the two queries:
348
+ ```sql
349
+ SELECT * FROM posts WHERE title NOT NULL AND id > 5 ORDER BY title
350
+ ```
351
+
352
+ #### Modules
353
+ Another way to scope a query, would be including modules into yours
354
+ query class. Let's define a module with activesupport concern.
355
+
356
+ ```ruby
357
+ module AScope
358
+ extend ActiveSupport::Concern
359
+
360
+ included do
361
+ where title == 'a scope'
362
+ end
363
+ end
364
+ ```
365
+ Note that we called the `where` macro inside the included method just like the
366
+ "activemodel concerns style". And then we can simply include the scope module into
367
+ the query class:
368
+ ```ruby
369
+ class AQuery < ApplicationQuery
370
+ from Post
371
+ include AScope
372
+ end
373
+ ```
374
+ It is important to notice that the module must be included after the `from` definition
375
+ due to the scope dependency on the `from` builds.
376
+
377
+ ### Query Parameters
378
+ The query class can be instantiate/execute with user parameters.
379
+ The `options` method will be available on the instance context of the class.
380
+ This data can be part of the query dynamic solutions in their features.
381
+ ```ruby
382
+ class PostQuery < ApplicationQuery
383
+ from Post
384
+ where title == :title_value
385
+
386
+ def title_value
387
+ options[:title]
388
+ end
389
+ end
390
+
391
+ # execute with option :title
392
+ PostQuery.execute(title: 'A Title') # => select * from posts where title = "A Title"
393
+ ```
394
+ On this example, the value for title condition is dynamic set by
395
+ the `options` parameter. A proc can be used also:
396
+ ```ruby
397
+ where title == proc { options[:title] }
398
+ ```
399
+
400
+ ## Development
401
+
402
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
403
+
404
+ 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 the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
405
+
406
+ ## Contributing
407
+
408
+ Bug reports and pull requests are welcome on GitHub at
409
+ https://github.com/marcosfelipe/activerecord-query.
410
+ This project is intended to be a safe,
411
+ welcoming space for collaboration, and contributors are
412
+ expected to adhere to the [code of conduct](https://github.com/rubygems/rubygems/blob/master/CODE_OF_CONDUCT.md).
413
+
414
+ ## License
415
+
416
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
417
+
418
+ ## Code of Conduct
419
+
420
+ Everyone interacting in the ActiveRecordQuery project's codebases, issue trackers, chat rooms and mailing lists is
421
+ expected to follow the [code of conduct](https://github.com/rubygems/rubygems/blob/master/CODE_OF_CONDUCT.md).
422
+
423
+ ## Author
424
+
425
+ Marcos Felipe (marcosfelipesilva54@gmail.com)
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/active_record_query/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "activerecord-query"
7
+ spec.version = ActiveRecordQuery::VERSION
8
+ spec.authors = ["Marcos Felipe"]
9
+ spec.email = ["marcosfelipesilva54@gmail.com"]
10
+ spec.summary = "Small DSL for query build with Active Record."
11
+ spec.description = "The DSL provides a nice and clean way to write ActiveRecord queries better than model scopes."
12
+ spec.homepage = "https://github.com/marcosfelipe/activerecord-query"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = ">= 2.6.0"
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = spec.homepage
18
+ spec.metadata["changelog_uri"] = spec.homepage + '/CHANGELOG.md'
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ spec.files = Dir['README.md', 'LICENSE',
22
+ 'CHANGELOG.md', 'lib/**/*.rb',
23
+ 'lib/**/*.rake',
24
+ '*.gemspec', '.github/*.md',
25
+ 'Gemfile', 'Rakefile']
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_dependency "activerecord", ">= 4.0", "< 8.0"
31
+ spec.add_dependency "activesupport", ">= 4.0", "< 8.0"
32
+
33
+ spec.add_development_dependency "bundler", '>= 1.3', '< 3.0'
34
+ spec.add_development_dependency "rake", '>= 1.0', '< 14.0'
35
+ spec.add_development_dependency "rspec", "~> 3.0"
36
+ spec.add_development_dependency "pry", '~> 0.14.0'
37
+ spec.add_development_dependency "sqlite3", '~> 1.0'
38
+ spec.add_development_dependency "simplecov", '~> 0.21.0'
39
+
40
+ # For more information and examples about making a new gem, check out our
41
+ # guide at: https://bundler.io/guides/creating_gem.html
42
+ end
@@ -0,0 +1,15 @@
1
+ module Arel # :nodoc: all
2
+ module Nodes
3
+ # The setter for value is necessary to evaluate
4
+ # when a symbol or proc is passed to it
5
+ class Casted
6
+ attr_writer :value
7
+ end
8
+
9
+ # The setter for expr is necessary to evaluate
10
+ # when a symbol or proc is passed to it
11
+ class Unary
12
+ alias :value= :expr=
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,42 @@
1
+ module ActiveRecordQuery
2
+ # The stacker class receives a class as context
3
+ # to define instance methods ordered to collect
4
+ # a list of data in the class instance
5
+ class ArgumentStacker
6
+ def initialize(context, stack_name)
7
+ @context = context
8
+ @stack_name = stack_name
9
+ end
10
+
11
+ def add(args)
12
+ context.define_method(next_method_name) do
13
+ args
14
+ end
15
+ end
16
+
17
+ def list
18
+ stacked_methods.map { |method| context.send(method) }.flatten
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :context, :stack_name
24
+
25
+ def next_method_name
26
+ current_number = last_stacked_method.to_s.scan(/\d+$/).first.to_i
27
+ "_#{stack_name}_#{current_number + 1}"
28
+ end
29
+
30
+ def last_stacked_method
31
+ stacked_methods.last
32
+ end
33
+
34
+ def stacked_methods
35
+ context_methods.grep(/^_#{stack_name}_/).sort
36
+ end
37
+
38
+ def context_methods
39
+ context.send(context.respond_to?(:instance_methods) ? :instance_methods : :methods)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,15 @@
1
+ module ActiveRecordQuery
2
+ # Extends the Arel attribute class.
3
+ # It sets some aliases for syntactic sugar.
4
+ class Column < Arel::Attributes::Attribute
5
+ alias_method :==, :eq
6
+ alias_method :!=, :not_eq
7
+ alias_method :=~, :matches
8
+ alias_method :>=, :gteq
9
+ alias_method :>, :gt
10
+ alias_method :<, :lt
11
+ alias_method :<=, :lteq
12
+ end
13
+ end
14
+
15
+
@@ -0,0 +1,16 @@
1
+ module ActiveRecordQuery
2
+ module Conditionable
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ include Conditions::Conditionable
7
+
8
+ add_feature :build_conditions
9
+ end
10
+
11
+ def build_conditions(scope)
12
+ conditions = Conditions::Builder.new(resource, self).build(self)
13
+ conditions ? scope.where(conditions) : scope
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module ActiveRecordQuery
2
+ module Featureable
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ def _query_features
7
+ @@_query_features
8
+ end
9
+
10
+ def add_feature(feature)
11
+ @@_query_features ||= []
12
+ @@_query_features << feature
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,22 @@
1
+ module ActiveRecordQuery
2
+ module Groupable
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ def group_by(*args)
7
+ arg_stacker = ArgumentStacker.new(self, :group_by)
8
+ arg_stacker.add(args)
9
+ end
10
+ end
11
+
12
+ included do
13
+ add_feature :build_group_by
14
+ end
15
+
16
+ def build_group_by(scope)
17
+ arg_stacker = ArgumentStacker.new(self, :group_by)
18
+ args = arg_stacker.list
19
+ args.present? ? scope.group(*args.flatten) : scope
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ module ActiveRecordQuery
2
+ module Havingable
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ attr_reader :_having
7
+
8
+ def having(*args)
9
+ arg_stacker = ArgumentStacker.new(self, :having)
10
+ arg_stacker.add(args)
11
+ end
12
+ end
13
+
14
+ included do
15
+ add_feature :build_having
16
+ end
17
+
18
+ def build_having(scope)
19
+ arg_stacker = ArgumentStacker.new(self, :having)
20
+ args = ExpressionParser.new(self).parse(arg_stacker.list)
21
+ args.present? ? scope.having(*args) : scope
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,20 @@
1
+ module ActiveRecordQuery
2
+ module Identifiable
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ def from(model_class)
7
+ raise(ArgumentError, 'Resource must be a ActiveRecord::Base object.') unless model_class.new.is_a?(ActiveRecord::Base)
8
+ define_method(:resource) do
9
+ model_class
10
+ end
11
+
12
+ model_class.column_names.each do |column|
13
+ define_singleton_method(column) do
14
+ Column.new(model_class.arel_table, column)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,43 @@
1
+ module ActiveRecordQuery
2
+ module Joinable
3
+ extend ActiveSupport::Concern
4
+
5
+ class JoinedResource
6
+ def initialize(resource)
7
+ @resource = resource
8
+ end
9
+
10
+ def method_missing(m, *args, &block)
11
+ Column.new(Arel::Table.new(resource), m)
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :resource
17
+ end
18
+
19
+ class_methods do
20
+ def join(*args)
21
+ arg_stacker = ArgumentStacker.new(self, :join)
22
+ arg_stacker.add(args)
23
+ args.to_s.split(/\W+/).compact.each do |resource_name|
24
+ define_singleton_method(resource_name) do
25
+ JoinedResource.new(resource_name.pluralize)
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ included do
32
+ add_feature :build_joins
33
+ end
34
+
35
+ def build_joins(scope)
36
+ arg_stacker = ArgumentStacker.new(self, :join)
37
+ arg_stacker.list.each do |join_params|
38
+ scope = scope.joins(join_params)
39
+ end
40
+ scope
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,21 @@
1
+ module ActiveRecordQuery
2
+ module Limitable
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ def limit(value)
7
+ define_method('limit') do
8
+ value
9
+ end
10
+ end
11
+ end
12
+
13
+ included do
14
+ add_feature :build_limit
15
+ end
16
+
17
+ def build_limit(scope)
18
+ respond_to?(:limit) ? scope.limit(limit) : scope
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ module ActiveRecordQuery
2
+ module Offsetable
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ def offset(value)
7
+ define_method('offset') do
8
+ value
9
+ end
10
+ end
11
+ end
12
+
13
+ included do
14
+ add_feature :build_offset
15
+ end
16
+
17
+ def build_offset(scope)
18
+ respond_to?(:offset) ? scope.offset(offset) : scope
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ module ActiveRecordQuery
2
+ module Orderable
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ def order_by(*args)
7
+ arg_stacker = ArgumentStacker.new(self, :order_by)
8
+ arg_stacker.add(args)
9
+ end
10
+ end
11
+
12
+ included do
13
+ add_feature :build_order
14
+ end
15
+
16
+ def build_order(scope)
17
+ arg_stacker = ArgumentStacker.new(self, :order_by)
18
+ args = arg_stacker.list
19
+ args.present? ? scope.order(args) : scope
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ module ActiveRecordQuery
2
+ module Selectable
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ def select(*args)
7
+ arg_stacker = ArgumentStacker.new(self, :select)
8
+ arg_stacker.add(args)
9
+ end
10
+ end
11
+
12
+ included do
13
+ add_feature :build_select
14
+ end
15
+
16
+ def build_select(scope)
17
+ arg_stacker = ArgumentStacker.new(self, :select)
18
+ args = arg_stacker.list
19
+ args.present? ? scope.select(*args) : scope
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,36 @@
1
+ module ActiveRecordQuery
2
+ module Conditions
3
+ class Builder
4
+ def initialize(resource, context)
5
+ @resource = resource
6
+ @context = context
7
+ end
8
+
9
+ def build(group)
10
+ chain_conditions = nil
11
+ arg_stacker = ArgumentStacker.new(group, :condition)
12
+ arg_stacker.list.each do |chain_link|
13
+ next unless executable?(chain_link)
14
+ if chain_link.respond_to?(:where)
15
+ group = chain_link.new
16
+ chain_conditions = chain_conditions.present? ? chain_conditions.send(chain_link::glue, build(group)) : build(group)
17
+ else
18
+ arel_condition = ExpressionParser.new(context).parse(chain_link.condition)
19
+ chain_conditions = chain_conditions.present? ? chain_conditions.send(chain_link.type, arel_condition) : arel_condition
20
+ end
21
+ end
22
+ resource.arel_table.grouping(chain_conditions) if chain_conditions
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :resource, :context
28
+
29
+ def executable?(chain_link)
30
+ condition_options = chain_link.options.to_h
31
+ return true unless condition_options[:if].present?
32
+ context.send(condition_options[:if])
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveRecordQuery
2
+ # Conditions (where or wor) work linking each other,
3
+ # this class identify what are the rules for a link
4
+ ChainLink = Struct.new(:type, :condition, :options)
5
+ end
@@ -0,0 +1,10 @@
1
+ require_relative 'conditionable'
2
+
3
+ module ActiveRecordQuery
4
+ # ConditionGroup stacks ChainLink, WhereGroup and WorGroup when
5
+ # 'where' and 'wor' methods are called
6
+ class ConditionGroup
7
+ include Conditions::Conditionable
8
+ cattr_reader :options
9
+ end
10
+ end
@@ -0,0 +1,29 @@
1
+ module ActiveRecordQuery
2
+ module Conditions
3
+ module Conditionable
4
+ extend ActiveSupport::Concern
5
+
6
+ class_methods do
7
+ def where(*args, &block)
8
+ chain_link_definer(Class.new(WhereGroup), args, &block)
9
+ end
10
+
11
+ def wor(*args, &block)
12
+ chain_link_definer(Class.new(WorGroup), args, &block)
13
+ end
14
+
15
+ def chain_link_definer(group_type, args)
16
+ arg_stacker = ArgumentStacker.new(self, :condition)
17
+ if block_given?
18
+ yield group_type
19
+ arg_stacker.add(group_type)
20
+ else
21
+ condition = args[0]
22
+ options = args[1]
23
+ arg_stacker.add(ChainLink.new(group_type.glue, condition, options))
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,9 @@
1
+ module ActiveRecordQuery
2
+ # Identifies a type of ConditionGroup with glue set to 'and' operator
3
+ # This class is used whenever a 'where' condition is called with a block
4
+ class WhereGroup < ConditionGroup
5
+ def self.glue
6
+ :and
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module ActiveRecordQuery
2
+ # Identifies a type of ConditionGroup with glue set to 'or' operator
3
+ # This class is used whenever a 'wor' condition is called with a block
4
+ class WorGroup < ConditionGroup
5
+ def self.glue
6
+ :or
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,40 @@
1
+ module ActiveRecordQuery
2
+ # It evaluates an expression to execute on the context
3
+ # whenever a symbol or a proc is passed as value on the expression.
4
+ class ExpressionParser
5
+ def initialize(context)
6
+ @context = context
7
+ end
8
+
9
+ def parse(expression)
10
+ if expression.respond_to?(:each)
11
+ expression = expression.map(&:clone)
12
+ expression.each { |arg| parse_arg(arg) }
13
+ else
14
+ expression = expression.clone
15
+ parse_arg(expression)
16
+ end
17
+ expression
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :context
23
+
24
+ def parse_arg(arg)
25
+ if arg.respond_to?(:right) && arg.right.respond_to?(:value)
26
+ arg.right.value = parse_dynamic_values(arg.right.value)
27
+ end
28
+ end
29
+
30
+ def parse_dynamic_values(value)
31
+ if value.is_a?(Symbol)
32
+ context.send(value)
33
+ elsif value.is_a?(Proc)
34
+ context.instance_exec(&value)
35
+ else
36
+ value
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordQuery
4
+ VERSION = "0.1.1"
5
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'arel'
5
+ Dir.glob(File.join(__dir__, 'active_record_query/**/*.rb')) do |f|
6
+ require f
7
+ end
8
+
9
+ module ActiveRecordQuery
10
+ # The base class collects all the activerecord query features
11
+ # and the execute method processes all the features.
12
+ # The api user must inherit from this class in order to build
13
+ # a new query definition.
14
+ class Base
15
+ include Identifiable
16
+ include Featureable
17
+ include Selectable
18
+ include Orderable
19
+ include Limitable
20
+ include Joinable
21
+ include Groupable
22
+ include Offsetable
23
+ include Havingable
24
+ include Conditionable
25
+
26
+ class << self
27
+ def execute(options = {})
28
+ new(options).execute
29
+ end
30
+ end
31
+
32
+ def initialize(options = {})
33
+ @options = options
34
+ end
35
+
36
+ def execute
37
+ query = resource.all
38
+ self.class._query_features.each do |query_feature|
39
+ query = send(query_feature, query)
40
+ end
41
+ query
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :options
47
+ end
48
+ end
metadata ADDED
@@ -0,0 +1,210 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerecord-query
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Marcos Felipe
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-11-05 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: '4.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '8.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '4.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '8.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: activesupport
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '4.0'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '8.0'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '4.0'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '8.0'
53
+ - !ruby/object:Gem::Dependency
54
+ name: bundler
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '1.3'
60
+ - - "<"
61
+ - !ruby/object:Gem::Version
62
+ version: '3.0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '1.3'
70
+ - - "<"
71
+ - !ruby/object:Gem::Version
72
+ version: '3.0'
73
+ - !ruby/object:Gem::Dependency
74
+ name: rake
75
+ requirement: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '1.0'
80
+ - - "<"
81
+ - !ruby/object:Gem::Version
82
+ version: '14.0'
83
+ type: :development
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '1.0'
90
+ - - "<"
91
+ - !ruby/object:Gem::Version
92
+ version: '14.0'
93
+ - !ruby/object:Gem::Dependency
94
+ name: rspec
95
+ requirement: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - "~>"
98
+ - !ruby/object:Gem::Version
99
+ version: '3.0'
100
+ type: :development
101
+ prerelease: false
102
+ version_requirements: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - "~>"
105
+ - !ruby/object:Gem::Version
106
+ version: '3.0'
107
+ - !ruby/object:Gem::Dependency
108
+ name: pry
109
+ requirement: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - "~>"
112
+ - !ruby/object:Gem::Version
113
+ version: 0.14.0
114
+ type: :development
115
+ prerelease: false
116
+ version_requirements: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - "~>"
119
+ - !ruby/object:Gem::Version
120
+ version: 0.14.0
121
+ - !ruby/object:Gem::Dependency
122
+ name: sqlite3
123
+ requirement: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - "~>"
126
+ - !ruby/object:Gem::Version
127
+ version: '1.0'
128
+ type: :development
129
+ prerelease: false
130
+ version_requirements: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - "~>"
133
+ - !ruby/object:Gem::Version
134
+ version: '1.0'
135
+ - !ruby/object:Gem::Dependency
136
+ name: simplecov
137
+ requirement: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - "~>"
140
+ - !ruby/object:Gem::Version
141
+ version: 0.21.0
142
+ type: :development
143
+ prerelease: false
144
+ version_requirements: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - "~>"
147
+ - !ruby/object:Gem::Version
148
+ version: 0.21.0
149
+ description: The DSL provides a nice and clean way to write ActiveRecord queries better
150
+ than model scopes.
151
+ email:
152
+ - marcosfelipesilva54@gmail.com
153
+ executables: []
154
+ extensions: []
155
+ extra_rdoc_files: []
156
+ files:
157
+ - CHANGELOG.md
158
+ - Gemfile
159
+ - README.md
160
+ - Rakefile
161
+ - activerecord-query.gemspec
162
+ - lib/active_record_query/arel/nodes.rb
163
+ - lib/active_record_query/argument_stacker.rb
164
+ - lib/active_record_query/column.rb
165
+ - lib/active_record_query/concerns/conditionable.rb
166
+ - lib/active_record_query/concerns/featureable.rb
167
+ - lib/active_record_query/concerns/groupable.rb
168
+ - lib/active_record_query/concerns/havingable.rb
169
+ - lib/active_record_query/concerns/identifiable.rb
170
+ - lib/active_record_query/concerns/joinable.rb
171
+ - lib/active_record_query/concerns/limitable.rb
172
+ - lib/active_record_query/concerns/offsetable.rb
173
+ - lib/active_record_query/concerns/orderable.rb
174
+ - lib/active_record_query/concerns/selectable.rb
175
+ - lib/active_record_query/conditions/builder.rb
176
+ - lib/active_record_query/conditions/chain_link.rb
177
+ - lib/active_record_query/conditions/condition_group.rb
178
+ - lib/active_record_query/conditions/conditionable.rb
179
+ - lib/active_record_query/conditions/where_group.rb
180
+ - lib/active_record_query/conditions/wor_group.rb
181
+ - lib/active_record_query/expression_parser.rb
182
+ - lib/active_record_query/version.rb
183
+ - lib/activerecord-query.rb
184
+ homepage: https://github.com/marcosfelipe/activerecord-query
185
+ licenses:
186
+ - MIT
187
+ metadata:
188
+ homepage_uri: https://github.com/marcosfelipe/activerecord-query
189
+ source_code_uri: https://github.com/marcosfelipe/activerecord-query
190
+ changelog_uri: https://github.com/marcosfelipe/activerecord-query/CHANGELOG.md
191
+ post_install_message:
192
+ rdoc_options: []
193
+ require_paths:
194
+ - lib
195
+ required_ruby_version: !ruby/object:Gem::Requirement
196
+ requirements:
197
+ - - ">="
198
+ - !ruby/object:Gem::Version
199
+ version: 2.6.0
200
+ required_rubygems_version: !ruby/object:Gem::Requirement
201
+ requirements:
202
+ - - ">="
203
+ - !ruby/object:Gem::Version
204
+ version: '0'
205
+ requirements: []
206
+ rubygems_version: 3.3.3
207
+ signing_key:
208
+ specification_version: 4
209
+ summary: Small DSL for query build with Active Record.
210
+ test_files: []