#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DockerfileInstructionError {
Empty,
UnknownInstruction,
MissingArguments,
}
impl fmt::Display for DockerfileInstructionError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("Dockerfile instruction cannot be empty"),
Self::UnknownInstruction => formatter.write_str("unknown Dockerfile instruction"),
Self::MissingArguments => formatter.write_str("Dockerfile instruction needs arguments"),
}
}
}
impl Error for DockerfileInstructionError {}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum DockerfileInstructionKind {
From,
Run,
Copy,
Add,
Cmd,
Entrypoint,
Env,
Arg,
Workdir,
Expose,
Label,
User,
Volume,
Healthcheck,
Stopsignal,
Shell,
}
impl DockerfileInstructionKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::From => "FROM",
Self::Run => "RUN",
Self::Copy => "COPY",
Self::Add => "ADD",
Self::Cmd => "CMD",
Self::Entrypoint => "ENTRYPOINT",
Self::Env => "ENV",
Self::Arg => "ARG",
Self::Workdir => "WORKDIR",
Self::Expose => "EXPOSE",
Self::Label => "LABEL",
Self::User => "USER",
Self::Volume => "VOLUME",
Self::Healthcheck => "HEALTHCHECK",
Self::Stopsignal => "STOPSIGNAL",
Self::Shell => "SHELL",
}
}
}
impl fmt::Display for DockerfileInstructionKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for DockerfileInstructionKind {
type Err = DockerfileInstructionError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.trim().to_ascii_uppercase().as_str() {
"FROM" => Ok(Self::From),
"RUN" => Ok(Self::Run),
"COPY" => Ok(Self::Copy),
"ADD" => Ok(Self::Add),
"CMD" => Ok(Self::Cmd),
"ENTRYPOINT" => Ok(Self::Entrypoint),
"ENV" => Ok(Self::Env),
"ARG" => Ok(Self::Arg),
"WORKDIR" => Ok(Self::Workdir),
"EXPOSE" => Ok(Self::Expose),
"LABEL" => Ok(Self::Label),
"USER" => Ok(Self::User),
"VOLUME" => Ok(Self::Volume),
"HEALTHCHECK" => Ok(Self::Healthcheck),
"STOPSIGNAL" => Ok(Self::Stopsignal),
"SHELL" => Ok(Self::Shell),
_ => Err(DockerfileInstructionError::UnknownInstruction),
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct DockerfileInstruction {
kind: DockerfileInstructionKind,
arguments: String,
}
impl DockerfileInstruction {
pub fn new(
kind: DockerfileInstructionKind,
arguments: impl AsRef<str>,
) -> Result<Self, DockerfileInstructionError> {
let arguments = arguments.as_ref().trim();
if arguments.is_empty() {
return Err(DockerfileInstructionError::MissingArguments);
}
Ok(Self {
kind,
arguments: arguments.to_string(),
})
}
pub fn from(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
Self::new(DockerfileInstructionKind::From, arguments)
}
#[must_use]
pub fn run(arguments: impl AsRef<str>) -> Self {
Self {
kind: DockerfileInstructionKind::Run,
arguments: arguments.as_ref().trim().to_string(),
}
}
pub fn copy(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
Self::new(DockerfileInstructionKind::Copy, arguments)
}
pub fn add(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
Self::new(DockerfileInstructionKind::Add, arguments)
}
pub fn cmd(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
Self::new(DockerfileInstructionKind::Cmd, arguments)
}
pub fn entrypoint(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
Self::new(DockerfileInstructionKind::Entrypoint, arguments)
}
pub fn env(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
Self::new(DockerfileInstructionKind::Env, arguments)
}
pub fn arg(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
Self::new(DockerfileInstructionKind::Arg, arguments)
}
pub fn workdir(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
Self::new(DockerfileInstructionKind::Workdir, arguments)
}
pub fn expose(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
Self::new(DockerfileInstructionKind::Expose, arguments)
}
pub fn label(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
Self::new(DockerfileInstructionKind::Label, arguments)
}
pub fn user(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
Self::new(DockerfileInstructionKind::User, arguments)
}
pub fn volume(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
Self::new(DockerfileInstructionKind::Volume, arguments)
}
pub fn healthcheck(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
Self::new(DockerfileInstructionKind::Healthcheck, arguments)
}
pub fn stopsignal(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
Self::new(DockerfileInstructionKind::Stopsignal, arguments)
}
pub fn shell(arguments: impl AsRef<str>) -> Result<Self, DockerfileInstructionError> {
Self::new(DockerfileInstructionKind::Shell, arguments)
}
#[must_use]
pub const fn kind(&self) -> DockerfileInstructionKind {
self.kind
}
#[must_use]
pub fn arguments(&self) -> &str {
&self.arguments
}
}
impl fmt::Display for DockerfileInstruction {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{} {}", self.kind, self.arguments)
}
}
impl FromStr for DockerfileInstruction {
type Err = DockerfileInstructionError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(DockerfileInstructionError::Empty);
}
let Some((keyword, arguments)) = trimmed.split_once(char::is_whitespace) else {
return Err(DockerfileInstructionError::MissingArguments);
};
Self::new(keyword.parse()?, arguments)
}
}
impl TryFrom<&str> for DockerfileInstruction {
type Error = DockerfileInstructionError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
#[cfg(test)]
mod tests {
use super::{DockerfileInstruction, DockerfileInstructionKind};
#[test]
fn parses_and_renders_instruction_lines() -> Result<(), Box<dyn std::error::Error>> {
let from: DockerfileInstruction = "FROM rust:1.95".parse()?;
let copy = DockerfileInstruction::copy("src/ /app/src/")?;
assert_eq!(from.kind(), DockerfileInstructionKind::From);
assert_eq!(from.arguments(), "rust:1.95");
assert_eq!(copy.to_string(), "COPY src/ /app/src/");
assert_eq!(
DockerfileInstruction::run("cargo test").to_string(),
"RUN cargo test"
);
Ok(())
}
}