ruby-montage 0.5.1 → 1.0.0
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 +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
|
[](https://codeclimate.com/github/EditLLC/ruby-montage)
|
2
2
|
[](https://circleci.com/gh/EditLLC/ruby-montage/tree/master)
|
3
3
|
[](http://codecov.io/github/EditLLC/ruby-montage?branch=master)
|
4
|
+
[](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
|