pgrx_sql_entity_graph/
control_file.rs

1//LICENSE Portions Copyright 2019-2021 ZomboDB, LLC.
2//LICENSE
3//LICENSE Portions Copyright 2021-2023 Technology Concepts & Design, Inc.
4//LICENSE
5//LICENSE Portions Copyright 2023-2023 PgCentral Foundation, Inc. <contact@pgcentral.org>
6//LICENSE
7//LICENSE All rights reserved.
8//LICENSE
9//LICENSE Use of this source code is governed by the MIT license that can be found in the LICENSE file.
10/*!
11
12`pgrx_module_magic!()` related macro expansion for Rust to SQL translation
13
14> Like all of the [`sql_entity_graph`][crate] APIs, this is considered **internal**
15> to the `pgrx` framework and very subject to change between versions. While you may use this, please do it with caution.
16
17*/
18use super::{SqlGraphEntity, SqlGraphIdentifier, ToSql};
19use std::collections::HashMap;
20use std::path::PathBuf;
21use thiserror::Error;
22
23/// The parsed contents of a `.control` file.
24///
25/// ```rust
26/// use pgrx_sql_entity_graph::ControlFile;
27/// use std::convert::TryFrom;
28/// # fn main() -> eyre::Result<()> {
29/// # // arrays.control chosen because it does **NOT** use the @CARGO_VERSION@ variable
30/// let context = include_str!("../../pgrx-examples/arrays/arrays.control");
31/// let _control_file = ControlFile::try_from(context)?;
32/// # Ok(())
33/// # }
34/// ```
35#[derive(Debug, Clone, Hash, PartialOrd, Ord, PartialEq, Eq)]
36pub struct ControlFile {
37    pub comment: String,
38    pub default_version: String,
39    pub module_pathname: Option<String>,
40    pub relocatable: bool,
41    pub superuser: bool,
42    pub schema: Option<String>,
43    pub trusted: bool,
44}
45
46impl ControlFile {
47    /// Parse a `.control` file, performing all known pgrx dynamic variable substitutions.
48    ///
49    /// # Supported Dynamic Variable Substitutions
50    ///
51    /// `@CARGO_VERSION@`:  Replaced with the value of the environment variable `CARGO_PKG_VERSION`,
52    ///                     which is set by cargo, or failing that, `cargo-pgrx` using the package
53    ///                     version from the extension's `Cargo.toml` file
54    ///
55    /// # Errors
56    ///
57    /// Returns a `ControlFileError` if any of the required fields are missing from the input string
58    /// or if any required environment variables (for dynamic variable substitution) are missing
59    ///
60    /// ```rust
61    /// use pgrx_sql_entity_graph::ControlFile;
62    /// # fn main() -> eyre::Result<()> {
63    /// # // arrays.control chosen because it does **NOT** use the @CARGO_VERSION@ variable
64    /// let context = include_str!("../../pgrx-examples/arrays/arrays.control");
65    /// let _control_file = ControlFile::from_str(context)?;
66    /// # Ok(())
67    /// # }
68    /// ```
69    #[allow(clippy::should_implement_trait)]
70    pub fn from_str(input: &str) -> Result<Self, ControlFileError> {
71        fn do_var_replacements(mut input: String) -> Result<String, ControlFileError> {
72            const CARGO_VERSION: &str = "@CARGO_VERSION@";
73
74            // endeavor to not require external values if they're not used by the input
75            if input.contains(CARGO_VERSION) {
76                input = input.replace(
77                    CARGO_VERSION,
78                    &std::env::var("CARGO_PKG_VERSION").map_err(|_| {
79                        ControlFileError::MissingEnvvar("CARGO_PKG_VERSION".to_string())
80                    })?,
81                );
82            }
83
84            Ok(input)
85        }
86
87        let mut temp = HashMap::new();
88        for line in input.lines() {
89            let parts: Vec<&str> = line.split('=').collect();
90
91            if parts.len() != 2 {
92                continue;
93            }
94
95            let (k, v) = (parts.first().unwrap().trim(), parts.get(1).unwrap().trim());
96
97            let v = v.trim_start_matches('\'');
98            let v = v.trim_end_matches('\'');
99
100            temp.insert(k, do_var_replacements(v.to_string())?);
101        }
102        let control_file = ControlFile {
103            comment: temp
104                .get("comment")
105                .ok_or(ControlFileError::MissingField { field: "comment" })?
106                .to_string(),
107            default_version: temp
108                .get("default_version")
109                .ok_or(ControlFileError::MissingField { field: "default_version" })?
110                .to_string(),
111            module_pathname: temp.get("module_pathname").map(|v| v.to_string()),
112            relocatable: temp
113                .get("relocatable")
114                .ok_or(ControlFileError::MissingField { field: "relocatable" })?
115                == "true",
116            superuser: temp
117                .get("superuser")
118                .ok_or(ControlFileError::MissingField { field: "superuser" })?
119                == "true",
120            schema: temp.get("schema").map(|v| v.to_string()),
121            trusted: if let Some(v) = temp.get("trusted") { v == "true" } else { false },
122        };
123
124        if !control_file.superuser && control_file.trusted {
125            // `trusted` is irrelevant if `superuser` is false.
126            return Err(ControlFileError::RedundantField { field: "trusted" });
127        }
128
129        Ok(control_file)
130    }
131}
132
133impl From<ControlFile> for SqlGraphEntity {
134    fn from(val: ControlFile) -> Self {
135        SqlGraphEntity::ExtensionRoot(val)
136    }
137}
138
139/// An error met while parsing a `.control` file.
140#[derive(Debug, Error)]
141pub enum ControlFileError {
142    #[error("Filesystem error reading control file")]
143    IOError {
144        #[from]
145        error: std::io::Error,
146    },
147    #[error("Missing field in control file! Please add `{field}`.")]
148    MissingField { field: &'static str },
149    #[error("Redundant field in control file! Please remove `{field}`.")]
150    RedundantField { field: &'static str },
151    #[error("Missing environment variable: {0}")]
152    MissingEnvvar(String),
153}
154
155impl TryFrom<PathBuf> for ControlFile {
156    type Error = ControlFileError;
157
158    fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
159        let contents = std::fs::read_to_string(value)?;
160        ControlFile::try_from(contents.as_str())
161    }
162}
163
164impl TryFrom<&str> for ControlFile {
165    type Error = ControlFileError;
166
167    fn try_from(input: &str) -> Result<Self, Self::Error> {
168        Self::from_str(input)
169    }
170}
171
172impl ToSql for ControlFile {
173    fn to_sql(&self, _context: &super::PgrxSql) -> eyre::Result<String> {
174        let comment = r#"
175/*
176This file is auto generated by pgrx.
177
178The ordering of items is not stable, it is driven by a dependency graph.
179*/
180"#;
181        Ok(comment.into())
182    }
183}
184
185impl SqlGraphIdentifier for ControlFile {
186    fn dot_identifier(&self) -> String {
187        "extension root".into()
188    }
189    fn rust_identifier(&self) -> String {
190        "root".into()
191    }
192
193    fn file(&self) -> Option<&'static str> {
194        None
195    }
196
197    fn line(&self) -> Option<u32> {
198        None
199    }
200}