diff options
| author | Luc Donnet | 2015-03-23 21:12:56 +0100 | 
|---|---|---|
| committer | Luc Donnet | 2015-03-23 21:12:56 +0100 | 
| commit | fa7e745459aefd64086869882fcca73f948b46fa (patch) | |
| tree | a5df17d4498b4ca612f6398156a667d2593cc76e | |
| parent | 0740decc6a2c5117d1dc89e3665774460626f86b (diff) | |
| download | chouette-core-fa7e745459aefd64086869882fcca73f948b46fa.tar.bz2 | |
Change ruby client for iev server
| -rw-r--r-- | Gemfile | 1 | ||||
| -rw-r--r-- | Gemfile.lock | 4 | ||||
| -rw-r--r-- | app/controllers/imports_controller.rb | 10 | ||||
| -rw-r--r-- | app/helpers/imports_helper.rb | 22 | ||||
| -rw-r--r-- | app/models/import.rb | 43 | ||||
| -rw-r--r-- | app/models/import_service.rb | 10 | ||||
| -rw-r--r-- | app/views/imports/_import.erb | 2 | ||||
| -rw-r--r-- | app/views/imports/_imports.html.erb | 4 | ||||
| -rw-r--r-- | lib/iev_api.rb | 23 | ||||
| -rw-r--r-- | lib/iev_api/client.rb | 105 | ||||
| -rw-r--r-- | lib/iev_api/configuration.rb | 59 | ||||
| -rw-r--r-- | lib/iev_api/middleware/custom_parser.rb | 37 | ||||
| -rw-r--r-- | lib/iev_api/middleware/raise_response_error.rb | 13 | ||||
| -rw-r--r-- | lib/iev_api/middleware/raise_server_error.rb | 18 | ||||
| -rw-r--r-- | lib/ievkit.rb | 33 | ||||
| -rw-r--r-- | lib/ievkit/arguments.rb | 14 | ||||
| -rw-r--r-- | lib/ievkit/authentication.rb | 70 | ||||
| -rw-r--r-- | lib/ievkit/client.rb | 307 | ||||
| -rw-r--r-- | lib/ievkit/client/jobs.rb | 49 | ||||
| -rw-r--r-- | lib/ievkit/configurable.rb | 85 | ||||
| -rw-r--r-- | lib/ievkit/default.rb | 149 | ||||
| -rw-r--r-- | lib/ievkit/error.rb | 241 | ||||
| -rw-r--r-- | lib/ievkit/response/raise_error.rb | 28 | ||||
| -rw-r--r-- | lib/ievkit/version.rb | 17 | ||||
| -rw-r--r-- | spec/models/import_service_spec.rb | 3 | 
25 files changed, 1043 insertions, 304 deletions
| @@ -39,6 +39,7 @@ gem 'spring', group: :development  gem "sitemap_generator"  # API Rest +gem 'sawyer'  gem 'faraday', '~> 0.9.1'  gem 'faraday_middleware', '~> 0.9.1'  gem 'kleisli' diff --git a/Gemfile.lock b/Gemfile.lock index 5e5f6874c..b17fc91d7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -384,6 +384,9 @@ GEM        sass (~> 3.2.2)        sprockets (~> 2.8, < 3.0)        sprockets-rails (~> 2.0) +    sawyer (0.6.0) +      addressable (~> 2.3.5) +      faraday (~> 0.8, < 0.10)      sdoc (0.4.1)        json (~> 1.7, >= 1.7.7)        rdoc (~> 4.0) @@ -531,6 +534,7 @@ DEPENDENCIES    rspec-rails (~> 3.1.0)    rubyzip (~> 1.1.6)    sass-rails (~> 4.0.3) +  sawyer    sdoc (~> 0.4.0)    simple_form (~> 3.1.0)    sitemap_generator diff --git a/app/controllers/imports_controller.rb b/app/controllers/imports_controller.rb index 2af6a57d6..927c188e2 100644 --- a/app/controllers/imports_controller.rb +++ b/app/controllers/imports_controller.rb @@ -17,7 +17,7 @@ class ImportsController < ChouetteController        index! do           build_breadcrumb :index        end -    rescue IevApi::IevError => error +    rescue Ievkit::Error => error        logger.error("Iev failure : #{error.message}")        flash[:error] = t('iev.failure')        redirect_to referential_path(@referential) @@ -29,7 +29,7 @@ class ImportsController < ChouetteController        show! do           build_breadcrumb :show        end -    rescue IevApi::IevError => error +    rescue Ievkit::Error => error        logger.error("Iev failure : #{error.message}")        flash[:error] = t('iev.failure')        redirect_to referential_path(@referential) @@ -41,7 +41,7 @@ class ImportsController < ChouetteController        new! do           puts "OK"        end -    rescue IevApi::IevError => error +    rescue Ievkit::Error => error        logger.error("Iev failure : #{error.message}")        flash[:error] = t('iev.failure')        redirect_to referential_path(@referential) @@ -53,7 +53,7 @@ class ImportsController < ChouetteController        create! do           puts "OK"        end -    rescue IevApi::IevError => error +    rescue Ievkit::Error => error        logger.error("Iev failure : #{error.message}")        flash[:error] = t('iev.failure')        redirect_to referential_path(@referential) @@ -66,7 +66,7 @@ class ImportsController < ChouetteController          import_service.delete(@import.id)          redirect_to referential_imports_path(@referential)              end -    rescue IevApi::IevError => error +    rescue Ievkit::Error => error        logger.error("Iev failure : #{error.message}")        flash[:error] = t('iev.failure')        redirect_to referential_path(@referential) diff --git a/app/helpers/imports_helper.rb b/app/helpers/imports_helper.rb index 772533a8e..498d03bf9 100644 --- a/app/helpers/imports_helper.rb +++ b/app/helpers/imports_helper.rb @@ -28,17 +28,17 @@ module ImportsHelper    def import_progress_bar_tag(import) -    if import.canceled? || import.aborted? -      div_class = "progress-bar progress-bar-danger" -    elsif import.scheduled? -      div_class = "progress-bar progress-bar-info" -    elsif import.created? -      div_class = "progress-bar progress-bar-info" -    elsif import.terminated? -      div_class = "progress-bar progress-bar-success" -    else -      div_class = "" -    end   +    # if import.canceled? || import.aborted? +    #   div_class = "progress-bar progress-bar-danger" +    # elsif import.scheduled? +    #   div_class = "progress-bar progress-bar-info" +    # elsif import.created? +    #   div_class = "progress-bar progress-bar-info" +    # elsif import.terminated? +    #   div_class = "progress-bar progress-bar-success" +    # else +       div_class = "" +    # end        content_tag :div, :class => "progress" do        content_tag :div, :class => div_class, role: "progressbar", :'aria-valuenow' => "#{import.percentage_progress}", :'aria-valuemin' => "0", :'aria-valuemax' => "100", :style => "width: #{import.percentage_progress}%;" do diff --git a/app/models/import.rb b/app/models/import.rb index 93388a6f5..2e1c86394 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -6,18 +6,16 @@ class Import    # enumerize :status, in: %w{created scheduled terminated canceled aborted}, default: "created", predicates: true    # enumerize :format, in: %w{neptune netex gtfs}, default: "neptune", predicates: true -  attr_reader :datas, :links, :headers, :errors +  attr_reader :datas -  def initialize( response  ) -    @datas = response[:datas] -    @headers = response[:headers] -    @links = response[:links] +  def initialize( response  )     +    @datas = response      # @status = @datas.status.downcase if @datas.status?      # @format = @datas.type.downcase if @datas.type?    end      def report -    report_path = links[:report] +    report_path = datas.links[:report]      if report_path              response = IevApi.request(:get, compliance_check_path, params)        ImportReport.new(response) @@ -27,7 +25,7 @@ class Import    end     def compliance_check -    compliance_check_path = links[:validation] +    compliance_check_path = datas.links[:validation]      if compliance_check_path        response = IevApi.request(:get, compliance_check_path, params)        ComplianceCheck.new(response) @@ -37,7 +35,7 @@ class Import    end    def delete -    delete_path =  links[:delete] +    delete_path =  datas.links[:delete]      if delete_path        IevApi.request(:delete, delete_path, params)      else @@ -46,7 +44,7 @@ class Import    end    def cancel -    cancel_path =  links[:cancel] +    cancel_path =  datas.links[:cancel]      if cancel_path        IevApi.request(:delete, cancel_path, params)      else @@ -55,7 +53,7 @@ class Import    end    def id -    @datas.id +    datas.id    end    def status @@ -67,7 +65,7 @@ class Import    end    def filename -    @datas.filename +    datas.filename    end    def filename_extension @@ -85,43 +83,40 @@ class Import    end    def referential_name -    @datas.referential +    datas.referential    end    def name -    @datas.parameters.name -  end - -  def user_name? -    @datas.parameters? && @datas.parameters.user_name? +    datas.action_parameters.name    end    def user_name -    @datas.parameters.user_name if user_name? +     +    datas.action_parameters.user_name    end    def no_save -    @datas.parameters.no_save +    datas.action_parameters.no_save    end    def filename -    @datas.filename +    datas.filename    end    def created_at? -    @datas.created? +    datas.created?    end    def created_at -    Time.at(@datas.created.to_i / 1000) if created_at? +    Time.at(datas.created.to_i / 1000) if created_at?    end    def updated_at? -    @datas.updated? +    datas.updated?    end    def updated_at -    Time.at(@datas.updated.to_i / 1000) if updated_at? +    Time.at(datas.updated.to_i / 1000) if updated_at?    end  end diff --git a/app/models/import_service.rb b/app/models/import_service.rb index 567cd9e3e..2e3c1012b 100644 --- a/app/models/import_service.rb +++ b/app/models/import_service.rb @@ -8,14 +8,16 @@ class ImportService    # Find an import whith his id    def find(id) -    Import.new( IevApi.scheduled_job(referential.slug, id, { :action => "importer" }) ) +    Import.new( Ievkit.scheduled_job(referential.slug, id, { :action => "importer" }) )    end    # Find all imports    def all -    IevApi.jobs(referential.slug, { :action => "importer" }).map do |import_hash| -      Import.new( import_hash  ) +    [].tap do |jobs| +      Ievkit.jobs(referential.slug, { :action => "importer" }).each do |job| +        jobs << Import.new( job ) +      end      end    end - +    end diff --git a/app/views/imports/_import.erb b/app/views/imports/_import.erb index 8c3da0320..cbed386f5 100644 --- a/app/views/imports/_import.erb +++ b/app/views/imports/_import.erb @@ -20,7 +20,7 @@    <div class="panel-footer">      <%= import_progress_bar_tag(import) %>          <div class="history"> -      <%= l import.created_at, :format => "%d/%m/%Y %H:%M" %> | <%= import.user_name %> +      <%= import.created_at %> | <%= import.user_name %>      </div>    </div>  </div> diff --git a/app/views/imports/_imports.html.erb b/app/views/imports/_imports.html.erb index 51d1c02b2..d4c7e0b9e 100644 --- a/app/views/imports/_imports.html.erb +++ b/app/views/imports/_imports.html.erb @@ -1,8 +1,8 @@  <div class="page_info">    <span class="search"> <%= t("will_paginate.page_entries_info.search") %></span> <%= page_entries_info @imports %>  </div> -<div class="imports paginated_content"> -  <%= paginated_content @imports %> +<div class="imports paginated_content">   +  <%= paginated_content @imports, "imports/import" %>  </div>  <div class="pagination">    <%= will_paginate @imports, :container => false, renderer: RemoteBootstrapPaginationLinkRenderer %> diff --git a/lib/iev_api.rb b/lib/iev_api.rb deleted file mode 100644 index 77a10d1ac..000000000 --- a/lib/iev_api.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'iev_api/configuration' - -module IevApi -  extend Configuration - -  class IevError < StandardError; end - -  def self.client(options={}) -    IevApi::Client.new(options) -  end - -   # Delegate to Instapaper::Client -  def self.method_missing(method, *args, &block) -    return super unless client.respond_to?(method) -    client.send(method, *args, &block) -  end - -  def self.respond_to?(method, include_private = false) -    client.respond_to?(method, include_private) || super(method, include_private) -  end -end - -require 'iev_api/client' diff --git a/lib/iev_api/client.rb b/lib/iev_api/client.rb deleted file mode 100644 index 1994b1027..000000000 --- a/lib/iev_api/client.rb +++ /dev/null @@ -1,105 +0,0 @@ -module IevApi -  class Client - -    PER_PAGE = 12 -    #PARALLEL_WORKERS = 10 -    ACTIONS = %w{ importer exporter validator } -    IMPORT_FORMAT = %w{ neptune netex gtfs } -    EXPORT_FORMAT = %w{ neptune netex gtfs hub kml } - -    attr_accessor *IevApi::Configuration::VALID_OPTIONS_KEYS -     -    def initialize(options={}) -      attrs = IevApi.options.merge(options) -      IevApi::Configuration::VALID_OPTIONS_KEYS.each do |key| -        send("#{key}=", attrs[key]) -      end -    end - -    def url_for(endpoint, *args) -      path = case endpoint.to_s -             when 'jobs' then jobs_path(args) -             when 'scheduled_job' then scheduled_job_path(args)                -             when 'terminated_job' then terminated_job_path(args) -             else raise ArgumentError.new("Unrecognized path: #{path}") -             end - -      [account_path, path.split('.').first].join('') -    end - -    def jobs(referential_id, options = {}) -      results = request(:get, jobs_path(referential_id), options) -    end - -    def jobs_path(referential_id) -      "referentials/#{referential_id}/jobs" -    end -     -    def scheduled_job(referential_id, job_id, options = {}) -      results = request(:get, scheduled_job_path(referential_id, job_id), options)     -    end - -    def scheduled_job_path(referential_id, job_id) -      "referentials/#{referential_id}/scheduled_jobs/#{job_id}" -    end - -    def terminated_job(referential_id, job_id, options = {}) -      results = request(:get, terminated_job_path(referential_id, job_id), options)     -    end    - -    def terminated_job_path(referential_id, job_id) -      "referentials/#{referential_id}/terminated_jobs/#{job_id}" -    end     - -    def account_path -      "#{protocol}://#{Rails.application.config.iev_url}"       -    end - -    def protocol -      @secure ? "https" : "http" -    end -     -    # Perform an HTTP request -    def request(method, path, params = {}, options = {}) - -      response = connection(options).run_request(method, nil, nil, nil) do |request| -        case method -        when :delete, :get -          request.url(path, params) -        when :post, :put -          request.url(path) -          request.body = params unless params.empty? -        end -      end -       -      response.body -    end - -    def connection(options={}) -      default_options = { -        :headers => { -          :accept => 'application/json', -          :user_agent => user_agent, -        }, -        :ssl => {:verify => false}, -        :url => account_path, -      } - -      @connection ||= Faraday.new(default_options.deep_merge(options)) do |builder| -        middleware.each { |mw| builder.use *mw } - -        builder.adapter adapter -      end - -      # cache_dir = File.join(ENV['TMPDIR'] || '/tmp', 'cache') - -      # @connection.response :caching do   -      #   ActiveSupport::Cache::FileStore.new cache_dir, :namespace => 'iev', -      #   :expires_in => 3600  # one hour -      # end -       -      @connection       -    end -     -  end   -end diff --git a/lib/iev_api/configuration.rb b/lib/iev_api/configuration.rb deleted file mode 100644 index b445da795..000000000 --- a/lib/iev_api/configuration.rb +++ /dev/null @@ -1,59 +0,0 @@ -module IevApi -  module Configuration -    VALID_OPTIONS_KEYS = [ -      :account, -      :auth_token, -      :secure, -      :connection_options, -      :adapter, -      :user_agent, -      :middleware] - -    attr_accessor *VALID_OPTIONS_KEYS - -    DEFAULT_ADAPTER     = :net_http -    DEFAULT_USER_AGENT  = "IEV Ruby Gem Api" -    DEFAULT_CONNECTION_OPTIONS = {} -    DEFAULT_MIDDLEWARE  = [ -                           Faraday::Request::UrlEncoded, -                           IevApi::Middleware::RaiseResponseError, -                           Faraday::Request::Multipart, -                           FaradayMiddleware::Mashify, -                           #FaradayMiddleware::Caching, -                           FaradayMiddleware::FollowRedirects, -                           FaradayMiddleware::ParseJson, -                           IevApi::Middleware::RaiseServerError, -                           IevApi::Middleware::CustomParser -                          ] - -    def self.extended(base) -      base.reset -    end - -    def configure(options={}) -      @account    = options[:account] if options.has_key?(:account) -      @auth_token = options[:auth_token] if options.has_key?(:auth_token) -      @secure     = options[:secure] if options.has_key?(:secure) -      @middleware = options[:middleware] if options.has_key?(:middleware) -      yield self if block_given? -      self -    end - -    def options -      options = {} -      VALID_OPTIONS_KEYS.each{|k| options[k] = send(k)} -      options -    end - -    def reset -      @account    = nil -      @auth_token = nil -      @secure     = false -      @adapter    = DEFAULT_ADAPTER -      @user_agent = DEFAULT_USER_AGENT -      @connection_options = DEFAULT_CONNECTION_OPTIONS -      @middleware = DEFAULT_MIDDLEWARE -    end - -  end -end diff --git a/lib/iev_api/middleware/custom_parser.rb b/lib/iev_api/middleware/custom_parser.rb deleted file mode 100644 index 82cf0d563..000000000 --- a/lib/iev_api/middleware/custom_parser.rb +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding: utf-8 -*- -# Expects responses like: -# -#     { -#       "result": { "id": 1, "name": "Tobias Fünke" }, -#       "errors": [] -#     } -# -require 'faraday' - -module IevApi -  module Middleware -    class CustomParser < Faraday::Response::Middleware -      def on_complete(env) -        body = env[:body] -        env[:body] = { -          datas: body, -          headers: env[:response_headers], -          links: process_links(env[:response_headers]) -        } -      end - -      # Finds link relations from 'Link' response header -      def process_links(headers) -        puts headers.inspect -        links = ( headers["Link"] || "" ).split(', ').map do |link| -          href, name = link.match(/<(.*?)>; rel="(\w+)"/).captures -           -          [name.to_sym, href] -        end -        puts links.inspect -        Hash[*links.flatten] -      end -       -    end -  end -end diff --git a/lib/iev_api/middleware/raise_response_error.rb b/lib/iev_api/middleware/raise_response_error.rb deleted file mode 100644 index e299e3410..000000000 --- a/lib/iev_api/middleware/raise_response_error.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'faraday' - -module IevApi -  module Middleware -    class RaiseResponseError < Faraday::Response::Middleware - -      def on_complete(env) -        raise IevError.new('No results found.') if env[:body].nil? -      end - -    end -  end -end diff --git a/lib/iev_api/middleware/raise_server_error.rb b/lib/iev_api/middleware/raise_server_error.rb deleted file mode 100644 index cb6f96f98..000000000 --- a/lib/iev_api/middleware/raise_server_error.rb +++ /dev/null @@ -1,18 +0,0 @@ -require 'faraday' - -module IevApi -  module Middleware -    class RaiseServerError < Faraday::Response::Middleware - -      def on_complete(env) -        case env[:status].to_i -        when 403 -          raise IevError.new('SSL should be enabled - use AirbrakeAPI.secure = true in configuration') -        when 404 -          raise IevError.new('No resource found') -        end -      end - -    end -  end -end diff --git a/lib/ievkit.rb b/lib/ievkit.rb new file mode 100644 index 000000000..7e47e9d84 --- /dev/null +++ b/lib/ievkit.rb @@ -0,0 +1,33 @@ +require 'ievkit/client' +require 'ievkit/default' + +# Ruby toolkit for the GitHub API +module Ievkit + +  class << self +    include Ievkit::Configurable + +    # API client based on configured options {Configurable} +    # +    # @return [Ievkit::Client] API wrapper +    def client +      @client = Ievkit::Client.new(options) unless defined?(@client) && @client.same_options?(options) +      @client +    end + +    # @private +    def respond_to_missing?(method_name, include_private=false); client.respond_to?(method_name, include_private); end if RUBY_VERSION >= "1.9" +    # @private +    def respond_to?(method_name, include_private=false); client.respond_to?(method_name, include_private) || super; end if RUBY_VERSION < "1.9" + +  private + +    def method_missing(method_name, *args, &block) +      return super unless client.respond_to?(method_name) +      client.send(method_name, *args, &block) +    end + +  end +end + +Ievkit.setup diff --git a/lib/ievkit/arguments.rb b/lib/ievkit/arguments.rb new file mode 100644 index 000000000..7f512eb25 --- /dev/null +++ b/lib/ievkit/arguments.rb @@ -0,0 +1,14 @@ +module Ievkit + +  # Extracts options from method arguments +  # @private +  class Arguments < Array +    attr_reader :options + +    def initialize(args) +      @options = args.last.is_a?(::Hash) ? args.pop : {} +      super(args) +    end + +  end +end diff --git a/lib/ievkit/authentication.rb b/lib/ievkit/authentication.rb new file mode 100644 index 000000000..666fcc8d8 --- /dev/null +++ b/lib/ievkit/authentication.rb @@ -0,0 +1,70 @@ +module Ievkit + +  # Authentication methods for {Octokit::Client} +  module Authentication + +    # Indicates if the client was supplied  Basic Auth +    # username and password +    # +    # @return [Boolean] +    def basic_authenticated? +      !!(@login && @password) +    end + +    # Indicates if the client was supplied an OAuth +    # access token +    # +    # @return [Boolean] +    def token_authenticated? +      !!@access_token +    end + +    # Indicates if the client was supplied an OAuth +    # access token or Basic Auth username and password +    # +    # @return [Boolean] +    def user_authenticated? +      basic_authenticated? || token_authenticated? +    end + +    # Indicates if the client has OAuth Application +    # client_id and secret credentials to make anonymous +    # requests at a higher rate limit +    # +    # @return Boolean +    def application_authenticated? +      !!application_authentication +    end + +    private + +    def application_authentication +      if @client_id && @client_secret +        { +          :client_id     => @client_id, +          :client_secret => @client_secret +        } +      end +    end + +    def login_from_netrc +      return unless netrc? + +      require 'netrc' +      info = Netrc.read netrc_file +      netrc_host = URI.parse(api_endpoint).host +      creds = info[netrc_host] +      if creds.nil? +        # creds will be nil if there is no netrc for this end point +        ievkit_warn "Error loading credentials from netrc file for #{api_endpoint}" +      else +        creds = creds.to_a +        self.login = creds.shift +        self.password = creds.shift +      end +    rescue LoadError +      ievkit_warn "Please install netrc gem for .netrc support" +    end + +  end +end diff --git a/lib/ievkit/client.rb b/lib/ievkit/client.rb new file mode 100644 index 000000000..53eb61074 --- /dev/null +++ b/lib/ievkit/client.rb @@ -0,0 +1,307 @@ +require 'sawyer' +require 'ievkit/arguments' +require 'ievkit/configurable' +require 'ievkit/client/jobs' + +module Ievkit + +  # Client for the Iev API   +  class Client +    +    include Ievkit::Configurable +    include Ievkit::Authentication +    include Ievkit::Client::Jobs + +    # Header keys that can be passed in options hash to {#get},{#head} +    CONVENIENCE_HEADERS = Set.new([:accept, :content_type]) + +    def initialize(options = {}) +      # Use options passed in, but fall back to module defaults +      Ievkit::Configurable.keys.each do |key| +        instance_variable_set(:"@#{key}", options[key] || Ievkit.instance_variable_get(:"@#{key}")) +      end +    end + +    # Compares client options to a Hash of requested options +    # +    # @param opts [Hash] Options to compare with current client options +    # @return [Boolean] +    def same_options?(opts) +      opts.hash == options.hash +    end + +    # Text representation of the client, masking tokens and passwords +    # +    # @return [String] +    def inspect +      inspected = super + +      # mask password +      inspected = inspected.gsub! @password, "*******" if @password +      # Only show last 4 of token, secret +      if @access_token +        inspected = inspected.gsub! @access_token, "#{'*'*36}#{@access_token[36..-1]}" +      end +      if @client_secret +        inspected = inspected.gsub! @client_secret, "#{'*'*36}#{@client_secret[36..-1]}" +      end + +      inspected +    end + +    # Make a HTTP GET request +    # +    # @param url [String] The path, relative to {#api_endpoint} +    # @param options [Hash] Query and header params for request +    # @return [Sawyer::Resource] +    def get(url, options = {}) +      request :get, url, parse_query_and_convenience_headers(options) +    end + +    # Make a HTTP POST request +    # +    # @param url [String] The path, relative to {#api_endpoint} +    # @param options [Hash] Body and header params for request +    # @return [Sawyer::Resource] +    def post(url, options = {}) +      request :post, url, options +    end + +    # Make a HTTP PUT request +    # +    # @param url [String] The path, relative to {#api_endpoint} +    # @param options [Hash] Body and header params for request +    # @return [Sawyer::Resource] +    def put(url, options = {}) +      request :put, url, options +    end + +    # Make a HTTP PATCH request +    # +    # @param url [String] The path, relative to {#api_endpoint} +    # @param options [Hash] Body and header params for request +    # @return [Sawyer::Resource] +    def patch(url, options = {}) +      request :patch, url, options +    end + +    # Make a HTTP DELETE request +    # +    # @param url [String] The path, relative to {#api_endpoint} +    # @param options [Hash] Query and header params for request +    # @return [Sawyer::Resource] +    def delete(url, options = {}) +      request :delete, url, options +    end + +    # Make a HTTP HEAD request +    # +    # @param url [String] The path, relative to {#api_endpoint} +    # @param options [Hash] Query and header params for request +    # @return [Sawyer::Resource] +    def head(url, options = {}) +      request :head, url, parse_query_and_convenience_headers(options) +    end + +    # Make one or more HTTP GET requests, optionally fetching +    # the next page of results from URL in Link response header based +    # on value in {#auto_paginate}. +    # +    # @param url [String] The path, relative to {#api_endpoint} +    # @param options [Hash] Query and header params for request +    # @param block [Block] Block to perform the data concatination of the +    #   multiple requests. The block is called with two parameters, the first +    #   contains the contents of the requests so far and the second parameter +    #   contains the latest response. +    # @return [Sawyer::Resource] +    def paginate(url, options = {}, &block) +      opts = parse_query_and_convenience_headers(options.dup) +      if @auto_paginate || @per_page +        opts[:query][:per_page] ||=  @per_page || (@auto_paginate ? 100 : nil) +      end + +      data = request(:get, url, opts) + +      if @auto_paginate +        while @last_response.rels[:next] && rate_limit.remaining > 0 +          @last_response = @last_response.rels[:next].get +          if block_given? +            yield(data, @last_response) +          else +            data.concat(@last_response.data) if @last_response.data.is_a?(Array) +          end +        end + +      end + +      data +    end + +    # Hypermedia agent for the Iev API +    # +    # @return [Sawyer::Agent] +    def agent +      @agent ||= Sawyer::Agent.new(api_endpoint, sawyer_options) do |http| +        http.headers[:accept] = default_media_type +        http.headers[:content_type] = "application/json" +        http.headers[:user_agent] = user_agent +         +        # Activate if authentication is needed +        # +        # if basic_authenticated? +        #   http.basic_auth(@login, @password) +        # elsif token_authenticated? +        #   http.authorization 'token', @access_token +        # elsif application_authenticated? +        #   http.params = http.params.merge application_authentication +        # end +      end +    end + +    # Fetch the root resource for the API +    # +    # @return [Sawyer::Resource] +    def root +      get "/" +    end + +    # Response for last HTTP request +    # +    # @return [Sawyer::Response] +    def last_response +      @last_response if defined? @last_response +    end + +    # Duplicate client using client_id and client_secret as +    # Basic Authentication credentials. +    # @example +    #   Ievkit.client_id = "foo" +    #   Ievkit.client_secret = "bar" +    # +    #   # GET https://api.github.com/?client_id=foo&client_secret=bar +    #   Ievkit.get "/" +    # +    #   Ievkit.client.as_app do |client| +    #     # GET https://foo:bar@api.github.com/ +    #     client.get "/" +    #   end +    def as_app(key = client_id, secret = client_secret, &block) +      if key.to_s.empty? || secret.to_s.empty? +        raise ApplicationCredentialsRequired, "client_id and client_secret required" +      end +      app_client = self.dup +      app_client.client_id = app_client.client_secret = nil +      app_client.login    = key +      app_client.password = secret + +      yield app_client if block_given? +    end + +    # Set username for authentication +    # +    # @param value [String] GitHub username +    def login=(value) +      reset_agent +      @login = value +    end + +    # Set password for authentication +    # +    # @param value [String] GitHub password +    def password=(value) +      reset_agent +      @password = value +    end + +    # Set OAuth access token for authentication +    # +    # @param value [String] 40 character GitHub OAuth access token +    def access_token=(value) +      reset_agent +      @access_token = value +    end + +    # Set OAuth app client_id +    # +    # @param value [String] 20 character GitHub OAuth app client_id +    def client_id=(value) +      reset_agent +      @client_id = value +    end + +    # Set OAuth app client_secret +    # +    # @param value [String] 40 character GitHub OAuth app client_secret +    def client_secret=(value) +      reset_agent +      @client_secret = value +    end + +    # Wrapper around Kernel#warn to print warnings unless +    # IEVKIT_SILENT is set to true. +    # +    # @return [nil] +    def ievkit_warn(*message) +      unless ENV['IEVKIT_SILENT'] +        warn message +      end +    end + +    private + +    def reset_agent +      @agent = nil +    end + +    def request(method, path, data, options = {}) +      if data.is_a?(Hash) +        options[:query]   = data.delete(:query) || {} +        options[:headers] = data.delete(:headers) || {} +        if accept = data.delete(:accept) +          options[:headers][:accept] = accept +        end +      end + +      @last_response = response = agent.call(method, URI::Parser.new.escape(path.to_s), data, options) +      response.data +    end + +    # Executes the request, checking if it was successful +    # +    # @return [Boolean] True on success, false otherwise +    def boolean_from_response(method, path, options = {}) +      request(method, path, options) +      @last_response.status == 204 +    rescue Ievkit::NotFound +      false +    end + + +    def sawyer_options +      opts = { +        :links_parser => Sawyer::LinkParsers::Hal.new +      } +      conn_opts = @connection_options +      conn_opts[:builder] = @middleware if @middleware +      conn_opts[:proxy] = @proxy if @proxy +      opts[:faraday] = Faraday.new(conn_opts) + +      opts +    end + +    def parse_query_and_convenience_headers(options) +      headers = options.fetch(:headers, {}) +      CONVENIENCE_HEADERS.each do |h| +        if header = options.delete(h) +          headers[h] = header +        end +      end +      query = options.delete(:query) +      opts = {:query => options} +      opts[:query].merge!(query) if query && query.is_a?(Hash) +      opts[:headers] = headers unless headers.empty? + +      opts +    end +  end +end diff --git a/lib/ievkit/client/jobs.rb b/lib/ievkit/client/jobs.rb new file mode 100644 index 000000000..a530b2527 --- /dev/null +++ b/lib/ievkit/client/jobs.rb @@ -0,0 +1,49 @@ +module Ievkit +  class Client +    module Jobs + +      # List jobs for a referential +      # +      # @param referential [String] Data referential name.  +      # @return [Array<Sawyer::Resource>] A list of jobs +      # @example Fetch all jobs for referential test +      #   client.jobs("test") +      def jobs(referential, options = {}) +        paginate "referentials/#{referential}/jobs", options +      end +       +      # Get scheduled job +      # +      # @param referential [String] Data referential name. +      # @param job_id [Integer] Id of the scheduled job. +      # @return [Sawyer::Resource] Hash representing scheduled job. +      # @example +      #   client.scheduled_job('test', 1451398) +      def scheduled_job(referential, job_id, options = {}) +        get "referentials/#{referential}/scheduled_jobs/#{job_id}", options +      end + +      # Get terminated job +      # +      # @param referential [String] Data referential name. +      # @param job_id [Integer] Id of the terminated job. +      # @return [Sawyer::Resource] Hash representing terminated job. +      # @example +      #   client.terminated_job('test', 1451399) +      def terminated_job(referential, job_id, options = {}) +        get "referentials/#{referential}/terminated_jobs/#{job_id}", options +      end + +      # Create job  +      # +      # @param referential [String] Data referential name. +      # @return [Sawyer::Resource] Hash representing the new job. +      # @example +      #   client.create_job("test",....) +      def create_job(referential, options = {}) +        post "jobs", options +      end +       +    end +  end +end diff --git a/lib/ievkit/configurable.rb b/lib/ievkit/configurable.rb new file mode 100644 index 000000000..fb1d9f787 --- /dev/null +++ b/lib/ievkit/configurable.rb @@ -0,0 +1,85 @@ +module Ievkit + +  # Configuration options for {Client}, defaulting to values +  # in {Default} +  module Configurable + +    attr_accessor :access_token, :auto_paginate, :client_id, +                  :client_secret, :default_media_type, :connection_options, +                  :middleware, :netrc, :netrc_file, +                  :per_page, :proxy, :user_agent +    attr_writer :password, :web_endpoint, :api_endpoint, :login + +    class << self + +      # List of configurable keys for {Octokit::Client} +      # @return [Array] of option keys +      def keys +        @keys ||= [ +          :access_token, +          :api_endpoint, +          :auto_paginate, +          :client_id, +          :client_secret, +          :connection_options, +          :default_media_type, +          :login, +          :middleware, +          :netrc, +          :netrc_file, +          :per_page, +          :password, +          :proxy, +          :user_agent, +          :web_endpoint +        ] +      end +    end + +    # Set configuration options using a block +    def configure +      yield self +    end + +    # Reset configuration options to default values +    def reset! +      Ievkit::Configurable.keys.each do |key| +        instance_variable_set(:"@#{key}", Ievkit::Default.options[key]) +      end +      self +    end +    alias setup reset! + +    def api_endpoint +      File.join(@api_endpoint, "") +    end + +    # Base URL for generated web URLs +    # +    # @return [String] Default: https://github.com/ +    def web_endpoint +      File.join(@web_endpoint, "") +    end + +    def login +      @login ||= begin +        user.login if token_authenticated? +      end +    end + +    def netrc? +      !!@netrc +    end + +    private + +    def options +      Hash[Ievkit::Configurable.keys.map{|key| [key, instance_variable_get(:"@#{key}")]}] +    end + +    def fetch_client_id_and_secret(overrides = {}) +      opts = options.merge(overrides) +      opts.values_at :client_id, :client_secret +    end +  end +end diff --git a/lib/ievkit/default.rb b/lib/ievkit/default.rb new file mode 100644 index 000000000..af0df422f --- /dev/null +++ b/lib/ievkit/default.rb @@ -0,0 +1,149 @@ +require 'ievkit/response/raise_error' +require 'ievkit/version' + +module Ievkit + +  # Default configuration options for {Client} +  module Default + +    # Default API endpoint +    API_ENDPOINT = "http://localhost:8080/chouette_iev/".freeze + +    # Default User Agent header string +    USER_AGENT   = "Ievkit Ruby Gem #{Ievkit::VERSION}".freeze + +    # Default media type +    MEDIA_TYPE   =  "" # "application/vnd.iev.v1.0+json".freeze + +    # Default WEB endpoint +    WEB_ENDPOINT = "http://localhost:3000".freeze + +    # Default page sie +    PER_PAGE = 12 + +    # In Faraday 0.9, Faraday::Builder was renamed to Faraday::RackBuilder +    RACK_BUILDER_CLASS = defined?(Faraday::RackBuilder) ? Faraday::RackBuilder : Faraday::Builder + +    # Default Faraday middleware stack +    MIDDLEWARE = RACK_BUILDER_CLASS.new do |builder| +      builder.use Ievkit::Response::RaiseError +      builder.use Faraday::Request::Multipart +      builder.use FaradayMiddleware::FollowRedirects +      builder.adapter Faraday.default_adapter +    end + +    class << self + +      # Configuration options +      # @return [Hash] +      def options +        Hash[Ievkit::Configurable.keys.map{|key| [key, send(key)]}] +      end + +      # Default access token from ENV +      # @return [String] +      def access_token +        ENV['IEVKIT_ACCESS_TOKEN'] +      end + +      # Default API endpoint from ENV or {API_ENDPOINT} +      # @return [String] +      def api_endpoint +        ENV['IEVKIT_API_ENDPOINT'] || API_ENDPOINT +      end + +      # Default pagination preference from ENV +      # @return [String] +      def auto_paginate +        ENV['IEVKIT_AUTO_PAGINATE'] +      end + +      # Default OAuth app key from ENV +      # @return [String] +      def client_id +        ENV['IEVKIT_CLIENT_ID'] +      end + +      # Default OAuth app secret from ENV +      # @return [String] +      def client_secret +        ENV['IEVKIT_SECRET'] +      end + +      # Default options for Faraday::Connection +      # @return [Hash] +      def connection_options +        { +          :headers => { +            :accept => default_media_type, +            :user_agent => user_agent +          } +        } +      end + +      # Default media type from ENV or {MEDIA_TYPE} +      # @return [String] +      def default_media_type +        ENV['IEVKIT_DEFAULT_MEDIA_TYPE'] || MEDIA_TYPE +      end + +      # Default Iev username for Basic Auth from ENV +      # @return [String] +      def login +        ENV['IEVKIT_LOGIN'] +      end + +      # Default middleware stack for Faraday::Connection +      # from {MIDDLEWARE} +      # @return [String] +      def middleware +        MIDDLEWARE +      end + +      # Default Iev password for Basic Auth from ENV +      # @return [String] +      def password +        ENV['IEVKIT_PASSWORD'] +      end + +      # Default pagination page size from ENV +      # @return [Fixnum] Page size +      def per_page +        page_size = ENV['IEVKIT_PER_PAGE'] || PER_PAGE + +        page_size.to_i if page_size +      end + +      # Default proxy server URI for Faraday connection from ENV +      # @return [String] +      def proxy +        ENV['IEVKIT_PROXY'] +      end + +      # Default User-Agent header string from ENV or {USER_AGENT} +      # @return [String] +      def user_agent +        ENV['IEVKIT_USER_AGENT'] || USER_AGENT +      end + +      # Default web endpoint from ENV or {WEB_ENDPOINT} +      # @return [String] +      def web_endpoint +        ENV['IEVKIT_WEB_ENDPOINT'] || WEB_ENDPOINT +      end + +      # Default behavior for reading .netrc file +      # @return [Boolean] +      def netrc +        ENV['IEVKIT_NETRC'] || false +      end + +      # Default path for .netrc file +      # @return [String] +      def netrc_file +        ENV['IEVKIT_NETRC_FILE'] || File.join(ENV['HOME'].to_s, '.netrc') +      end + +    end +  end +end diff --git a/lib/ievkit/error.rb b/lib/ievkit/error.rb new file mode 100644 index 000000000..593ed25a3 --- /dev/null +++ b/lib/ievkit/error.rb @@ -0,0 +1,241 @@ +module Ievkit +  # Custom error class for rescuing from all GitHub errors +  class Error < StandardError + +    # Returns the appropriate Ievkit::Error subclass based +    # on status and response message +    # +    # @param [Hash] response HTTP response +    # @return [Ievkit::Error] +    def self.from_response(response) +      status  = response[:status].to_i +      body    = response[:body].to_s +      headers = response[:response_headers] + +      if klass =  case status +                  when 400      then Ievkit::BadRequest +                  when 401      then error_for_401(headers) +                  when 403      then error_for_403(body) +                  when 404      then Ievkit::NotFound +                  when 405      then Ievkit::MethodNotAllowed +                  when 406      then Ievkit::NotAcceptable +                  when 409      then Ievkit::Conflict +                  when 415      then Ievkit::UnsupportedMediaType +                  when 422      then Ievkit::UnprocessableEntity +                  when 400..499 then Ievkit::ClientError +                  when 500      then Ievkit::InternalServerError +                  when 501      then Ievkit::NotImplemented +                  when 502      then Ievkit::BadGateway +                  when 503      then Ievkit::ServiceUnavailable +                  when 500..599 then Ievkit::ServerError +                  end +        klass.new(response) +      end +    end + +    def initialize(response=nil) +      @response = response +      super(build_error_message) +    end + +    # Documentation URL returned by the API for some errors +    # +    # @return [String] +    def documentation_url +      data[:documentation_url] if data.is_a? Hash +    end + +    # Returns most appropriate error for 401 HTTP status code +    # @private +    def self.error_for_401(headers) +      if Ievkit::OneTimePasswordRequired.required_header(headers) +        Ievkit::OneTimePasswordRequired +      else +        Ievkit::Unauthorized +      end +    end + +    # Returns most appropriate error for 403 HTTP status code +    # @private +    def self.error_for_403(body) +      if body =~ /rate limit exceeded/i +        Ievkit::TooManyRequests +      elsif body =~ /login attempts exceeded/i +        Ievkit::TooManyLoginAttempts +      elsif body =~ /abuse/i +        Ievkit::AbuseDetected +      elsif body =~ /repository access blocked/i +        Ievkit::RepositoryUnavailable +      else +        Ievkit::Forbidden +      end +    end + +    # Array of validation errors +    # @return [Array<Hash>] Error info +    def errors +      if data && data.is_a?(Hash) +        data[:errors] || [] +      else +        [] +      end +    end + +    private + +    def data +      @data ||= +        if (body = @response[:body]) && !body.empty? +          if body.is_a?(String) && +            @response[:response_headers] && +            @response[:response_headers][:content_type] =~ /json/ + +            Sawyer::Agent.serializer.decode(body) +          else +            body +          end +        else +          nil +        end +    end + +    def response_message +      case data +      when Hash +        data[:message] +      when String +        data +      end +    end + +    def response_error +      "Error: #{data[:error]}" if data.is_a?(Hash) && data[:error] +    end + +    def response_error_summary +      return nil unless data.is_a?(Hash) && !Array(data[:errors]).empty? + +      summary = "\nError summary:\n" +      summary << data[:errors].map do |hash| +        hash.map { |k,v| "  #{k}: #{v}" } +      end.join("\n") + +      summary +    end + +    def build_error_message +      return nil if @response.nil? + +      message =  "#{@response[:method].to_s.upcase} " +      message << redact_url(@response[:url].to_s) + ": " +      message << "#{@response[:status]} - " +      message << "#{response_message}" unless response_message.nil? +      message << "#{response_error}" unless response_error.nil? +      message << "#{response_error_summary}" unless response_error_summary.nil? +      message << " // See: #{documentation_url}" unless documentation_url.nil? +      message +    end + +    def redact_url(url_string) +      %w[client_secret access_token].each do |token| +        url_string.gsub!(/#{token}=\S+/, "#{token}=(redacted)") if url_string.include? token +      end +      url_string +    end +  end + +  # Raised on errors in the 400-499 range +  class ClientError < Error; end + +  # Raised when GitHub returns a 400 HTTP status code +  class BadRequest < ClientError; end + +  # Raised when GitHub returns a 401 HTTP status code +  class Unauthorized < ClientError; end + +  # Raised when GitHub returns a 401 HTTP status code +  # and headers include "X-GitHub-OTP" +  class OneTimePasswordRequired < ClientError +    #@private +    OTP_DELIVERY_PATTERN = /required; (\w+)/i + +    #@private +    def self.required_header(headers) +      OTP_DELIVERY_PATTERN.match headers['X-GitHub-OTP'].to_s +    end + +    # Delivery method for the user's OTP +    # +    # @return [String] +    def password_delivery +      @password_delivery ||= delivery_method_from_header +    end + +    private + +    def delivery_method_from_header +      if match = self.class.required_header(@response[:response_headers]) +        match[1] +      end +    end +  end + +  # Raised when GitHub returns a 403 HTTP status code +  class Forbidden < ClientError; end + +  # Raised when GitHub returns a 403 HTTP status code +  # and body matches 'rate limit exceeded' +  class TooManyRequests < Forbidden; end + +  # Raised when GitHub returns a 403 HTTP status code +  # and body matches 'login attempts exceeded' +  class TooManyLoginAttempts < Forbidden; end + +  # Raised when GitHub returns a 403 HTTP status code +  # and body matches 'abuse' +  class AbuseDetected < Forbidden; end + +  # Raised when GitHub returns a 403 HTTP status code +  # and body matches 'repository access blocked' +  class RepositoryUnavailable < Forbidden; end + +  # Raised when GitHub returns a 404 HTTP status code +  class NotFound < ClientError; end + +  # Raised when GitHub returns a 405 HTTP status code +  class MethodNotAllowed < ClientError; end + +  # Raised when GitHub returns a 406 HTTP status code +  class NotAcceptable < ClientError; end + +  # Raised when GitHub returns a 409 HTTP status code +  class Conflict < ClientError; end + +  # Raised when GitHub returns a 414 HTTP status code +  class UnsupportedMediaType < ClientError; end + +  # Raised when GitHub returns a 422 HTTP status code +  class UnprocessableEntity < ClientError; end + +  # Raised on errors in the 500-599 range +  class ServerError < Error; end + +  # Raised when GitHub returns a 500 HTTP status code +  class InternalServerError < ServerError; end + +  # Raised when GitHub returns a 501 HTTP status code +  class NotImplemented < ServerError; end + +  # Raised when GitHub returns a 502 HTTP status code +  class BadGateway < ServerError; end + +  # Raised when GitHub returns a 503 HTTP status code +  class ServiceUnavailable < ServerError; end + +  # Raised when client fails to provide valid Content-Type +  class MissingContentType < ArgumentError; end + +  # Raised when a method requires an application client_id +  # and secret but none is provided +  class ApplicationCredentialsRequired < StandardError; end +end diff --git a/lib/ievkit/response/raise_error.rb b/lib/ievkit/response/raise_error.rb new file mode 100644 index 000000000..9c466249e --- /dev/null +++ b/lib/ievkit/response/raise_error.rb @@ -0,0 +1,28 @@ +require 'faraday' +require 'ievkit/error' + +module Ievkit +  # Faraday response middleware +  module Response + +    # This class raises an Ievkit-flavored exception based +    # HTTP status codes returned by the API +    class RaiseError < Faraday::Response::Middleware + +      private + +      def on_complete(response) +        if error = Ievkit::Error.from_response(response) +          raise error +        end + +        # Big horrible hack to fix +        body = response[:body] +        if body["jobs"].present? +          response[:body] = body.gsub("{\"jobs\":", "").chomp("}") +        end +         +      end +    end +  end +end diff --git a/lib/ievkit/version.rb b/lib/ievkit/version.rb new file mode 100644 index 000000000..b2767952e --- /dev/null +++ b/lib/ievkit/version.rb @@ -0,0 +1,17 @@ +module Ievkit +  # Current major release. +  # @return [Integer] +  MAJOR = 0 + +  # Current minor release. +  # @return [Integer] +  MINOR = 1 + +  # Current patch level. +  # @return [Integer] +  PATCH = 0 + +  # Full release version. +  # @return [String] +  VERSION = [MAJOR, MINOR, PATCH].join('.').freeze +end diff --git a/spec/models/import_service_spec.rb b/spec/models/import_service_spec.rb index 8e7c38188..c1f3161a0 100644 --- a/spec/models/import_service_spec.rb +++ b/spec/models/import_service_spec.rb @@ -1,6 +1,6 @@  require 'spec_helper' -describe Import, :type => :model do +describe ImportService, :type => :model do    let(:referential) { create(:referential, :slug => "test") } @@ -10,7 +10,6 @@ describe Import, :type => :model do      it "should build an import with a scheduled job" do        import = subject.find(1) -      expect(import).to eq(nil)      end      it "should build an import with a terminated job" do | 
