1use 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 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 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#[derive(Debug, Default, Serialize, Deserialize)]
39pub struct Config {
40 pub sources: BTreeMap<String, Source>,
42}
43
44impl Config {
45 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 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
86#[serde(tag = "type")]
87pub enum Source {
88 CratesIo {
90 name: String,
92 version: semver::Version,
94 sha256: String,
96 },
97 Registry {
99 registry: String,
101 name: String,
103 version: semver::Version,
105 sha256: String,
107 },
108 Git {
110 url: url::Url,
114 rev: String,
116 sha256: String,
118 },
119 Nix {
121 #[serde(flatten)]
123 file: NixFile,
124 #[serde(skip_serializing_if = "Option::is_none")]
126 attr: Option<String>,
127 },
128}
129
130#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Eq, Hash)]
132pub enum NixFile {
133 #[serde(rename = "import")]
135 Import(String),
136 #[serde(rename = "package")]
138 Package(String),
139}
140
141impl Display for NixFile {
142 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143 match self {
144 Self::Import(path) => write!(f, "import {}", path),
145 Self::Package(path) => write!(f, "pkgs.callPackage {} {{}}", path),
146 }
147 }
148}
149
150impl NixFile {
151 pub fn as_command(&self) -> String {
153 match self {
154 Self::Import(path) => format!("--import '{}'", path),
155 Self::Package(path) => format!("--package '{}'", path),
156 }
157 }
158}
159
160impl Source {
161 pub fn name(&self) -> Option<&str> {
163 match self {
164 Source::CratesIo { name, .. } => Some(name),
165 Source::Git { url, .. } => {
166 let path = url.path();
167 let after_last_slash = path.split('/').last().unwrap_or(path);
168 let without_dot_git = after_last_slash
169 .strip_suffix(".git")
170 .unwrap_or(after_last_slash);
171 Some(without_dot_git)
172 }
173 Source::Nix {
174 attr: Some(attr), ..
175 } => attr.split('.').last().or(if attr.trim().is_empty() {
176 None
177 } else {
178 Some(attr.trim())
179 }),
180 _ => None,
181 }
182 }
183}
184
185impl Display for Source {
186 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187 match self {
188 Source::CratesIo {
189 name,
190 version,
191 sha256,
192 } => write!(f, "{} {} from crates.io: {}", name, version, sha256),
193 Source::Registry {
194 name,
195 version,
196 sha256,
197 registry,
198 ..
199 } => write!(f, "{} {} from {}: {}", name, version, registry, sha256),
200 Source::Git { url, rev, sha256 } => write!(f, "{}#{} via git: {}", url, rev, sha256),
201 Source::Nix { file, attr: None } => write!(f, "{}", file),
202 Source::Nix {
203 file,
204 attr: Some(attr),
205 } => write!(f, "({}).{}", file, attr),
206 }
207 }
208}
209
210impl Source {
211 pub fn as_command(&self, name: &str) -> String {
213 match self {
214 Source::CratesIo {
215 name: crate_name,
216 version,
217 ..
218 } => format!("cratesIo --name '{}' '{}' '{}'", name, crate_name, version),
219 Source::Registry {
220 name: crate_name,
221 version,
222 registry,
223 ..
224 } => format!(
225 "registry --registry '{}' --name '{}' '{}' '{}'",
226 registry, name, crate_name, version
227 ),
228 Source::Git { url, rev, .. } => {
229 format!("git --name '{}' '{}' --rev {}", name, url, rev)
230 }
231 Source::Nix { file, attr: None } => {
232 format!("nix --name '{}' {}", name, file.as_command())
233 }
234 Source::Nix {
235 file,
236 attr: Some(attr),
237 } => format!("nix --name '{}' {} '{}'", name, file.as_command(), attr),
238 }
239 }
240}