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 Git {
99 url: url::Url,
103 rev: String,
105 sha256: String,
107 },
108 Nix {
110 #[serde(flatten)]
112 file: NixFile,
113 #[serde(skip_serializing_if = "Option::is_none")]
115 attr: Option<String>,
116 },
117}
118
119#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Eq, Hash)]
121pub enum NixFile {
122 #[serde(rename = "import")]
124 Import(String),
125 #[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 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 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 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}