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