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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c2251111d886899ecbf95641a1985d604ec569d6
4
- data.tar.gz: f63b762b828e1200f86fc62cbddde07aada43181
3
+ metadata.gz: 7e3dfa2aad01ef822edd6bc91a7b316694912d0f
4
+ data.tar.gz: 08a3fc32a12b85d0144b8be7855c01cdd2a2d4b6
5
5
  SHA512:
6
- metadata.gz: 2bcd8a615fda2c6052a44aa8cb6c4b4155570784c39932a78dfc77dea55e257f5a30346e52dd529f6eed2a1db2171e6a8e07d393d922d337cf484b6c0c734c1c
7
- data.tar.gz: 2009c3a8c71ecdbd8ac699a2c4c618ee1b431d692d665c97968ac67ba20d7c924560fe3c5d81a3f28ce0902959b11effaa0a9a9bddb49a8e395cb440f011652c
6
+ metadata.gz: 4b7a79943fbca4d1ca5b7bcf1abc72e9383881a6def914937dc9c6aad289e396a45d3458c39ee6620dcf68829ee05fe37f13ff7b8d96d1c353c36e3665448454
7
+ data.tar.gz: a1cc37f0d95359cefaab29a32a6b51fcacb717df19e486df23c1780651a684d743330415d595a6a5f02d6526daeebe3d8441a575a39c7367e919161fd75e0759
data/.gitignore CHANGED
@@ -12,3 +12,5 @@
12
12
  tmp.json
13
13
  ruby-montage-*.gem
14
14
  /build
15
+ *.swp
16
+ *.scratch
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
@@ -8,5 +8,9 @@ Rake::TestTask.new do |t|
8
8
  t.verbose = true
9
9
  end
10
10
 
11
+ task :console do
12
+ exec 'pry -r montage -I ./lib'
13
+ end
14
+
11
15
  desc "Run tests"
12
- task :default => :test
16
+ task :default => :test
data/bin/console CHANGED
@@ -9,6 +9,3 @@ require "montage"
9
9
  # (If you use this, don't forget to add pry to your Gemfile!)
10
10
  require "pry"
11
11
  Pry.start
12
-
13
- require "irb"
14
- IRB.start
@@ -13,22 +13,61 @@ module Montage
13
13
  include Schemas
14
14
  include Documents
15
15
 
16
- attr_accessor :token, :username, :password, :domain, :api_version, :url_prefix
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
- raise MissingAttributeError, "You must declare the domain attribute" unless @domain
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
- def default_url_prefix
25
- "https://#{domain}.mntge.com"
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
- def content_type
29
- "application/json"
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
- def build_response(resource_name, &block)
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.url_prefix = "#{url_prefix || default_url_prefix}/api/v#{api_version}/"
105
- f.headers["User-Agent"] = "Montage Ruby v#{Montage::VERSION}"
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
- # Params:
7
- # schema - *Required* The name of the schema to run the query against
8
- # query - *Optional* A Montage::Query object to pass along with the request
9
- #
10
- # Returns a Montage::Response
11
- #
12
- def documents(schema, query = {})
13
- post("schemas/#{schema}/query/", "document", query)
14
- end
15
-
16
- # Public: Fetch a document
17
- #
18
- # Params:
19
- # schema - *Required* The name of the schema to fetch the document from
20
- # document_uuid - *Required* The uuid of the document to fetch
21
- #
22
- # Returns a Montage::Response
23
- #
24
- def document(schema, document_uuid)
25
- get("schemas/#{schema}/#{document_uuid}/", "document")
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
@@ -1,5 +1,7 @@
1
1
  module Montage
2
+ class ClauseFormatError < StandardError; end
3
+ class InvalidAttributeFormat < StandardError; end
4
+ class InvalidEnvironment < StandardError; end
2
5
  class MissingAttributeError < StandardError; end
3
-
4
6
  class QueryError < StandardError; end
5
7
  end
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 :query
11
+ attr_accessor :options
12
+ attr_reader :schema
11
13
 
12
- def initialize
13
- @query = { filter: {} }
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
- # Defines the limit to apply to the query, defaults to nil
17
- #
18
- # Merges a hash:
19
- # { limit: 10 }
42
+ # Validates the Montage::Query schema attribute
20
43
  #
21
- # Returns self
44
+ # * *Returns* :
45
+ # - A boolean
22
46
  #
23
- def limit(max = nil)
24
- clone.tap { |r| r.query.merge!(limit: max) }
47
+ def schema_valid?
48
+ @schema.is_a?(String) && @schema.index(/\W+/).nil?
25
49
  end
26
50
 
27
- # Defines the offset to apply to the query, defaults to nil
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
- # Merges a hash:
30
- # { offset: 10 }
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 a copy of self
59
+ # * *Returns* :
60
+ # - The updated array
33
61
  #
34
- def offset(value = nil)
35
- clone.tap { |r| r.query.merge!(offset: value) }
36
- end
62
+ def merge_array(query_param)
63
+ arr = options["$query"]
64
+ position = arr.index(arr.assoc(query_param[0]))
37
65
 
38
- # Defines the order clause for the query and merges it into the query hash
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
- direction = clause.split(" ")[1]
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
- # Parses the SQL string passed into the method
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
- # Returns a hash:
74
- # parse_string_clause("foo <= 1")
75
- # => { foo__lte: 1.0 }
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
- # Adds a where clause to the query filter hash
87
+ # Defines the offset to apply to the query, defaults to nil
79
88
  #
80
- # Accepts either a Hash or a String
81
- # where(foo: 1)
82
- # where("foo > 1")
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
- # Merges a hash:
85
- # { foo: 1 }
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
- # Returns a copy of self
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.query[:filter].merge!(QueryParser.new(clause).parse) }
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.query.merge!(pluck: args.map(&:to_s))}
153
+ clone.tap { |r| r.merge_array(["$pluck", args.map(&:to_s)]) }
97
154
  end
98
155
 
99
- # Specifies and index to use on a query. RethinkDB isn't as smart as some other
100
- # database engines when selecting a query plan, but it does let you specify
101
- # which index to use
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.query.merge!(index: field) }
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.query.merge!(pluck: [column_name.to_s]) }
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
- @query.to_json
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
@@ -1,3 +1,3 @@
1
1
  module Montage
2
- VERSION = "0.5.1"
2
+ VERSION = "1.0.0"
3
3
  end
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.5.1
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: 2015-08-04 00:00:00.000000000 Z
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/query_parser.rb
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
@@ -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