procfile/
lib.rs

1//! # procfile
2//!
3//! A rust library for parsing Procfile(s).
4//!
5//! ## Examples
6//!
7//! ```rust
8//! let my_procfile = "web: cargo run";
9//! let parsed = procfile::parse(my_procfile).expect("Failed parsing procfile");
10//! let web_process = parsed.get("web").expect("Failed getting web process");
11//!
12//! assert_eq!("cargo", web_process.command);
13//! assert_eq!(vec!["run"], web_process.options);
14//! ```
15
16use 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
28/// Parses a Procfile string.
29///
30/// # Examples
31///
32/// ```rust
33/// use procfile;
34///
35/// let my_procfile = "web: cargo run";
36/// let parsed = procfile::parse(my_procfile).expect("Failed parsing procfile");
37/// let web_process = parsed.get("web").expect("Failed getting web process");
38///
39/// assert_eq!("cargo", web_process.command);
40/// assert_eq!(vec!["run"], web_process.options);
41/// ```
42///
43/// # Errors
44///
45/// - When building the regex fails
46/// - When either the command, options, and the process name don't exist but the regex matched
47pub 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        /// Represents a single process.
105        #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
106        pub struct Process<'a> {
107            /// The command to use. (e.g. `cargo`)
108            pub command: &'a str,
109            /// The command options. (e.g. `["build", "--release"]`)
110            pub options: Vec<&'a str>,
111        }
112    } else {
113        /// Represents a single process.
114        #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
115        pub struct Process<'a> {
116            /// The command to use. (e.g. `cargo`)
117            pub command: &'a str,
118            /// The command options. (e.g. `["build", "--release"]`)
119            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}