ready_set_sdk/
lifecycle.rs1use std::ffi::OsString;
4
5use crate::CapabilityId;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum LifecycleRequest {
10 Ready {
12 capability: CapabilityId,
14 },
15 Set {
17 capability: CapabilityId,
19 args: Vec<OsString>,
21 },
22 Go {
24 capability: CapabilityId,
26 args: Vec<OsString>,
28 },
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct LifecycleRequestError {
34 message: String,
35}
36
37impl LifecycleRequestError {
38 #[must_use]
40 pub fn message(&self) -> &str {
41 &self.message
42 }
43}
44
45impl std::fmt::Display for LifecycleRequestError {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 f.write_str(&self.message)
48 }
49}
50
51impl std::error::Error for LifecycleRequestError {}
52
53pub fn parse_lifecycle_request(
64 args: impl IntoIterator<Item = OsString>,
65) -> Result<Option<LifecycleRequest>, LifecycleRequestError> {
66 let mut args = args.into_iter();
67 drop(args.next());
68 let Some(command) = args.next() else {
69 return Ok(None);
70 };
71 let Some(command) = command.to_str() else {
72 return Ok(None);
73 };
74
75 match command {
76 "__ready" => {
77 let capability = required_capability(command, args.next())?;
78 if args.next().is_some() {
79 return Err(error("__ready accepts exactly one capability argument"));
80 }
81 Ok(Some(LifecycleRequest::Ready { capability }))
82 },
83 "__set" => {
84 let capability = required_capability(command, args.next())?;
85 Ok(Some(LifecycleRequest::Set {
86 capability,
87 args: args.collect(),
88 }))
89 },
90 "__go" => {
91 let capability = required_capability(command, args.next())?;
92 Ok(Some(LifecycleRequest::Go {
93 capability,
94 args: args.collect(),
95 }))
96 },
97 _ => Ok(None),
98 }
99}
100
101fn required_capability(
102 command: &str,
103 raw: Option<OsString>,
104) -> Result<CapabilityId, LifecycleRequestError> {
105 let Some(raw) = raw else {
106 return Err(error(format!("{command} requires a capability argument")));
107 };
108 let Some(raw) = raw.to_str() else {
109 return Err(error(format!(
110 "{command} capability argument must be valid UTF-8"
111 )));
112 };
113 if raw.is_empty() {
114 return Err(error(format!(
115 "{command} capability argument must not be empty"
116 )));
117 }
118 Ok(CapabilityId::from(raw))
119}
120
121fn error(message: impl Into<String>) -> LifecycleRequestError {
122 LifecycleRequestError {
123 message: message.into(),
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 fn os(args: &[&str]) -> Vec<OsString> {
132 args.iter().map(OsString::from).collect()
133 }
134
135 #[test]
136 fn ignores_non_lifecycle_commands() {
137 assert_eq!(
138 parse_lifecycle_request(os(&["plugin", "run"])).unwrap(),
139 None
140 );
141 }
142
143 #[test]
144 fn parses_ready_request() {
145 let request = parse_lifecycle_request(os(&["plugin", "__ready", "linting"]))
146 .unwrap()
147 .unwrap();
148 assert_eq!(
149 request,
150 LifecycleRequest::Ready {
151 capability: "linting".into()
152 }
153 );
154 }
155
156 #[test]
157 fn parses_set_request_with_passthrough_args() {
158 let request = parse_lifecycle_request(os(&[
159 "plugin",
160 "__set",
161 "formatting",
162 "--dry-run",
163 "--force",
164 ]))
165 .unwrap()
166 .unwrap();
167 assert_eq!(
168 request,
169 LifecycleRequest::Set {
170 capability: "formatting".into(),
171 args: os(&["--dry-run", "--force"]),
172 }
173 );
174 }
175
176 #[test]
177 fn rejects_ready_extra_args() {
178 let err =
179 parse_lifecycle_request(os(&["plugin", "__ready", "linting", "extra"])).unwrap_err();
180 assert!(err.to_string().contains("exactly one"));
181 }
182}