Skip to main content

anodizer_core/config/
source.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Deserializer, Serialize};
3
4use super::StringOrU32;
5
6// ---------------------------------------------------------------------------
7// SourceConfig
8// ---------------------------------------------------------------------------
9
10/// An individual file entry for the source archive, supporting src/dst mapping
11/// and file metadata overrides.
12#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
13#[serde(default)]
14pub struct SourceFileEntry {
15    /// Source file path or glob pattern.
16    pub src: String,
17    /// Destination path within the archive prefix directory.
18    pub dst: Option<String>,
19    /// Strip the parent directory from the source path.
20    pub strip_parent: Option<bool>,
21    /// File metadata overrides.
22    pub info: Option<SourceFileInfo>,
23}
24
25/// File metadata overrides for source archive entries.
26#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
27#[serde(default)]
28pub struct SourceFileInfo {
29    /// File owner.
30    pub owner: Option<String>,
31    /// File group.
32    pub group: Option<String>,
33    /// File permissions mode. Accepts a YAML int (decimal) or an
34    /// octal-prefixed string (`"0o755"`, `"0755"`). Stored as a `u32` after
35    /// parsing — see [`StringOrU32`].
36    pub mode: Option<StringOrU32>,
37    /// Modification time in RFC3339 format (supports templates).
38    pub mtime: Option<String>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
42#[serde(default)]
43pub struct SourceConfig {
44    /// When true, generate a source code archive for the release.
45    pub enabled: Option<bool>,
46    /// Archive format for the source tarball: tar.gz, tgz, tar, or zip (default: tar.gz).
47    pub format: Option<String>,
48    /// Filename template for the source archive (supports templates).
49    pub name_template: Option<String>,
50    /// Prefix prepended to all paths inside the archive (supports templates).
51    /// Defaults to name_template value. Use this to set a different prefix than the archive name.
52    pub prefix_template: Option<String>,
53    /// Extra files to include in the source archive. Accepts strings (glob patterns) or objects with src/dst/info.
54    #[serde(default, deserialize_with = "deserialize_source_files")]
55    #[schemars(schema_with = "source_files_schema")]
56    pub files: Vec<SourceFileEntry>,
57}
58
59impl SourceConfig {
60    /// Whether source archive generation is enabled (default: false).
61    pub fn is_enabled(&self) -> bool {
62        self.enabled.unwrap_or(false)
63    }
64
65    /// Archive format to use (default: "tar.gz").
66    pub fn archive_format(&self) -> &str {
67        self.format.as_deref().unwrap_or("tar.gz")
68    }
69
70    /// Apply defaults to `prefix_template`: when unset and `name_template` is
71    /// set, default `prefix_template` to the same string as `name_template`.
72    ///
73    /// Q-src3: the doc has long claimed "Defaults to `name_template` value",
74    /// but downstream consumers were reading the raw `Option<String>` and
75    /// substituting empty string. Filling the field at defaults-resolution
76    /// time honors the documented contract — stage code that already
77    /// renders the (now-Some) field needs no behavioral change.
78    ///
79    /// GoReleaser `internal/pipe/sourcearchive/source.go:49-57` defaults to
80    /// empty; this is anodize-additive (more ergonomic default), aligning
81    /// behavior with the long-standing doc.
82    pub fn apply_prefix_template_default(&mut self) {
83        if self.prefix_template.is_none()
84            && let Some(ref name_tpl) = self.name_template
85        {
86            self.prefix_template = Some(name_tpl.clone());
87        }
88    }
89}
90
91/// Helper schema function for the source files field (accepts strings, objects, or mixed arrays).
92fn source_files_schema(
93    generator: &mut schemars::r#gen::SchemaGenerator,
94) -> schemars::schema::Schema {
95    let mut schema = generator.subschema_for::<Vec<SourceFileEntry>>();
96    if let schemars::schema::Schema::Object(ref mut obj) = schema {
97        obj.metadata().description = Some(
98            "Extra files for the source archive. Accepts strings (glob patterns), objects with src/dst/info, or a mixed array.".to_owned(),
99        );
100    }
101    schema
102}
103
104/// Custom deserializer for the source `files` field.
105/// Accepts:
106///   - null/missing → empty vec (via serde default)
107///   - a single string → vec of one SourceFileEntry with that src
108///   - a single object → vec of one SourceFileEntry
109///   - an array of mixed strings/objects → vec of SourceFileEntry
110fn deserialize_source_files<'de, D>(deserializer: D) -> Result<Vec<SourceFileEntry>, D::Error>
111where
112    D: Deserializer<'de>,
113{
114    use serde::de::{self, SeqAccess, Visitor};
115
116    struct SourceFilesVisitor;
117
118    impl<'de> Visitor<'de> for SourceFilesVisitor {
119        type Value = Vec<SourceFileEntry>;
120
121        fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122            f.write_str("a string, a source file entry object, or an array of strings/objects")
123        }
124
125        fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
126            Ok(vec![SourceFileEntry {
127                src: v.to_string(),
128                ..Default::default()
129            }])
130        }
131
132        fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
133            let mut entries = Vec::new();
134            while let Some(value) = seq.next_element::<serde_yaml_ng::Value>()? {
135                match value {
136                    serde_yaml_ng::Value::String(s) => {
137                        entries.push(SourceFileEntry {
138                            src: s,
139                            ..Default::default()
140                        });
141                    }
142                    other => {
143                        let entry =
144                            SourceFileEntry::deserialize(other).map_err(de::Error::custom)?;
145                        entries.push(entry);
146                    }
147                }
148            }
149            Ok(entries)
150        }
151
152        fn visit_map<M: de::MapAccess<'de>>(self, map: M) -> Result<Self::Value, M::Error> {
153            let entry = SourceFileEntry::deserialize(de::value::MapAccessDeserializer::new(map))?;
154            Ok(vec![entry])
155        }
156
157        fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
158            Ok(Vec::new())
159        }
160
161        fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
162            Ok(Vec::new())
163        }
164    }
165
166    deserializer.deserialize_any(SourceFilesVisitor)
167}