Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ gem "rubocop", "~> 1.21"
group :development do
gem "dotenv"
end

group :test do
gem "webmock"
end
14 changes: 14 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ PATH
GEM
remote: https://rubygems.org/
specs:
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
ast (2.4.2)
bigdecimal (3.1.9)
crack (1.0.0)
bigdecimal
rexml
dotenv (3.1.7)
faraday (1.10.4)
faraday-em_http (~> 1.0)
Expand All @@ -35,6 +41,7 @@ GEM
faraday-retry (1.0.3)
faraday_middleware (1.2.1)
faraday (~> 1.0)
hashdiff (1.1.2)
json (2.9.1)
language_server-protocol (3.17.0.4)
minitest (5.25.4)
Expand All @@ -43,10 +50,12 @@ GEM
parser (3.3.7.0)
ast (~> 2.4.1)
racc
public_suffix (6.0.1)
racc (1.8.1)
rainbow (3.1.1)
rake (13.2.1)
regexp_parser (2.10.0)
rexml (3.4.0)
rubocop (1.71.0)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
Expand All @@ -64,6 +73,10 @@ GEM
unicode-display_width (3.1.4)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
webmock (3.25.0)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)

PLATFORMS
ruby
Expand All @@ -75,6 +88,7 @@ DEPENDENCIES
rake (~> 13.0)
ruber!
rubocop (~> 1.21)
webmock

BUNDLED WITH
2.5.22
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ If bundler is not being used to manage dependencies, install the gem by executin
```bash
gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
```
## Cache
Ruber uses a caching solution to improve efficiency (e.g., for caching tokens). By default, it uses a simple in-memory cache, but you can change the cache method by setting the `Ruber.cache` attribute


```ruby
Ruber.cache = Redis.new
# or
Ruber.cache = Rails.cache
# or any object that responds to read/write/delete/clear
Ruber.cache = YourCustomCache.new
```

## Usage

Expand Down
4 changes: 3 additions & 1 deletion lib/ruber.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
require_relative "ruber/version"
require "forwardable"
require "ruber/configuration"
require "ruber/authenticator"

# a Ruby wrapper for Uber API
module Ruber
autoload :Client, "ruber/client"
autoload :Error, "ruber/error"
autoload :Authenticator, "ruber/authenticator"

DEFAULT_API_BASE = "https://api.uber.com/v1"

Expand All @@ -16,7 +18,7 @@ class << self

def_delegators(
:configuration, :customer_id, :client_id, :client_secret,
:customer_id=, :client_id=, :client_secret=
:customer_id=, :client_id=, :client_secret=, :cache, :cache=
)
end
end
61 changes: 61 additions & 0 deletions lib/ruber/authenticator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# frozen_string_literal: true

require "faraday"
require "json"

module Ruber
class Authenticator
OAUTH_URL = "https://auth.uber.com/oauth/v2/token"
GRANT_TYPE = "client_credentials"
SCOPE = "eats.deliveries"

class << self
def access_token
@access_token = cached_token || fetch_new_token

@access_token = refresh_access_token if token_expired?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

en qué contexto puede pasar que el token esté expirado acá?

se supone que cached_token guarda el token en caché por el tiempo de expiración que te devuelve la misma API, o no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah ahí vi que también modificaste el write del caché para que el token quede guardado


@access_token
end

def refresh_access_token
Ruber.cache.delete(cache_key)

fetch_new_token
end

def cache_key
@cache_key ||= "#{Ruber.customer_id}_#{Ruber.client_id}_access_token"
end

private

def token_expired?
cached_token[:expires_at] < Time.now
end

def cached_token
Ruber.cache.read(cache_key)
end

def fetch_new_token
response = Faraday.post(
OAUTH_URL,
{
client_id: Ruber.client_id,
client_secret: Ruber.client_secret,
grant_type: GRANT_TYPE,
scope: SCOPE
}
)

data = JSON.parse(response.body.to_s)

expires_at = Time.now + data["expires_in"].to_i
Ruber.cache.write(cache_key, { token: data["access_token"], expires_at: expires_at })

data["access_token"]
end
end
end
end
25 changes: 25 additions & 0 deletions lib/ruber/cache.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

module Ruber
class NullCache
def read(key) = memory_store[key]
def write(key, value, _options = {}) = memory_store[key] = value
def clear = memory_store.clear
def delete(key) = memory_store.delete(key)
def memory_store = @memory_store ||= {}
Comment on lines +5 to +9
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Está bien que el método por defecto sea cachear en memoria? No conviene que sea un FileStore? Porque si se usa memoria se va a pedir el token en cada vuelta y Uber podría penalizarte o que superes la quota.

Entiendo que la idea es que los usuarios indiquen cual es su cache store de preferencia, pero me parece que estaría bueno que ya venga un store útil por defecto.

Qué decís?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

en realidad primero había hecho uno que devolvía todo nil y listo, pero para los tests esto es conveniente porque funciona como si tuvieras algún caché. No estoy seguro de qué convenga hacer por defecto.

end

class << self
def cache=(store)
unless %i[read write clear delete].all? { |method| store.respond_to?(method) }
raise ArgumentError, "cache_store must respond to read, write, clear, and delete"
end

@cache = store
end

def cache
@cache ||= NullCache.new
end
end
end
24 changes: 21 additions & 3 deletions lib/ruber/configuration.rb
Original file line number Diff line number Diff line change
@@ -1,18 +1,36 @@
# frozen_string_literal: true

require_relative "configuration/null_cache"

module Ruber
class Configuration
attr_accessor :customer_id, :client_id, :client_secret

def cache
@cache ||= NullCache.new
end

def cache=(store)
unless %i[read write clear delete].all? { |method| store.respond_to?(method) }
raise ArgumentError, "cache_store must respond to read, write, clear, and delete"
end

@cache = store
end
end

class << self
def configuration
@configuration ||= Configuration.new
end

def configuration=(config_hash)
config_hash.each do |key, value|
configuration.send "#{key}=", value
def configuration=(config)
if config.is_a?(Hash)
config.each do |key, value|
configuration.send "#{key}=", value
end
else
@configuration = config
end

configuration
Expand Down
13 changes: 13 additions & 0 deletions lib/ruber/configuration/null_cache.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

module Ruber
class Configuration
class NullCache
def read(key) = memory_store[key]
def write(key, value, _options = {}) = memory_store[key] = value
def clear = memory_store.clear
def delete(key) = memory_store.delete(key)
def memory_store = @memory_store ||= {}
end
end
end
61 changes: 61 additions & 0 deletions test/ruber/authenticator_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# frozen_string_literal: true

require "test_helper"
require "webmock/minitest"

module Ruber
class AuthenticatorTest < Minitest::Test
include WebMock::API

def setup
Ruber.cache.clear
end

def test_access_token_fetches_and_caches_token
stub_token_request(access_token: "new_token", expires_in: 3600)

token = Authenticator.access_token
cached_token = Ruber.cache.read(Authenticator.cache_key)

assert_equal "new_token", token
assert_equal "new_token", cached_token[:token]
end

def test_refresh_access_token_forces_new_token
Ruber.cache.write(Authenticator.cache_key, { token: "old_token", expires_at: Time.now + 3600 })

stub_token_request(access_token: "refreshed_token", expires_in: 3600)

token = Authenticator.refresh_access_token
cached_token = Ruber.cache.read(Authenticator.cache_key)

assert_equal "refreshed_token", token
assert_equal "refreshed_token", cached_token[:token]
end

def test_refresh_access_token_if_expired
Ruber.cache.write(Authenticator.cache_key, { token: "expired_token", expires_at: Time.now - 3600 })

stub_token_request(access_token: "refreshed_token", expires_in: 3600)

token = Authenticator.access_token
cached_token = Ruber.cache.read(Authenticator.cache_key)

assert_equal "refreshed_token", token
assert_equal "refreshed_token", cached_token[:token]
end

private

def stub_token_request(access_token:, expires_in:)
stub_request(:post, Authenticator::OAUTH_URL)
.to_return(
status: 200,
body: {
access_token: access_token,
expires_in: expires_in
}.to_json
)
end
end
end
40 changes: 33 additions & 7 deletions test/ruber/configuration_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,37 @@
module Ruber
class ConfigurationTest < Minitest::Test
def setup
@config_values = { customer_id: "1111", client_id: "2222", client_secret: "a-secret" }
@config_values = {
customer_id: "1111",
client_id: "2222",
client_secret: "a-secret"
}

@custom_cache = Class.new do
def read(key) = memory_store[key]
def write(key, value, _options = {}) = memory_store[key] = value
def clear = memory_store.clear
def delete(key) = memory_store.delete(key)
def memory_store = @memory_store ||= {}
end.new

Ruber.configuration = nil
end

def test_configure
Ruber.configure do |config|
config.customer_id = @config_values[:customer_id]
config.client_id = @config_values[:client_id]
config.client_secret = @config_values[:client_secret]
config.customer_id = @config_values[:customer_id]
config.client_id = @config_values[:client_id]
config.client_secret = @config_values[:client_secret]
end

assert_configuration_values
end

def test_direct_configuration
Ruber.customer_id = @config_values[:customer_id]
Ruber.client_id = @config_values[:client_id]
Ruber.client_secret = @config_values[:client_secret]
Ruber.customer_id = @config_values[:customer_id]
Ruber.client_id = @config_values[:client_id]
Ruber.client_secret = @config_values[:client_secret]

assert_configuration_values
end
Expand All @@ -38,6 +52,18 @@ def test_invalid_configuration_set
end
end

def test_cache_can_be_set_explicitly
Ruber.cache = @custom_cache

assert_equal @custom_cache, Ruber.cache
end

def test_cache_must_respond_to_read_write_clear_and_delete
assert_raises(ArgumentError) do
Ruber.cache = Object.new
end
end

private

def assert_configuration_values
Expand Down
1 change: 1 addition & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
require "ruber"

require "minitest/autorun"
require "webmock/minitest"