use std::fmt;
#[derive(Clone)]
pub enum AuthMethod {
Bearer(String),
OAuth(String),
Basic(BasicAuth),
ApiKey {
prefix: Option<String>,
key: String,
},
}
#[derive(Clone)]
pub struct BasicAuth {
pub username: String,
pub password: Option<String>,
}
impl fmt::Debug for BasicAuth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BasicAuth")
.field("username", &self.username)
.field("password", &"[REDACTED]")
.finish()
}
}
impl fmt::Debug for AuthMethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AuthMethod::Bearer(_) => f.debug_tuple("Bearer").field(&"[REDACTED]").finish(),
AuthMethod::OAuth(_) => f.debug_tuple("OAuth").field(&"[REDACTED]").finish(),
AuthMethod::Basic(_) => f.debug_tuple("Basic").field(&"[REDACTED]").finish(),
AuthMethod::ApiKey { prefix, .. } => f
.debug_struct("ApiKey")
.field("prefix", prefix)
.field("key", &"[REDACTED]")
.finish(),
}
}
}
#[derive(Debug, Clone)]
pub struct Configuration {
pub(crate) base_path: String,
pub(crate) user_agent: Option<String>,
pub(crate) client: reqwest::Client,
pub(crate) auth: Option<AuthMethod>,
}
impl Default for Configuration {
fn default() -> Self {
Configuration {
base_path: "http://localhost".to_owned(),
user_agent: Some("OpenFGA-Rust-SDK/1.x".to_owned()),
client: reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.expect("failed to build reqwest client"),
auth: None,
}
}
}
impl Configuration {
pub fn builder() -> ConfigurationBuilder {
ConfigurationBuilder::default()
}
pub(crate) fn apply_to_request(
&self,
req_builder: reqwest::RequestBuilder,
) -> reqwest::RequestBuilder {
let req_builder = if let Some(ua) = &self.user_agent {
req_builder.header(reqwest::header::USER_AGENT, ua.as_str())
} else {
req_builder
};
match &self.auth {
Some(AuthMethod::Bearer(token)) | Some(AuthMethod::OAuth(token)) => {
req_builder.bearer_auth(token)
}
Some(AuthMethod::Basic(creds)) => {
req_builder.basic_auth(&creds.username, creds.password.as_deref())
}
Some(AuthMethod::ApiKey { prefix, key }) => {
let header_val = match prefix {
Some(p) => format!("{} {}", p, key),
None => key.clone(),
};
req_builder.header(reqwest::header::AUTHORIZATION, header_val)
}
None => req_builder,
}
}
}
#[derive(Debug)]
pub struct ConfigurationBuilder {
base_path: String,
user_agent: Option<String>,
client: Option<reqwest::Client>,
auth: Option<AuthMethod>,
timeout: std::time::Duration,
}
impl Default for ConfigurationBuilder {
fn default() -> Self {
ConfigurationBuilder {
base_path: "http://localhost".to_owned(),
user_agent: Some("OpenFGA-Rust-SDK/1.x".to_owned()),
client: None,
auth: None,
timeout: std::time::Duration::from_secs(30),
}
}
}
impl ConfigurationBuilder {
pub fn base_path(mut self, base_path: impl Into<String>) -> Self {
self.base_path = base_path.into();
self
}
pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
self.user_agent = Some(ua.into());
self
}
pub fn client(mut self, client: reqwest::Client) -> Self {
self.client = Some(client);
self
}
pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
self.timeout = timeout;
self
}
pub fn bearer_token(mut self, token: impl Into<String>) -> Self {
self.auth = Some(AuthMethod::Bearer(token.into()));
self
}
pub fn oauth_token(mut self, token: impl Into<String>) -> Self {
self.auth = Some(AuthMethod::OAuth(token.into()));
self
}
pub fn basic_auth(
mut self,
username: impl Into<String>,
password: Option<impl Into<String>>,
) -> Self {
self.auth = Some(AuthMethod::Basic(BasicAuth {
username: username.into(),
password: password.map(Into::into),
}));
self
}
pub fn api_key(mut self, key: impl Into<String>, prefix: Option<impl Into<String>>) -> Self {
self.auth = Some(AuthMethod::ApiKey {
key: key.into(),
prefix: prefix.map(Into::into),
});
self
}
pub fn build(self) -> Configuration {
Configuration {
base_path: self.base_path,
user_agent: self.user_agent,
client: self.client.unwrap_or_else(|| {
reqwest::Client::builder()
.timeout(self.timeout)
.build()
.expect("failed to build reqwest client")
}),
auth: self.auth,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::apis::{Error, ResponseContent, urlencode};
#[test]
fn builder_default_base_path_is_localhost() {
let config = Configuration::builder().build();
assert_eq!(config.base_path, "http://localhost");
}
#[test]
fn builder_bearer_token_sets_bearer_auth() {
let config = Configuration::builder().bearer_token("my-token").build();
assert!(
matches!(&config.auth, Some(AuthMethod::Bearer(t)) if t == "my-token"),
"expected Bearer(\"my-token\"), got {:?}",
config.auth
);
}
#[test]
fn builder_oauth_token_sets_oauth_auth() {
let config = Configuration::builder().oauth_token("oauth-tok").build();
assert!(
matches!(&config.auth, Some(AuthMethod::OAuth(t)) if t == "oauth-tok"),
"expected OAuth(\"oauth-tok\"), got {:?}",
config.auth
);
}
#[test]
fn builder_basic_auth_has_named_fields() {
let config = Configuration::builder()
.basic_auth("user", Some("pass"))
.build();
match &config.auth {
Some(AuthMethod::Basic(creds)) => {
assert_eq!(creds.username, "user");
assert_eq!(creds.password.as_deref(), Some("pass"));
}
other => panic!("expected Basic auth, got {:?}", other),
}
}
#[test]
fn builder_api_key_with_prefix() {
let config = Configuration::builder()
.api_key("my-key", Some("Token"))
.build();
match &config.auth {
Some(AuthMethod::ApiKey { prefix, key }) => {
assert_eq!(key, "my-key");
assert_eq!(prefix.as_deref(), Some("Token"));
}
other => panic!("expected ApiKey auth, got {:?}", other),
}
}
#[test]
fn builder_last_auth_setter_wins() {
let config = Configuration::builder()
.bearer_token("first")
.oauth_token("second")
.build();
assert!(
matches!(&config.auth, Some(AuthMethod::OAuth(t)) if t == "second"),
"expected OAuth(\"second\") to win, got {:?}",
config.auth
);
}
#[test]
fn builder_custom_base_path_is_preserved() {
let config = Configuration::builder()
.base_path("https://prod.example.com")
.build();
assert_eq!(config.base_path, "https://prod.example.com");
}
#[test]
fn error_display_response_error_with_entity_includes_entity_details() {
let rc: ResponseContent<String> = ResponseContent {
status: reqwest::StatusCode::BAD_REQUEST,
content: String::from("raw body"),
entity: Some(String::from("detail string")),
};
let err: Error<String> = Error::ResponseError(rc);
let display = format!("{err}");
assert!(
display.contains("status code"),
"display should contain 'status code', got: {display}"
);
assert!(
display.contains("detail string"),
"display should contain entity details, got: {display}"
);
}
#[test]
fn error_display_response_error_without_entity_shows_status_only() {
let rc: ResponseContent<String> = ResponseContent {
status: reqwest::StatusCode::BAD_REQUEST,
content: String::from("raw body"),
entity: None,
};
let err: Error<String> = Error::ResponseError(rc);
let display = format!("{err}");
assert!(
display.contains("status code 400"),
"display should contain 'status code 400', got: {display}"
);
}
#[test]
fn error_display_serde_error_shows_serde_module() {
let serde_err = serde_json::from_str::<i32>("not-a-number").unwrap_err();
let err: Error<String> = Error::Serde(serde_err);
let display = format!("{err}");
assert!(
display.starts_with("error in serde:"),
"display should start with 'error in serde:', got: {display}"
);
}
#[test]
fn urlencode_encodes_slashes_and_spaces() {
let encoded = urlencode("store/id with space");
assert!(
encoded.contains("%2F"),
"slash should be percent-encoded, got: {encoded}"
);
assert!(
encoded.contains('+') || encoded.contains("%20"),
"space should be encoded, got: {encoded}"
);
}
#[test]
fn urlencode_empty_string_returns_empty() {
let encoded = urlencode("");
assert_eq!(encoded, "", "empty string should encode to empty string");
}
}