muxr_core/
server_runner.rs1use std::ffi::OsStr;
2use std::ffi::OsString;
3use std::path::PathBuf;
4
5use rootcause::report;
6
7use crate::EXTERNAL_LAYOUT_ARG;
8use crate::SessionName;
9
10#[derive(Clone, Debug, Eq, PartialEq)]
12pub struct ServerRunnerArgs {
13 pub external_layout: Option<PathBuf>,
15 pub session: SessionName,
17}
18
19impl ServerRunnerArgs {
20 pub fn parse(args: &[OsString]) -> rootcause::Result<Self> {
25 let Some((session, rest)) = args.split_first() else {
26 return Err(report!("missing muxr server session"));
27 };
28 let session = self::parse_session_arg(session)?;
29 let mut external_layout = None;
30 let mut rest = rest.iter();
31
32 while let Some(flag) = rest.next() {
33 if flag == OsStr::new(EXTERNAL_LAYOUT_ARG) {
34 let Some(layout) = rest.next() else {
35 return Err(report!("missing muxr server layout").attach(format!("session={session}")));
36 };
37 if external_layout.replace(PathBuf::from(layout.clone())).is_some() {
38 return Err(report!("duplicate muxr server layout").attach(format!("session={session}")));
39 }
40 } else {
41 return Err(report!("unexpected muxr server args").attach(format!("args={args:?}")));
42 }
43 }
44
45 Ok(Self {
46 external_layout,
47 session,
48 })
49 }
50
51 #[must_use]
53 pub fn argv(&self) -> Vec<OsString> {
54 let mut args = vec![OsString::from(self.session.as_ref())];
55 if let Some(external_layout) = &self.external_layout {
56 args.push(OsString::from(EXTERNAL_LAYOUT_ARG));
57 args.push(external_layout.as_os_str().to_owned());
58 }
59 args
60 }
61}
62
63fn parse_session_arg(raw: &OsStr) -> rootcause::Result<SessionName> {
64 let Some(raw) = raw.to_str() else {
65 return Err(report!("invalid muxr server session").attach("reason=session must be valid UTF-8"));
66 };
67 raw.parse()
68}
69
70#[cfg(test)]
71mod tests {
72 use std::ffi::OsStr;
73 use std::path::Path;
74
75 use rstest::rstest;
76 use test_that::prelude::*;
77
78 use super::*;
79
80 #[test]
81 fn test_parse_when_session_is_supplied_returns_session() -> rootcause::Result<()> {
82 let args = ServerRunnerArgs::parse(&args(&["work"]))?;
83
84 assert_that!(args.session.as_ref(), eq("work"));
85 assert_that!(args.external_layout, eq(None));
86 Ok(())
87 }
88
89 #[test]
90 fn test_parse_when_layout_is_supplied_returns_layout() -> rootcause::Result<()> {
91 let args = ServerRunnerArgs::parse(&args(&["work", "--layout", ".config/muxr/layouts/work.json"]))?;
92
93 assert_that!(args.session.as_ref(), eq("work"));
94 assert_that!(
95 args.external_layout.as_deref().and_then(Path::to_str),
96 eq(Some(".config/muxr/layouts/work.json"))
97 );
98 Ok(())
99 }
100
101 #[test]
102 fn test_argv_when_layout_is_supplied_returns_runner_args() -> rootcause::Result<()> {
103 let args = ServerRunnerArgs {
104 external_layout: Some(PathBuf::from(".config/muxr/layouts/work.json")),
105 session: "work".parse()?,
106 };
107
108 assert_that!(
109 args.argv(),
110 eq(vec![
111 OsString::from("work"),
112 OsString::from(EXTERNAL_LAYOUT_ARG),
113 OsStr::new(".config/muxr/layouts/work.json").to_owned()
114 ])
115 );
116 Ok(())
117 }
118
119 #[cfg(unix)]
120 #[test]
121 fn test_parse_when_layout_is_not_utf8_preserves_path_bytes() -> rootcause::Result<()> {
122 use std::os::unix::ffi::OsStrExt;
123 use std::os::unix::ffi::OsStringExt;
124
125 let raw_layout = OsString::from_vec(b"layout-\xFF.json".to_vec());
126 let parsed =
127 ServerRunnerArgs::parse(&[OsString::from("work"), OsString::from(EXTERNAL_LAYOUT_ARG), raw_layout])?;
128
129 assert_that!(parsed.session.as_ref(), eq("work"));
130 assert_that!(
131 parsed
132 .external_layout
133 .as_deref()
134 .map(Path::as_os_str)
135 .map(OsStr::as_bytes),
136 eq(Some(b"layout-\xFF.json".as_slice()))
137 );
138 Ok(())
139 }
140
141 #[rstest]
142 #[case::missing_session(&[])]
143 #[case::extra_args(&["work", "extra"])]
144 #[case::missing_layout(&["work", "--layout"])]
145 fn test_parse_when_args_are_invalid_returns_error(#[case] raw: &[&str]) {
146 assert_that!(ServerRunnerArgs::parse(&args(raw)), err(anything()));
147 }
148
149 fn args(raw: &[&str]) -> Vec<OsString> {
150 raw.iter().map(OsString::from).collect()
151 }
152}