1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
use std::{path::PathBuf, process::Command};

use crate::commands::errors::{InvocationError, NonZeroExitCode, SignalTermination};
use pavex::blueprint::Blueprint;

/// The configuration for `pavex`'s `generate` command.
///
/// You can use [`Client::generate`] to start building the command configuration.
///
/// [`Client::generate`]: crate::Client::generate
pub struct GenerateBuilder {
    cmd: Command,
    diagnostics_path: Option<PathBuf>,
    blueprint: Blueprint,
    output_directory: PathBuf,
    check: bool,
}

/// The representation of this command used in error messages.
static GENERATE_DEBUG_COMMAND: &str = "pavex [...] generate [...]";

impl GenerateBuilder {
    pub(crate) fn new(cmd: Command, blueprint: Blueprint, output_directory: PathBuf) -> Self {
        Self {
            diagnostics_path: None,
            blueprint,
            cmd,
            output_directory,
            check: false,
        }
    }

    /// Generate the runtime library for the application.
    ///
    /// This will invoke `pavex` with the chosen configuration.
    /// It won't return until `pavex` has finished running.
    ///
    /// If `pavex` exits with a non-zero status code, this will return an error.
    pub fn execute(self) -> Result<(), GenerateError> {
        let mut cmd = self
            .command()
            .map_err(GenerateError::BlueprintPersistenceError)?;
        let status = cmd
            .status()
            .map_err(|e| InvocationError {
                source: e,
                command: GENERATE_DEBUG_COMMAND,
            })
            .map_err(GenerateError::InvocationError)?;
        if !status.success() {
            if let Some(code) = status.code() {
                return Err(GenerateError::NonZeroExitCode(NonZeroExitCode {
                    code,
                    command: GENERATE_DEBUG_COMMAND,
                }));
            } else {
                return Err(GenerateError::SignalTermination(SignalTermination {
                    command: GENERATE_DEBUG_COMMAND,
                }));
            }
        }
        Ok(())
    }

    /// Assemble the `std::process::Command` that will be used to invoke `pavex`,
    /// but do not run it.
    /// It **will** persist the blueprint to a file, though.
    ///
    /// This method can be useful if you need to customize the command before running it.  
    /// If that's not your usecase, consider using [`GenerateBuilder::execute`] instead.
    pub fn command(mut self) -> Result<Command, BlueprintPersistenceError> {
        // TODO: Pass the blueprint via `stdin` instead of writing it to a file.
        let bp_path = self.output_directory.join("blueprint.ron");
        self.blueprint
            .persist(&bp_path)
            .map_err(|source| BlueprintPersistenceError { source })?;

        self.cmd
            .arg("generate")
            .arg("-b")
            .arg(bp_path)
            .arg("-o")
            .arg(self.output_directory)
            .stdout(std::process::Stdio::inherit())
            .stderr(std::process::Stdio::inherit());

        if let Some(path) = self.diagnostics_path {
            self.cmd.arg("--diagnostics").arg(path);
        }
        if self.check {
            self.cmd.arg("--check");
        }
        Ok(self.cmd)
    }

    /// Set the path to the file that Pavex will use to serialize diagnostic
    /// information about the application.
    ///
    /// Diagnostics are primarily used for debugging the generator itself.
    ///
    /// If this is not set, Pavex will not persist any diagnostic information.
    pub fn diagnostics_path(mut self, path: PathBuf) -> Self {
        self.diagnostics_path = Some(path);
        self
    }

    /// Enable check mode.
    ///
    /// In check mode, `pavex generate` verifies that the generated server SDK is up-to-date.  
    /// If it isn't, it returns an error without updating the SDK.
    pub fn check(mut self) -> Self {
        self.check = true;
        self
    }

    /// Disable check mode.
    ///
    /// `pavex` will regenerate the server SDK and update it on disk if it is outdated.
    pub fn no_check(mut self) -> Self {
        self.check = false;
        self
    }
}

#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum GenerateError {
    #[error(transparent)]
    InvocationError(InvocationError),
    #[error(transparent)]
    SignalTermination(SignalTermination),
    #[error(transparent)]
    NonZeroExitCode(NonZeroExitCode),
    #[error(transparent)]
    BlueprintPersistenceError(BlueprintPersistenceError),
}

#[derive(Debug, thiserror::Error)]
#[error("Failed to persist the blueprint to a file")]
pub struct BlueprintPersistenceError {
    #[source]
    source: anyhow::Error,
}