require 'errors' require 'faraday' require 'faraday_middleware' require 'uri' require 'helix_web_services_client/version' require 'json' # The client object handles authenticating and making calls to Helix Web Services. # # See our user guide online at: # https://swarm.workshop.perforce.com/projects/perforce-software-helix-web-services/view/main/build/doc/p4ws.html#ruby_client_sdk_overview class HelixWebServicesClient # The Helix Versioning Engine login attr_accessor :user # Upon successful login, we store the P4 ticket return value here. attr_accessor :ticket # Typically, Helix Web Services is mounted under /hws behind a reverse proxy. # If a path is specified in the originating URL, we save the prefix here, # and preprend it to every request. attr_accessor :prefix # The API level to use. Defaults to 78 (2015.1) attr_accessor :api_level # Some values to initialize are only used by this class, and are not passed # on to the Faraday initializer INITIALIZE_LOCAL_OPTIONS = [:user, :password, :ticket, :prefix, :api_level, :settings, :debug] # Client initialization can handle ensuring a valid security token to the # server. # # Any client created via new should take care to call `close()`. # # ## Available Options # # These options are used to configure the underlying Faraday connection: # # * `:url` - String base URL. # * `:params` - Hash of URI query unencoded key/value pairs. # * `:header` - Hash of unencoded HTTP header key/value pairs. # * `:request` - Hash of request options. # * `:ssl` - Hash of SSL options. # * `:proxy` - Hash of Proxy options. # # These options are specific to Helix Web Services: # # * `:user` - The Helix Versioning Engine login # * `:password` - If set, we will generate a ticket using this password during initialization # * `:ticket` - If not nil, we will use this ticket as our authentication password # * `:debug` - Add response logging if set to true # # @param options [Hash] See the section available options above def initialize(options) @api_level = options.key?(:api_level) ? options[:api_level] : '78' # Filter out options we pass to Faraday faraday_options = options.select { |k| !INITIALIZE_LOCAL_OPTIONS.include?(k) } @conn = Faraday.new(faraday_options) do |conn| conn.request :multipart conn.request :url_encoded conn.response :logger if options[:debug] conn.adapter :net_http end @conn.headers['Accept'] = 'application/json' if options.key?(:url) url = URI(options[:url]) @prefix = url.path ? url.path : '' end @user = options[:user] if options.key?(:user) @ticket = options[:ticket] if options.key?(:ticket) if options.key?(:settings) options[:settings].each do |key, value| add_setting(key, value) end end if options.key?(:password) and @user response = @conn.post(path_for('/projects/v1/login'), user: user, password: options[:password]) assert_ok(response) @ticket = JSON.parse(response.body)['ticket'] end set_auth(user, ticket) unless ticket.nil? end def close end # Set an override for all requests. # # See "configuration" in HWS documentation. # # This will automatically add the right prefix for overriding an HWS setting. # # @param key {String} The setting value as indicated in documentation, e.g., `P4PORT` # @param value {String} The value to set def add_setting(key, value) # Note: Rack will automatically convert all hyphens to underscores... # but Nginx will (by default) block all underscores. Find a happy middle. @conn.headers["X-Perforce-Helix-Web-Services-#{key.gsub('_', '-')}"] = value end # Remove a setting added via add_setting def remove_setting(key) @conn.headers.delete("X-Perforce-Helix-Web-Services-#{key}") end # Return any of the X-Perforce... headers that are set def settings ret = {} @conn.headers.each { |key, value| match = /^X-Perforce-Helix-Web-Services-(.*)$/.match(key) ret[match[1]] = value unless match.nil? } return ret end # Provides standard I/O style interface, where when called in a block, # will automatically close() the client when done. Otherwise, your code # should call client.close() manually. def self.open(connection) client = Client.new(connection) if block_given? yield client else return client end ensure client.close if block_given? && client end # Note: this class is really just common implementation. Methods are # generally defined in other files that reopen this class. def set_auth(user, token) @conn.basic_auth(user, token) end def p4_ticket?(str) /^[a-zA-Z0-9]{32,}$/.match(str) != nil end # Runs the method against Helix Web Services, checks for errors, then parses # the JSON response. # # @param method [Symbol] HTTP method, for example, :get, :post, :delete # @param path [String] URL path part (no URI parameters) for the method # @param params [Hash] URI parameters to send in def execute_method_no_body(method, path, params = nil) response = run_method_no_body(method, path, params) JSON.parse(response.body) if response.body && !response.body.empty? end # Runs the method against Helix Web Services, checks for errors, then parses # the JSON response. # # This variation will send the body (expected to be a Hash) as JSON to the # server. # # @param method [Symbol] HTTP method, for example, :get, :post, :delete # @param path [String] URL path part (no URI parameters) for the method # @param params [Hash] URI parameters to send in # @param body [Hash] The Request content (which will be converted to JSON) def execute_method_with_body(method, path, params = nil, body = nil) response = run_method_with_body(method, path, params, body) JSON.parse(response.body) if response.body && !response.body.empty? end def run_method_no_body(method, path, params = nil) path = path_for(path) response = @conn.send(method, path, params) assert_ok(response) response end def run_method_with_body(method, path, params = nil, body = nil) if !body && params body = params params = nil end path = path_for(path) if params params_hash = Faraday::Utils::ParamsHash.new params_hash.merge!(params) path += "?#{params_hash.to_query}" end response = @conn.send(method, path, body) assert_ok(response) response end # Basically just prepends the prefix to our subpath, typically, '/p4'. def path_for(subpath) if @prefix.nil? || @prefix.empty? subpath elsif subpath.nil? or subpath.empty? @prefix else File.join(@prefix, subpath) end end # Raises an error when the response is not 200. Some errors may have # diagnostic information in the response body, so we pass that on as well def assert_ok(response) return unless response.status >= 400 if response.status == 400 begin messages = JSON.parse(response.body) rescue Exception messages = response.body end fail Errors::BadRequest.new(messages) elsif response.status == 403 fail Errors::Unauthenticated.new, 'Illegal login or password' elsif response.status == 404 fail Errors::ResourceNotFound.new, 'Required resource not found' elsif response.status == 500 && response.body messages = nil begin messages = JSON.parse(response.body) rescue Exception => e messages = response.body end fail Errors::PerforceProblem.new(messages), 'Unknown issue from the Perforce server' else fail Errors::ServerError.new, "Unknown problem. Response code: #{response.status}" end end def hve_path(subpath) "/p4/v#{api_level}/#{subpath}" end # Return the product version ID of the Helix Web Services instance def version response = run_method_no_body(:get, '/status') response.headers['X-Helix-Web-Services-Version'] end end require 'helix_web_services_client/branches' require 'helix_web_services_client/changes' require 'helix_web_services_client/clients' require 'helix_web_services_client/commands' require 'helix_web_services_client/config' require 'helix_web_services_client/counters' require 'helix_web_services_client/depots' require 'helix_web_services_client/files' require 'helix_web_services_client/groups' require 'helix_web_services_client/helix_sync' require 'helix_web_services_client/jobs' require 'helix_web_services_client/labels' require 'helix_web_services_client/login' require 'helix_web_services_client/projects' require 'helix_web_services_client/protections' require 'helix_web_services_client/servers' require 'helix_web_services_client/streams' require 'helix_web_services_client/triggers' require 'helix_web_services_client/users' require 'helix_web_services_client/git_fusion_repo' require 'helix_web_services_client/git_fusion_keys' require 'git_fusion_strings'