inquery 0.0.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.
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