openfga 1.0.1

Rust SDK for OpenFGA — the open-source authorization system
Documentation
/*
 * OpenFGA
 *
 * A high performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar.
 *
 * The version of the OpenAPI document: 1.x
 * Contact: community@openfga.dev
 * Generated by: https://openapi-generator.tech
 */

use std::fmt;

///
/// Exactly one variant should be active per [`Configuration`]. Using
/// [`ConfigurationBuilder`] enforces this at construction time — the last
/// auth setter wins, removing the ambiguity of the old multi-field design.
#[derive(Clone)]
pub enum AuthMethod {
    /// A pre-issued static bearer token.
    Bearer(String),
    /// An OAuth2 access token — sent identically to [`Bearer`](AuthMethod::Bearer) on the wire.
    OAuth(String),
    /// HTTP Basic authentication.
    Basic(BasicAuth),
    /// A raw key sent as the `Authorization` header value, with optional prefix.
    ApiKey {
        /// Optional prefix (e.g. `"Bearer"` or `"Token"`).
        prefix: Option<String>,
        key: String,
    },
}

/// HTTP Basic authentication credentials with named fields.
///
/// Replaces the former `(String, Option<String>)` tuple alias that gave no
/// indication of which position was username vs. password.
#[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(),
        }
    }
}

/// Runtime configuration for every API call.
///
/// Construct via [`Configuration::builder()`] to get compile-time safety on
/// which auth method is active and to avoid direct field mutation.
///
/// # Example
/// ```no_run
/// use openfga::apis::configuration::Configuration;
///
/// let config = Configuration::builder()
///     .base_path("https://api.fga.example.com")
///     .bearer_token("my-token")
///     .build();
/// ```
#[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 {
    /// Create a [`ConfigurationBuilder`] to construct a `Configuration`.
    pub fn builder() -> ConfigurationBuilder {
        ConfigurationBuilder::default()
    }

    /// Apply this configuration's `User-Agent` header and auth credentials to
    /// a request builder. Called internally by every API function, replacing the
    /// former pair of free functions `set_user_agent` + `apply_auth`.
    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,
        }
    }
}

/// Builder for [`Configuration`].
///
/// Auth methods are mutually exclusive setters: calling more than one simply
/// overwrites the previous choice, so the last setter wins. No hidden
/// priority list exists.
#[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 {
    /// Set the OpenFGA server base URL (e.g. `"https://api.fga.example.com"`).
    pub fn base_path(mut self, base_path: impl Into<String>) -> Self {
        self.base_path = base_path.into();
        self
    }

    /// Override the `User-Agent` header sent with every request.
    pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
        self.user_agent = Some(ua.into());
        self
    }

    /// Provide a pre-built [`reqwest::Client`] (e.g. with custom TLS settings
    /// or connection pool tuning). A default client is used if not set.
    pub fn client(mut self, client: reqwest::Client) -> Self {
        self.client = Some(client);
        self
    }

    /// Override the default 30-second request timeout applied to every request.
    ///
    /// Has no effect when a pre-built client is supplied via [`.client()`].
    pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
        self.timeout = timeout;
        self
    }

    /// Authenticate with a static bearer token.
    pub fn bearer_token(mut self, token: impl Into<String>) -> Self {
        self.auth = Some(AuthMethod::Bearer(token.into()));
        self
    }

    /// Authenticate with an OAuth2 access token.
    pub fn oauth_token(mut self, token: impl Into<String>) -> Self {
        self.auth = Some(AuthMethod::OAuth(token.into()));
        self
    }

    /// Authenticate with HTTP Basic credentials.
    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
    }

    /// Authenticate with a raw API key sent as the `Authorization` header.
    ///
    /// The header value will be `"<prefix> <key>"` if a prefix is provided,
    /// or just `"<key>"` otherwise.
    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
    }

    /// Consume the builder and produce a [`Configuration`].
    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};

    // ── GROUP A: ConfigurationBuilder ──────────────────────────────────────────

    #[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)) => {
                // Access by field name, not tuple index — proves AP-2 fix
                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();
        // Last setter (oauth_token) must win — no hidden priority list
        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");
    }

    // ── GROUP B: Error Display ─────────────────────────────────────────────────

    #[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}"
        );
        // entity debug repr should appear
        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}"
        );
    }

    // ── GROUP C: urlencode ─────────────────────────────────────────────────────

    #[test]
    fn urlencode_encodes_slashes_and_spaces() {
        let encoded = urlencode("store/id with space");
        // url::form_urlencoded encodes '/' as %2F and space as '+'
        assert!(
            encoded.contains("%2F"),
            "slash should be percent-encoded, got: {encoded}"
        );
        // space is encoded as '+' by form_urlencoded
        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");
    }
}