use crate::json::escape_json_string;
#[doc(alias = "introspection")]
#[derive(Debug, Clone, Default)]
pub struct IntrospectionResponse {
active: bool,
scope: Option<String>,
client_id: Option<String>,
username: Option<String>,
sub: Option<String>,
token_type: Option<String>,
exp: Option<u64>,
iat: Option<u64>,
aud: Option<String>,
iss: Option<String>,
}
impl IntrospectionResponse {
#[must_use]
pub fn active() -> Self {
Self {
active: true,
..Self::default()
}
}
#[must_use]
pub fn inactive() -> Self {
Self {
active: false,
..Self::default()
}
}
#[must_use]
#[inline]
pub fn is_active(&self) -> bool {
self.active
}
#[must_use]
pub fn scope(mut self, scope: impl Into<String>) -> Self {
self.scope = Some(scope.into());
self
}
#[must_use]
pub fn client_id(mut self, client_id: impl Into<String>) -> Self {
self.client_id = Some(client_id.into());
self
}
#[must_use]
pub fn username(mut self, username: impl Into<String>) -> Self {
self.username = Some(username.into());
self
}
#[must_use]
pub fn subject(mut self, sub: impl Into<String>) -> Self {
self.sub = Some(sub.into());
self
}
#[must_use]
pub fn token_type(mut self, token_type: impl Into<String>) -> Self {
self.token_type = Some(token_type.into());
self
}
#[must_use]
pub fn expiration(mut self, exp: u64) -> Self {
self.exp = Some(exp);
self
}
#[must_use]
pub fn issued_at(mut self, iat: u64) -> Self {
self.iat = Some(iat);
self
}
#[must_use]
pub fn audience(mut self, aud: impl Into<String>) -> Self {
self.aud = Some(aud.into());
self
}
#[must_use]
pub fn issuer(mut self, iss: impl Into<String>) -> Self {
self.iss = Some(iss.into());
self
}
#[must_use]
pub fn to_json(&self) -> String {
if !self.active {
return r#"{"active":false}"#.to_owned();
}
let mut parts: Vec<String> = vec![r#""active":true"#.to_owned()];
let mut push_str_member = |key: &str, value: &Option<String>| {
if let Some(v) = value {
parts.push(format!(
"{}:{}",
escape_json_string(key),
escape_json_string(v)
));
}
};
push_str_member("scope", &self.scope);
push_str_member("client_id", &self.client_id);
push_str_member("username", &self.username);
push_str_member("token_type", &self.token_type);
push_str_member("sub", &self.sub);
push_str_member("aud", &self.aud);
push_str_member("iss", &self.iss);
if let Some(exp) = self.exp {
parts.push(format!(r#""exp":{exp}"#));
}
if let Some(iat) = self.iat {
parts.push(format!(r#""iat":{iat}"#));
}
format!("{{{}}}", parts.join(","))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::json::JsonValue;
#[test]
fn inactive_is_canonical() {
assert_eq!(
IntrospectionResponse::inactive().to_json(),
r#"{"active":false}"#,
);
}
#[test]
fn inactive_suppresses_all_other_fields() {
let mut resp = IntrospectionResponse::inactive();
resp.scope = Some("openid".into());
resp.sub = Some("user-1".into());
resp.exp = Some(123);
assert_eq!(resp.to_json(), r#"{"active":false}"#);
}
#[test]
fn active_full_round_trips_through_parser() {
let json = IntrospectionResponse::active()
.scope("openid profile email")
.client_id("entropy_website")
.username("frodo")
.subject("user-123")
.token_type("Bearer")
.audience("api.example.com")
.issuer("https://auth.example.com")
.expiration(1_700_003_600)
.issued_at(1_700_000_000)
.to_json();
let v = JsonValue::parse(&json).unwrap();
assert_eq!(v.get_bool("active"), Some(true));
assert_eq!(v.get_str("scope"), Some("openid profile email"));
assert_eq!(v.get_str("client_id"), Some("entropy_website"));
assert_eq!(v.get_str("username"), Some("frodo"));
assert_eq!(v.get_str("sub"), Some("user-123"));
assert_eq!(v.get_str("token_type"), Some("Bearer"));
assert_eq!(v.get_str("aud"), Some("api.example.com"));
assert_eq!(v.get_str("iss"), Some("https://auth.example.com"));
assert_eq!(v.get_i64("exp"), Some(1_700_003_600));
assert_eq!(v.get_i64("iat"), Some(1_700_000_000));
}
#[test]
fn active_minimal_only_has_active() {
let json = IntrospectionResponse::active().to_json();
assert_eq!(json, r#"{"active":true}"#);
}
#[test]
fn omitted_fields_are_absent() {
let json = IntrospectionResponse::active().scope("openid").to_json();
let v = JsonValue::parse(&json).unwrap();
assert_eq!(v.get_str("scope"), Some("openid"));
assert_eq!(v.get("client_id"), None);
assert_eq!(v.get("sub"), None);
assert_eq!(v.get("exp"), None);
}
#[test]
fn escapes_special_characters() {
let json = IntrospectionResponse::active()
.username("a\"b\\c")
.to_json();
let v = JsonValue::parse(&json).unwrap();
assert_eq!(v.get_str("username"), Some("a\"b\\c"));
}
#[test]
fn is_active_accessor() {
assert!(IntrospectionResponse::active().is_active());
assert!(!IntrospectionResponse::inactive().is_active());
}
}