ruby-montage 0.5.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/README.md +2 -0
- data/Rakefile +5 -1
- data/bin/console +0 -3
- data/lib/montage/client.rb +140 -16
- data/lib/montage/client/documents.rb +44 -22
- data/lib/montage/errors.rb +3 -1
- data/lib/montage/query.rb +141 -66
- data/lib/montage/query/order_parser.rb +80 -0
- data/lib/montage/query/query_parser.rb +205 -0
- data/lib/montage/version.rb +1 -1
- metadata +4 -3
- data/lib/montage/query_parser.rb +0 -116
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7e3dfa2aad01ef822edd6bc91a7b316694912d0f
|
4
|
+
data.tar.gz: 08a3fc32a12b85d0144b8be7855c01cdd2a2d4b6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4b7a79943fbca4d1ca5b7bcf1abc72e9383881a6def914937dc9c6aad289e396a45d3458c39ee6620dcf68829ee05fe37f13ff7b8d96d1c353c36e3665448454
|
7
|
+
data.tar.gz: a1cc37f0d95359cefaab29a32a6b51fcacb717df19e486df23c1780651a684d743330415d595a6a5f02d6526daeebe3d8441a575a39c7367e919161fd75e0759
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
[![Code Climate](https://codeclimate.com/github/EditLLC/ruby-montage/badges/gpa.svg)](https://codeclimate.com/github/EditLLC/ruby-montage)
|
2
2
|
[![Circle CI](https://circleci.com/gh/EditLLC/ruby-montage/tree/master.svg?style=svg)](https://circleci.com/gh/EditLLC/ruby-montage/tree/master)
|
3
3
|
[![codecov.io](http://codecov.io/github/EditLLC/ruby-montage/coverage.svg?branch=master)](http://codecov.io/github/EditLLC/ruby-montage?branch=master)
|
4
|
+
[![Codetree](https://codetree.com/images/managed-with-codetree.svg)](https://codetree.com/projects/a8d5)
|
4
5
|
|
5
6
|
# Ruby Montage
|
6
7
|
|
@@ -33,6 +34,7 @@ client = Montage::Client.new do |c|
|
|
33
34
|
c.api_version # Optional, defaults to 1
|
34
35
|
c.domain = "test" # Your Montage subdomain
|
35
36
|
c.url_prefix = "https://testco.mtnge.com" # Optional, defaults to the montage dev server
|
37
|
+
c.environment = 'production' # Optional, defaults to production. Valid options are 'production' and 'development'
|
36
38
|
end
|
37
39
|
|
38
40
|
response = client.auth
|
data/Rakefile
CHANGED
data/bin/console
CHANGED
data/lib/montage/client.rb
CHANGED
@@ -13,22 +13,61 @@ module Montage
|
|
13
13
|
include Schemas
|
14
14
|
include Documents
|
15
15
|
|
16
|
-
attr_accessor :token,
|
17
|
-
|
16
|
+
attr_accessor :token,
|
17
|
+
:username,
|
18
|
+
:password,
|
19
|
+
:domain,
|
20
|
+
:api_version,
|
21
|
+
:environment
|
22
|
+
|
23
|
+
# Initializes the client instance
|
24
|
+
#
|
25
|
+
# * *Attributes* :
|
26
|
+
# - +token+ -> API access token required for requests, does not expire
|
27
|
+
# - +username+ -> Montage username credential
|
28
|
+
# - +password+ -> Montage password credential
|
29
|
+
# - +domain+ -> Project subdomain, required for initialization
|
30
|
+
# - +api_version+ -> API version to query against, defaults to 1
|
31
|
+
# - +environment+ -> Specifies desired environment for requests, defaults
|
32
|
+
# to 'production'. Valid options are 'development' and 'production'.
|
33
|
+
# * *Returns* :
|
34
|
+
# - A valid Montage::Client instance
|
35
|
+
# * *Raises* :
|
36
|
+
# - +MissingAttributeError+ -> If the domain attribute is not specified
|
37
|
+
# - +InvalidEnvironment+ -> If the environment attribute is not set to 'production' or 'development'
|
38
|
+
#
|
18
39
|
def initialize
|
19
40
|
@api_version = 1
|
41
|
+
@environment ||= "production"
|
20
42
|
yield(self) if block_given?
|
21
|
-
|
43
|
+
fail MissingAttributeError, "You must declare the domain attribute" unless @domain
|
44
|
+
fail InvalidEnvironment, "Valid options are 'production' and 'development'" unless environment_valid?
|
22
45
|
end
|
23
46
|
|
24
|
-
|
25
|
-
|
47
|
+
# Verifies the Montage::Client instance environment
|
48
|
+
#
|
49
|
+
# * *Returns* :
|
50
|
+
# - A boolean
|
51
|
+
#
|
52
|
+
def environment_valid?
|
53
|
+
%w(production development).include?(@environment)
|
26
54
|
end
|
27
55
|
|
28
|
-
|
29
|
-
|
56
|
+
# Generates a base url for requests using the supplied environment and domain
|
57
|
+
#
|
58
|
+
# * *Returns* :
|
59
|
+
# - A string containing the constructed url
|
60
|
+
#
|
61
|
+
def default_url_prefix
|
62
|
+
return "https://#{domain}.dev.montagehot.club" if @environment == "development"
|
63
|
+
return "https://#{domain}.mntge.com" if @environment == "production"
|
30
64
|
end
|
31
65
|
|
66
|
+
# Attempts to authenticate with the Montage API
|
67
|
+
#
|
68
|
+
# * *Returns* :
|
69
|
+
# - A hash containing a valid token or an error string, oh no!
|
70
|
+
#
|
32
71
|
def auth
|
33
72
|
build_response("token") do
|
34
73
|
connection.post do |req|
|
@@ -39,6 +78,18 @@ module Montage
|
|
39
78
|
end
|
40
79
|
end
|
41
80
|
|
81
|
+
# Requests resources from the Montage API, TODO:ADD EXAMPLES
|
82
|
+
#
|
83
|
+
# * *Args* :
|
84
|
+
# - +url+ -> The url of the targeted resource
|
85
|
+
# - +resource_name+ -> The name of the targeted resource
|
86
|
+
# - +options+ -> A hash of desired options
|
87
|
+
# * *Returns* :
|
88
|
+
# * A Montage::Response Object containing:
|
89
|
+
# - A http status code
|
90
|
+
# - The response body
|
91
|
+
# - The resource name
|
92
|
+
#
|
42
93
|
def get(url, resource_name, options = {})
|
43
94
|
build_response(resource_name) do
|
44
95
|
connection.get do |req|
|
@@ -48,6 +99,18 @@ module Montage
|
|
48
99
|
end
|
49
100
|
end
|
50
101
|
|
102
|
+
# Posts to the Montage API with a JSON options string, TODO:ADD EXAMPLES
|
103
|
+
#
|
104
|
+
# * *Args* :
|
105
|
+
# - +url+ -> The url of the targeted resource
|
106
|
+
# - +resource_name+ -> The name of the targeted resource
|
107
|
+
# - +options+ -> A hash of desired options
|
108
|
+
# * *Returns* :
|
109
|
+
# * A Montage::Response Object containing:
|
110
|
+
# - A http status code
|
111
|
+
# - The response body
|
112
|
+
# - The resource name
|
113
|
+
#
|
51
114
|
def post(url, resource_name, options = {})
|
52
115
|
build_response(resource_name) do
|
53
116
|
connection.post do |req|
|
@@ -57,6 +120,18 @@ module Montage
|
|
57
120
|
end
|
58
121
|
end
|
59
122
|
|
123
|
+
# Updates an existing Montage resource with a JSON options string, TODO:ADD EXAMPLES
|
124
|
+
#
|
125
|
+
# * *Args* :
|
126
|
+
# - +url+ -> The url of the targeted resource
|
127
|
+
# - +resource_name+ -> The name of the targeted resource
|
128
|
+
# - +options+ -> A hash of desired options
|
129
|
+
# * *Returns* :
|
130
|
+
# * A Montage::Response Object containing:
|
131
|
+
# - A http status code
|
132
|
+
# - The response body
|
133
|
+
# - The resource name
|
134
|
+
#
|
60
135
|
def put(url, resource_name, options = {})
|
61
136
|
build_response(resource_name) do
|
62
137
|
connection.put do |req|
|
@@ -66,6 +141,18 @@ module Montage
|
|
66
141
|
end
|
67
142
|
end
|
68
143
|
|
144
|
+
# Removes an existing Montage resource with a JSON options string, TODO:ADD EXAMPLES
|
145
|
+
#
|
146
|
+
# * *Args* :
|
147
|
+
# - +url+ -> The url of the targeted resource
|
148
|
+
# - +resource_name+ -> The name of the targeted resource
|
149
|
+
# - +options+ -> A hash of desired options
|
150
|
+
# * *Returns* :
|
151
|
+
# * A Montage::Response Object containing:
|
152
|
+
# - A http status code
|
153
|
+
# - The response body
|
154
|
+
# - The resource name
|
155
|
+
#
|
69
156
|
def delete(url, resource_name, options = {})
|
70
157
|
build_response(resource_name) do
|
71
158
|
connection.delete do |req|
|
@@ -75,37 +162,74 @@ module Montage
|
|
75
162
|
end
|
76
163
|
end
|
77
164
|
|
165
|
+
# Sets the authentication token on the client instance and http headers
|
166
|
+
#
|
167
|
+
# * *Returns* :
|
168
|
+
# - A string with the proper token interpolated
|
169
|
+
#
|
78
170
|
def set_token(token)
|
79
171
|
@token = token
|
80
172
|
connection.headers["Authorization"] = "Token #{token}"
|
81
173
|
end
|
82
174
|
|
175
|
+
# Checks the response body for an errors key and a successful http status code
|
176
|
+
#
|
177
|
+
# * *Args* :
|
178
|
+
# - +response+ -> The Montage API response
|
179
|
+
# * *Returns* :
|
180
|
+
# - A boolean
|
181
|
+
#
|
83
182
|
def response_successful?(response)
|
84
183
|
return false if response.body["errors"]
|
85
184
|
response.success?
|
86
185
|
end
|
87
186
|
|
88
|
-
|
187
|
+
# Instantiates a response object based on the yielded block
|
188
|
+
#
|
189
|
+
# * *Args* :
|
190
|
+
# - +resource_name+ -> The name of the Montage resource
|
191
|
+
# * *Returns* :
|
192
|
+
# * A Montage::Response Object containing:
|
193
|
+
# - A http status code
|
194
|
+
# - The response body
|
195
|
+
# - The resource name
|
196
|
+
#
|
197
|
+
def build_response(resource_name)
|
89
198
|
response = yield
|
90
199
|
resource = response_successful?(response) ? resource_name : "error"
|
91
200
|
|
92
201
|
response_object = Montage::Response.new(response.status, response.body, resource)
|
93
202
|
|
94
|
-
if resource_name == "token" && response.success?
|
95
|
-
set_token(response_object.token.value)
|
96
|
-
end
|
203
|
+
set_token(response_object.token.value) if resource_name == "token" && response.success?
|
97
204
|
|
98
205
|
response_object
|
99
206
|
end
|
100
207
|
|
208
|
+
# Supplies the Faraday connection with proper headers
|
209
|
+
#
|
210
|
+
# * *Returns* :
|
211
|
+
# - A hash of instance specific headers for requests
|
212
|
+
#
|
213
|
+
def connection_headers
|
214
|
+
{
|
215
|
+
"User-Agent" => "Montage Ruby v#{Montage::VERSION}",
|
216
|
+
"Content-Type" => "application/json",
|
217
|
+
"Accept" => "*/*",
|
218
|
+
"Authorization" => "Token #{token}",
|
219
|
+
"Referer" => "#{default_url_prefix}/"
|
220
|
+
}
|
221
|
+
end
|
222
|
+
|
223
|
+
# Creates an Faraday connection instance for requests
|
224
|
+
#
|
225
|
+
# * *Returns* :
|
226
|
+
# - A Faraday connection object with an instance specific configuration
|
227
|
+
#
|
101
228
|
def connection
|
102
229
|
@connect ||= Faraday.new do |f|
|
103
230
|
f.adapter :net_http
|
104
|
-
f.
|
105
|
-
f.
|
106
|
-
f.headers["Content-Type"] = content_type
|
107
|
-
f.headers["Accept"] = "*/*"
|
108
|
-
f.headers["Authorization"] = "Token #{token}"
|
231
|
+
f.headers = connection_headers
|
232
|
+
f.url_prefix = "#{default_url_prefix}/api/v#{api_version}/"
|
109
233
|
f.response :json, content_type: /\bjson$/
|
110
234
|
end
|
111
235
|
end
|
@@ -1,28 +1,50 @@
|
|
1
1
|
module Montage
|
2
2
|
class Client
|
3
3
|
module Documents
|
4
|
-
# Public: Get a list of documents
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
# Returns
|
11
|
-
#
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
|
25
|
-
|
4
|
+
# Public: Get a list of documents. Batch and single queries are supported
|
5
|
+
# via the formatting listed below.
|
6
|
+
#
|
7
|
+
# * *Args* :
|
8
|
+
# - +queries+ -> A Montage::Query object or batch of objects to pass
|
9
|
+
# along with the request
|
10
|
+
# * *Returns* :
|
11
|
+
# - A Montage::Response with a raw body that will resemble:
|
12
|
+
# {
|
13
|
+
# "data"=> {
|
14
|
+
# "query1"=> [
|
15
|
+
# {
|
16
|
+
# "name"=>"happy accidents everywhere",
|
17
|
+
# "price"=>"999,999,999",
|
18
|
+
# "signed"=>true,
|
19
|
+
# "id"=>"-1"
|
20
|
+
# }
|
21
|
+
# ]
|
22
|
+
# }
|
23
|
+
# }
|
24
|
+
# * *Examples* :
|
25
|
+
# - A Single Query :
|
26
|
+
#
|
27
|
+
# {
|
28
|
+
# "query1" => {
|
29
|
+
# "$schema" => "bob_ross_paintings",
|
30
|
+
# "$query" => [
|
31
|
+
# ["$filter", [
|
32
|
+
# ["rating", ['$gt', 8]]
|
33
|
+
# ]],
|
34
|
+
# ["$limit", 1]
|
35
|
+
# ]
|
36
|
+
# }
|
37
|
+
# }
|
38
|
+
#
|
39
|
+
# - Batch Queries :
|
40
|
+
#
|
41
|
+
# {
|
42
|
+
# query1: { '$schema': 'bob_ross_paintings', ... },
|
43
|
+
# query2: { '$schema': 'happy_little_trees', ... }
|
44
|
+
# }
|
45
|
+
#
|
46
|
+
def documents(queries)
|
47
|
+
post("query/", "document", queries)
|
26
48
|
end
|
27
49
|
|
28
50
|
# Public: Get the set of documents for the given cursor
|
data/lib/montage/errors.rb
CHANGED
data/lib/montage/query.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'montage/errors'
|
2
|
-
require 'montage/query_parser'
|
2
|
+
require 'montage/query/query_parser'
|
3
|
+
require 'montage/query/order_parser'
|
3
4
|
require 'montage/support'
|
4
5
|
require 'json'
|
5
6
|
|
@@ -7,113 +8,187 @@ module Montage
|
|
7
8
|
class Query
|
8
9
|
include Montage::Support
|
9
10
|
|
10
|
-
attr_accessor :
|
11
|
+
attr_accessor :options
|
12
|
+
attr_reader :schema
|
11
13
|
|
12
|
-
|
13
|
-
|
14
|
+
# Initializes the query instance via a params hash
|
15
|
+
#
|
16
|
+
# * *Attributes* :
|
17
|
+
# - +schema+ -> The name of the schema you wish to query. Alphanumeric
|
18
|
+
# characters and underscores are allowed.
|
19
|
+
# - +options+ -> A query hash containing desired options
|
20
|
+
# * *Returns* :
|
21
|
+
# - A valid Montage::Query instance
|
22
|
+
# * *Raises* :
|
23
|
+
# - +InvalidAttributeFormat+ -> If the declared schema is not a string or
|
24
|
+
# contains non-alphanumeric characters. Underscore ("_") is allowed!
|
25
|
+
# * *Examples* :
|
26
|
+
# @query = Montage::Query.new(schema: 'testing')
|
27
|
+
# => <Montage::Query:ID @query={"$schema"=>"testing", "$query"=>[["$filter", []]]}, @schema="testing">
|
28
|
+
#
|
29
|
+
def initialize(params = {})
|
30
|
+
@schema = params[:schema]
|
31
|
+
@options = {
|
32
|
+
"$schema" => @schema,
|
33
|
+
"$query" => [
|
34
|
+
["$filter", []]
|
35
|
+
]
|
36
|
+
}
|
37
|
+
fail(
|
38
|
+
InvalidAttributeFormat, "Schema attribute must be declared and valid!"
|
39
|
+
) unless schema_valid?
|
14
40
|
end
|
15
41
|
|
16
|
-
#
|
17
|
-
#
|
18
|
-
# Merges a hash:
|
19
|
-
# { limit: 10 }
|
42
|
+
# Validates the Montage::Query schema attribute
|
20
43
|
#
|
21
|
-
# Returns
|
44
|
+
# * *Returns* :
|
45
|
+
# - A boolean
|
22
46
|
#
|
23
|
-
def
|
24
|
-
|
47
|
+
def schema_valid?
|
48
|
+
@schema.is_a?(String) && @schema.index(/\W+/).nil?
|
25
49
|
end
|
26
50
|
|
27
|
-
#
|
51
|
+
# Adds a query parameter to Montage::Query instances in the form of
|
52
|
+
# an array. Checks for existing array elements and replaces them if found.
|
28
53
|
#
|
29
|
-
#
|
30
|
-
#
|
54
|
+
# * *Args* :
|
55
|
+
# - +query_param+ -> A query-modifing parameter in the form of an array.
|
56
|
+
# Composed of a ReQON supported string as a designator and an
|
57
|
+
# associated value.
|
31
58
|
#
|
32
|
-
# Returns
|
59
|
+
# * *Returns* :
|
60
|
+
# - The updated array
|
33
61
|
#
|
34
|
-
def
|
35
|
-
|
36
|
-
|
62
|
+
def merge_array(query_param)
|
63
|
+
arr = options["$query"]
|
64
|
+
position = arr.index(arr.assoc(query_param[0]))
|
37
65
|
|
38
|
-
|
39
|
-
|
40
|
-
# Accepts either a string or a hash:
|
41
|
-
# order("foo asc") or
|
42
|
-
# order(:foo => :asc) or
|
43
|
-
# order(:foo => "asc")
|
44
|
-
#
|
45
|
-
# Defaults the direction to asc if no value is passed in for that, or if it is not a valid value
|
46
|
-
#
|
47
|
-
# Merges a hash:
|
48
|
-
# { order: "foo asc" }
|
49
|
-
#
|
50
|
-
# Returns a copy of self
|
51
|
-
#
|
52
|
-
def order(clause = {})
|
53
|
-
if clause.is_a?(Hash)
|
54
|
-
direction = clause.values.first.to_s
|
55
|
-
field = clause.keys.first.to_s
|
66
|
+
if position.nil?
|
67
|
+
arr.push(query_param)
|
56
68
|
else
|
57
|
-
|
58
|
-
field = clause.split(" ")[0]
|
59
|
-
direction = "asc" unless %w(asc desc).include?(direction)
|
69
|
+
arr[position] = query_param
|
60
70
|
end
|
61
|
-
|
62
|
-
clone.tap{ |r| r.query.merge!(order_by: field, ordering: direction) }
|
63
71
|
end
|
64
72
|
|
65
|
-
#
|
66
|
-
#
|
67
|
-
# Raises an exception if it is not a valid query (at least three "words"):
|
68
|
-
# parse_string_clause("foo bar")
|
69
|
-
#
|
70
|
-
# Raises an exception if the operator given is not a valid operator
|
71
|
-
# parse_string_clause("foo * 'bar'")
|
73
|
+
# Defines the limit to apply to the query, defaults to nil
|
72
74
|
#
|
73
|
-
#
|
74
|
-
#
|
75
|
-
#
|
75
|
+
# * *Args* :
|
76
|
+
# - +max+ -> The max number of desired results
|
77
|
+
# * *Returns* :
|
78
|
+
# - An updated copy of self
|
79
|
+
# * *Examples* :
|
80
|
+
# @query.limit(99).options
|
81
|
+
# => {"$schema"=>"testing", "$query"=>[["$filter", []], ["$limit", 99]]}
|
76
82
|
#
|
83
|
+
def limit(max = nil)
|
84
|
+
clone.tap { |r| r.merge_array(["$limit", max]) }
|
85
|
+
end
|
77
86
|
|
78
|
-
#
|
87
|
+
# Defines the offset to apply to the query, defaults to nil
|
79
88
|
#
|
80
|
-
#
|
81
|
-
#
|
82
|
-
#
|
89
|
+
# * *Args* :
|
90
|
+
# - +value+ -> The desired offset value
|
91
|
+
# * *Returns* :
|
92
|
+
# - An updated copy of self
|
93
|
+
# * *Examples* :
|
94
|
+
# @query.offset(14).options
|
95
|
+
# => {"$schema"=>"testing", "$query"=>[["$filter", []], ["$offset", 14]]}
|
83
96
|
#
|
84
|
-
|
85
|
-
|
97
|
+
def offset(value = nil)
|
98
|
+
clone.tap { |r| r.merge_array(["$offset", value]) }
|
99
|
+
end
|
100
|
+
|
101
|
+
# Defines the order clause for the query and merges it into the query array.
|
102
|
+
# See Montage::OrderParser for specifics
|
103
|
+
#
|
104
|
+
# * *Args* :
|
105
|
+
# - +clause+ -> A hash or string value containing the field to order by
|
106
|
+
# and the direction. Valid directions are "asc" and "desc". String
|
107
|
+
# values will default to "asc" if omitted or incorrect.
|
108
|
+
# * *Returns* :
|
109
|
+
# - An updated copy of self
|
110
|
+
# * *Examples* :
|
111
|
+
# - String
|
112
|
+
# @query.order("foo asc").options
|
113
|
+
# => {"$schema"=>"testing", "$query"=>[["$filter", []], ["$order_by", ["$asc", "foo"]]]}
|
114
|
+
# - Hash
|
115
|
+
# @query.order(:foo => :asc).options
|
116
|
+
# => {"$schema"=>"testing", "$query"=>[["$filter", []], ["$order_by", ["$asc", "foo"]]]}
|
86
117
|
#
|
87
|
-
|
118
|
+
def order(clause = {})
|
119
|
+
clone.tap { |r| r.merge_array(OrderParser.new(clause).parse) }
|
120
|
+
end
|
121
|
+
|
122
|
+
# Adds a where clause to the ReQON filter array. See Montage::QueryParser
|
123
|
+
# for specifics
|
124
|
+
#
|
125
|
+
# * *Args* :
|
126
|
+
# - +clause+ -> A hash or string containing desired options
|
127
|
+
# * *Returns* :
|
128
|
+
# - A copy of self
|
129
|
+
# * *Examples* :
|
130
|
+
# - String
|
131
|
+
# @query.where("tree_happiness_level >= 99").options
|
132
|
+
# => {"$schema"=>"test", "$query"=>[["$filter", [["tree_happiness_level", ["$__gte", 99]]]]]}
|
133
|
+
# - Hash
|
134
|
+
# @query.where(cloud_type: "almighty").options
|
135
|
+
# => {"$schema"=>"test", "$query"=>[["$filter", [["cloud_type", "almighty"]]]]}
|
88
136
|
#
|
89
137
|
def where(clause)
|
90
|
-
clone.tap { |r| r.
|
138
|
+
clone.tap { |r| r.merge_array(["$filter", QueryParser.new(clause).parse]) }
|
91
139
|
end
|
92
140
|
|
93
141
|
# Select a set of columns from the result set
|
94
142
|
#
|
143
|
+
# * *Args* :
|
144
|
+
# - +Array+ -> Accepts multiple column names as a string or symbol
|
145
|
+
# * *Example* :
|
146
|
+
# @query.select("id", "name")
|
147
|
+
# @query.select(:id, :name)
|
148
|
+
# => {"$schema"=>"test", "$query"=>[["$filter", []], ["$pluck", ["id", "name"]]]}
|
149
|
+
# * *Returns* :
|
150
|
+
# - A copy of self
|
151
|
+
#
|
95
152
|
def select(*args)
|
96
|
-
clone.tap { |r| r.
|
153
|
+
clone.tap { |r| r.merge_array(["$pluck", args.map(&:to_s)]) }
|
97
154
|
end
|
98
155
|
|
99
|
-
# Specifies
|
100
|
-
#
|
101
|
-
#
|
156
|
+
# Specifies an index to use on a query.
|
157
|
+
#
|
158
|
+
# * *Notes* :
|
159
|
+
# - RethinkDB isn't as smart as some other database engines when selecting
|
160
|
+
# a query plan, but it does let you specify which index to use
|
161
|
+
# * *Args* :
|
162
|
+
# - +field+ -> The index value in string format
|
163
|
+
# * *Example* :
|
164
|
+
# - @query.index('foo').options
|
165
|
+
# => {"$schema"=>"test", "$query"=>[["$filter", []], ["$index", "foo"]]}
|
166
|
+
# * *Returns* :
|
167
|
+
# - A copy of self
|
102
168
|
#
|
103
169
|
def index(field)
|
104
|
-
clone.tap { |r| r.
|
170
|
+
clone.tap { |r| r.merge_array(["$index", field]) }
|
105
171
|
end
|
106
172
|
|
107
173
|
# Pluck just one column from the result set
|
108
174
|
#
|
175
|
+
# * *Args* :
|
176
|
+
# - +column_name+ -> Accepts a single string or symbol value for the column
|
177
|
+
# * *Example* :
|
178
|
+
# @query.pluck(:id)
|
179
|
+
# @query.pluck("id")
|
180
|
+
# => {"$schema"=>"test", "$query"=>[["$filter", []], ["$pluck", ["id"]]]}
|
181
|
+
# * *Returns* :
|
182
|
+
# - A copy of self
|
183
|
+
#
|
109
184
|
def pluck(column_name)
|
110
|
-
clone.tap { |r| r.
|
185
|
+
clone.tap { |r| r.merge_array(["$pluck", [column_name.to_s]]) }
|
111
186
|
end
|
112
187
|
|
113
188
|
# Parses the current query hash and returns a JSON string
|
114
189
|
#
|
115
190
|
def to_json
|
116
|
-
@
|
191
|
+
@options.to_json
|
117
192
|
end
|
118
193
|
end
|
119
194
|
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'montage/errors'
|
2
|
+
|
3
|
+
module Montage
|
4
|
+
class OrderParser
|
5
|
+
|
6
|
+
attr_reader :clause
|
7
|
+
|
8
|
+
# Creates an OrderParser instance based on a clause argument. The instance
|
9
|
+
# can then be parsed into a valid ReQON string for queries
|
10
|
+
#
|
11
|
+
# * *Args* :
|
12
|
+
# - +clause+ -> A hash or string ordering value
|
13
|
+
# * *Returns* :
|
14
|
+
# - A Montage::OrderParser instance
|
15
|
+
# * *Raises* :
|
16
|
+
# - +ClauseFormatError+ -> If a blank string clause or a hash without
|
17
|
+
# a valid direction is found.
|
18
|
+
#
|
19
|
+
def initialize(clause)
|
20
|
+
@clause = clause
|
21
|
+
fail(
|
22
|
+
ClauseFormatError, "Order direction missing or blank clause found!"
|
23
|
+
) unless clause_valid?
|
24
|
+
end
|
25
|
+
|
26
|
+
# Validates clause arguments by checking a hash for a full word match of
|
27
|
+
# "asc" or "desc". String clauses are rejected if they are blank or
|
28
|
+
# consist entirely of whitespace
|
29
|
+
#
|
30
|
+
# * *Returns* :
|
31
|
+
# - A boolean
|
32
|
+
#
|
33
|
+
def clause_valid?
|
34
|
+
if @clause.is_a?(Hash)
|
35
|
+
@clause.flatten.find do |e|
|
36
|
+
return true unless (/\basc\b|\bdesc\b/).match(e).nil?
|
37
|
+
end
|
38
|
+
else
|
39
|
+
return true unless @clause.split.empty?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Parses a hash clause
|
44
|
+
#
|
45
|
+
# * *Returns* :
|
46
|
+
# - A ReQON formatted array
|
47
|
+
# * *Examples* :
|
48
|
+
# @clause = { test: "asc"}
|
49
|
+
# @clause.parse
|
50
|
+
# => ["$order_by", ["$asc", "test"]]
|
51
|
+
#
|
52
|
+
def parse_hash
|
53
|
+
direction = clause.values.first
|
54
|
+
field = clause.keys.first.to_s
|
55
|
+
["$order_by", ["$#{direction}", field]]
|
56
|
+
end
|
57
|
+
|
58
|
+
# Parses a string clause, defaults direction to asc if missing or invalid
|
59
|
+
#
|
60
|
+
# * *Returns* :
|
61
|
+
# - A ReQON formatted array
|
62
|
+
# * *Examples* :
|
63
|
+
# @clause = "happy_trees desc"
|
64
|
+
# @clause.parse
|
65
|
+
# => ["$order_by", ["$desc", "happy_trees"]]
|
66
|
+
#
|
67
|
+
def parse_string
|
68
|
+
direction = clause.split[1]
|
69
|
+
field = clause.split[0]
|
70
|
+
direction = "asc" unless %w(asc desc).include?(direction)
|
71
|
+
["$order_by", ["$#{direction}", field]]
|
72
|
+
end
|
73
|
+
|
74
|
+
# Determines clause datatype and triggers the correct parsing
|
75
|
+
#
|
76
|
+
def parse
|
77
|
+
@clause.is_a?(Hash) ? parse_hash : parse_string
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
require 'montage/errors'
|
2
|
+
require 'montage/support'
|
3
|
+
require 'json'
|
4
|
+
require 'montage/operators'
|
5
|
+
|
6
|
+
module Montage
|
7
|
+
class QueryParser
|
8
|
+
include Montage::Support
|
9
|
+
|
10
|
+
attr_reader :query
|
11
|
+
|
12
|
+
TYPE_MAP = {
|
13
|
+
is_i?: :to_i,
|
14
|
+
is_f?: :to_f,
|
15
|
+
is_s?: :to_s
|
16
|
+
}
|
17
|
+
|
18
|
+
# Creates a QueryParser instance based on a query argument. The instance
|
19
|
+
# can then be parsed into a ReQON compatible array and used as a filter
|
20
|
+
# for queries
|
21
|
+
#
|
22
|
+
# * *Args* :
|
23
|
+
# - +query+ -> A hash or string that includes the database column name, a
|
24
|
+
# logical operator, and the associated value
|
25
|
+
# * *Returns* :
|
26
|
+
# - A Montage::QueryParser instance
|
27
|
+
#
|
28
|
+
def initialize(query)
|
29
|
+
@query = query
|
30
|
+
end
|
31
|
+
|
32
|
+
# Parse the column name from the specific query part
|
33
|
+
#
|
34
|
+
# * *Args* :
|
35
|
+
# - +part+ -> The query string
|
36
|
+
# - +splitter+ -> The value to split on
|
37
|
+
# * *Returns* :
|
38
|
+
# - A string value for the provided column name
|
39
|
+
# * *Examples* :
|
40
|
+
# @part = "bobross = 'amazing'"
|
41
|
+
# get_column_name(@part, " ")
|
42
|
+
# => "bobross"
|
43
|
+
#
|
44
|
+
def get_column_name(part, splitter = " ")
|
45
|
+
part.downcase.split(splitter)[0].strip
|
46
|
+
end
|
47
|
+
|
48
|
+
# Grabs the proper query operator from the string
|
49
|
+
#
|
50
|
+
# * *Args* :
|
51
|
+
# - +part+ -> The query string
|
52
|
+
# * *Returns* :
|
53
|
+
# - An array containing the supplied logical operator and the Montage
|
54
|
+
# equivalent
|
55
|
+
# * *Examples* :
|
56
|
+
# @part = "tree_happiness_level > 9"
|
57
|
+
# get_query_operator(@part)
|
58
|
+
# => [">", "__gt"]
|
59
|
+
#
|
60
|
+
def get_query_operator(part)
|
61
|
+
operator = Montage::Operators.find_operator(part)
|
62
|
+
[operator.operator, operator.montage_operator]
|
63
|
+
end
|
64
|
+
|
65
|
+
# Extract the condition set from the given clause
|
66
|
+
#
|
67
|
+
# * *Args* :
|
68
|
+
# - +clause+ -> The query string
|
69
|
+
# - +splitter+ -> The logical operator to split on
|
70
|
+
# * *Returns* :
|
71
|
+
# - The value portion of the query
|
72
|
+
# * *Examples* :
|
73
|
+
# @part = "tree_happiness_level > 9"
|
74
|
+
# parse_condition_set(@part, "<")
|
75
|
+
# => "9"
|
76
|
+
#
|
77
|
+
def parse_condition_set(clause, splitter = " ")
|
78
|
+
clause.split(/#{splitter}/i)[-1].strip
|
79
|
+
end
|
80
|
+
|
81
|
+
# Parse a single portion of the query string. String values representing
|
82
|
+
# a float or interger are coerced into actual numerical values. Newline
|
83
|
+
# characters are removed and single quotes are replaced with double quotes
|
84
|
+
#
|
85
|
+
# * *Args* :
|
86
|
+
# - +part+ -> The value element extracted from the query string
|
87
|
+
# * *Returns* :
|
88
|
+
# - A parsed form of the value element
|
89
|
+
# * *Examples* :
|
90
|
+
# @part = "9"
|
91
|
+
# parse_part(@part)
|
92
|
+
# => 9
|
93
|
+
#
|
94
|
+
def parse_part(part)
|
95
|
+
parsed_part = JSON.parse(part) rescue part
|
96
|
+
|
97
|
+
if is_i?(parsed_part)
|
98
|
+
parsed_part.to_i
|
99
|
+
elsif is_f?(parsed_part)
|
100
|
+
parsed_part.to_f
|
101
|
+
elsif parsed_part =~ /\(.*\)/
|
102
|
+
to_array(parsed_part)
|
103
|
+
elsif parsed_part.is_a?(Array)
|
104
|
+
parsed_part
|
105
|
+
else
|
106
|
+
parsed_part.gsub(/('|')/, "")
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Get all the parts of the query string
|
111
|
+
#
|
112
|
+
# * *Args* :
|
113
|
+
# - +str+ -> The query string
|
114
|
+
# * *Returns* :
|
115
|
+
# - An array containing the column name, the montage operator, and the
|
116
|
+
# value for comparison.
|
117
|
+
# * *Raises* :
|
118
|
+
# - +QueryError+ -> When incomplete queries or queries without valid
|
119
|
+
# operators are initialized
|
120
|
+
# * *Examples* :
|
121
|
+
# @part = "tree_happiness_level > 9"
|
122
|
+
# get_parts(@part)
|
123
|
+
# => ["tree_happiness_level", "__gt", 9]
|
124
|
+
#
|
125
|
+
def get_parts(str)
|
126
|
+
operator, montage_operator = get_query_operator(str)
|
127
|
+
|
128
|
+
fail QueryError, "Invalid Montage query operator!" unless montage_operator
|
129
|
+
|
130
|
+
column_name = get_column_name(str, operator)
|
131
|
+
|
132
|
+
fail QueryError, "Your query has an undetermined error" unless column_name
|
133
|
+
|
134
|
+
value = parse_part(parse_condition_set(str, operator))
|
135
|
+
|
136
|
+
[column_name, montage_operator, value]
|
137
|
+
end
|
138
|
+
|
139
|
+
# Parse a hash type query
|
140
|
+
#
|
141
|
+
# * *Returns* :
|
142
|
+
# - A ReQON compatible array
|
143
|
+
# * *Examples* :
|
144
|
+
# @test = Montage::QueryParser.new(tree_status: "happy")
|
145
|
+
# @test.parse_hash
|
146
|
+
# => [["tree_status", "happy"]]
|
147
|
+
#
|
148
|
+
def parse_hash
|
149
|
+
query.map do |key, value|
|
150
|
+
new_key = value.is_a?(Array) ? "#{key}__in" : key
|
151
|
+
["#{new_key}", value]
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Parse a string type query. Splits multiple conditions on case insensitive
|
156
|
+
# "and" strings that do not fall within single quotations. Note that the
|
157
|
+
# Montage equals operator is supplied as a blank string
|
158
|
+
#
|
159
|
+
# * *Returns* :
|
160
|
+
# - A ReQON compatible array
|
161
|
+
# * *Examples* :
|
162
|
+
# @test = Montage::QueryParser.new("tree_happiness_level > 9")
|
163
|
+
# @test.parse_string
|
164
|
+
# => [["tree_happiness_level", ["$__gt", 9]]]
|
165
|
+
#
|
166
|
+
def parse_string
|
167
|
+
query.split(/\band\b(?=(?:[^']|'[^']*')*$)/i).map do |part|
|
168
|
+
column_name, operator, value = get_parts(part)
|
169
|
+
if operator == ""
|
170
|
+
["#{column_name}", value]
|
171
|
+
else
|
172
|
+
["#{column_name}", ["$#{operator}", value]]
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Determines query datatype and triggers the correct parsing
|
178
|
+
#
|
179
|
+
def parse
|
180
|
+
if query.is_a?(Hash)
|
181
|
+
parse_hash
|
182
|
+
else
|
183
|
+
parse_string
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Takes a string value and splits it into an array
|
188
|
+
# Will coerce all values into the type of the first type
|
189
|
+
#
|
190
|
+
# * *Args* :
|
191
|
+
# - +value+ -> A string value
|
192
|
+
# * *Returns* :
|
193
|
+
# - A array form of the value argument
|
194
|
+
# * *Examples* :
|
195
|
+
# @part = "(1, 2, 3)"
|
196
|
+
# to_array(@part)
|
197
|
+
# => [1, 2, 3]
|
198
|
+
#
|
199
|
+
def to_array(value)
|
200
|
+
values = value.gsub(/('|\(|\))/, "").split(',')
|
201
|
+
type = [:is_i?, :is_f?].find(Proc.new { :is_s? }) { |t| send(t, values.first) }
|
202
|
+
values.map { |v| v.send(TYPE_MAP[type]) }
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
data/lib/montage/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-montage
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- dphaener
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-02-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -214,7 +214,8 @@ files:
|
|
214
214
|
- lib/montage/operators/not.rb
|
215
215
|
- lib/montage/operators/not_in.rb
|
216
216
|
- lib/montage/query.rb
|
217
|
-
- lib/montage/
|
217
|
+
- lib/montage/query/order_parser.rb
|
218
|
+
- lib/montage/query/query_parser.rb
|
218
219
|
- lib/montage/resource.rb
|
219
220
|
- lib/montage/resources.rb
|
220
221
|
- lib/montage/resources/document.rb
|
data/lib/montage/query_parser.rb
DELETED
@@ -1,116 +0,0 @@
|
|
1
|
-
require 'montage/errors'
|
2
|
-
require 'montage/support'
|
3
|
-
require 'json'
|
4
|
-
require 'montage/operators'
|
5
|
-
|
6
|
-
module Montage
|
7
|
-
class QueryParser
|
8
|
-
include Montage::Support
|
9
|
-
|
10
|
-
attr_reader :query
|
11
|
-
|
12
|
-
TYPE_MAP = {
|
13
|
-
is_i?: :to_i,
|
14
|
-
is_f?: :to_f,
|
15
|
-
is_s?: :to_s
|
16
|
-
}
|
17
|
-
|
18
|
-
def initialize(query)
|
19
|
-
@query = query
|
20
|
-
end
|
21
|
-
|
22
|
-
# Parse the column name from the specific query part
|
23
|
-
#
|
24
|
-
def get_column_name(part, splitter = " ")
|
25
|
-
part.downcase.split(splitter)[0].strip
|
26
|
-
end
|
27
|
-
|
28
|
-
# Grabs the proper query operator from the string
|
29
|
-
#
|
30
|
-
def get_query_operator(part)
|
31
|
-
operator = Montage::Operators.find_operator(part)
|
32
|
-
[operator.operator, operator.montage_operator]
|
33
|
-
end
|
34
|
-
|
35
|
-
# Extract the condition set from the given clause
|
36
|
-
#
|
37
|
-
def parse_condition_set(clause, splitter = " ")
|
38
|
-
clause.split(/#{splitter}/i)[-1].strip
|
39
|
-
end
|
40
|
-
|
41
|
-
# Parse a single portion of the query string
|
42
|
-
#
|
43
|
-
def parse_part(part)
|
44
|
-
parsed_part = JSON.parse(part) rescue part
|
45
|
-
|
46
|
-
if is_i?(parsed_part)
|
47
|
-
parsed_part.to_i
|
48
|
-
elsif is_f?(parsed_part)
|
49
|
-
parsed_part.to_f
|
50
|
-
elsif parsed_part =~ /\(.*\)/
|
51
|
-
to_array(parsed_part)
|
52
|
-
elsif parsed_part.is_a?(Array)
|
53
|
-
parsed_part
|
54
|
-
else
|
55
|
-
parsed_part.gsub(/('|')/, "")
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
# Get all the parts of the query string
|
60
|
-
#
|
61
|
-
def get_parts(str)
|
62
|
-
operator, montage_operator = get_query_operator(str)
|
63
|
-
|
64
|
-
raise QueryError, "The operator you have used is not a valid Montage query operator" unless montage_operator
|
65
|
-
|
66
|
-
column_name = get_column_name(str, operator)
|
67
|
-
|
68
|
-
raise QueryError, "Your query has an undetermined error" unless column_name
|
69
|
-
|
70
|
-
value = parse_part(parse_condition_set(str, operator))
|
71
|
-
|
72
|
-
[column_name, montage_operator, value]
|
73
|
-
end
|
74
|
-
|
75
|
-
# Parse a hash type query
|
76
|
-
#
|
77
|
-
def parse_hash
|
78
|
-
Hash[
|
79
|
-
query.map do |key, value|
|
80
|
-
new_key = value.is_a?(Array) ? "#{key}__in".to_sym : key
|
81
|
-
[new_key, value]
|
82
|
-
end
|
83
|
-
]
|
84
|
-
end
|
85
|
-
|
86
|
-
# Parse a string type query
|
87
|
-
#
|
88
|
-
def parse_string
|
89
|
-
Hash[
|
90
|
-
query.split(/\band\b(?=([^']*'[^']*')*[^']*$)/i).map do |part|
|
91
|
-
column_name, operator, value = get_parts(part)
|
92
|
-
["#{column_name}#{operator}".to_sym, value]
|
93
|
-
end
|
94
|
-
]
|
95
|
-
end
|
96
|
-
|
97
|
-
# Parse the clause into a Montage query
|
98
|
-
#
|
99
|
-
def parse
|
100
|
-
if query.is_a?(Hash)
|
101
|
-
parse_hash
|
102
|
-
else
|
103
|
-
parse_string
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
# Takes a string value and splits it into an array
|
108
|
-
# Will coerce all values into the type of the first type
|
109
|
-
#
|
110
|
-
def to_array(value)
|
111
|
-
values = value.gsub(/('|\(|\))/, "").split(',')
|
112
|
-
type = [:is_i?, :is_f?].find(Proc.new { :is_s? }) { |t| send(t, values.first) }
|
113
|
-
values.map { |v| v.send(TYPE_MAP[type]) }
|
114
|
-
end
|
115
|
-
end
|
116
|
-
end
|