module CloudQuery
class Client
attr_reader :account
attr_writer :secret
# Create a new instance of the client
#
# It's highly recommended to set options :account
# and :secret. Creating a client without an account
# and secret isn't very useful.
#
# ==== Acceptable options:
# :account => (default => nil)
# :secret => (default => nil)
# :document_id_method => (default => nil)
# :secure => Boolean (use HTTPS, default => true)
#
# If +document_id_method+ is set, it will be called on each
# document as a part of +add_documents+ and +update_documents+
# which should inject an '#.#' key-value pair as a
# simple way to tie app PKs to doc ids.
def initialize(options={})
# unless options[:account] && options[:secret]
# raise "Client requires :account => and :secret => "
# end
@account = options[:account]
@secret = options[:secret]
@secure = options[:secure] != false # must pass false for insecure
@document_id_method = options[:document_id_method]
end
## Account management
# Retrieve the API secret for an +account+, using the +password+ (uses HTTPS)
def self.get_secret(account, password)
auth = Request.new(:path => "#{PATH}/auth")
curl = Curl::Easy.new(auth.url) do |c|
c.enable_cookies = true
c.cookiejar = COOKIE_JAR
end
params = Rack::Utils.build_query({"name" => account, "password" => password})
curl.http_post(params)
if (curl.response_code/100) == 2
curl.url = Request.new(:path => "#{PATH}/#{API_PATHS[:account]}/#{account}").url
curl.http_get
response = JSON.parse(curl.body_str)
response['result']['secret']
else
STDERR.puts "Error: #{curl.response_code} #{Rack::Utils::HTTP_STATUS_CODES[curl.response_code]}"
end
end
# change password
# optionally change the secret
def self.change_password(account, old_password, new_password, new_secret=nil)
secret = get_secret(account, old_password)
c = Client.new(:account => account, :secret => secret, :secure => true)
a = c.get_account()['result']
a['password'] = new_password
a.delete('secret')
if new_secret != nil
a['secret'] = new_secret
end
c.update_account(a)
return (new_secret != nil ? new_secret : secret);
end
# Get the account document
def get_account
send_request get(account_path)
end
# Update the account document.
#
# Use this method to change the API secret:
# update_account({'secret' => 'your-new-secret'})
def update_account(account_doc={})
body = JSON.generate(account_doc)
send_request put(account_path, body)
end
# Delete the account.
#
# ==== BEWARE: THIS WILL ACTUALLY DELETE YOUR ACCOUNT.
def delete_account!
send_request delete(account_path)
end
## Schema management
# Add a schema to the account. +xml+ can be a +String+
# or +File+-like (responds to :read)
def add_schema(xml)
body = xml.respond_to?(:read) ? xml.read : xml
request = post(build_path(API_PATHS[:schema]), body)
send_request(request, CONTENT_TYPES[:xml])
end
# Delete a schema from the account, by name
# if cascade is true, all documents with the specified schema will be deleted
# (use with care)
def delete_schema(schema_name, cascade=nil)
c = cascade ? {:cascade => 'true'} : nil
send_request delete(build_path(
API_PATHS[:schema],
Rack::Utils.escape("$.name:\"#{schema_name}\"")
), c)
end
# Get the schemas for the account.
#
# NOTE: returned format is not the same as accepted for input
def get_schemas
send_request get(build_path(API_PATHS[:schema]))
end
## Index management
# Add one or more indexes to the account, by name or id
def add_indexes(*indexes)
body = JSON.generate(indexes.flatten)
send_request post(build_path(API_PATHS[:indexes]), body)
end
# Delete one or more indexes from the account, by name or id
# indexes = '*' will delete all indexes
def delete_indexes(*indexes)
indexes = url_pipe_join(indexes)
send_request delete(build_path(API_PATHS[:indexes], indexes))
end
# Get the indexes from the account. Returns a list of ids
def get_indexes
send_request get(build_path(API_PATHS[:indexes]))
end
## Document management
# Add documents to the specified +index+
#
# index = name or +id+, docs = {} or +Array+ of {}.
#
# Documents with key '#.#' and an existing value will be updated.
#
# If +schemas+ is not +nil+, ensures existence of the
# specified schemas on each document.
def add_documents(index, docs, schemas=[], fieldmode=nil)
fm = fieldmode != nil ? {:fieldmode => fieldmode} : nil
request = post(
build_path(API_PATHS[:documents], index, url_pipe_join(schemas), fm),
JSON.generate(identify_documents(docs))
)
send_request request
end
# Update documents in the specified +index+
# index = name or +id+, docs = {} or +Array+ of {}.
#
# Documents lacking the key '#.#' will be created.
#
# If +schemas+ is not +nil+, ensures existence of the
# specified schemas on each document.
def update_documents(index, docs, schemas=[], fieldmode=nil)
fm = fieldmode != nil ? {:fieldmode => fieldmode} : nil
request = put(
build_path(API_PATHS[:documents], index, url_pipe_join(schemas), fm),
JSON.generate(identify_documents(docs))
)
send_request request
end
# Modify documents in the +index+ matching +query+
#
# modifications = {...data...} to update all matching
# documents.
#
# If +schemas+ is not +nil+, ensures existence of the
# specified schemas on each document.
def modify_documents(index, query, modifications, schemas=[], fieldmode=nil)
fm = fieldmode != nil ? "?fieldmode=#{fieldmode}" : ""
request = put(
build_path(API_PATHS[:documents], index, url_pipe_join(schemas), Rack::Utils.escape(query), fm),
JSON.generate(modifications)
)
send_request request
end
# Delete documents in the +index+ matching +query+
#
# query => defaults to '*'
# index => may be an id, index name, or Array of ids or names.
#
# Operates on all indexes if +index+ = +nil+ or '*'
#
# ==== BEWARE: If +query+ = +nil+ this will delete ALL documents in +index+.
#
# ==== Acceptable options:
# :sort => a string ("[+|-]schema.field"), or a list thereof (default => index-order)
# :offset => integer offset into the result set (default => 0)
# :limit => integer limit on number of documents returned per index (default => )
#
# If +schemas+ is not +nil+, ensures existence of the
# specified schemas on each document.
def delete_documents(index, query, options={}, schemas=[])
if options[:sort]
options[:sort] = Array(options[:sort]).flatten.join(',')
end
request = delete(
build_path(API_PATHS[:documents],
url_pipe_join(index),
url_pipe_join(schemas),
Rack::Utils.escape(query)
),
options
)
send_request request
end
# Get documents matching +query+
#
# query => defaults to '*'
# index => may be an id, index name, or Array of ids or names.
#
# Operates on all indexes if +index+ = +nil+ or '*'
#
# ==== Acceptable options:
# :fields => a field name, a prefix match (e.g. 'trans*'), or Array of fields (default => '*')
# :sort => a string ("[+|-]schema.field"), or a list thereof (default => index-order)
# :offset => integer offset into the result set (default => 0)
# :limit => integer limit on number of documents returned per index (default => )
# :fieldmode => 'short' or 'long'/nil, if 'short' then field names will be returned in their
# short form (no schema name prefix), this can cause naming collisions depending
# on use schema definition and data etc.
#
# If +schemas+ is not +nil+, ensures existence of the
# specified schemas on each document.
def get_documents(index, query, options={}, schemas=[])
if fields = options.delete(:fields)
fields = url_pipe_join(fields)
end
if options[:sort]
options[:sort] = Array(options[:sort]).flatten.join(',')
end
request = get(
build_path(API_PATHS[:documents],
url_pipe_join(index),
url_pipe_join(schemas),
url_pipe_join(query),
fields
),
options
)
send_request request
end
# Count documents matching +query+
#
# query => defaults to '*'
# index => may be an id, index name, or Array of ids or names.
#
# Operates on all indexes if +index+ = +nil+ or '*'
#
# If +schemas+ is not +nil+, ensures existence of the
# specified schemas on each document.
def count_documents(index, query, schemas)
get_documents(index, query, {:fields => '@count'}, schemas)
end
private
def build_path(*path_elements)
path_elements.flatten.compact.unshift(PATH).join('/')
end
def account_path
build_path(API_PATHS[:account], @account)
end
def build_request(options={})
Request.new default_request_params.merge(options)
end
def get(path, params={})
build_request(:method => 'GET', :path => path, :params => params)
end
def delete(path, params={})
build_request(:method => 'DELETE', :path => path, :params => params)
end
def post(path, doc, params={})
build_request(:method => 'POST', :path => path, :body => doc, :params => params)
end
def put(path, doc, params={})
build_request(:method => 'PUT', :path => path, :body => doc, :params => params)
end
def default_request_params
{
:account => @account,
:secret => @secret,
:scheme => @secure ? 'https' : 'http',
}
end
def send_request(request, content_type=nil)
response = execute_request(request.method, request.url, request.headers, request.body, content_type)
status_code = response.first
if (200..299).include?(status_code)
begin
result = JSON.parse(response.last)
rescue JSON::ParserError => e
result = {"REASON" => e.message}
end
else
result = {"REASON" => "Error: #{status_code} #{Rack::Utils::HTTP_STATUS_CODES[status_code]}"}
end
result.merge!({'STATUS' => status_code})
end
def execute_request(method, url, headers, body, content_type=nil)
content_type ||= CONTENT_TYPES[:json]
curl = Curl::Easy.new(url) do |c|
c.headers = headers
c.headers['Content-Type'] = content_type
c.encoding = 'gzip'
end
case method
when 'GET'
curl.http_get
when 'DELETE'
curl.http_delete
when 'POST'
curl.http_post(body)
when 'PUT'
curl.http_put(body)
end
[curl.response_code, curl.header_str, curl.body_str]
end
def url_pipe_join(arr, default_value='*')
arr = Array(arr).flatten
if arr.empty?
default_value
else
Rack::Utils.escape(arr.join('|'))
end
end
def identify_documents(docs)
[docs] if docs.is_a?(Hash)
if @document_id_method
docs.each { |d| d.send(@document_id_method) }
end
docs
end
end
end