Code Monkey home page Code Monkey logo

my_api_client's Introduction

my_api_client's People

Contributors

ashimomura avatar dependabot-preview[bot] avatar dependabot[bot] avatar okumud avatar renovate-bot avatar ryz310 avatar st-1985 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

Forkers

okumud ashimomura

my_api_client's Issues

my_api_client does not support header validation

Some APIs show error details in response headers, such as "www-authenticate" header:

HTTP/2 401
x-xss-protection: 1; mode=block
x-line-request-id: 3bb43d42-ca94-450f-bcc4-5f912eeb1925
x-frame-options: DENY
x-content-type-options: nosniff
www-authenticate: Bearer error=“invalid_token”, error_description=“accessToken expired(2)”
server: envoy
pragma: no-cache
expires: 0
date: Wed, 19 Oct 2022 06:39:13 GMT
content-type: application/json
content-length: 104
cache-control: no-cache, no-store, max-age=0, must-revalidate

Unfortunately, my_api_client does not support header validation.

Add generator spec

On #10 and #13, the generator was broken but I couldn't know before release.
It needs testing on the CI in the future.

Support boolean on error handling

The Facebook graph API occasionally returns error which having is_transient: true like as following. It has possibility to succeed by retrying. But currentry error_handling does not support Boolean. I'd like to support it.

{
  "error": {
    "message": "An unexpected error has occurred. Please retry your request later.",
    "type": "OAuthException",
    "is_transient": true,
    "code": 2,
    "fbtrace_id": "HbP+i+BdlMX"
  }
}

Cannot stub multiple methods

Following code may not work as expected.

my_api_client_stub(ExampleApiClient, :get_user, response: { id: 12_345 })
my_api_client_stub(ExampleApiClient, :set_user)

Because #my_api_client_stub creates new double instance for each time, so the method stubbing is not take over.

def my_api_client_stub(klass, action, response: nil, raise: nil)
instance = instance_double(klass)
allow(klass).to receive(:new).and_return(instance)

Avoid sleep on testing

When I write following code, testing by rspec will freeze 4 minutes:

retry_on SomeError, wait: 1.minute, attempts: 3

I want to avoid sleep on testing.

POST application/x-www-form-urlencoded でリクエストするときのデータにハッシュ形式で指定できない

my_api_client gem を使って以下でリクエストしようとしたが、データにハッシュ形式で指定すると意図したリクエストにならない。

HTTP メソッド : POST
HTTP リクエストヘッダー : application/x-www-form-urlencoded

サンプルコード

body = {
  hoge: 'hoge',
  fuga: 'fuga'
}
headers = { 'Content-Type': 'application/x-www-form-urlencoded' }
post 'hoge/fuga', body: body, headers: headers

原因は sawyer gem の以下の箇所でデータをエンコードする際に、デフォルトの sawyer だと JSON 文字列形式に変換されてしまうため。

https://github.com/lostisland/sawyer/blob/a541a68be349a7982e24b80d94fe380a37b4805c/lib/sawyer/agent.rb#L96


とりあえずワークアラウンドとして以下のようにキーバリューのクエリ形式で渡すようにしたところ意図したとおりのリクエストができた。

body = {
  hoge: 'hoge',
  fuga: 'fuga'
}.to_a.map { |a| a.join('=') }.join('&')

Change the duration format to milliseconds

Currently, MyApiClient logs a request duration as seconds.

e.g. API request POST https://example.com: "Duration 0.029339311 sec"

Almost API response times are within 1 second, so the duration format should be milliseconds.

Can't request to URL which includes port numbert

When I try to request to http://localhost:3000/v1/line/channels by below

class ChannelApiClient < ::ApplicationApiClient
  endpoint 'http://localhost:3000/v1/line'

  def get_channels
    get 'channels', headers: headers
  end

actual request is going to be

API request `GET http://localhost/v1/line/channels`: "Start"

As you can see, port numbet (3000) is removed

I think this problem comes from

def schema_and_hostname
uri = URI.parse(endpoint)
"#{uri.scheme}://#{uri.host}"
end

because schema_and_hostname dosen't include uri.port

I'd really appreciate it if you could fix it in your free time.

Support to use initialize parameters on stubbing method

In the following code, initialization parameters may change the API response. Unfortunately, current stubbing helper can not change response parameters by the initialize parameters. I would like to support the feature in the future.

class UserApiClient < ::ApplicationApiClient
   endpoint 'https://example.com'

  attr_reader :version

  def initialize(version:)
    @version = version
  end
end

`#stub_api_client_all` や `#stub_api_client` で例外のレスポンスをスタブ化する際に任意の HTTP ステータスコードを渡したい

概要

See. https://github.com/ryz310/my_api_client/blob/v0.20.0/README.jp.md#raise-option

README の上記箇所を参照したところ例外のレスポンスをスタブ化するには #stub_api_client_all#stub_api_client の raise オプションとともに response オプションを利用すれば良さそうでした。

ただ、例外のレスポンスとして任意の HTTP ステータスコードを返そうと思ったのですが、以下の箇所で HTTP のステータスコードが 400 で決め打ちになっていたためできませんでした。

期待する動作

以下のように、#stub_api_client_all#stub_api_client で例外のテストをする際にレスポンスの HTTP ステータスコードも外部から注入できるようにしたいです。

stub_api_client_all(
  ExampleApiClient,
  request: {
    raise: MyApiClient::Error,
    response: { status: 404, message: 'error' }
  }
)
# or
stub_api_client_all(
  ExampleApiClient,
  request: {
    raise: MyApiClient::Error,
    status: 404,
    response: { message: 'error' },
  }
)

error_handling で status と json の組み合わせが意図したとおりに定義されていない

MyApiClient クラスを継承した以下のような SampleApiClient クラスを作成し、RSpec を実行したところエラーに遭遇しテストが落ちました。

  • sample_api_client.rb
class SampleApiClient < MyApiClient
  retry_on MyApiClient::ServerError, wait: 0.1.seconds, attempts: 3
  error_handling status: 200, json: :forbid_nil, raise: SampleApiClientError
end
  • sample_api_client_spec.rb
RSpec.describe SampleApiClient, type: :api_client do
  it do
    expect { subject }
      .to be_handled_as_an_error(MyApiClient::ServerError)
      .after_retry(3).times
      .when_receive(status_code: 500)
  end
end
  • error
expected to be handled as MyApiClient::ServerError, but it was handled as SampleApiClientError

ソースコードに以下の定義が status code : 200 かつ body : nil という条件定義のように見えますが、実際は status code : 500 かつ body : nil という条件にもひっかかってしまっているようです。

  error_handling status: 200, json: :forbid_nil, raise: SampleApiClientError

status code : 500 かつ body : nil の場合は上記エラーハンドルにひっかかからないように( 上記エラーハンドル定義の場合は status code : 200 かつ body :nil の場合のみひっかかる )修正をお願いできますでしょうか。

Unexpected result with Sawyer::Resource

An API is expected to return following result.

{
  "age_range": {
    "max": 30,
    "min": 20
  }
}

An instance which implemented with MyApiClient returns that result as Sawyer::Resource.

result = ApiClient.request
result[:max] # => 30

However, if the API returns following result, above execution will unexpected result.

{
  "age_range": {
    "min": 20
  }
}
result = ApiClient.request
result[:max] # => [:min, 20]

The cause is into the Sawyer::Resouce implementation. Sawyer::Resouce#[] calls #method_missing and searches the key from inner fields.
Ordinarily it will return nil if does not find the key.

However, :max is Enumerable module instance so it will happen that calls Enumerable#max method.

Add `return` option to `#error_handling`

Some API returns 404 Not Found as normal operation.
In the case, the error handler raises error is not kindness.

I hope to return nil or false in that case.

# Example.
class SomeApiClient < MyApiClient::Base
  endpoint 'https://example.com'

  error_handling status_code: 404, return: nil

  def get_resource
    get 'path/to/resource'
  end
end

api_client = SomeApiClient.new
response = api_client.get_resource # 404 Not Found
# => nil

Return values are nil after retrying

The function of retry_on is to retry the process and return the correct value when an error is detected. But currentry, it returns always nil.

The following code may have a problem. The # rescue_with_handler provided by ActiveSupport performs retry processing, but nobody seems to return the result value.

def call(*args)
@args = args
send(*args)
rescue StandardError => e
@retry_count ||= 0
raise unless rescue_with_handler(e)
end

Shoulda-matchers for my_api_client

I'd like to write spec as following.

RSpec.describe Google::People::ApiClient, type: :api_client do  
  let(:api_client) { described_class.new(access_token: 'access_token') }
  
  describe '#get_people' do
    subject(:request!) { api_client.get_people }
    
    it do
      request!
      expect(api_client)
        .to have_requested_to(:get, 'https://people.googleapis.com/v1/people/me')
        .with(body: nil, headers: headers, query: query)
    end

    describe 'error handling' do
      context 'when the API returns 200 OK' do
        let(:body) do
          { resourceName: 'people/12345678901234567890' }.to_json
        end
        
        it { expect { request! }.not_to handle_error.when_receive(status: 200, body: body) }
      end
      
      context 'when the API returns 200 OK, but response body is empty' do
        it do 
          expect { request! }
            .to handle_error(raise: Google::People::Errors::EmptyResponse)
            .when_receive(status: 200, body: nil) 
        end
      end
      
      context 'when the API returns 400 Bad Request' do
        let(:body) do
          {
            error: {
              code: 400,
              message: 'Invalid personFields mask path: "hoge".',
              status: 'INVALID_ARGUMENT',
            },
          }.to_json
        end
        
        it do 
          expect { request! }
            .to handle_error(raise: MyApiClient::ClientError)
            .when_receive(status: 400, body: body) 
        end
      end
      
      context 'when the API returns error code: 403' do
        let(:body) do
          {
            error: {
              code: 403,
              message: 'People API has not been used in project xxx before or it is disabled.',
              status: 'PERMISSION_DENIED',
            },
          }.to_json
        end
        
        it do 
          expect { request! }
            .to handle_error(raise: Google::People::Errors::ApiIsDisabled)
            .when_receive(status: 403, body: body) 
        end
      end
    end
  end
end

Your .dependabot/config.yml contained invalid details

Dependabot encountered the following error when parsing your .dependabot/config.yml:

Automerging is not enabled for this account. You can enable it from the [account settings](https://app.dependabot.com/accounts/ryz310/settings) screen in your Dependabot dashboard.

Please update the config file to conform with Dependabot's specification using our docs and online validator.

Add `only` and `except` options to `#error_handling`

class SomeApiClient < MyApiClient::Base
  endpoint 'https://example.com'

  error_handling status_code: 400, raise: Errors::BadRequest, except: :create_resource
  error_handling status_code: 404, raise: Errors::NotFound, only: :get_resource

  def get_resource
    get 'path/to/resource'
  end

  def create_resource(body)
    post 'path/to/resource', body: body
  end
end

Support JSON:API

Refs: https://jsonapi.org/

Here’s an example response from a blog that implements JSON:API:

{
  "links": {
    "self": "http://example.com/articles",
    "next": "http://example.com/articles?page[offset]=2",
    "last": "http://example.com/articles?page[offset]=10"
  },
  "data": [{
    "type": "articles",
    "id": "1",
    "attributes": {
      "title": "JSON:API paints my bikeshed!"
    },
    "relationships": {
      "author": {
        "links": {
          "self": "http://example.com/articles/1/relationships/author",
          "related": "http://example.com/articles/1/author"
        },
        "data": { "type": "people", "id": "9" }
      },
      "comments": {
        "links": {
          "self": "http://example.com/articles/1/relationships/comments",
          "related": "http://example.com/articles/1/comments"
        },
        "data": [
          { "type": "comments", "id": "5" },
          { "type": "comments", "id": "12" }
        ]
      }
    },
    "links": {
      "self": "http://example.com/articles/1"
    }
  }],
  "included": [{
    "type": "people",
    "id": "9",
    "attributes": {
      "firstName": "Dan",
      "lastName": "Gebhardt",
      "twitter": "dgeb"
    },
    "links": {
      "self": "http://example.com/people/9"
    }
  }, {
    "type": "comments",
    "id": "5",
    "attributes": {
      "body": "First!"
    },
    "relationships": {
      "author": {
        "data": { "type": "people", "id": "2" }
      }
    },
    "links": {
      "self": "http://example.com/comments/5"
    }
  }, {
    "type": "comments",
    "id": "12",
    "attributes": {
      "body": "I like XML better"
    },
    "relationships": {
      "author": {
        "data": { "type": "people", "id": "9" }
      }
    },
    "links": {
      "self": "http://example.com/comments/12"
    }
  }]
}

Adding a before callback to the `.retry_on` feature

For example, if an error occurs such as an access token expiring, it is useful to be able to regenerate the access token before retry processing. The current .retry_on feature is only useful in limited cases, but adding a before callback would be an enhancement.

Use `request_to` matcher without `with` option

I want to use request_to matcher without with option. But I've occurred following error:

Failure/Error:
       expect { api_request }.not_to request_to(:post, 'https://example.com/api')

     NoMethodError:
       undefined method `[]' for nil:NilClass
     # /usr/local/bundle/ruby/2.7.0/gems/my_api_client-0.17.0/lib/my_api_client/rspec/matchers/request_to.rb:11:in `block (2 levels) in <top (required)>'

The stack trace shows code that:

match do |api_request|
disable_logging
@expected = {
request_line: request_line(expected_method, expected_url, expected_options[:query]),
body: expected_options[:body],
headers: expected_options[:headers],
}.compact

The URL included in the logger is incomplete

The log GET http:/v3.3/me/friends is wrong URL. It is not considering schema and domain name.

I, [2019-06-19T15:12:33.753081 #58]  INFO -- : API request `GET http:/v3.3/me/friends`: "Start"
I, [2019-06-19T15:12:34.225469 #58]  INFO -- : API request `GET http:/v3.3/me/friends`: "Duration 0.4365701 sec"
I, [2019-06-19T15:12:34.260660 #58]  INFO -- : API request `GET http:/v3.3/me/friends`: "Success (200)"

The reason is that the Faraday instance is initialized by Sawyer::Agent#initialize but it has not yet been called at timing of Logger instance initialization.

def _request(http_method, pathname, headers, query, body, logger)
processed_path = [common_path, pathname].join('/').gsub('//', '/')
request_params = Params::Request.new(http_method, processed_path, headers, query, body)
request_logger = Logger.new(logger, faraday, http_method, processed_path)
call(:_execute, request_params, request_logger)
end

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.