1use std::fmt::{Display, Formatter, Result as FmtResult};
17
18use cfg_if::cfg_if;
19use dashmap::DashMap;
20use lazy_static::lazy_static;
21#[cfg(feature = "rayon")]
22use rayon::prelude::*;
23use regex::Regex;
24
25pub type Error = Box<dyn std::error::Error + Send + Sync>;
26pub type Result<T, E = Error> = std::result::Result<T, E>;
27
28pub fn parse<'a>(content: &'a str) -> Result<DashMap<&'a str, Process>> {
48 lazy_static! {
49 static ref REGEX: Regex =
50 Regex::new(r"^([A-Za-z0-9_]+):\s*(.+)$").expect("Failed building regex");
51 }
52
53 let map: DashMap<&'a str, Process> = DashMap::new();
54
55 #[cfg(feature = "rayon")]
56 content.split('\n').par_bridge().for_each(|line| match REGEX.captures(line) {
57 Some(captures) => {
58 let details = captures
59 .get(2)
60 .expect("Failed getting command and options")
61 .as_str()
62 .trim()
63 .split(' ')
64 .collect::<Vec<_>>();
65
66 let name = captures.get(1).expect("Failed getting process name").as_str();
67
68 map.insert(name, Process {
69 command: details[0],
70 options: details[1..].to_vec(),
71 });
72 },
73 None => (),
74 });
75
76 #[cfg(not(feature = "rayon"))]
77 content.split('\n').for_each(|line| match REGEX.captures(line) {
78 Some(captures) => {
79 let details = captures
80 .get(2)
81 .expect("Failed getting command and options")
82 .as_str()
83 .trim()
84 .split(' ')
85 .collect::<Vec<_>>();
86
87 let name = captures.get(1).expect("Failed getting process name").as_str();
88
89 map.insert(name, Process {
90 command: details[0],
91 options: details[1..].to_vec(),
92 });
93 },
94 None => (),
95 });
96
97 Ok(map)
98}
99
100cfg_if! {
101 if #[cfg(feature = "serde")] {
102 use serde::{Serialize, Deserialize};
103
104 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
106 pub struct Process<'a> {
107 pub command: &'a str,
109 pub options: Vec<&'a str>,
111 }
112 } else {
113 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
115 pub struct Process<'a> {
116 pub command: &'a str,
118 pub options: Vec<&'a str>,
120 }
121 }
122}
123
124impl<'a> Display for Process<'a> {
125 fn fmt(&self, f: &mut Formatter) -> FmtResult {
126 write!(f, "{} {}", self.command, self.options.join(" "))
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn one_process() {
136 let procfile = "web: node a.js --option-1 --option-2";
137 let parsed = parse(procfile).unwrap();
138
139 assert!(parsed.contains_key("web"));
140
141 let process = parsed.get("web").unwrap();
142
143 assert_eq!("node", process.command);
144 assert_eq!(vec!["a.js", "--option-1", "--option-2"], process.options)
145 }
146
147 #[test]
148 fn multiple_process() {
149 let procfile = "\
150web: py b.py --my-option
151worker: gcc c.c
152 ";
153
154 let parsed = parse(procfile).unwrap();
155
156 assert!(parsed.contains_key("web") && parsed.contains_key("worker"));
157
158 let web = parsed.get("web").unwrap();
159 let worker = parsed.get("worker").unwrap();
160
161 assert_eq!("py", web.command);
162 assert_eq!("gcc", worker.command);
163 assert_eq!(vec!["b.py", "--my-option"], web.options);
164 assert_eq!(vec!["c.c"], worker.options);
165 }
166
167 #[test]
168 fn no_process() {
169 let procfile = "";
170 let parsed = parse(procfile).unwrap();
171
172 assert!(parsed.is_empty());
173 }
174
175 #[test]
176 fn invalid_process() {
177 let procfile = "hedhehiidhodhidhiodiedhidwhio";
178 let parsed = parse(procfile).unwrap();
179
180 assert!(parsed.is_empty());
181 }
182
183 #[test]
184 fn test_display() {
185 let procfile = "web: node index.mjs --verbose";
186 let parsed = parse(procfile).unwrap();
187 let web_process = &*parsed.get("web").unwrap();
188
189 assert_eq!("node index.mjs --verbose", &format!("{}", web_process));
190 }
191}