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.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.releaser_config +4 -0
- data/.rubocop.yml +42 -0
- data/.travis.yml +9 -0
- data/.yardopts +1 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +288 -0
- data/RUBY_VERSION +1 -0
- data/Rakefile +36 -0
- data/VERSION +1 -0
- data/doc/Inquery.html +119 -0
- data/doc/Inquery/Exceptions.html +115 -0
- data/doc/Inquery/Exceptions/Base.html +127 -0
- data/doc/Inquery/Exceptions/InvalidRelation.html +131 -0
- data/doc/Inquery/Exceptions/UnknownCallSignature.html +131 -0
- data/doc/Inquery/Mixins.html +117 -0
- data/doc/Inquery/Mixins/RelationValidation.html +334 -0
- data/doc/Inquery/Mixins/RelationValidation/ClassMethods.html +190 -0
- data/doc/Inquery/Mixins/SchemaValidation.html +124 -0
- data/doc/Inquery/Mixins/SchemaValidation/ClassMethods.html +192 -0
- data/doc/Inquery/Query.html +736 -0
- data/doc/Inquery/Query/Chainable.html +476 -0
- data/doc/_index.html +254 -0
- data/doc/class_list.html +58 -0
- data/doc/css/common.css +1 -0
- data/doc/css/full_list.css +57 -0
- data/doc/css/style.css +339 -0
- data/doc/file.README.html +365 -0
- data/doc/file_list.html +60 -0
- data/doc/frames.html +26 -0
- data/doc/index.html +365 -0
- data/doc/js/app.js +219 -0
- data/doc/js/full_list.js +181 -0
- data/doc/js/jquery.js +4 -0
- data/doc/method_list.html +147 -0
- data/doc/top-level-namespace.html +112 -0
- data/inquery.gemspec +58 -0
- data/lib/inquery.rb +10 -0
- data/lib/inquery/exceptions.rb +7 -0
- data/lib/inquery/mixins/relation_validation.rb +100 -0
- data/lib/inquery/mixins/schema_validation.rb +27 -0
- data/lib/inquery/query.rb +50 -0
- data/lib/inquery/query/chainable.rb +53 -0
- data/test/db/models.rb +20 -0
- data/test/db/schema.rb +20 -0
- data/test/inquery/query/chainable_test.rb +67 -0
- data/test/inquery/query_test.rb +47 -0
- data/test/queries/group/fetch_as_json.rb +13 -0
- data/test/queries/group/fetch_green.rb +11 -0
- data/test/queries/group/fetch_red.rb +11 -0
- data/test/queries/group/filter_with_color.rb +12 -0
- data/test/queries/user/fetch_all.rb +9 -0
- data/test/queries/user/fetch_in_group.rb +13 -0
- data/test/queries/user/fetch_in_group_rel.rb +17 -0
- data/test/test_helper.rb +26 -0
- metadata +265 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.releaser_config
ADDED
data/.rubocop.yml
ADDED
@@ -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
|
data/.travis.yml
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--markup=markdown --readme=README.md --no-private lib/**/*.rb
|
data/Gemfile
ADDED
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.
|
data/README.md
ADDED
@@ -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.
|
data/RUBY_VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.0.0-p353
|
data/Rakefile
ADDED
@@ -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
|