Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Requests: declarative and unified interface #58

Merged
merged 6 commits into from
Feb 12, 2024
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: 1 addition & 3 deletions lib/scalingo/api/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,7 @@ def self.register_handler!(method_name, klass)
def inspect
str = %(<#{self.class}:0x#{object_id.to_s(16)} url:"#{@url}" methods:)

methods = self.class.instance_methods - Scalingo::API::Client.instance_methods
str << methods.to_s

str << self.class.instance_methods(false).to_s
str << ">"
str
end
Expand Down
77 changes: 74 additions & 3 deletions lib/scalingo/api/endpoint.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require "addressable/template"
require "forwardable"

module Scalingo
Expand All @@ -6,19 +7,89 @@ class Endpoint
extend Forwardable
attr_reader :client

# Add a handler for a given endpoint
%i[get post put patch delete].each do |method|
# @example
# class Example < API::Endpoint
# get :all, "some-endpoint/{id}/subthings{?query*}", optional: [:query]
# post :create, "some-endpoint", root_key: :subthing
# end
define_singleton_method(method) do |name, path, **default_attrs, &default_block|
# @example
# endpoint = Example.new
# endpoint.all(id: "1", query: {page: 1})
# endpoint.create(name: "thing")
define_method(name) do |**runtime_attrs, &runtime_block|
params = {**default_attrs, **runtime_attrs}

request(method, path, **params) do |req|
default_block&.call(req, params)
runtime_block&.call(req, params)
end
end
end

# Those methods are not meant to be used outside of a class definition
private_class_method method
end

def initialize(client)
@client = client
end

def_delegator :client, :connection
def_delegator :client, :database_connection

# Perform a request to the API
def request(method, path, body: nil, root_key: nil, connected: true, basic: nil, dry_run: false, **params, &block)
# path can be an URI template
# see https://github.com/sporkmonger/addressable?tab=readme-ov-file#uri-templates
# see https://www.rfc-editor.org/rfc/rfc6570.txt
template = Addressable::Template.new(path)

# If the template has keys, we need to expand it with the params
if template.keys.present?
# We assume every variable in the template is required
expected_keys = Set.new(template.keys.map(&:to_sym))
# ... but we can opt out by specifying :optional when performing the request or in the endpoint definition
expected_keys -= params[:optional] if params[:optional].present?

# if any required key is missing, raise an error with the missing keys,
# as if it was a regular keyword argument that was not supplied
if expected_keys.present?
received_keys = Set.new(params.keys.map(&:to_sym))

unless received_keys.superset?(expected_keys)
missings = (expected_keys - received_keys).map { |item| sprintf("%p", item) }.join(" ")
raise ArgumentError, "missing keyword: #{missings}"
end
end

# Now, we can expand the template with the supplied params
actual_path = template.expand(params).to_s
else
# Otherwise, it's not a template but a string to be used as it is
actual_path = path
end

# we nest the given body under the root_key if it's present
request_body = body
request_body = {root_key => body} if request_body && root_key

# We can use the client in either connected or unconnected mode
conn = connected ? client.authenticated_connection : client.unauthenticated_connection

# We can specify basic auth credentials if needed
conn.request :authorization, :basic, basic[:user], basic[:password] if basic.present?

# Finally, perform the request
conn.public_send(method, actual_path, request_body, &block)
end

def inspect
str = %(<#{self.class}:0x#{object_id.to_s(16)} base_url:"#{@client.url}" endpoints:)

methods = self.class.instance_methods - Scalingo::API::Endpoint.instance_methods

str << methods.to_s
str << self.class.instance_methods(false).to_s
str << ">"
str
end
Expand Down
47 changes: 4 additions & 43 deletions lib/scalingo/auth/keys.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,9 @@

module Scalingo
class Auth::Keys < API::Endpoint
def all(headers = nil, &block)
data = nil

connection.get(
"keys",
data,
headers,
&block
)
end

def show(id, headers = nil, &block)
data = nil

connection.get(
"keys/#{id}",
data,
headers,
&block
)
end

def create(payload, headers = nil, &block)
data = {key: payload}

connection.post(
"keys",
data,
headers,
&block
)
end

def destroy(id, headers = nil, &block)
data = nil

connection.delete(
"keys/#{id}",
data,
headers,
&block
)
end
get :all, "keys"
get :show, "keys/{id}"
post :create, "keys", root_key: :key
delete :destroy, "keys/{id}"
end
end
47 changes: 4 additions & 43 deletions lib/scalingo/auth/scm_integrations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,9 @@

module Scalingo
class Auth::ScmIntegrations < API::Endpoint
def all(headers = nil, &block)
data = nil

connection.get(
"scm_integrations",
data,
headers,
&block
)
end

def show(id, headers = nil, &block)
data = nil

connection.get(
"scm_integrations/#{id}",
data,
headers,
&block
)
end

def create(payload, headers = nil, &block)
data = {scm_integration: payload}

connection.post(
"scm_integrations",
data,
headers,
&block
)
end

def destroy(id, headers = nil, &block)
data = nil

connection.delete(
"scm_integrations/#{id}",
data,
headers,
&block
)
end
get :all, "scm_integrations"
get :show, "scm_integrations/{id}"
post :create, "scm_integrations", root_key: :scm_integration
delete :destroy, "scm_integrations/{id}"
end
end
67 changes: 5 additions & 62 deletions lib/scalingo/auth/tokens.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,67 +2,10 @@

module Scalingo
class Auth::Tokens < API::Endpoint
def exchange(token, headers = nil, &block)
data = nil

authorization = Faraday::Utils.basic_header_from("", token)

request_headers = {
Faraday::Request::Authorization::KEY => authorization
}

request_headers.update(headers) if headers

client.unauthenticated_connection.post(
"tokens/exchange",
data,
request_headers,
&block
)
end

def all(headers = nil, &block)
data = nil

connection.get(
"tokens",
data,
headers,
&block
)
end

def create(payload, headers = nil, &block)
data = {token: payload}

connection.post(
"tokens",
data,
headers,
&block
)
end

def renew(id, headers = nil, &block)
data = nil

connection.patch(
"tokens/#{id}/renew",
data,
headers,
&block
)
end

def destroy(id, headers = nil, &block)
data = nil

connection.delete(
"tokens/#{id}",
data,
headers,
&block
)
end
post :exchange, "tokens/exchange", connected: false
get :all, "tokens"
post :create, "tokens", root_key: :token
patch :renew, "tokens/{id}/renew"
delete :destroy, "tokens/{id}"
end
end
51 changes: 4 additions & 47 deletions lib/scalingo/auth/two_factor_auth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,9 @@

module Scalingo
class Auth::TwoFactorAuth < API::Endpoint
TOTP_PROVIDER = "totp"
DEFAULT_PROVIDER = TOTP_PROVIDER
SUPPORTED_PROVIDERS = [TOTP_PROVIDER]

def status(headers = nil, &block)
data = nil

connection.get(
"client/tfa",
data,
headers,
&block
)
end

def initiate(provider = DEFAULT_PROVIDER, headers = nil, &block)
data = {tfa: {provider: provider}}

connection.post(
"client/tfa",
data,
headers,
&block
)
end

def validate(attempt, headers = nil, &block)
data = {tfa: {attempt: attempt}}

connection.post(
"client/tfa/validate",
data,
headers,
&block
)
end

def disable(headers = nil, &block)
data = nil

connection.delete(
"client/tfa",
data,
headers,
&block
)
end
get :status, "client/tfa"
post :initiate, "client/tfa", root_key: :tfa
post :validate, "client/tfa/validate", root_key: :tfa
delete :disable, "client/tfa"
end
end
35 changes: 3 additions & 32 deletions lib/scalingo/auth/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,8 @@

module Scalingo
class Auth::User < API::Endpoint
def self(headers = nil, &block)
data = nil

connection.get(
"users/self",
data,
headers,
&block
)
end

def update(payload, headers = nil, &block)
data = {user: payload}

connection.put(
"users/account",
data,
headers,
&block
)
end

def stop_free_trial(headers = nil, &block)
data = nil

connection.post(
"users/stop_free_trial",
data,
headers,
&block
)
end
get :self, "users/self"
put :update, "users/account", root_key: :user
post :stop_free_trial, "users/stop_free_trial"
end
end
Loading