dockerfile_builder/
instruction_builder.rs

1//! # Type-safe interfaces for building Instructions
2//!
3//!
4//! This module provides the definition of Instruction Builders and their fields, following the
5//! [Dockerfile reference].
6//!
7//! [Dockerfile reference]: https://docs.docker.com/engine/reference/builder/
8//!
9//! ## Usage
10//!
11//! All build and setter methods for Instruction Builders are automatically generated and follow the same format.
12//!
13//! For example:
14//!
15//! `ExposeBuilder` is the builder struct for `Expose`.
16//!
17//! ```rust
18//! pub struct ExposeBuilder {
19//!     pub port: u16,
20//!     pub protocol: Option<String>,
21//! }
22//! ```
23//!
24//! `Expose` can be constructed as follow:
25//!
26//! ```rust
27//! # use dockerfile_builder::instruction_builder::{ExposeBuilder, PortProtocol};
28//! let expose = ExposeBuilder::builder()
29//!     .port(80)
30//!     .protocol(PortProtocol::Tcp)
31//!     .build()
32//!     .unwrap();
33//! ```
34//!
35//! Note that:
36//! * The setter method names are identical to the fields names.
37//! * For fields with `Option<inner_type>` type: The argument type is the inner_type. It is
38//! optional to set these fields.
39//! * Use `build()` to complete building the instruction. `build()` returns
40//! `Result<InstructionBuilder, std::err::Err>` to safely handle errors.
41//!
42//!
43//! For fields with `Vec<_>` or `Option<Vec<_>>` type, it is possible to set each element of the Vec.
44//!
45//! For example:
46//!
47//! `RunBuilder` is the builder struct for `Run`.
48//!
49//! ```
50//! pub struct RunBuilder {
51//!     pub commands: Vec<String>,
52//! }
53//! ```
54//!
55//! `Run` can be constructed as follow:
56//! ```
57//! # use dockerfile_builder::instruction_builder::RunBuilder;
58//! let run = RunBuilder::builder()
59//!     .command("source $HOME/.bashrc")
60//!     .command("echo $HOME")
61//!     .build()
62//!     .unwrap();
63//! assert_eq!(
64//!     run.to_string(),
65//!     "RUN source $HOME/.bashrc && echo $HOME",
66//! );
67//! ```
68//!
69
70use std::time::Duration;
71
72use crate::instruction::{
73    Instruction, ADD, ARG, CMD, COPY, ENTRYPOINT, ENV, EXPOSE, FROM, HEALTHCHECK, LABEL, ONBUILD,
74    RUN, SHELL, STOPSIGNAL, USER, VOLUME, WORKDIR,
75};
76use anyhow::{anyhow, Result};
77use dockerfile_builder_macros::InstructionBuilder;
78use url::Url;
79
80/// Builder struct for [`FROM`] instruction
81///
82/// Format according to [Dockerfile
83/// reference](https://docs.docker.com/engine/reference/builder/#from):
84/// * `FROM [--platform=<platform>] <image> [AS <name>]`
85/// or
86/// * `FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]`
87/// or
88/// * `FROM [--platform=<platform>] <image>[@<digest>] [AS <name>]`
89///
90/// Example:
91/// ```
92/// # use dockerfile_builder::instruction_builder::FromBuilder;
93/// // Build FROM with image and name
94/// let from = FromBuilder::builder()
95///     .image("cargo-chef")
96///     .name("chef")
97///     .build()
98///     .unwrap();
99/// assert_eq!(from.to_string(), "FROM cargo-chef AS chef");
100///
101/// // Build FROM with image, name, and tag
102/// let from = FromBuilder::builder()
103///     .image("cargo-chef")
104///     .tag("latest")
105///     .name("chef")
106///     .build()
107///     .unwrap();
108/// assert_eq!(from.to_string(), "FROM cargo-chef:latest AS chef");
109/// ```
110///
111/// [FROM]: dockerfile_builder::instruction::FROM
112#[derive(Debug, InstructionBuilder)]
113#[instruction_builder(
114    instruction_name = FROM,
115    value_method = value,
116)]
117pub struct FromBuilder {
118    pub image: String,
119    pub name: Option<String>,
120    pub tag: Option<String>,
121    pub digest: Option<String>,
122    pub platform: Option<String>,
123}
124
125impl FromBuilder {
126    fn value(&self) -> Result<String> {
127        if self.tag.is_some() && self.digest.is_some() {
128            return Err(anyhow!("Dockerfile image can only have tag OR digest"));
129        }
130
131        let tag_or_digest = if let Some(t) = &self.tag {
132            Some(format!(":{}", t))
133        } else {
134            self.digest.as_ref().map(|d| format!("@{}", d))
135        };
136
137        Ok(format!(
138            "{}{}{}{}",
139            self.platform
140                .as_ref()
141                .map(|s| format!("--platform={} ", s))
142                .unwrap_or_default(),
143            &self.image,
144            tag_or_digest
145                .as_ref()
146                .map(|s| s.to_string())
147                .unwrap_or_default(),
148            self.name
149                .as_ref()
150                .map(|s| format!(" AS {}", s))
151                .unwrap_or_default(),
152        ))
153    }
154}
155
156/// Builder struct for [`ENV`] instruction
157///
158/// Format according to [Dockerfile
159/// reference](https://docs.docker.com/engine/reference/builder/#env):
160/// * `ENV <key>=<value>`
161///
162/// Example:
163/// ```
164/// # use dockerfile_builder::instruction_builder::EnvBuilder;
165/// let env = EnvBuilder::builder()
166///     .key("foo")
167///     .value("bar")
168///     .build()
169///     .unwrap();
170/// assert_eq!(env.to_string(), "ENV foo=\"bar\"");
171/// ```
172///
173/// [ENV]: dockerfile_builder::instruction::ENV
174#[derive(Debug, InstructionBuilder)]
175#[instruction_builder(
176    instruction_name = ENV,
177    value_method = value,
178)]
179pub struct EnvBuilder {
180    pub key: String,
181    pub value: String,
182}
183
184impl EnvBuilder {
185    fn value(&self) -> Result<String> {
186        Ok(format!("{}=\"{}\"", self.key, self.value))
187    }
188}
189
190/// Builder struct for [`RUN`] instruction (shell form)
191///
192/// * `RunBuilder` constructs the shell form for [`RUN`] by default.
193/// To construct the exec form of `RUN`, use [`RunExecBuilder`].
194///
195/// Format according to [Dockerfile
196/// reference](https://docs.docker.com/engine/reference/builder/#run):
197/// * `RUN command`
198///
199/// Example:
200/// ```
201/// # use dockerfile_builder::instruction_builder::RunBuilder;
202/// // build RUN with a single command
203/// let run = RunBuilder::builder()
204///     .command("source $HOME/.bashrc")
205///     .build().unwrap();
206/// assert_eq!(run.to_string(), "RUN source $HOME/.bashrc");
207///
208/// // build RUN with multiple commands, commands are separated by ` && `
209/// let run = RunBuilder::builder()
210///     .command("source $HOME/.bashrc")
211///     .command("echo $HOME")
212///     .build().unwrap();
213/// assert_eq!(
214///     run.to_string(),
215///     "RUN source $HOME/.bashrc && echo $HOME",
216/// );
217///
218/// // build RUN with multiple commands using a Vec
219/// let run = RunBuilder::builder()
220///     .commands(vec!["source $HOME/.bashrc", "echo $HOME"])
221///     .build().unwrap();
222/// assert_eq!(
223///     run.to_string(),
224///     "RUN source $HOME/.bashrc && echo $HOME",
225/// );
226/// ```
227///
228/// [RUN]: dockerfile_builder::instruction::RUN
229// TODO: Flag options for RUN
230#[derive(Debug, InstructionBuilder)]
231#[instruction_builder(
232    instruction_name = RUN,
233    value_method = value,
234)]
235pub struct RunBuilder {
236    #[instruction_builder(each = command)]
237    pub commands: Vec<String>,
238}
239
240impl RunBuilder {
241    fn value(&self) -> Result<String> {
242        Ok(self.commands.join(" && "))
243    }
244}
245
246/// Builder struct for [`RUN`] instruction (exec form)
247///
248/// * RunBuilder constructs the exec form for [`RUN`].
249/// To construct the shell form, use [`RunBuilder`].
250///
251/// Format according to [Dockerfile
252/// reference](https://docs.docker.com/engine/reference/builder/#run):
253/// * `RUN ["executable", "param1", "param2"]`
254///
255/// Example:
256/// ```
257/// # use dockerfile_builder::instruction_builder::RunExecBuilder;
258/// // build RUN by adding multiple params
259/// let run = RunExecBuilder::builder()
260///     .executable("mybin.exe")
261///     .param("-f")
262///     .param("-c")
263///     .build().unwrap();
264/// assert_eq!(run.to_string(), r#"RUN ["mybin.exe", "-f", "-c"]"#);
265///
266/// // build RUN with multiple params using a vec
267/// let run = RunExecBuilder::builder()
268///     .executable("mybin.exe")
269///     .params(vec!["-f", "-c"])
270///     .build().unwrap();
271/// assert_eq!(run.to_string(), r#"RUN ["mybin.exe", "-f", "-c"]"#);
272/// ```
273///
274/// [RUN]: dockerfile_builder::instruction::RUN
275#[derive(Debug, InstructionBuilder)]
276#[instruction_builder(
277    instruction_name = RUN,
278    value_method = value,
279)]
280pub struct RunExecBuilder {
281    pub executable: String,
282    #[instruction_builder(each = param)]
283    pub params: Option<Vec<String>>,
284}
285
286impl RunExecBuilder {
287    fn value(&self) -> Result<String> {
288        let params = match self.params.clone() {
289            Some(param_vec) => {
290                if param_vec.is_empty() {
291                    String::new()
292                } else {
293                    format!(r#", "{}""#, param_vec.join(r#"", ""#))
294                }
295            }
296            None => String::new(),
297        };
298        Ok(format!(r#"["{}"{}]"#, self.executable, params))
299    }
300}
301
302/// Builder struct for [`CMD`] instruction (shell form)
303///
304/// * CmdBuilder constructs the shell form for [`CMD`] by default.
305/// To construct the exec form or CMD in combination with ENTRYPOINT, use [`CmdExecBuilder`].
306///
307/// Format according to [Dockerfile
308/// reference](https://docs.docker.com/engine/reference/builder/#cmd):
309/// * `CMD command param1 param2`
310///
311/// Example:
312/// ```
313/// # use dockerfile_builder::instruction_builder::CmdBuilder;
314/// // build CMD by adding multiple params
315/// let cmd = CmdBuilder::builder()
316///     .command(r#"echo "this is a test""#)
317///     .param("| wc")
318///     .param("-l")
319///     .build().unwrap();
320/// assert_eq!(cmd.to_string(), r#"CMD echo "this is a test" | wc -l"#);
321///
322/// // build CMD with multiple params using a vec    
323/// let cmd = CmdBuilder::builder()
324///     .command(r#"echo "this is a test""#)
325///     .params(vec!["| wc", "-l"])
326///     .build().unwrap();
327/// assert_eq!(cmd.to_string(), r#"CMD echo "this is a test" | wc -l"#);
328/// ```
329///
330/// [CMD]: dockerfile_builder::instruction::CMD
331#[derive(Debug, InstructionBuilder)]
332#[instruction_builder(
333    instruction_name = CMD,
334    value_method = value,
335)]
336pub struct CmdBuilder {
337    pub command: String,
338    #[instruction_builder(each = param)]
339    pub params: Option<Vec<String>>,
340}
341
342impl CmdBuilder {
343    fn value(&self) -> Result<String> {
344        let params = match self.params.clone() {
345            Some(param_vec) => {
346                if param_vec.is_empty() {
347                    String::new()
348                } else {
349                    format!(r#" {}"#, param_vec.join(" "))
350                }
351            }
352            None => String::new(),
353        };
354        Ok(format!("{}{}", self.command, params))
355    }
356}
357
358/// Builder struct for [`CMD`] instruction (exec form)
359///
360/// * CmdBuilder constructs the exec form for [`CMD`].
361/// To construct the shell form, use [`CmdBuilder`].
362///
363/// Format according to [Dockerfile
364/// reference](https://docs.docker.com/engine/reference/builder/#cmd):
365/// * `CMD ["executable", "param1", "param2"]`
366/// OR
367/// * `CMD ["param1","param2"]` (as default parameters to ENTRYPOINT)
368///
369/// Example:
370/// ```
371/// # use dockerfile_builder::instruction_builder::CmdExecBuilder;
372/// // build CMD with a single param
373/// let cmd = CmdExecBuilder::builder()
374///     .executable("/usr/bin/wc")
375///     .param("--help")
376///     .build().unwrap();
377/// assert_eq!(cmd.to_string(), r#"CMD ["/usr/bin/wc", "--help"]"#);
378///
379/// // build CMD for ENTRYPOINT
380/// let cmd = CmdExecBuilder::builder()
381///     .param("-l")
382///     .param("8000")
383///     .build().unwrap();
384/// assert_eq!(cmd.to_string(), r#"CMD ["-l", "8000"]"#);
385/// ```
386///
387/// [CMD]: dockerfile_builder::instruction::CMD
388#[derive(Debug, InstructionBuilder)]
389#[instruction_builder(
390    instruction_name = CMD,
391    value_method = value,
392)]
393pub struct CmdExecBuilder {
394    pub executable: Option<String>,
395    #[instruction_builder(each = param)]
396    pub params: Option<Vec<String>>,
397}
398
399impl CmdExecBuilder {
400    fn value(&self) -> Result<String> {
401        if self.executable.is_none() && self.params.is_none() {
402            return Err(anyhow!("CMD cannot be empty"));
403        }
404        let params = match self.params.clone() {
405            Some(param_vec) => {
406                if self.executable.is_none() && param_vec.is_empty() {
407                    return Err(anyhow!("CMD cannot be empty"));
408                } else if param_vec.is_empty() {
409                    String::new()
410                } else if self.executable.is_none() {
411                    format!(r#""{}""#, param_vec.join(r#"", ""#))
412                } else {
413                    format!(r#", "{}""#, param_vec.join(r#"", ""#))
414                }
415            }
416            None => String::new(),
417        };
418        Ok(format!(
419            r#"[{}{}]"#,
420            self.executable
421                .as_ref()
422                .map(|e| format!(r#""{}""#, e))
423                .unwrap_or_default(),
424            params,
425        ))
426    }
427}
428
429/// Builder struct for [`LABEL`] instruction
430///
431/// Format according to [Dockerfile
432/// reference](https://docs.docker.com/engine/reference/builder/#label):
433/// * `LABEL <key>=<value>`
434///
435/// Example:
436/// ```
437/// # use dockerfile_builder::instruction_builder::LabelBuilder;
438/// let label = LabelBuilder::builder()
439///     .key("foo")
440///     .value("bar")
441///     .build()
442///     .unwrap();
443/// assert_eq!(label.to_string(), "LABEL foo=\"bar\"");
444/// ```
445///
446/// [LABEL]: dockerfile_builder::instruction::LABEL
447///
448// TODO: The official format is
449// * `LABEL <key>=<value> <key>=<value> <key>=<value> ...`
450// Use `each` to support the multiple format.
451#[derive(Debug, InstructionBuilder)]
452#[instruction_builder(
453    instruction_name = LABEL,
454    value_method = value,
455)]
456pub struct LabelBuilder {
457    pub key: String,
458    pub value: String,
459}
460
461impl LabelBuilder {
462    fn value(&self) -> Result<String> {
463        Ok(format!("{}=\"{}\"", self.key, self.value))
464    }
465}
466
467/// Builder struct for [`EXPOSE`] instruction
468///
469/// Format according to [Dockerfile
470/// reference](https://docs.docker.com/engine/reference/builder/#expose):
471/// * `EXPOSE <port>`
472/// or
473/// * `EXPOSE <port>/<protocol>`
474///
475/// Example:
476/// ```
477/// # use dockerfile_builder::instruction_builder::{ExposeBuilder, PortProtocol};
478/// let expose = ExposeBuilder::builder()
479///     .port(80)
480///     .protocol(PortProtocol::Udp)
481///     .build()
482///     .unwrap();
483/// assert_eq!(expose.to_string(), "EXPOSE 80/udp");
484/// ```
485///
486/// [EXPOSE]: dockerfile_builder::instruction::EXPOSE
487#[derive(Debug, InstructionBuilder)]
488#[instruction_builder(
489    instruction_name = EXPOSE,
490    value_method = value,
491)]
492pub struct ExposeBuilder {
493    pub port: u16,
494    pub protocol: Option<PortProtocol>,
495}
496
497#[derive(Clone, Debug)]
498pub enum PortProtocol {
499    Tcp,
500    Udp,
501}
502
503impl ToString for PortProtocol {
504    fn to_string(&self) -> String {
505        match self {
506            Self::Tcp => "tcp",
507            Self::Udp => "udp",
508        }
509        .to_string()
510    }
511}
512
513impl ExposeBuilder {
514    fn value(&self) -> Result<String> {
515        Ok(format!(
516            "{}{}",
517            self.port,
518            self.protocol
519                .as_ref()
520                .map(|p| format!("/{}", p.to_string()))
521                .unwrap_or_default()
522        ))
523    }
524}
525
526/// Builder struct for [`ADD`] instruction
527///
528/// Format according to [Dockerfile
529/// reference](https://docs.docker.com/engine/reference/builder/#add):
530/// * `ADD [--chown=<chown>] [--chmod=<chmod>] <src>... <dest>`
531///
532/// Example:
533/// ```
534/// # use dockerfile_builder::instruction_builder::AddBuilder;
535/// let add = AddBuilder::builder()
536///     .chown("myuser:mygroup")
537///     .chmod(655)
538///     .src("home")
539///     .dest("/mydir/")
540///     .build().unwrap();
541/// assert_eq!(add.to_string(), "ADD --chown=myuser:mygroup --chmod=655 home /mydir/");
542/// ```
543///
544/// [ADD]: dockerfile_builder::instruction::ADD
545#[derive(Debug, InstructionBuilder)]
546#[instruction_builder(
547    instruction_name = ADD,
548    value_method = value,
549)]
550pub struct AddBuilder {
551    #[instruction_builder(each = src)]
552    pub sources: Vec<String>,
553    pub dest: String,
554    pub chown: Option<String>,
555    pub chmod: Option<u16>,
556}
557
558impl AddBuilder {
559    fn value(&self) -> Result<String> {
560        Ok(format!(
561            "{}{}{} {}",
562            self.chown
563                .as_ref()
564                .map(|c| format!("--chown={} ", c))
565                .unwrap_or_default(),
566            self.chmod
567                .as_ref()
568                .map(|c| format!("--chmod={} ", c))
569                .unwrap_or_default(),
570            self.sources.join(" "),
571            self.dest,
572        ))
573    }
574}
575
576/// Builder struct for [`ADD`] instruction (http src)
577///
578/// Format according to [Dockerfile
579/// reference](https://docs.docker.com/engine/reference/builder/#add):
580/// * `ADD --checksum=<checksum> <src> <dest>`
581///
582/// Example:
583/// ```
584/// # use dockerfile_builder::instruction_builder::AddHttpBuilder;
585/// let add = AddHttpBuilder::builder()
586///     .checksum("sha256::123")
587///     .src("http://example.com/foobar")
588///     .dest("/")
589///     .build().unwrap();
590/// assert_eq!(add.to_string(), "ADD --checksum=sha256::123 http://example.com/foobar /");
591/// ```
592///
593/// [ADD]: dockerfile_builder::instruction::ADD
594#[derive(Debug, InstructionBuilder)]
595#[instruction_builder(
596    instruction_name = ADD,
597    value_method = value,
598)]
599pub struct AddHttpBuilder {
600    pub src: String,
601    pub dest: String,
602    pub checksum: Option<String>,
603}
604
605impl AddHttpBuilder {
606    fn value(&self) -> Result<String> {
607        if let Err(e) = Url::parse(&self.src) {
608            return Err(anyhow!("{e}"));
609        }
610        Ok(format!(
611            "{}{} {}",
612            self.checksum
613                .as_ref()
614                .map(|c| format!("--checksum={} ", c))
615                .unwrap_or_default(),
616            self.src,
617            self.dest,
618        ))
619    }
620}
621
622/// Builder struct for [`ADD`] instruction (git repository)
623///
624/// Format according to [Dockerfile
625/// reference](https://docs.docker.com/engine/reference/builder/#add):
626/// * `ADD [--keep-git-dir=<boolean>] <git ref> <dir>`
627///
628/// [ADD]: dockerfile_builder::instruction::ADD
629#[derive(Debug, InstructionBuilder)]
630#[instruction_builder(
631    instruction_name = ADD,
632    value_method = value,
633)]
634pub struct AddGitBuilder {
635    pub git_ref: String,
636    pub dir: String,
637    pub keep_git_dir: Option<bool>,
638}
639
640impl AddGitBuilder {
641    fn value(&self) -> Result<String> {
642        if let Err(e) = Url::parse(&self.git_ref) {
643            return Err(anyhow!("{e}"));
644        }
645        Ok(format!(
646            "{}{} {}",
647            self.keep_git_dir
648                .as_ref()
649                .map(|c| format!("--keep-git-dir={} ", c))
650                .unwrap_or_default(),
651            self.git_ref,
652            self.dir,
653        ))
654    }
655}
656
657/// Builder struct for [`COPY`] instruction
658///
659/// Format according to [Dockerfile
660/// reference](https://docs.docker.com/engine/reference/builder/#copy):
661/// * `COPY [--chown=<chown>] [--chmod=<chmod>] [--from=<from>] [--link] <src>... <dest>`
662///
663/// Example:
664/// ```
665/// # use dockerfile_builder::instruction_builder::CopyBuilder;
666/// let copy = CopyBuilder::builder()
667///     .chown("55:mygroup")
668///     .chmod(644)
669///     .src("files")
670///     .src("more_files")
671///     .dest("/somedir/")
672///     .build().unwrap();
673/// assert_eq!(copy.to_string(), "COPY --chown=55:mygroup --chmod=644 files more_files /somedir/");
674/// ```
675///
676/// [COPY]: dockerfile_builder::instruction::COPY
677// TODO: Add flag [--from=]
678#[derive(Debug, InstructionBuilder)]
679#[instruction_builder(
680    instruction_name = COPY,
681    value_method = value,
682)]
683pub struct CopyBuilder {
684    #[instruction_builder(each = src)]
685    pub sources: Vec<String>,
686    pub dest: String,
687    pub chown: Option<String>,
688    pub chmod: Option<u16>,
689    pub from: Option<String>,
690    pub link: Option<bool>,
691}
692
693impl CopyBuilder {
694    fn value(&self) -> Result<String> {
695        Ok(format!(
696            "{}{}{}{}{} {}",
697            self.chown
698                .as_ref()
699                .map(|c| format!("--chown={} ", c))
700                .unwrap_or_default(),
701            self.chmod
702                .as_ref()
703                .map(|c| format!("--chmod={} ", c))
704                .unwrap_or_default(),
705            self.link
706                .as_ref()
707                .map(|c| match c {
708                    true => "--link ".to_string(),
709                    false => "".to_string(),
710                })
711                .unwrap_or_default(),
712            self.from
713                .as_ref()
714                .map(|c| format!("--from={} ", c))
715                .unwrap_or_default(),
716            self.sources.join(" "),
717            self.dest,
718        ))
719    }
720}
721
722/// Builder struct for [`ENTRYPOINT`] instruction (shell form)
723///
724/// * EntrypointBuilder constructs the shell form for [`ENTRYPOINT`] by default.
725/// To construct the exec form, use [`EntrypointExecBuilder`].
726///
727/// Format according to [Dockerfile
728/// reference](https://docs.docker.com/engine/reference/builder/#entrypoint):
729/// * `ENTRYPOINT command param1 param2`
730///
731/// Example:
732/// ```
733/// # use dockerfile_builder::instruction_builder::EntrypointBuilder;
734/// // build ENTRYPOINT with params
735/// let entrypoint = EntrypointBuilder::builder()
736///     .command("some command")
737///     .param("-f")
738///     .param("-c")
739///     .build().unwrap();
740/// assert_eq!(entrypoint.to_string(), "ENTRYPOINT some command -f -c");
741///
742/// // build ENTRYPOINT with a param vec
743/// let entrypoint = EntrypointBuilder::builder()
744///     .command("some command")
745///     .params(vec!["-f", "-c"])
746///     .build().unwrap();
747/// assert_eq!(entrypoint.to_string(), "ENTRYPOINT some command -f -c");
748/// ```
749///
750/// [ENTRYPOINT]: dockerfile_builder::instruction::ENTRYPOINT
751#[derive(Debug, InstructionBuilder)]
752#[instruction_builder(
753    instruction_name = ENTRYPOINT,
754    value_method = value,
755)]
756pub struct EntrypointBuilder {
757    pub command: String,
758    #[instruction_builder(each = param)]
759    pub params: Option<Vec<String>>,
760}
761
762impl EntrypointBuilder {
763    fn value(&self) -> Result<String> {
764        let params = match self.params.clone() {
765            Some(param_vec) => {
766                if param_vec.is_empty() {
767                    String::new()
768                } else {
769                    format!(r#" {}"#, param_vec.join(r#" "#))
770                }
771            }
772            None => String::new(),
773        };
774
775        Ok(format!("{}{}", self.command, params))
776    }
777}
778
779/// Builder struct for [`ENTRYPOINT`] instruction (exec form)
780///
781/// * EntrypointExecBuilder constructs the exec form for [`ENTRYPOINT`].
782/// To construct the shell form, use [`EntrypointBuilder`].
783///
784/// Format according to [Dockerfile
785/// reference](https://docs.docker.com/engine/reference/builder/#entrypoint):
786/// * `ENTRYPOINT ["executable", "param1", "param2"]`
787///
788/// Example:
789/// ```
790/// # use dockerfile_builder::instruction_builder::EntrypointExecBuilder;
791/// // build ENTRYPOINT with params
792/// let entrypoint = EntrypointExecBuilder::builder()
793///     .executable("/usr/sbin/apache2ctl")
794///     .param("-D")
795///     .param("FOREGROUND")
796///     .build().unwrap();
797/// assert_eq!(entrypoint.to_string(), r#"ENTRYPOINT ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"]"#);
798///
799/// // build ENTRYPOINT with a param vec
800/// let entrypoint = EntrypointExecBuilder::builder()
801///     .executable("/usr/sbin/apache2ctl")
802///     .params(vec!["-D", "FOREGROUND"])
803///     .build().unwrap();
804/// assert_eq!(entrypoint.to_string(), r#"ENTRYPOINT ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"]"#);
805/// ```
806///
807/// [ENTRYPOINT]: dockerfile_builder::instruction::ENTRYPOINT
808#[derive(Debug, InstructionBuilder)]
809#[instruction_builder(
810    instruction_name = ENTRYPOINT,
811    value_method = value,
812)]
813pub struct EntrypointExecBuilder {
814    pub executable: String,
815    #[instruction_builder(each = param)]
816    pub params: Option<Vec<String>>,
817}
818
819impl EntrypointExecBuilder {
820    fn value(&self) -> Result<String> {
821        let params = match self.params.clone() {
822            Some(param_vec) => {
823                if param_vec.is_empty() {
824                    String::new()
825                } else {
826                    format!(r#", "{}""#, param_vec.join(r#"", ""#))
827                }
828            }
829            None => String::new(),
830        };
831        Ok(format!(r#"["{}"{}]"#, self.executable, params))
832    }
833}
834
835/// Builder struct for [`VOLUME`] instruction
836///
837/// Format according to [Dockerfile
838/// reference](https://docs.docker.com/engine/reference/builder/#volume):
839/// * `VOLUME <path>...`
840///
841/// [VOLUME]: dockerfile_builder::instruction::VOLUME
842#[derive(Debug, InstructionBuilder)]
843#[instruction_builder(
844    instruction_name = VOLUME,
845    value_method = value,
846)]
847pub struct VolumeBuilder {
848    #[instruction_builder(each = path)]
849    pub paths: Vec<String>,
850}
851
852impl VolumeBuilder {
853    fn value(&self) -> Result<String> {
854        Ok(self.paths.join(" "))
855    }
856}
857
858/// Builder struct for [`USER`] instruction
859///
860/// Format according to [Dockerfile
861/// reference](https://docs.docker.com/engine/reference/builder/#user):
862/// * `USER <user>`
863/// or
864/// * `USER <user>:<group>`
865/// [USER]: dockerfile_builder::instruction::USER
866#[derive(Debug, InstructionBuilder)]
867#[instruction_builder(
868    instruction_name = USER,
869    value_method = value,
870)]
871pub struct UserBuilder {
872    pub user: String,
873    pub group: Option<String>,
874}
875
876impl UserBuilder {
877    fn value(&self) -> Result<String> {
878        Ok(format!(
879            "{}{}",
880            self.user,
881            self.group
882                .as_ref()
883                .map(|g| format!(":{}", g))
884                .unwrap_or_default()
885        ))
886    }
887}
888
889/// Builder struct for [`WORKDIR`] instruction
890///
891/// Format according to [Dockerfile
892/// reference](https://docs.docker.com/engine/reference/builder/#workdir):
893/// * `WORKDIR <path>`
894///
895/// [WORKDIR]: dockerfile_builder::instruction::WORKDIR
896#[derive(Debug, InstructionBuilder)]
897#[instruction_builder(
898    instruction_name = WORKDIR,
899    value_method = value,
900)]
901pub struct WorkdirBuilder {
902    pub path: String,
903}
904
905impl WorkdirBuilder {
906    fn value(&self) -> Result<String> {
907        Ok(self.path.to_string())
908    }
909}
910
911/// Builder struct for [`ARG`] instruction
912///
913/// Format according to [Dockerfile
914/// reference](https://docs.docker.com/engine/reference/builder/#arg):
915/// * `ARG <name>[=<value>]`
916///
917/// [ARG]: dockerfile_builder::instruction::ARG
918#[derive(Debug, InstructionBuilder)]
919#[instruction_builder(
920    instruction_name = ARG,
921    value_method = value,
922)]
923pub struct ArgBuilder {
924    pub name: String,
925    pub value: Option<String>,
926}
927
928impl ArgBuilder {
929    fn value(&self) -> Result<String> {
930        let value = match &self.value {
931            Some(value) => format!("{}=\"{}\"", self.name, value),
932            None => self.name.to_string(),
933        };
934        Ok(value)
935    }
936}
937
938/// Builder struct for [`ONBUILD`] instruction
939///
940/// Format according to [Dockerfile
941/// reference](https://docs.docker.com/engine/reference/builder/#onbuild):
942/// * `ONBUILD <INSTRUCTION>`
943///
944/// [ONBUILD]: dockerfile_builder::instruction::ONBUILD
945#[derive(Debug, InstructionBuilder)]
946#[instruction_builder(
947    instruction_name = ONBUILD,
948    value_method = value,
949)]
950pub struct OnbuildBuilder {
951    pub instruction: Instruction,
952}
953
954impl OnbuildBuilder {
955    fn value(&self) -> Result<String> {
956        match &self.instruction {
957            Instruction::ONBUILD(_) => Err(anyhow!(
958                "Chaining ONBUILD instructions using ONBUILD ONBUILD isn’t allowed"
959            )),
960            Instruction::FROM(_) => Err(anyhow!(
961                "ONBUILD instruction may not trigger FROM instruction"
962            )),
963            ins => Ok(ins.to_string()),
964        }
965    }
966}
967
968/// Builder struct for [`STOPSIGNAL`] instruction
969///
970/// Format according to [Dockerfile
971/// reference](https://docs.docker.com/engine/reference/builder/#stopsignal):
972/// * `STOPSIGNAL <signal>`
973///
974/// [STOPSIGNAL]: dockerfile_builder::instruction::STOPSIGNAL
975#[derive(Debug, InstructionBuilder)]
976#[instruction_builder(
977    instruction_name = STOPSIGNAL,
978    value_method = value,
979)]
980pub struct StopsignalBuilder {
981    pub signal: String,
982}
983
984impl StopsignalBuilder {
985    fn value(&self) -> Result<String> {
986        Ok(self.signal.to_string())
987    }
988}
989
990/// Builder struct for [`HEALTHCHECK`] instruction
991///
992/// Format according to [Dockerfile
993/// reference](https://docs.docker.com/engine/reference/builder/#healthcheck):
994/// * `HEALTHCHECK [--interval=DURATION] [--timeout=DURATION]
995/// [--start-period=DURATION] [--start-interval=DURATION] [--retries=N] CMD <command>`
996///
997/// [HEALTHCHECK]: dockerfile_builder::instruction::HEALTHCHECK
998#[derive(Debug, InstructionBuilder)]
999#[instruction_builder(
1000    instruction_name = HEALTHCHECK,
1001    value_method = value,
1002)]
1003pub struct HealthcheckBuilder {
1004    pub cmd: Option<CMD>,
1005    pub interval: Option<Duration>,
1006    pub timeout: Option<Duration>,
1007    pub start_period: Option<Duration>,
1008    pub start_interval: Option<Duration>,
1009    pub retries: Option<i32>,
1010}
1011
1012impl HealthcheckBuilder {
1013    fn value(&self) -> Result<String> {
1014        match self.cmd.is_some() {
1015            true => Ok(format!(
1016                "{}{}{}{}{}{}",
1017                self.interval
1018                    .as_ref()
1019                    .map(|i| format!("--interal={} ", i.as_secs()))
1020                    .unwrap_or_default(),
1021                self.timeout
1022                    .as_ref()
1023                    .map(|t| format!("--timeout={} ", t.as_secs()))
1024                    .unwrap_or_default(),
1025                self.start_period
1026                    .as_ref()
1027                    .map(|s| format!("--start-period={} ", s.as_secs()))
1028                    .unwrap_or_default(),
1029                self.start_interval
1030                    .as_ref()
1031                    .map(|s| format!("--start-interval={} ", s.as_secs()))
1032                    .unwrap_or_default(),
1033                self.retries
1034                    .as_ref()
1035                    .map(|r| format!("--retries={} ", r))
1036                    .unwrap_or_default(),
1037                self.cmd.clone().unwrap(),
1038            )),
1039            false => Ok("NONE".to_string()),
1040        }
1041    }
1042
1043    /// Convenience method for setting HEALTHCHECK NONE.
1044    /// ```Rust
1045    /// HealthcheckBuilder::set_none() == "HEALTHCHECK NONE".to_string()
1046    /// ```
1047    pub fn set_none() -> Result<String> {
1048        Ok("HEALTHCHECK NONE".to_string())
1049    }
1050}
1051
1052/// Builder struct for [`SHELL`] instruction
1053///
1054/// Format according to [Dockerfile
1055/// reference](https://docs.docker.com/engine/reference/builder/#shell):
1056/// * `SHELL ["executable", "params"]`
1057///
1058/// Example:
1059/// ```
1060/// # use dockerfile_builder::instruction_builder::ShellBuilder;
1061/// // build SHELL with params
1062/// let shell = ShellBuilder::builder()
1063///     .executable("cmd")
1064///     .param("/S")
1065///     .param("/C")
1066///     .build().unwrap();
1067/// assert_eq!(shell.to_string(), r#"SHELL ["cmd", "/S", "/C"]"#);
1068///
1069/// // build SHELL with a param vec
1070/// let shell = ShellBuilder::builder()
1071///     .executable("cmd")
1072///     .params(vec!["/S", "/C"])
1073///     .build().unwrap();
1074/// assert_eq!(shell.to_string(), r#"SHELL ["cmd", "/S", "/C"]"#);
1075/// ```
1076///
1077/// [SHELL]: dockerfile_builder::instruction::SHELL
1078#[derive(Debug, InstructionBuilder)]
1079#[instruction_builder(
1080    instruction_name = SHELL,
1081    value_method = value,
1082)]
1083pub struct ShellBuilder {
1084    pub executable: String,
1085    #[instruction_builder(each = param)]
1086    pub params: Option<Vec<String>>,
1087}
1088
1089impl ShellBuilder {
1090    fn value(&self) -> Result<String> {
1091        let params = match self.params.clone() {
1092            Some(param_vec) => {
1093                if param_vec.is_empty() {
1094                    String::new()
1095                } else {
1096                    format!(r#", "{}""#, param_vec.join(r#"", ""#))
1097                }
1098            }
1099            None => String::new(),
1100        };
1101        Ok(format!(r#"["{}"{}]"#, self.executable, params))
1102    }
1103}