Skip to main content

use_dockerfile/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// Error returned when a Dockerfile instruction line is invalid.
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum DockerfileInstructionError {
10    /// The line was empty after trimming.
11    Empty,
12    /// The instruction keyword was not recognized.
13    UnknownInstruction,
14    /// The instruction had no argument text.
15    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/// Common Dockerfile instruction keywords.
31#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
32pub enum DockerfileInstructionKind {
33    /// `FROM`.
34    From,
35    /// `RUN`.
36    Run,
37    /// `COPY`.
38    Copy,
39    /// `ADD`.
40    Add,
41    /// `CMD`.
42    Cmd,
43    /// `ENTRYPOINT`.
44    Entrypoint,
45    /// `ENV`.
46    Env,
47    /// `ARG`.
48    Arg,
49    /// `WORKDIR`.
50    Workdir,
51    /// `EXPOSE`.
52    Expose,
53    /// `LABEL`.
54    Label,
55    /// `USER`.
56    User,
57    /// `VOLUME`.
58    Volume,
59    /// `HEALTHCHECK`.
60    Healthcheck,
61    /// `STOPSIGNAL`.
62    Stopsignal,
63    /// `SHELL`.
64    Shell,
65}
66
67impl DockerfileInstructionKind {
68    /// Returns the uppercase Dockerfile keyword.
69    #[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/// A single Dockerfile instruction line.
125#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
126pub struct DockerfileInstruction {
127    kind: DockerfileInstructionKind,
128    arguments: String,
129}
130
131impl DockerfileInstruction {
132    /// Creates an instruction from a kind and argument text.
133    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    /// Creates a `FROM` instruction.
148    pub fn from(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
149        Self::new(DockerfileInstructionKind::From, arguments)
150    }
151
152    /// Creates a `RUN` instruction.
153    #[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    /// Creates a `COPY` instruction.
162    pub fn copy(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
163        Self::new(DockerfileInstructionKind::Copy, arguments)
164    }
165
166    /// Creates an `ADD` instruction.
167    pub fn add(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
168        Self::new(DockerfileInstructionKind::Add, arguments)
169    }
170
171    /// Creates a `CMD` instruction.
172    pub fn cmd(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
173        Self::new(DockerfileInstructionKind::Cmd, arguments)
174    }
175
176    /// Creates an `ENTRYPOINT` instruction.
177    pub fn entrypoint(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
178        Self::new(DockerfileInstructionKind::Entrypoint, arguments)
179    }
180
181    /// Creates an `ENV` instruction.
182    pub fn env(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
183        Self::new(DockerfileInstructionKind::Env, arguments)
184    }
185
186    /// Creates an `ARG` instruction.
187    pub fn arg(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
188        Self::new(DockerfileInstructionKind::Arg, arguments)
189    }
190
191    /// Creates a `WORKDIR` instruction.
192    pub fn workdir(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
193        Self::new(DockerfileInstructionKind::Workdir, arguments)
194    }
195
196    /// Creates an `EXPOSE` instruction.
197    pub fn expose(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
198        Self::new(DockerfileInstructionKind::Expose, arguments)
199    }
200
201    /// Creates a `LABEL` instruction.
202    pub fn label(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
203        Self::new(DockerfileInstructionKind::Label, arguments)
204    }
205
206    /// Creates a `USER` instruction.
207    pub fn user(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
208        Self::new(DockerfileInstructionKind::User, arguments)
209    }
210
211    /// Creates a `VOLUME` instruction.
212    pub fn volume(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
213        Self::new(DockerfileInstructionKind::Volume, arguments)
214    }
215
216    /// Creates a `HEALTHCHECK` instruction.
217    pub fn healthcheck(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
218        Self::new(DockerfileInstructionKind::Healthcheck, arguments)
219    }
220
221    /// Creates a `STOPSIGNAL` instruction.
222    pub fn stopsignal(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
223        Self::new(DockerfileInstructionKind::Stopsignal, arguments)
224    }
225
226    /// Creates a `SHELL` instruction.
227    pub fn shell(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
228        Self::new(DockerfileInstructionKind::Shell, arguments)
229    }
230
231    /// Returns the instruction kind.
232    #[must_use]
233    pub const fn kind(&self) -> DockerfileInstructionKind {
234        self.kind
235    }
236
237    /// Returns the instruction argument text.
238    #[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}