github_actions/
command.rs1use std::fmt::Display;
2
3pub struct Property<'a>(pub &'a str, pub &'a str);
4
5impl<'a> Display for Property<'a> {
6 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
7 f.write_fmt(format_args!("{}={}", self.0, escape_property(self.1)))
8 }
9}
10
11pub struct Properties<'a>(pub Vec<Property<'a>>);
12
13impl<'a> From<Vec<(&'a str, &'a str)>> for Properties<'a> {
14 fn from(value: Vec<(&'a str, &'a str)>) -> Self {
15 Properties(value.into_iter().map(|(k, v)| Property(k, v)).collect())
16 }
17}
18
19impl<'a> From<&[(&'a str, &'a str)]> for Properties<'a> {
20 fn from(value: &[(&'a str, &'a str)]) -> Self {
21 Properties(value.iter().map(|(k, v)| Property(k, v)).collect())
22 }
23}
24
25impl<'a, const N: usize> From<[(&'a str, &'a str); N]> for Properties<'a> {
26 fn from(value: [(&'a str, &'a str); N]) -> Self {
27 Properties(value.into_iter().map(|(k, v)| Property(k, v)).collect())
28 }
29}
30
31impl<'a> Display for Properties<'a> {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 f.write_str(
34 self.0
35 .iter()
36 .map(|v| v.to_string())
37 .collect::<Vec<_>>()
38 .join(",")
39 .as_str(),
40 )
41 }
42}
43
44pub struct Command<'a> {
45 pub command: &'a str,
46 pub value: &'a str,
47 pub properties: Option<Properties<'a>>,
48}
49
50impl<'a> Display for Command<'a> {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 match &self.properties {
53 Some(properties) => f.write_fmt(format_args!(
54 "::{} {}::{}",
55 self.command,
56 properties,
57 escape_data(self.value)
58 )),
59 None => f.write_fmt(format_args!(
60 "::{}::{}",
61 self.command,
62 escape_data(self.value)
63 )),
64 }
65 }
66}
67
68#[derive(Default)]
69pub struct CommandWithProperties<'a> {
70 pub command: &'a str,
71 pub value: &'a str,
72 pub title: Option<&'a str>,
73 pub file: Option<&'a str>,
74 pub col: Option<usize>,
75 pub end_column: Option<usize>,
76 pub line: Option<usize>,
77 pub end_line: Option<usize>,
78}
79
80impl<'a> Display for CommandWithProperties<'a> {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 let col = self.col.map(|v| v.to_string());
83 let end_column = self.end_column.map(|v| v.to_string());
84 let line = self.line.map(|v| v.to_string());
85 let end_line = self.line.map(|v| v.to_string());
86 let params: Vec<(&str, &str)> = vec![
87 ("title", self.title),
88 ("file", self.file),
89 ("col", col.as_deref()),
90 ("endColumn", end_column.as_deref()),
91 ("line", line.as_deref()),
92 ("endLine", end_line.as_deref()),
93 ]
94 .into_iter()
95 .filter_map(|(k, v)| v.map(|v| (k, v)))
96 .collect();
97
98 Command {
99 command: self.command,
100 value: self.value,
101 properties: Some(params.into()),
102 }
103 .fmt(f)
104 }
105}
106
107pub fn escape_data<T: AsRef<str>>(s: T) -> String {
108 s.as_ref()
109 .replace('%', "%25")
110 .replace('\r', "%0D")
111 .replace('\n', "%0A")
112}
113
114pub fn escape_property<T: AsRef<str>>(s: T) -> String {
115 s.as_ref()
116 .replace('%', "%25")
117 .replace('\r', "%0D")
118 .replace('\n', "%0A")
119 .replace(':', "%3A")
120 .replace(',', "%2C")
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn test_message() {
129 let message = Command {
130 command: "command",
131 properties: None,
132 value: "some message",
133 };
134
135 assert_eq!("::command::some message", message.to_string());
136 }
137
138 #[test]
139 fn test_message_with_property() {
140 let message = Command {
141 command: "command",
142 properties: Some(Properties(vec![Property("title", "value")])),
143 value: "some message",
144 };
145
146 assert_eq!("::command title=value::some message", message.to_string());
147 }
148
149 #[test]
150 fn test_message_with_properties() {
151 let message = Command {
152 command: "command",
153 properties: Some(Properties(vec![
154 Property("title", "value"),
155 Property("line", "1"),
156 ])),
157 value: "some message",
158 };
159
160 assert_eq!(
161 "::command title=value,line=1::some message",
162 message.to_string()
163 );
164 }
165}