Creates new API bindings instance @param [Hash] config API bindings configuration @option config [String] :uri base URL of the server @option config [String] :username username to access the API @option config [String] :password username to access the API @option config [Hash] :oauth options to access API using OAuth
* *:consumer_key* (String) OAuth key * *:consumer_secret* (String) OAuth secret * *:options* (Hash) options passed to OAuth
@option config [AbstractCredentials] :credentials object implementing {AbstractCredentials}
interface e.g. {https://github.com/theforeman/hammer-cli-foreman/blob/master/lib/hammer_cli_foreman/credentials.rb HammerCLIForeman::BasicCredentials} This is prefered way to pass credentials. Credentials acquired form :credentials object take precedence over explicite params
@option config [Hash] :headers additional headers to send with the requests @option config [String] :api_version ('1') version of the API @option config [String] :language prefered locale for the API description @option config [String] :apidoc_cache_base_dir ('~/.cache/apipie_bindings') base
directory for building apidoc_cache_dir
@option config [String] :apidoc_cache_dir (apidoc_cache_base_dir+'/<URI>') where
to cache the JSON description of the API
@option config [String] :apidoc_cache_name ('default.json') name of te cache file.
If there is cache in the :apidoc_cache_dir, it is used.
@option config [String] :apidoc_authenticated (true) whether or not does the call to
obtain API description use authentication. It is useful to avoid unnecessary prompts for credentials
@option config [Hash] :fake_responses ({}) responses to return if used in dry run mode @option config [Bool] :dry_run (false) dry run mode allows to test your scripts
and not touch the API. The results are taken form exemples in the API description or from the :fake_responses
@option config [Bool] :aggressive_cache_checking (false) check before every request
if the local cache of API description (JSON) is up to date. By default it is checked *after* each API request
@option config [Object] :logger (Logger.new(STDERR)) custom logger class @option config [Number] :timeout API request timeout in seconds, use -1 to disable timeout @option config [Symbol] :follow_redirects (:default) Possible values are :always, :never and :default.
The :default is to only redirect in GET and HEAD requests (RestClient default)
@option config [AbstractAuthenticator] :authenticator API request authenticator @param [Hash] options params
that are passed to ResClient as-is @raise
[ApipieBindings::ConfigurationError] when no :uri
or
:apidoc_cache_dir
is provided @example connect to a server
ApipieBindings::API.new({:uri => 'http://localhost:3000/', :username => 'admin', :password => 'changeme', :api_version => '2', :aggressive_cache_checking => true})
@example connect with a local API description
ApipieBindings::API.new({:apidoc_cache_dir => 'test/unit/data', :apidoc_cache_name => 'architecture'})
# File lib/apipie_bindings/api.rb, line 58 def initialize(config, options={}) if config[:uri].nil? && config[:apidoc_cache_dir].nil? raise ApipieBindings::ConfigurationError.new('Either :uri or :apidoc_cache_dir needs to be set') end @uri = config[:uri] @api_version = config[:api_version] || 1 @language = config[:language] apidoc_cache_base_dir = config[:apidoc_cache_base_dir] || File.join(File.expand_path('~/.cache'), 'apipie_bindings') @apidoc_cache_dir = config[:apidoc_cache_dir] || File.join(apidoc_cache_base_dir, @uri.tr(':/', '_'), "v#{@api_version}") @apidoc_cache_name = config[:apidoc_cache_name] || set_default_name @apidoc_authenticated = (config[:apidoc_authenticated].nil? ? true : config[:apidoc_authenticated]) @follow_redirects = config.fetch(:follow_redirects, :default) @dry_run = config[:dry_run] || false @aggressive_cache_checking = config[:aggressive_cache_checking] || false @fake_responses = config[:fake_responses] || {} @logger = config[:logger] unless @logger @logger = Logger.new(STDERR) @logger.level = Logger::ERROR end config = config.dup headers = { :content_type => 'application/json', :accept => "application/json;version=#{@api_version}" } headers.merge!({ "Accept-Language" => language }) if language headers.merge!(config[:headers]) unless config[:headers].nil? headers.merge!(options.delete(:headers)) unless options[:headers].nil? log.debug "Global headers: #{inspect_data(headers)}" log.debug "Follow redirects: #{@follow_redirects.to_s}" if config[:authenticator] @authenticator = config[:authenticator] else @authenticator = legacy_authenticator(config) end if (config[:timeout] && config[:timeout].to_i < 0) config[:timeout] = (RestClient.version < '1.7.0') ? -1 : nil end # RestClient < 1.7.0 does not support ssl_ca_path use ssl_ca_file instead if options[:ssl_ca_path] && !RestClient::Request.method_defined?(:ssl_opts) parsed_uri = URI.parse(@uri) cert_file = File.join(options[:ssl_ca_path], "#{parsed_uri.host}.pem") if File.exist?(cert_file) options[:ssl_ca_file] = cert_file log.warn "ssl_ca_path is not supported by RestClient. ssl_ca_file = #{cert_file} was used instead." end end @resource_config = { :timeout => config[:timeout], :headers => headers, :verify_ssl => true }.merge(options) @config = config end
# File lib/apipie_bindings/api.rb, line 137 def apidoc @apidoc = @apidoc || load_apidoc || retrieve_apidoc @apidoc end
# File lib/apipie_bindings/api.rb, line 122 def apidoc_cache_file File.join(@apidoc_cache_dir, "#{@apidoc_cache_name}#{cache_extension}") end
Call an action in the API. It finds most fitting route based on given parameters with other attributes neccessary to do an API call. If in #dry_run mode {#initialize} it finds fake response data in examples or user provided data. At the end when the response format is JSON it is parsed and returned as ruby objects. If server supports checksum sending the internal cache with API description is checked and updated if needed @param [Symbol] resource_name name of the resource @param [Symbol] action_name name of the action @param [Hash] params parameters to be send in the request @param [Hash] headers extra headers to be sent with the request @param [Hash] options options to influence the how the call is processed
* *:response* (Symbol) *:raw* - skip parsing JSON in response * *:with_authentication* (Bool) *true* - use rest client with/without auth configuration * *:skip_validation* (Bool) *false* - skip validation of parameters
@example show user data
call(:users, :show, :id => 1)
# File lib/apipie_bindings/api.rb, line 178 def call(resource_name, action_name, params={}, headers={}, options={}) check_cache if @aggressive_cache_checking resource = resource(resource_name) action = resource.action(action_name) action.validate!(params) unless options[:skip_validation] options[:fake_response] = find_match(fake_responses, resource_name, action_name, params) || action.examples.first if dry_run? call_action(action, params, headers, options) end
# File lib/apipie_bindings/api.rb, line 188 def call_action(action, params={}, headers={}, options={}) route = action.find_route(params) return http_call( route.method, route.path(params), params.reject { |par, _| route.params_in_path.include? par.to_s }, headers, options) end
# File lib/apipie_bindings/api.rb, line 270 def check_cache begin response = http_call('get', "/apidoc/apipie_checksum", {}, { :accept => "application/json" }) response['checksum'] rescue nil end end
# File lib/apipie_bindings/api.rb, line 265 def clean_cache @apidoc = nil Dir["#{@apidoc_cache_dir}/*#{cache_extension}"].each { |f| File.delete(f) } end
# File lib/apipie_bindings/api.rb, line 133 def clear_credentials @authenticator.clear if @authenticator && @authenticator.respond_to?(:clear) end
# File lib/apipie_bindings/api.rb, line 142 def dry_run? @dry_run ? true : false end
# File lib/apipie_bindings/api.rb, line 146 def has_resource?(name) apidoc[:docs][:resources].has_key? name end
Low level call to the API. Suitable for calling
actions not covered by apipie documentation. For all other cases use
{#call} @param [String] http_method one of get
,
put
, post
, destroy
,
patch
@param [String] path URL path that should be called
@param [Hash] params parameters to be send in the request @param [Hash]
headers extra headers to be sent with the request @param [Hash] options
options to influence the how the call is processed
* *:response* (Symbol) *:raw* - skip parsing JSON in response * *:reduce_response_log* (Bool) - do not show response content in the log. * *:with_authentication* (Bool) *true* - use rest client with/without auth configuration
@example show user data
http_call('get', '/api/users/1')
# File lib/apipie_bindings/api.rb, line 209 def http_call(http_method, path, params={}, headers={}, options={}) headers ||= { } args = [http_method] if %w[post put].include?(http_method.to_s) #If using multi-part forms, the paramaters should not be json if ((headers.include?(:content_type)) and (headers[:content_type] == "multipart/form-data")) args << params else args << params.to_json end else headers[:params] = params if params end log.info "Server: #{@uri}" log.info "#{http_method.to_s.upcase} #{path}" log.debug "Params: #{inspect_data(params)}" log.debug "Headers: #{inspect_data(headers)}" args << headers if headers if dry_run? empty_response = ApipieBindings::Example.new('', '', '', 200, '') ex = options[:fake_response ] || empty_response response = create_fake_response(ex.status, ex.response, http_method, URI.join(@uri || 'http://example.com', path).to_s, args) else apidoc_without_auth = (path =~ /\/apidoc\//) && !@apidoc_authenticated authenticate = options[:with_authentication].nil? ? !apidoc_without_auth : options[:with_authentication] begin client = authenticate ? authenticated_client : unauthenticated_client response = call_client(client, path, args) update_cache(response.headers[:apipie_checksum]) rescue => e log.error e.message log.debug inspect_data(e) override_e = @authenticator.error(e) if authenticate && @authenticator raise override_e.is_a?(StandardError) ? override_e : e end end result = options[:response] == :raw ? response : process_data(response) log.debug "Response: %s" % (options[:reduce_response_log] ? "Received OK" : inspect_data(result)) log.debug "Response headers: #{inspect_data(response.headers)}" if response.respond_to?(:headers) result end
# File lib/apipie_bindings/api.rb, line 126 def load_apidoc check_cache if @aggressive_cache_checking if File.exist?(apidoc_cache_file) JSON.parse(File.read(apidoc_cache_file), :symbolize_names => true) end end
# File lib/apipie_bindings/api.rb, line 303 def log @logger end
# File lib/apipie_bindings/api.rb, line 150 def resource(name) ApipieBindings::Resource.new(name, self) end
List available resources @return [Array<ApipieBindings::Resource>]
# File lib/apipie_bindings/api.rb, line 156 def resources apidoc[:docs][:resources].keys.map { |res| resource(res) } end
# File lib/apipie_bindings/api.rb, line 279 def retrieve_apidoc FileUtils.mkdir_p(@apidoc_cache_dir) unless File.exists?(@apidoc_cache_dir) if language response = retrieve_apidoc_call("/apidoc/v#{@api_version}.#{language}.json", :safe => true) language_family = language.split('_').first if !response && language_family != language response = retrieve_apidoc_call("/apidoc/v#{@api_version}.#{language_family}.json", :safe => true) end end unless response begin response = retrieve_apidoc_call("/apidoc/v#{@api_version}.json") rescue Exception => e raise ApipieBindings::DocLoadingError.new( "Could not load data from #{@uri}: #{e.message}\n" " - is your server down?\n" " - was rake apipie:cache run when using apipie cache? (typical production settings)", e) end end File.open(apidoc_cache_file, "w") { |f| f.write(response.body) } log.debug "New apidoc loaded from the server" load_apidoc end
# File lib/apipie_bindings/api.rb, line 257 def update_cache(cache_name) if !cache_name.nil? && (cache_name != @apidoc_cache_name) clean_cache log.debug "Cache expired. (#{@apidoc_cache_name} -> #{cache_name})" @apidoc_cache_name = cache_name end end
# File lib/apipie_bindings/api.rb, line 367 def authenticated_client resource_config = @resource_config.dup resource_config[:authenticator] = get_authenticator log.debug "Using authenticator: #{resource_config[:authenticator].name}" RestClient::Resource.new(@config[:uri], resource_config) end
# File lib/apipie_bindings/api.rb, line 406 def cache_extension language ? ".#{language}.json" : ".json" end
# File lib/apipie_bindings/api.rb, line 322 def call_client(client, path, args) block = rest_client_call_block client[path].send(*args, &block) end
# File lib/apipie_bindings/api.rb, line 450 def create_fake_request(args) RestClient::Request.new(args) end
# File lib/apipie_bindings/api.rb, line 435 def create_fake_response(status, body, method, path, args=[]) request_args = {:method => args.shift || method, :url => path} request_args[:params] = args.shift if %w[post put].include?(method.to_s) request_args[:headers] = args.shift net_http_resp = Net::HTTPResponse.new(1.0, status, "") if RestClient.version >= '2.0.0' RestClient::Response.create(body, net_http_resp, create_fake_request(request_args)) elsif RestClient.version >= '1.8.0' RestClient::Response.create(body, net_http_resp, request_args, create_fake_request(request_args)) else RestClient::Response.create(body, net_http_resp, request_args) end end
# File lib/apipie_bindings/api.rb, line 358 def exception_with_response(response) begin klass = RestClient::Exceptions::EXCEPTIONS_MAP.fetch(response.code) rescue KeyError raise RestClient.RequestFailed.new(response, response.code) end raise klass.new(response, response.code) end
# File lib/apipie_bindings/api.rb, line 394 def find_match(fakes, resource, action, params) resource = fakes[[resource, action]] if resource if resource.has_key?(params) return resource[params] elsif resource.has_key?(:default) return resource[:default] end end return nil end
# File lib/apipie_bindings/api.rb, line 380 def get_authenticator raise ApipieBindings::AuthenticatorMissingError if @authenticator.nil? @authenticator end
# File lib/apipie_bindings/api.rb, line 428 def inspect_data(obj) ApipieBindings::Utils.inspect_data(obj.respond_to?(:response) ? process_data(obj.response) : obj) rescue => e log.debug "Error during inspecting response: #{e.message}" '' end
# File lib/apipie_bindings/api.rb, line 309 def legacy_authenticator(config) if config[:user] || config[:username] || config[:password] Authenticators::BasicAuth.new(config[:user] || config[:username], config[:password]) elsif config[:credentials] && config[:credentials].respond_to?(:to_params) log.warn("Credentials are now deprecated, use custom authenticator instead.") Authenticators::CredentialsLegacy.new(config[:credentials]) elsif config[:oauth] log.warn("Passing oauth credentials in hash is now deprecated, use oauth authenticator instead.") oauth = config[:oauth] Authenticators::Oauth.new(oauth[:consumer_key], oauth[:consumer_secret], oauth[:options]) end end
# File lib/apipie_bindings/api.rb, line 419 def process_data(response) data = begin JSON.parse((response.respond_to?(:body) ? response.body : response) || '') rescue JSON::ParserError response.respond_to?(:body) ? response.body : response || '' end return data end
# File lib/apipie_bindings/api.rb, line 327 def rest_client_call_block Proc.new do |response, request, result, &block| # include request for rest_client < 1.8.0 response.request ||= request request.args[:authenticator].response(response) if request && request.args[:authenticator] if [301, 302, 307].include?(response.code) && [:always, :never].include?(@follow_redirects) if @follow_redirects == :always log.debug "Response redirected to #{response.headers[:location]}" if response.method(:follow_redirection).arity == 0 # rest-client > 1.8 response.follow_redirection(&block) else # rest-client <= 1.8 response.follow_redirection(request, result, &block) end else raise exception_with_response(response) end else if response.method(:return!).arity.zero? # 2.0.0+ response.return!(&block) else response.return!(request, result, &block) end end end end
# File lib/apipie_bindings/api.rb, line 385 def retrieve_apidoc_call(path, options={}) begin http_call('get', path, {}, {:accept => "application/json"}, {:response => :raw, :reduce_response_log => true}) rescue raise unless options[:safe] end end
# File lib/apipie_bindings/api.rb, line 410 def set_default_name(default='default') cache_file = Dir["#{@apidoc_cache_dir}/*#{cache_extension}"].first if cache_file File.basename(cache_file, cache_extension) else default end end
# File lib/apipie_bindings/api.rb, line 376 def unauthenticated_client @client_without_auth ||= RestClient::Resource.new(@config[:uri], @resource_config) end