1use std::collections::HashMap;
4use std::fmt;
5
6use strum_macros::EnumString;
7
8#[derive(Debug, EnumString)]
9#[strum(serialize_all = "lowercase")]
10pub enum Protocol {
12 Tcp,
13 Udp,
14}
15
16#[derive(Debug, EnumString)]
17#[strum(serialize_all = "UPPERCASE")]
18pub enum Instruction {
20 Add {
21 checksum: Option<String>,
22 chown: Option<String>,
23 chmod: Option<String>,
24 link: Option<String>,
25 sources: Vec<String>,
26 destination: String,
27 },
28 Arg(HashMap<String, Option<String>>),
29 Cmd(Vec<String>),
30 Copy {
31 from: Option<String>,
32 chown: Option<String>,
33 chmod: Option<String>,
34 link: Option<String>,
35 sources: Vec<String>,
36 destination: String,
37 },
38 Entrypoint(Vec<String>),
39 Env(HashMap<String, String>),
40 Expose {
41 port: String,
42 protocol: Option<Protocol>,
43 },
44 From {
45 platform: Option<String>,
46 image: String,
47 alias: Option<String>,
48 },
49 Label(HashMap<String, String>),
50 Run {
51 mount: Option<String>,
52 network: Option<String>,
53 security: Option<String>,
54 command: Vec<String>,
55 },
56 Shell(Vec<String>),
57 User {
58 user: String,
59 group: Option<String>,
60 },
61 Volume {
62 mounts: Vec<String>,
63 },
64 Workdir {
65 path: String,
66 },
67 Comment(String),
71 Empty,
72}
73
74impl fmt::Display for Instruction {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 match self {
77 Instruction::Add {
78 checksum,
79 chown,
80 chmod,
81 link,
82 sources,
83 destination,
84 } => {
85 let options = vec![
86 helpers::format_instruction_option("checksum", checksum),
87 helpers::format_instruction_option("chown", chown),
88 helpers::format_instruction_option("chmod", chmod),
89 helpers::format_instruction_option("link", link),
90 ];
91 let options_string = helpers::format_options_string(options);
92 let prefix = if options_string.is_empty() {
93 String::new()
94 } else {
95 format!("{} ", options_string)
96 };
97 write!(f, "ADD {}{} {}", prefix, sources.join(" "), destination)
98 }
99 Instruction::Arg(args) => {
100 let arg_string = args
101 .iter()
102 .map(|(key, value)| {
103 if let Some(default) = value {
104 format!("{}={}", key, default)
105 } else {
106 key.to_owned()
107 }
108 })
109 .collect::<Vec<String>>()
110 .join(" ");
111 write!(f, "ARG {}", arg_string)
112 }
113 Instruction::Cmd(cmd) => write!(f, "CMD {:?}", cmd),
114 Instruction::Copy {
115 from,
116 chown,
117 chmod,
118 link,
119 sources,
120 destination,
121 } => {
122 let options = vec![
123 helpers::format_instruction_option("from", from),
124 helpers::format_instruction_option("chown", chown),
125 helpers::format_instruction_option("chmod", chmod),
126 helpers::format_instruction_option("link", link),
127 ];
128 let options_string = helpers::format_options_string(options);
129 let prefix = if options_string.is_empty() {
130 String::new()
131 } else {
132 format!("{} ", options_string)
133 };
134 write!(f, "COPY {}{} {}", prefix, sources.join(" "), destination)
135 }
136 Instruction::Entrypoint(entrypoint) => write!(f, "ENTRYPOINT {:?}", entrypoint),
137 Instruction::Env(env) => {
138 write!(f, "ENV {}", helpers::format_hash_map(env))
139 }
140 Instruction::Expose { port, protocol } => {
141 if let Some(protocol) = protocol {
142 write!(f, "EXPOSE {}/{:?}", port, protocol)
143 } else {
144 write!(f, "EXPOSE {}", port)
145 }
146 }
147 Instruction::From {
148 platform,
149 image,
150 alias,
151 } => {
152 let options = vec![helpers::format_instruction_option("platform", platform)];
153 let options_string = helpers::format_options_string(options);
154 let prefix = if options_string.is_empty() {
155 String::new()
156 } else {
157 format!("{} ", options_string)
158 };
159 let mut line = format!("FROM {}{}", prefix, image);
160
161 if let Some(alias) = alias {
162 line.push_str(&format!(" AS {}", alias));
163 }
164 write!(f, "FROM {}", line)
165 }
166 Instruction::Label(labels) => {
167 write!(f, "LABEL {}", helpers::format_hash_map(labels))
168 }
169 Instruction::Run {
170 mount,
171 network,
172 security,
173 command,
174 } => {
175 let options = vec![
176 helpers::format_instruction_option("mount", mount),
177 helpers::format_instruction_option("network", network),
178 helpers::format_instruction_option("security", security),
179 ];
180 let options_string = helpers::format_options_string(options);
181 write!(f, "RUN {}{:?}", options_string, command)
182 }
183 Instruction::Shell(shell) => write!(f, "SHELL {:?}", shell),
184 Instruction::User { user, group } => {
185 if let Some(group) = group {
186 write!(f, "USER {}:{}", user, group)
187 } else {
188 write!(f, "USER {}", user)
189 }
190 }
191 Instruction::Volume { mounts } => write!(f, "VOLUME {:?}", mounts),
192 Instruction::Workdir { path } => write!(f, "WORKDIR {}", path),
193 Instruction::Comment(comment) => write!(f, "{}", comment),
197 Instruction::Empty => write!(f, ""),
198 }
199 }
200}
201
202mod helpers {
203 use super::*;
204
205 pub fn format_instruction_option(key: &str, value: &Option<String>) -> String {
206 value
207 .as_ref()
208 .map(|v| format!("--{}={}", key, v))
209 .unwrap_or_default()
210 }
211
212 pub fn format_options_string(options: Vec<String>) -> String {
213 options
214 .into_iter()
215 .filter(|s| !s.is_empty())
216 .collect::<Vec<String>>()
217 .join(" ")
218 }
219
220 pub fn format_hash_map(pairs: &HashMap<String, String>) -> String {
221 pairs
222 .iter()
223 .map(|(key, value)| format!("{}=\"{}\"", key, value))
224 .collect::<Vec<String>>()
225 .join(" ")
226 }
227}