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