crate2nix/
config.rs

1//! Managing the `crate2nix.json` config.
2
3use anyhow::{Context, Error};
4use serde::{Deserialize, Serialize};
5use std::{
6    collections::BTreeMap,
7    fmt::Display,
8    fs::File,
9    io::{BufReader, BufWriter},
10    path::Path,
11};
12
13impl Config {
14    /// Read config from path.
15    pub fn read_from_or_default(path: &Path) -> Result<Config, Error> {
16        if !path.exists() {
17            return Ok(Config::default());
18        }
19
20        let file = File::open(path).context(format!("while opening {}", path.to_string_lossy()))?;
21        let reader = BufReader::new(file);
22        serde_json::from_reader(reader).context(format!(
23            "while deserializing config: {}",
24            path.to_string_lossy()
25        ))
26    }
27
28    /// Write config to path.
29    pub fn write_to(&self, path: &Path) -> Result<(), Error> {
30        let file =
31            File::create(path).context(format!("while opening {}", path.to_string_lossy()))?;
32        let writer = BufWriter::new(file);
33        Ok(serde_json::to_writer_pretty(writer, self)?)
34    }
35}
36
37/// The `crate2nix.json` config data.
38#[derive(Debug, Default, Serialize, Deserialize)]
39pub struct Config {
40    /// Out of tree sources.
41    pub sources: BTreeMap<String, Source>,
42}
43
44impl Config {
45    /// Add or replace a source. Returns the old source if there was one.
46    pub fn upsert_source(
47        &mut self,
48        explicit_name: Option<String>,
49        source: Source,
50    ) -> Option<Source> {
51        let name = explicit_name
52            .or_else(|| source.name().map(|s| s.to_string()))
53            .expect("No name given");
54        self.sources.insert(name, source)
55    }
56
57    /// Prints all sources to stdout.
58    pub fn print_sources(&self) {
59        if self.sources.is_empty() {
60            eprintln!("No sources configured.\n");
61            return;
62        }
63
64        let max_len = self
65            .sources
66            .keys()
67            .map(|n| n.len())
68            .max()
69            .unwrap_or_default();
70        for (name, source) in &self.sources {
71            println!("{:width$} {}", name, source, width = max_len);
72            println!();
73            println!(
74                "{:width$} crate2nix source add {}",
75                "",
76                source.as_command(name),
77                width = max_len
78            );
79            println!();
80        }
81    }
82}
83
84/// An out of tree source.
85#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
86#[serde(tag = "type")]
87pub enum Source {
88    /// Get the source from crates.io.
89    CratesIo {
90        /// The crate name.
91        name: String,
92        /// The exact crate version to fetch.
93        version: semver::Version,
94        /// The sha256 hash of the source.
95        sha256: String,
96    },
97    /// Get the source from git.
98    Git {
99        /// The URL of the git repository.
100        ///
101        /// E.g. https://github.com/kolloch/crate2nix.git
102        url: url::Url,
103        /// The revision hash.
104        rev: String,
105        /// The sha256 of the fetched result.
106        sha256: String,
107    },
108    /// Get the source from a nix expression.
109    Nix {
110        /// The nixfile to include.
111        #[serde(flatten)]
112        file: NixFile,
113        /// A Nix attribute path which will be resolved against the file.
114        #[serde(skip_serializing_if = "Option::is_none")]
115        attr: Option<String>,
116    },
117}
118
119/// A nix file path which is either included by `import` or `callPackage`.
120#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Eq, Hash)]
121pub enum NixFile {
122    /// A file path that should be imported.
123    #[serde(rename = "import")]
124    Import(String),
125    /// A file path the should be included by `pkgs.callPackage`.
126    #[serde(rename = "package")]
127    Package(String),
128}
129
130impl Display for NixFile {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        match self {
133            Self::Import(path) => write!(f, "import {}", path),
134            Self::Package(path) => write!(f, "pkgs.callPackage {} {{}}", path),
135        }
136    }
137}
138
139impl NixFile {
140    /// Returns the chosen file option as CLI string.
141    pub fn as_command(&self) -> String {
142        match self {
143            Self::Import(path) => format!("--import '{}'", path),
144            Self::Package(path) => format!("--package '{}'", path),
145        }
146    }
147}
148
149impl Source {
150    /// The name of the source.
151    pub fn name(&self) -> Option<&str> {
152        match self {
153            Source::CratesIo { name, .. } => Some(name),
154            Source::Git { url, .. } => {
155                let path = url.path();
156                let after_last_slash = path.split('/').last().unwrap_or(path);
157                let without_dot_git = after_last_slash
158                    .strip_suffix(".git")
159                    .unwrap_or(after_last_slash);
160                Some(without_dot_git)
161            }
162            Source::Nix {
163                attr: Some(attr), ..
164            } => attr.split('.').last().or(if attr.trim().is_empty() {
165                None
166            } else {
167                Some(attr.trim())
168            }),
169            _ => None,
170        }
171    }
172}
173
174impl Display for Source {
175    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        match self {
177            Source::CratesIo {
178                name,
179                version,
180                sha256,
181            } => write!(f, "{} {} from crates.io: {}", name, version, sha256),
182            Source::Git { url, rev, sha256 } => write!(f, "{}#{} via git: {}", url, rev, sha256),
183            Source::Nix { file, attr: None } => write!(f, "{}", file),
184            Source::Nix {
185                file,
186                attr: Some(attr),
187            } => write!(f, "({}).{}", file, attr),
188        }
189    }
190}
191
192impl Source {
193    /// Returns a CLI string to reproduce this source.
194    pub fn as_command(&self, name: &str) -> String {
195        match self {
196            Source::CratesIo {
197                name: crate_name,
198                version,
199                ..
200            } => format!("cratesIo --name '{}' '{}' '{}'", name, crate_name, version),
201            Source::Git { url, rev, .. } => {
202                format!("git --name '{}' '{}' --rev {}", name, url, rev)
203            }
204            Source::Nix { file, attr: None } => {
205                format!("nix --name '{}' {}", name, file.as_command())
206            }
207            Source::Nix {
208                file,
209                attr: Some(attr),
210            } => format!("nix --name '{}' {} '{}'", name, file.as_command(), attr),
211        }
212    }
213}