parelation 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 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