aauth_protocol/
requirement.rs1use crate::sf::{self, Member};
5
6pub const HEADER: &str = "aauth-requirement";
8
9#[derive(Clone, Debug, PartialEq, Eq)]
21#[non_exhaustive]
22pub enum Requirement {
23 AgentToken,
25 PersonToken,
27 AuthToken {
29 resource_token: String,
31 },
32 Approval,
34 Interaction {
36 url: Option<String>,
38 code: String,
40 },
41 Clarification,
43 Claims,
45}
46
47impl Requirement {
48 pub fn parse(raw: &str) -> Option<Self> {
51 let dict = sf::parse_dictionary(raw.trim()).ok()?;
52 let (_, Member::Item(item)) = dict.iter().find(|(k, _)| k == "requirement")? else {
53 return None;
54 };
55 let param = |name: &str| {
56 item.params
57 .iter()
58 .find(|(k, _)| k == name)
59 .and_then(|(_, v)| v.as_text())
60 .map(str::to_string)
61 };
62 Some(match item.bare.as_text()? {
63 "agent-token" => Self::AgentToken,
64 "person-token" => Self::PersonToken,
65 "auth-token" => Self::AuthToken {
66 resource_token: param("resource-token")?,
67 },
68 "approval" => Self::Approval,
69 "interaction" => Self::Interaction {
70 url: param("url"),
71 code: param("code")?,
72 },
73 "clarification" => Self::Clarification,
74 "claims" => Self::Claims,
75 _ => return None,
76 })
77 }
78
79 pub fn name(&self) -> &'static str {
81 match self {
82 Self::AgentToken => "agent-token",
83 Self::PersonToken => "person-token",
84 Self::AuthToken { .. } => "auth-token",
85 Self::Approval => "approval",
86 Self::Interaction { .. } => "interaction",
87 Self::Clarification => "clarification",
88 Self::Claims => "claims",
89 }
90 }
91
92 pub fn to_header(&self) -> String {
94 let mut out = format!("requirement={}", self.name());
95 match self {
96 Self::AuthToken { resource_token } => {
97 out.push_str(&format!(";resource-token={}", sf::quote(resource_token)));
98 }
99 Self::Interaction { url, code } => {
100 if let Some(url) = url {
101 out.push_str(&format!(";url={}", sf::quote(url)));
102 }
103 out.push_str(&format!(";code={}", sf::quote(code)));
104 }
105 _ => {}
106 }
107 out
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn requirements_parse_from_the_header() {
117 assert_eq!(
118 Requirement::parse(r#"requirement=auth-token; resource-token="a.b.c""#),
119 Some(Requirement::AuthToken {
120 resource_token: "a.b.c".into()
121 })
122 );
123 assert_eq!(
124 Requirement::parse("requirement=person-token"),
125 Some(Requirement::PersonToken)
126 );
127 assert_eq!(Requirement::parse("requirement=auth-token"), None);
128 assert_eq!(Requirement::parse("requirement=dance"), None);
129 assert_eq!(Requirement::parse("garbage(("), None);
130 }
131
132 #[test]
133 fn a_built_header_parses_back() {
134 for requirement in [
135 Requirement::AuthToken {
136 resource_token: "a.b.c".into(),
137 },
138 Requirement::Interaction {
139 url: Some("https://r.example/i".into()),
140 code: "ABCD-1234".into(),
141 },
142 Requirement::Approval,
143 ] {
144 assert_eq!(
145 Requirement::parse(&requirement.to_header()),
146 Some(requirement)
147 );
148 }
149 }
150}