# encoding: UTF-8 # frozen_string_literal: true require "openssl" require "net/http" require "json" require "uri" module Websy # https://lecklider.com/2016/09/decorating-ruby-s-net-http-for-fun-and-profit.html # class HttpDecorator # Timeouts OPEN_TIMEOUT = 10 # in seconds READ_TIMEOUT = 120 # in seconds # Content-types CONTENT_TYPE_JSON = "application/json" CONTENT_TYPE_FORM = "application/x-www-form-urlencoded" # CONTENT_TYPE_MULTIPART = "multipart/form-data; boundary=#{ Rack::Multipart::MULTIPART_BOUNDARY }" def initialize(url) if Websy.token.nil? raise AuthenticationError.new("No token provided") end # Build up our HTTP object uri = URI(url) @http = Net::HTTP.new(uri.hostname, uri.port) @http.use_ssl = true if uri.scheme == "https" @http.verify_mode = OpenSSL::SSL::VERIFY_NONE if uri.scheme == "https" @http.open_timeout = OPEN_TIMEOUT @http.read_timeout = READ_TIMEOUT # In local development we can log requests and responses to $stdout. # DO NOT EVER do this in production. EVER. if ENV["RACK_ENV"] == "development" @http.set_debug_output($stdout) end end # Open a connection for multiple calls. # - Accepts a block, otherwise just opens the connection. # - You'll need to close the connection if you just open it. def start if block_given? # Open the connection. @http.start unless @http.started? # Yield to the calling block. yield(self) # Clean up the connection. @http.finish if @http.started? else # Open the connection. @http.start unless @http.started? end end # Clean up the connection if needed. def finish @http.finish if @http.started? end # GET def get(path, **params) uri = URI.parse(path) uri.query = URI.encode_www_form(params) unless params.empty? request = Net::HTTP::Get.new(uri.to_s) parse fetch(request) end # POST def post(path, as: :json, **params) request = Net::HTTP::Post.new(path) case as when :json request.content_type = CONTENT_TYPE_JSON request.body = JSON.generate(params) unless params.empty? else request.content_type = CONTENT_TYPE_FORM request.body = URI.encode_www_form(params) unless params.empty? end parse fetch(request) end # DELETE def delete(path) request = Net::HTTP::Delete.new(path) parse fetch(request) end # PATCH def patch(path, as: :form, **params) request = Net::HTTP::Patch.new(path) case as when :json request.content_type = CONTENT_TYPE_JSON request.body = JSON.generate(params) unless params.empty? else request.content_type = CONTENT_TYPE_FORM request.body = URI.encode_www_form(params) unless params.empty? end parse fetch(request) end # PUT def put(path, as: :json, **params) request = Net::HTTP::Put.new(path) case as when :json request.content_type = CONTENT_TYPE_JSON request.body = JSON.generate(params) unless params.empty? else request.content_type = CONTENT_TYPE_FORM request.body = URI.encode_www_form(params) unless params.empty? end parse fetch(request) end # POST multipart def multipart(path, **params) request = Net::HTTP::Post.new(path) request.content_type = CONTENT_TYPE_MULTIPART request.body = Rack::Multipart::Generator.new( "file" => Rack::Multipart::UploadedFile.new(params["file"][:tempfile].path, params["file"][:type]) ).dump parse fetch(request) end private # Perform the request. def fetch(request) # Shore up default headers for the request. request["Accept"] = CONTENT_TYPE_JSON request["Connection"] = "keep-alive" request["User-Agent"] = "Websy Ruby v#{Websy::VERSION}" request["Authorization"] = "Token token=#{Websy.token}" # Actually make the request. response = @http.request(request) # Net::HTTPResponse.value will raise an error for non-200 responses. # Simpler than trying to detect every possible exception. response.value || response end def parse(response) # Parse the response as JSON if possible. if response.content_type == CONTENT_TYPE_JSON JSON.parse(response.body) # Otherwise just return the response body. else response.body end end end end