inquery 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.releaser_config +4 -0
  4. data/.rubocop.yml +42 -0
  5. data/.travis.yml +9 -0
  6. data/.yardopts +1 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE +21 -0
  9. data/README.md +288 -0
  10. data/RUBY_VERSION +1 -0
  11. data/Rakefile +36 -0
  12. data/VERSION +1 -0
  13. data/doc/Inquery.html +119 -0
  14. data/doc/Inquery/Exceptions.html +115 -0
  15. data/doc/Inquery/Exceptions/Base.html +127 -0
  16. data/doc/Inquery/Exceptions/InvalidRelation.html +131 -0
  17. data/doc/Inquery/Exceptions/UnknownCallSignature.html +131 -0
  18. data/doc/Inquery/Mixins.html +117 -0
  19. data/doc/Inquery/Mixins/RelationValidation.html +334 -0
  20. data/doc/Inquery/Mixins/RelationValidation/ClassMethods.html +190 -0
  21. data/doc/Inquery/Mixins/SchemaValidation.html +124 -0
  22. data/doc/Inquery/Mixins/SchemaValidation/ClassMethods.html +192 -0
  23. data/doc/Inquery/Query.html +736 -0
  24. data/doc/Inquery/Query/Chainable.html +476 -0
  25. data/doc/_index.html +254 -0
  26. data/doc/class_list.html +58 -0
  27. data/doc/css/common.css +1 -0
  28. data/doc/css/full_list.css +57 -0
  29. data/doc/css/style.css +339 -0
  30. data/doc/file.README.html +365 -0
  31. data/doc/file_list.html +60 -0
  32. data/doc/frames.html +26 -0
  33. data/doc/index.html +365 -0
  34. data/doc/js/app.js +219 -0
  35. data/doc/js/full_list.js +181 -0
  36. data/doc/js/jquery.js +4 -0
  37. data/doc/method_list.html +147 -0
  38. data/doc/top-level-namespace.html +112 -0
  39. data/inquery.gemspec +58 -0
  40. data/lib/inquery.rb +10 -0
  41. data/lib/inquery/exceptions.rb +7 -0
  42. data/lib/inquery/mixins/relation_validation.rb +100 -0
  43. data/lib/inquery/mixins/schema_validation.rb +27 -0
  44. data/lib/inquery/query.rb +50 -0
  45. data/lib/inquery/query/chainable.rb +53 -0
  46. data/test/db/models.rb +20 -0
  47. data/test/db/schema.rb +20 -0
  48. data/test/inquery/query/chainable_test.rb +67 -0
  49. data/test/inquery/query_test.rb +47 -0
  50. data/test/queries/group/fetch_as_json.rb +13 -0
  51. data/test/queries/group/fetch_green.rb +11 -0
  52. data/test/queries/group/fetch_red.rb +11 -0
  53. data/test/queries/group/filter_with_color.rb +12 -0
  54. data/test/queries/user/fetch_all.rb +9 -0
  55. data/test/queries/user/fetch_in_group.rb +13 -0
  56. data/test/queries/user/fetch_in_group_rel.rb +17 -0
  57. data/test/test_helper.rb +26 -0
  58. metadata +265 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9ab3cd729d1ad08041f09d90c2d3f95925dd4ba5
4
+ data.tar.gz: 00f6185d8d34785c27ba9c936b618188f0caa57b
5
+ SHA512:
6
+ metadata.gz: 57bac62a978855077de4f05449c4eebc1128cc72ecad1781104c614f6876dea8bc46aab2a2e95f7d57169a7506fb726510edfa6591bf54dd8306afb420a25939
7
+ data.tar.gz: e05f7bc9e5f42c3fdf53f1170cbc0a9c5004ec760d5a0982e645f79ae1cb9b606a9d3095d51ee8a9483b3dcf9e51a488e1f1aa6a207ef0ab9e5902960a43efd5
@@ -0,0 +1,16 @@
1
+ /bin/ruby
2
+ /.bundle
3
+ .DS_Store
4
+ *.tmproj
5
+ tmtags
6
+ /Gemfile.lock
7
+ *~
8
+ \#*
9
+ .\#*
10
+ *.swp
11
+ /vendor/gems/*
12
+ /vendor/bundle/*
13
+ /spec/reports
14
+ /.yardoc
15
+ .idea
16
+ /*.gem
@@ -0,0 +1,4 @@
1
+ version_file: VERSION
2
+ always_from_master: true
3
+ yard_path: doc
4
+ gem_style: github
@@ -0,0 +1,42 @@
1
+ AllCops:
2
+ Exclude:
3
+ - 'local/**/*'
4
+ - 'vendor/**/*'
5
+ - 'tmp/**/*'
6
+ - '*.gemspec'
7
+
8
+ DisplayCopNames: true
9
+
10
+ Metrics/MethodLength:
11
+ Enabled: false
12
+
13
+ Metrics/AbcSize:
14
+ Enabled: False
15
+
16
+ Metrics/CyclomaticComplexity:
17
+ Enabled: False
18
+
19
+ Metrics/PerceivedComplexity:
20
+ Enabled: False
21
+
22
+ Metrics/LineLength:
23
+ Max: 160
24
+
25
+ Style/IfUnlessModifier:
26
+ Enabled: false
27
+
28
+ Style/Documentation:
29
+ Enabled: false
30
+
31
+ Style/RedundantReturn:
32
+ Enabled: false
33
+
34
+ Style/GuardClause:
35
+ Enabled: false
36
+
37
+ Style/ClassAndModuleChildren:
38
+ Enabled: false
39
+ EnforcedStyle: compact
40
+ SupportedStyles:
41
+ - nested
42
+ - compact
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0
4
+ - 2.2
5
+ - 2.3.0
6
+ script:
7
+ - bundle install
8
+ - bundle exec rake test
9
+ - bundle exec rubocop
@@ -0,0 +1 @@
1
+ --markup=markdown --readme=README.md --no-private lib/**/*.rb
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify gem dependencies in the .gemspec file
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Sitrox
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 all
13
+ 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 THE
21
+ SOFTWARE.
@@ -0,0 +1,288 @@
1
+ # Inquery
2
+
3
+ A skeleton that allows extracting queries into atomic, reusable classes.
4
+
5
+ ## Installation
6
+
7
+ To install the **Inquery** gem:
8
+
9
+ ```sh
10
+ $ gem install inquery
11
+ ```
12
+
13
+ To install it using `bundler` (recommended for any application), add it
14
+ to your `Gemfile`:
15
+
16
+ ```ruby
17
+ gem 'inquery'
18
+ ```
19
+
20
+ ## Basic usage
21
+
22
+ ```ruby
23
+ class FetchUsersWithACar < Inquery::Query
24
+ schema(
25
+ color: :symbol
26
+ )
27
+
28
+ def call
29
+ User.joins(:cars).where(cars: { color: osparams.color })
30
+ end
31
+ end
32
+
33
+ FetchUsersWithACar.run
34
+ # => [<User id: 1 ...]
35
+ ```
36
+
37
+ Inquery offers its functionality trough two query base classes: {Inquery::Query}
38
+ and {Inquery::Query::Chainable}. See the following sections for detailed
39
+ explanations.
40
+
41
+ ## Basic queries
42
+
43
+ Basic queries inherit from {Inquery::Query}. They receive an optional set of
44
+ parameters and commonly return a relation / AR result. An optional `process`
45
+ method lets you perform additional result processing steps if needed (i.e.
46
+ converting the result to a hash or similar).
47
+
48
+ For this basic functionality, inherit from {Inquery::Query} and overwrite
49
+ the `call` and optionally the `process` method:
50
+
51
+ ```ruby
52
+ class FetchRedCarsAsJson
53
+ # The `call` method must be overwritten for every query. It is usually called
54
+ # via `run`.
55
+ def call
56
+ Car.where(color: 'red')
57
+ end
58
+
59
+ # The `process` method can optionally be overwritten. The base implementation
60
+ # just returns the unprocessed `results` argument.
61
+ def process(results)
62
+ results.to_json
63
+ end
64
+ end
65
+ ```
66
+
67
+ Queries can be called in various ways:
68
+
69
+ ```ruby
70
+ # Instantiates the query class and runs `call` and `process`.
71
+ FetchRedCarsAsJson.run(params = {})
72
+
73
+ # Instantiates the query class and runs `call`. No result processing
74
+ # is done.
75
+ FetchRedCarsAsJson.call(params = {})
76
+
77
+ # You can also instantiate the query class manually.
78
+ FetchRedCarsAsJson.new(params = {}).run
79
+
80
+ # Or just run the `call` method without `process`.
81
+ FetchRedCarsAsJson.new(params = {}).call
82
+ ```
83
+
84
+ Note that it's perfectly fine for some queries to return `nil`, i.e. if they're
85
+ writing queries that don't fetch any results.
86
+
87
+ ## Chainable queries
88
+
89
+ Chainable queries are queries that input and output an Active Record relation.
90
+ You can access the given relation using the method `relation`:
91
+
92
+ ```ruby
93
+ class Queries::User::FetchActive < Inquery::Query::Chainable
94
+ def call
95
+ relation.where(active: 1)
96
+ end
97
+ end
98
+ ```
99
+
100
+ Input and output relations may or may not be of the same AR class (i.e. you
101
+ could pass a relation of `Group`s and receive back a relation of corresponding
102
+ `User`s).
103
+
104
+ ### Relation validation
105
+
106
+ Chainable queries allow you to further specify and validate the relation it
107
+ receives. This is done using the static `relation` method:
108
+
109
+ ```ruby
110
+ class Queries::User::FetchActive < Inquery::Query::Chainable
111
+ # This will raise an exception when passing a relation which does not
112
+ # correspond to the `User` model.
113
+ relation class: 'User'
114
+
115
+ # ....
116
+ end
117
+ ```
118
+
119
+ The `relation` method accepts the following options:
120
+
121
+ * `class`
122
+
123
+ Allows to restrict the class (attribute `klass`) of the relation.
124
+ Use `nil` to not perform any checks. The `class` attribute will also
125
+ be taken to infer a default if no relation is given and you didn't
126
+ specify any `default`.
127
+
128
+ * `default`
129
+
130
+ This allows to specify a default relation that will be taken if no relation
131
+ is given. This must be specified as a Proc returning the relation. Set this
132
+ to `false` for no default. If this is set to `nil`, it will try to infer the
133
+ default from the option `class` (if given).
134
+
135
+ * `fields`
136
+
137
+ Allows to restrict the number of fields / values the relation must select.
138
+ This is particularly useful if you're using the query as a subquery and need
139
+ it to return exactly one field. Use `nil` to not perform any checks.
140
+
141
+ * `default_select`
142
+
143
+ If this is set to a symbol, the relation does not have any select fields
144
+ specified (`select_values` is empty) and `fields` is > 0, it will
145
+ automatically select the given field. This option defaults to `:id`. Use
146
+ `nil` to disable this behavior.
147
+
148
+ ### Using query classes as regular scopes
149
+
150
+ Chainable queries can also be used as regular AR model scopes:
151
+
152
+ ```ruby
153
+ class User < ActiveRecord::Base
154
+ scope :active, Queries::User::FetchActive
155
+ end
156
+
157
+ class Queries::User::FetchActive < Inquery::Query::Chainable
158
+ # Note that specifying either `class` or `default` is mandatory when using
159
+ # this query class as a scope. The reason for this is that, if the scope is
160
+ # otherwise empty, the class will receive `nil` from AR and therefore has no
161
+ # way of knowing which default class to take.
162
+ relation class: 'User'
163
+
164
+ def call
165
+ relation.where(active: 1)
166
+ end
167
+ end
168
+ ```
169
+
170
+ This approach allows to you use short and descriptive code like `User.active`
171
+ but have the possibly complex query code hidden in a separate, reusable class.
172
+
173
+ Note that when using classes as scopes, the `process` method will be ignored.
174
+
175
+ ### Using the given relation as subquery
176
+
177
+ In simple cases and all the examples above, we just extend the given relation
178
+ and return it again. It is also possible however to just use the given relation
179
+ as a subquery and return a completely new relation:
180
+
181
+
182
+ ```ruby
183
+ class FetchUsersInGroup < Inquery::Query::Chainable
184
+ # Here we do not specify any specific class, as we don't care for it as long
185
+ # as the relation returns exactly one field.
186
+ relation fields: 1
187
+
188
+ def call
189
+ return ::User.where(%(
190
+ id IN (
191
+ SELECT user_id FROM GROUPS_USERS WHERE group_id IN (
192
+ #{relation.to_sql}
193
+ )
194
+ )
195
+ ))
196
+ end
197
+ end
198
+ ```
199
+
200
+ This query could then be called in the following ways:
201
+
202
+ ```ruby
203
+ FetchUsersInGroup.run(
204
+ GroupsUser.where(user_id: 1).select(:group_id)
205
+ )
206
+
207
+ # In this example, we're not specifying any select for the relation we pass to
208
+ # the query class. This is fine because the query automatically defaults to
209
+ # selecting `id` if exactly one field is required (`fields: 1`) and no select is
210
+ # specifyed. You can control this further with the option `default_select`.
211
+ FetchUsersInGroup.run(Group.where(color: 'red'))
212
+ ```
213
+
214
+ ## Parameters
215
+
216
+ Both query classes can be parameterized using a hash called `params`. It is
217
+ recommended to specify and validate input parameters in every query. For this
218
+ purpose, Inquery provides the `schema` method witch integrates the
219
+ [Schemacop](https://github.com/sitrox/schemacop) validation Gem:
220
+
221
+ ```ruby
222
+ class SomeQueryClass < Inquery::Query
223
+ schema(
224
+ some_param: :integer,
225
+ some_other_param: {
226
+ hash: {
227
+ some_field: :string
228
+ }
229
+ }
230
+ )
231
+
232
+ # ...
233
+ end
234
+ ```
235
+
236
+ The schema is validated at query class instantiation. An exception will be
237
+ raised if the given params do not match the schema specified. See documentation
238
+ of the Schemacop Gem for more information on how to specify schemas.
239
+
240
+ Parameters can be accessed using either `params` or `osparams`. The method
241
+ `osparams` automatically wraps `params` in an `OpenStruct` for more convenient
242
+ access.
243
+
244
+ ```ruby
245
+ class SomeQueryClass < Inquery::Query
246
+ def run
247
+ User.where(
248
+ active: params[:active],
249
+ username: osparams.search
250
+ )
251
+ end
252
+ end
253
+ ```
254
+
255
+ ## Rails integration
256
+
257
+ While it is optional, Inquery has been written from the ground up to be
258
+ perfectly integrated into any Rails application. It has proven to be a winning
259
+ concept to extract all complex queries into separate classes that are
260
+ independently executable and testable.
261
+
262
+ ### Directory structure
263
+
264
+ While not enforced, it is encouraged to use the following structure for storing
265
+ your query classes:
266
+
267
+ * All domain-specific query classes reside in `app/queries`.
268
+ * They're in the module `Queries`.
269
+ * Queries are further grouped by the model they return (and not the model
270
+ they receive). For instance, a class fetching all active users could be
271
+ located at `Queries::User::FetchActive` and would reside under
272
+ `app/queries/user/fetch_active.rb`.
273
+
274
+ There are some key benefits to this approach:
275
+
276
+ * As it should, domain-specific code is located within `app/`.
277
+ * As queries are grouped by the model they return and consistently named,
278
+ they're easy to locate and it does not take much thought where to put and
279
+ how to name new query classes.
280
+ * As there is a single file per query class, it's a breeze to list all
281
+ queries, i.e. to check their naming for consistency.
282
+ * If you're using the same layout for your unit tests, it is absolutely
283
+ clear where to find the corresponding unit tests for each one of your
284
+ query classes.
285
+
286
+ ## Copyright
287
+
288
+ Copyright (c) 2016 Sitrox. See `LICENSE` for further details.
@@ -0,0 +1 @@
1
+ ruby-2.0.0-p353
@@ -0,0 +1,36 @@
1
+ task :gemspec do
2
+ gemspec = Gem::Specification.new do |spec|
3
+ spec.name = 'inquery'
4
+ spec.version = IO.read('VERSION').chomp
5
+ spec.authors = ['Sitrox']
6
+ spec.summary = %(
7
+ A skeleton that allows extracting queries into atomic, reusable classes.
8
+ )
9
+ spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
10
+ spec.executables = []
11
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
12
+ spec.require_paths = ['lib']
13
+
14
+ spec.add_development_dependency 'bundler', '~> 1.3'
15
+ spec.add_development_dependency 'rake'
16
+ spec.add_development_dependency 'sqlite3'
17
+ spec.add_development_dependency 'haml'
18
+ spec.add_development_dependency 'yard'
19
+ spec.add_development_dependency 'rubocop', '0.35.1'
20
+ spec.add_development_dependency 'redcarpet'
21
+ spec.add_dependency 'minitest'
22
+ spec.add_dependency 'activesupport'
23
+ spec.add_dependency 'activerecord'
24
+ spec.add_dependency 'schemacop', '>= 1.0.1'
25
+ end
26
+
27
+ File.open('inquery.gemspec', 'w') { |f| f.write(gemspec.to_ruby.strip) }
28
+ end
29
+
30
+ require 'rake/testtask'
31
+
32
+ Rake::TestTask.new do |t|
33
+ t.pattern = 'test/inquery/**/*_test.rb'
34
+ t.verbose = false
35
+ t.libs << 'test'
36
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1