activerecord-query 0.1.1

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: 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: []