1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum DockerfileInstructionError {
10 Empty,
12 UnknownInstruction,
14 MissingArguments,
16}
17
18impl fmt::Display for DockerfileInstructionError {
19 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
20 match self {
21 Self::Empty => formatter.write_str("Dockerfile instruction cannot be empty"),
22 Self::UnknownInstruction => formatter.write_str("unknown Dockerfile instruction"),
23 Self::MissingArguments => formatter.write_str("Dockerfile instruction needs arguments"),
24 }
25 }
26}
27
28impl Error for DockerfileInstructionError {}
29
30#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
32pub enum DockerfileInstructionKind {
33 From,
35 Run,
37 Copy,
39 Add,
41 Cmd,
43 Entrypoint,
45 Env,
47 Arg,
49 Workdir,
51 Expose,
53 Label,
55 User,
57 Volume,
59 Healthcheck,
61 Stopsignal,
63 Shell,
65}
66
67impl DockerfileInstructionKind {
68 #[must_use]
70 pub const fn as_str(self) -> &'static str {
71 match self {
72 Self::From => "FROM",
73 Self::Run => "RUN",
74 Self::Copy => "COPY",
75 Self::Add => "ADD",
76 Self::Cmd => "CMD",
77 Self::Entrypoint => "ENTRYPOINT",
78 Self::Env => "ENV",
79 Self::Arg => "ARG",
80 Self::Workdir => "WORKDIR",
81 Self::Expose => "EXPOSE",
82 Self::Label => "LABEL",
83 Self::User => "USER",
84 Self::Volume => "VOLUME",
85 Self::Healthcheck => "HEALTHCHECK",
86 Self::Stopsignal => "STOPSIGNAL",
87 Self::Shell => "SHELL",
88 }
89 }
90}
91
92impl fmt::Display for DockerfileInstructionKind {
93 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
94 formatter.write_str(self.as_str())
95 }
96}
97
98impl FromStr for DockerfileInstructionKind {
99 type Err = DockerfileInstructionError;
100
101 fn from_str(value: &str) -> Result<Self, Self::Err> {
102 match value.trim().to_ascii_uppercase().as_str() {
103 "FROM" => Ok(Self::From),
104 "RUN" => Ok(Self::Run),
105 "COPY" => Ok(Self::Copy),
106 "ADD" => Ok(Self::Add),
107 "CMD" => Ok(Self::Cmd),
108 "ENTRYPOINT" => Ok(Self::Entrypoint),
109 "ENV" => Ok(Self::Env),
110 "ARG" => Ok(Self::Arg),
111 "WORKDIR" => Ok(Self::Workdir),
112 "EXPOSE" => Ok(Self::Expose),
113 "LABEL" => Ok(Self::Label),
114 "USER" => Ok(Self::User),
115 "VOLUME" => Ok(Self::Volume),
116 "HEALTHCHECK" => Ok(Self::Healthcheck),
117 "STOPSIGNAL" => Ok(Self::Stopsignal),
118 "SHELL" => Ok(Self::Shell),
119 _ => Err(DockerfileInstructionError::UnknownInstruction),
120 }
121 }
122}
123
124#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
126pub struct DockerfileInstruction {
127 kind: DockerfileInstructionKind,
128 arguments: String,
129}
130
131impl DockerfileInstruction {
132 pub fn new(
134 kind: DockerfileInstructionKind,
135 arguments: impl AsRef<str>,
136 ) -> Result<Self, DockerfileInstructionError> {
137 let arguments = arguments.as_ref().trim();
138 if arguments.is_empty() {
139 return Err(DockerfileInstructionError::MissingArguments);
140 }
141 Ok(Self {
142 kind,
143 arguments: arguments.to_string(),
144 })
145 }
146
147 pub fn from(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
149 Self::new(DockerfileInstructionKind::From, arguments)
150 }
151
152 #[must_use]
154 pub fn run(arguments: impl AsRef<str>) -> Self {
155 Self {
156 kind: DockerfileInstructionKind::Run,
157 arguments: arguments.as_ref().trim().to_string(),
158 }
159 }
160
161 pub fn copy(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
163 Self::new(DockerfileInstructionKind::Copy, arguments)
164 }
165
166 pub fn add(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
168 Self::new(DockerfileInstructionKind::Add, arguments)
169 }
170
171 pub fn cmd(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
173 Self::new(DockerfileInstructionKind::Cmd, arguments)
174 }
175
176 pub fn entrypoint(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
178 Self::new(DockerfileInstructionKind::Entrypoint, arguments)
179 }
180
181 pub fn env(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
183 Self::new(DockerfileInstructionKind::Env, arguments)
184 }
185
186 pub fn arg(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
188 Self::new(DockerfileInstructionKind::Arg, arguments)
189 }
190
191 pub fn workdir(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
193 Self::new(DockerfileInstructionKind::Workdir, arguments)
194 }
195
196 pub fn expose(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
198 Self::new(DockerfileInstructionKind::Expose, arguments)
199 }
200
201 pub fn label(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
203 Self::new(DockerfileInstructionKind::Label, arguments)
204 }
205
206 pub fn user(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
208 Self::new(DockerfileInstructionKind::User, arguments)
209 }
210
211 pub fn volume(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
213 Self::new(DockerfileInstructionKind::Volume, arguments)
214 }
215
216 pub fn healthcheck(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
218 Self::new(DockerfileInstructionKind::Healthcheck, arguments)
219 }
220
221 pub fn stopsignal(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
223 Self::new(DockerfileInstructionKind::Stopsignal, arguments)
224 }
225
226 pub fn shell(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
228 Self::new(DockerfileInstructionKind::Shell, arguments)
229 }
230
231 #[must_use]
233 pub const fn kind(&self) -> DockerfileInstructionKind {
234 self.kind
235 }
236
237 #[must_use]
239 pub fn arguments(&self) -> &str {
240 &self.arguments
241 }
242}
243
244impl fmt::Display for DockerfileInstruction {
245 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
246 write!(formatter, "{} {}", self.kind, self.arguments)
247 }
248}
249
250impl FromStr for DockerfileInstruction {
251 type Err = DockerfileInstructionError;
252
253 fn from_str(value: &str) -> Result<Self, Self::Err> {
254 let trimmed = value.trim();
255 if trimmed.is_empty() {
256 return Err(DockerfileInstructionError::Empty);
257 }
258 let Some((keyword, arguments)) = trimmed.split_once(char::is_whitespace) else {
259 return Err(DockerfileInstructionError::MissingArguments);
260 };
261 Self::new(keyword.parse()?, arguments)
262 }
263}
264
265impl TryFrom<&str> for DockerfileInstruction {
266 type Error = DockerfileInstructionError;
267
268 fn try_from(value: &str) -> Result<Self, Self::Error> {
269 value.parse()
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::{DockerfileInstruction, DockerfileInstructionKind};
276
277 #[test]
278 fn parses_and_renders_instruction_lines() -> Result<(), Box<dyn std::error::Error>> {
279 let from: DockerfileInstruction = "FROM rust:1.95".parse()?;
280 let copy = DockerfileInstruction::copy("src/ /app/src/")?;
281
282 assert_eq!(from.kind(), DockerfileInstructionKind::From);
283 assert_eq!(from.arguments(), "rust:1.95");
284 assert_eq!(copy.to_string(), "COPY src/ /app/src/");
285 assert_eq!(
286 DockerfileInstruction::run("cargo test").to_string(),
287 "RUN cargo test"
288 );
289 Ok(())
290 }
291}