parelation 0.0.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
+ SHA1:
3
+ metadata.gz: 037f22447840bf0175afad3ffc9877399f52e45a
4
+ data.tar.gz: 245f4049e008a44245cc335f66efe0229e0702b3
5
+ SHA512:
6
+ metadata.gz: 4974134e76747012c27f44408d9804040d15c183a03bc3258000e8e04734d46288ba5f1e2e1213cd906678830042e9913ffd6c7b65f196b68f73954e58f82180
7
+ data.tar.gz: af3bcdb37c9ba6f84e27855816c6317ab809b800a5fe6519a5e9f76555da0169f9ebdd26cc1d2f318c54908ce4b3d87534fc55e341485c48950761e0395f2403
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+ gem "activerecord", "~> 4.1.0"
3
+ gemspec path: "../"
data/.gitignore ADDED
@@ -0,0 +1,23 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ .DS_Store
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
19
+ *.bundle
20
+ *.so
21
+ *.o
22
+ *.a
23
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ - 2.1.0
5
+ - rbx-2.2
6
+ gemfile:
7
+ - .gemfiles/4.1.gemfile
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org"
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Michael van Rooijen
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,288 @@
1
+ # Parelation
2
+
3
+ [![Code Climate](https://codeclimate.com/github/meskyanichi/parelation.png)](https://codeclimate.com/github/meskyanichi/parelation)
4
+ [![Build Status](https://travis-ci.org/meskyanichi/parelation.svg)](https://travis-ci.org/meskyanichi/parelation)
5
+
6
+ Parelation, for Rails/ActiveRecord 4.1.0+, allows you to query your ActiveRecord-mapped database easily, securely and quite flexibly using simple GET requests. It's used in your controller layer where it uses HTTP GET parameters to build on the ActiveRecord::Relation chain. This provides the client-side with the out-of-the-box flexibility to perform fairly dynamic queries without having to write boilerplate on the server.
7
+
8
+ This library was developed for- and extracted from [HireFire].
9
+
10
+ ### Compatibility
11
+
12
+ - Rails/ActiveRecord 4.1.0+
13
+ - Ruby (MRI) 2.0+
14
+ - Ruby (RBX) 2.2+
15
+
16
+ ### Installation
17
+
18
+ Compile and install the gem from source.
19
+
20
+ ```rb
21
+ gem "parelation", github: "meskyanichi/parelation"
22
+ ```
23
+
24
+ *This library won't be hosted on RubyGems.org until it's been tested more in development.*
25
+
26
+ ### Example
27
+
28
+ Here's an example to get an idea of how it works. We'll fetch the `50` most recently created and `open` tickets, and we only want their `id`, `name` and `message` attributes.
29
+
30
+ ```js
31
+ var params = {
32
+ "select[]": ["id", "name", "message"],
33
+ "where[state]": "open",
34
+ "order": "created_at:desc",
35
+ "limit": "50"
36
+ }
37
+
38
+ $.get("https://api.ticket.app/tickets", params, function(tickets){
39
+ console.log("Just fetched the 50 most recent and open tickets.")
40
+ $.each(tickets, function(ticket){
41
+ console.log("Ticket " + ticket.name + " loaded!")
42
+ })
43
+ })
44
+ ```
45
+
46
+ Simply include `Parelation::Helpers` and use the `parelate` method. This will ensure that the provided parameters are converted and applied to the `Ticket.all` criteria chain.
47
+
48
+ ```rb
49
+ class Api::V1::TicketsController < ApplicationController
50
+ include Parelation::Helpers
51
+
52
+ def index
53
+ render json: parelate(Ticket.all)
54
+ end
55
+ end
56
+ ```
57
+
58
+ You can also scope results to the `current_user`:
59
+
60
+ ```rb
61
+ class Api::V1::TicketsController < ApplicationController
62
+ include Parelation::Helpers
63
+
64
+ def index
65
+ render json: parelate(current_user.tickets)
66
+ end
67
+ end
68
+ ```
69
+
70
+ Using the same JavaScript, this'll now fetch the 50 most recent open tickets scoped to the `current_user`.
71
+
72
+
73
+ ### Parameter List (Reference)
74
+
75
+ Here follows a list of all possible query syntaxes. We'll assume we have a Ticket model to query on.
76
+
77
+ #### Select
78
+
79
+ ```
80
+ /tickets?select[]=id&select[]=name&select[]=message
81
+ ```
82
+
83
+ Translates to:
84
+
85
+ ```rb
86
+ Ticket.select(:id, :name, :message)
87
+ ```
88
+
89
+ #### Where
90
+
91
+ ```
92
+ /tickets?where[state]=open
93
+ ```
94
+
95
+ Translates to:
96
+
97
+ ```rb
98
+ Ticket.where(state: "open")
99
+ ```
100
+
101
+ You can also specify multiple multiple conditions:
102
+
103
+ ```
104
+ /tickets?where[state][]=open&where[state][]=pending
105
+ ```
106
+
107
+ Translates to:
108
+
109
+ ```rb
110
+ Ticket.where(state: ["open", "pending"])
111
+ ```
112
+
113
+ #### Where (directional)
114
+
115
+ * `where_gt` (greater than `>`)
116
+ * `where_gte` (greater than or equal to `>=`)
117
+ * `where_lt` (less than `<`)
118
+ * `where_lte` (less than or equal to `<=`)
119
+
120
+ ```
121
+ /tickets?where_gt[created_at]=2014-01-01T00:00:00Z
122
+ ```
123
+
124
+ Translates to:
125
+
126
+ ```rb
127
+ Ticket.where("'tickets'.'created_at' > '2014-01-01 00:00:00.000000'")
128
+ ```
129
+
130
+ You can also specify multiple conditions:
131
+
132
+ ```
133
+ /tickets?where_gt[created_at]=2014-01-01T00:00:00Z&where_gt[updated_at]=2014-01-01T00:00:00Z
134
+ ```
135
+
136
+ Translates to:
137
+
138
+ ```rb
139
+ Ticket
140
+ .where("'tickets'.'created_at' > '2014-01-01 00:00:00.000000'")
141
+ .where("'tickets'.'updated_at' > '2014-01-01 00:00:00.000000'")
142
+ ```
143
+
144
+ #### Query
145
+
146
+ ```
147
+ /tickets?query[memory leak]=name
148
+ ```
149
+
150
+ Translates to:
151
+
152
+ ```rb
153
+ Ticket.where("'tickets'.'name' LIKE '%memory leak%'")
154
+ ```
155
+
156
+ You can also specify multiple columns to scan:
157
+
158
+ ```
159
+ /tickets?query[memory leak]=name&query[memory leak]=message
160
+ ```
161
+
162
+ Translates to:
163
+
164
+ ```rb
165
+ Ticket.where("(
166
+ 'tickets'.'name' LIKE '%memory leak%' OR
167
+ 'tickets'.'message' LIKE '%memory leak%'
168
+ )")
169
+ ```
170
+
171
+ #### Order
172
+
173
+ ```
174
+ /tickets?order=created_at:desc
175
+ ```
176
+
177
+ Translates to:
178
+
179
+ ```rb
180
+ Ticket.order(created_at: :desc)
181
+ ```
182
+
183
+ You can also specify multiple order-operations:
184
+
185
+ ```
186
+ /tickets?order[]=created_at:desc&order[]=name:asc
187
+ ```
188
+
189
+ Translates to:
190
+
191
+ ```rb
192
+ Ticket.order(created_at: :desc, name: :asc)
193
+ ```
194
+
195
+ #### Limit
196
+
197
+ ```
198
+ /tickets?limit=50
199
+ ```
200
+
201
+ Translates to:
202
+
203
+ ```rb
204
+ Ticket.limit(50)
205
+ ```
206
+
207
+ #### Offset
208
+
209
+ ```
210
+ /tickets?offset=25
211
+ ```
212
+
213
+ Translates to:
214
+
215
+ ```rb
216
+ Ticket.offset(25)
217
+ ```
218
+
219
+
220
+ ### Error Handling
221
+
222
+ When invalid parameters were sent, you can rescue the exception and return a message to the client.
223
+
224
+ ```rb
225
+ class Api::V1::TicketsController < ApplicationController
226
+ include Parelation::Helpers
227
+
228
+ def index
229
+ render json: parelate(Ticket.all)
230
+ rescue Parelation::Errors::Parameter => error
231
+ render json: { error: error }, status: :bad_request
232
+ end
233
+ end
234
+ ```
235
+
236
+ This will tell client developers what parameter failed in the HTTP response. This exception generally occurs when there is a typo in the URL's parameters. Knowing which parameter failed (described in `error`) helps narrowing down the issue.
237
+
238
+
239
+ ### Contributing
240
+
241
+ Contributions are welcome, but please conform to these requirements:
242
+
243
+ - Ruby (MRI) 2.0+
244
+ - Ruby (RBX) 2.2+
245
+ - ActiveRecord 4.1.0+
246
+ - 100% Spec Coverage
247
+ - Generated by when running the test suite
248
+ - 100% [Passing Specs]
249
+ - Run test suite with `$ rspec spec`
250
+ - 4.0 [Code Climate Score]
251
+ - Run `$ rubycritic lib` to generate the score locally and receive tips
252
+ - No code smells
253
+ - No duplication
254
+
255
+ To start contributing, fork the project, clone it, and install the development dependencies:
256
+
257
+ ```
258
+ git clone git@github.com:USERNAME/parelation.git
259
+ cd parelation
260
+ bundle
261
+ ```
262
+
263
+ Ensure that everything works:
264
+
265
+ ```
266
+ rspec spec
267
+ rubycritic lib
268
+ ```
269
+
270
+ Create a new branch and start hacking:
271
+
272
+ ```
273
+ git checkout -b my-contributions
274
+ ```
275
+
276
+ Submit a pull request.
277
+
278
+
279
+ ### Author / License
280
+
281
+ Copyright (c) 2014 Michael van Rooijen ( [@meskyanichi] )<br />
282
+ Released under the MIT [License].
283
+
284
+ [@meskyanichi]: https://twitter.com/meskyanichi
285
+ [HireFire]: http://hirefire.io
286
+ [Passing Specs]: https://travis-ci.org/meskyanichi/parelation
287
+ [Code Climate Score]: https://codeclimate.com/github/meskyanichi/parelation
288
+ [License]: https://github.com/meskyanichi/parelation/blob/master/LICENSE
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+ RSpec::Core::RakeTask.new(:spec)
4
+ task default: :spec
@@ -0,0 +1,60 @@
1
+ class Parelation::Applier
2
+
3
+ # @return [Array] the list of active criteria classes
4
+ # that are used to narrow down database results.
5
+ #
6
+ CRITERIA = [
7
+ Parelation::Criteria::Select,
8
+ Parelation::Criteria::Limit,
9
+ Parelation::Criteria::Offset,
10
+ Parelation::Criteria::Order,
11
+ Parelation::Criteria::Query,
12
+ Parelation::Criteria::Where
13
+ ]
14
+
15
+ # @return [ActiveRecord::Relation]
16
+ #
17
+ attr_reader :chain
18
+
19
+ # @return [ActionController::Parameters, Hash]
20
+ #
21
+ attr_reader :params
22
+
23
+ # @param chain [ActionController::Relation] the base chain to build on.
24
+ # @param params [ActionController::Parameters, Hash] user input via params.
25
+ #
26
+ def initialize(chain, params)
27
+ @chain = chain
28
+ @params = params
29
+ end
30
+
31
+ # @return [ActiveRecord::Relation] the criteria-applied {#chain}.
32
+ #
33
+ def apply
34
+ @apply ||= apply_to_chain
35
+ end
36
+
37
+ private
38
+
39
+ # Iterates over each user-provided parameter and incrementally
40
+ # updates the {#chain} to incorporate the user-requested criteria.
41
+ #
42
+ # @return [ActiveRecord::Relation]
43
+ #
44
+ def apply_to_chain
45
+ params.each do |param, value|
46
+ CRITERIA.each do |criteria|
47
+ if criteria.match?(param)
48
+ begin
49
+ @chain = criteria.new(chain, param, value).call
50
+ rescue
51
+ raise Parelation::Errors::Parameter,
52
+ "the #{param} parameter is invalid"
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ chain
59
+ end
60
+ end
@@ -0,0 +1,24 @@
1
+ class Parelation::Criteria::Limit
2
+ include Parelation::Criteria
3
+
4
+ # @param param [String]
5
+ # @return [TrueClass, FalseClass]
6
+ #
7
+ def self.match?(param)
8
+ !!(param =~ /^limit$/)
9
+ end
10
+
11
+ # @return [ActiveRecord::Relation]
12
+ #
13
+ def call
14
+ chain.limit(limit)
15
+ end
16
+
17
+ private
18
+
19
+ # Alias for {#value}.
20
+ #
21
+ def limit
22
+ value
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ class Parelation::Criteria::Offset
2
+ include Parelation::Criteria
3
+
4
+ # @param param [String]
5
+ # @return [TrueClass, FalseClass]
6
+ #
7
+ def self.match?(param)
8
+ !!(param =~ /^offset$/)
9
+ end
10
+
11
+ # @return [ActiveRecord::Relation]
12
+ #
13
+ def call
14
+ chain.offset(offset)
15
+ end
16
+
17
+ private
18
+
19
+ # Alias for {#value}.
20
+ #
21
+ def offset
22
+ value
23
+ end
24
+ end
@@ -0,0 +1,46 @@
1
+ class Parelation::Criteria::Order::Object
2
+
3
+ # @return [String]
4
+ #
5
+ attr_reader :order
6
+
7
+ # @param order [String]
8
+ #
9
+ def initialize(order)
10
+ @order = order
11
+ end
12
+
13
+ # @return [Hash] returns criteria for {ActiveRecord::Relation}.
14
+ #
15
+ # @example
16
+ # { created_at: :asc }
17
+ #
18
+ def criteria
19
+ { field => direction }
20
+ end
21
+
22
+ private
23
+
24
+ # @return [String] the name of the field to perform the ordering on.
25
+ #
26
+ def field
27
+ parts.first || ""
28
+ end
29
+
30
+ # @return [Symbol, nil] the direction to order {#field},
31
+ # eiter :asc or :desc.
32
+ #
33
+ def direction
34
+ case parts.last
35
+ when "asc" then :asc
36
+ when "desc" then :desc
37
+ else nil
38
+ end
39
+ end
40
+
41
+ # @return [Array<String, nil>] the criteria chunks (separated by +:+).
42
+ #
43
+ def parts
44
+ @parts ||= order.split(":")
45
+ end
46
+ end
@@ -0,0 +1,31 @@
1
+ class Parelation::Criteria::Order
2
+ include Parelation::Criteria
3
+
4
+ require_relative "order/object"
5
+
6
+ # @param param [String]
7
+ # @return [TrueClass, FalseClass]
8
+ #
9
+ def self.match?(param)
10
+ !!(param =~ /^order$/)
11
+ end
12
+
13
+ # Applies the specified orderings to {#chain}.
14
+ #
15
+ # @return [ActiveRecord::Relation] the modified chain.
16
+ #
17
+ def call
18
+ orders.inject(chain) do |chain, order|
19
+ chain.order(order.criteria)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ # @return [Array<Parelation::Criteria::Order::Object>] an
26
+ # array of attributes to order.
27
+ #
28
+ def orders
29
+ @orders ||= [value].flatten.map { |order| Object.new(order) }
30
+ end
31
+ end
@@ -0,0 +1,55 @@
1
+ class Parelation::Criteria::Query
2
+ include Parelation::Criteria
3
+
4
+ # @param param [String]
5
+ # @return [TrueClass, FalseClass]
6
+ #
7
+ def self.match?(param)
8
+ !!(param =~ /^query$/)
9
+ end
10
+
11
+ # @return [ActiveRecord::Relation]
12
+ #
13
+ def call
14
+ chain.where(*criteria)
15
+ end
16
+
17
+ private
18
+
19
+ # @return [Array] containing {#chain} criteria.
20
+ #
21
+ def criteria
22
+ [sql] + args
23
+ end
24
+
25
+ # @return [String] an SQL statement based on the selected {#attributes}.
26
+ #
27
+ def sql
28
+ template = ->(field) { %Q{"#{chain.arel_table.name}"."#{field}" LIKE ?} }
29
+ attributes.map(&template).join(" OR ")
30
+ end
31
+
32
+ # @return [Array] containing the query, one for each field in {#attributes}.
33
+ #
34
+ def args
35
+ attributes.count.times.map { "%#{query}%" }
36
+ end
37
+
38
+ # @return [String] the "search" query to perform.
39
+ #
40
+ def query
41
+ @query ||= parts.first
42
+ end
43
+
44
+ # @return [Array<String>] an array of attributes to search in.
45
+ #
46
+ def attributes
47
+ @attributes ||= parts.last
48
+ end
49
+
50
+ # @return [Array] containing the query and attributes.
51
+ #
52
+ def parts
53
+ @parts ||= [value.keys.first, [value.values.first].flatten]
54
+ end
55
+ end
@@ -0,0 +1,24 @@
1
+ class Parelation::Criteria::Select
2
+ include Parelation::Criteria
3
+
4
+ # @param param [String]
5
+ # @return [TrueClass, FalseClass]
6
+ #
7
+ def self.match?(param)
8
+ !!(param =~ /^select$/)
9
+ end
10
+
11
+ # @return [ActiveRecord::Relation]
12
+ #
13
+ def call
14
+ chain.select(*attributes)
15
+ end
16
+
17
+ private
18
+
19
+ # Alias for {#value}.
20
+ #
21
+ def attributes
22
+ value
23
+ end
24
+ end