use anyhow::{Context, Result};
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
fmt::Debug,
fs::File,
io::BufReader,
path::{Path, PathBuf},
};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub(crate) struct LockFile {
#[serde(skip)]
pub(crate) path: PathBuf,
nodes: IndexMap<String, LockNode>,
root: String,
version: usize,
}
impl LockFile {
pub(crate) fn from_root(root: &Path) -> Result<Self> {
let path = root.join("flake.lock");
let file =
File::open(&path).with_context(|| format!("Failed to open LockFile at {path:?}"))?;
let reader = BufReader::new(file);
let mut lockfile: LockFile = serde_json::from_reader(reader)?;
lockfile.path = path;
Ok(lockfile)
}
pub(crate) fn get_node_via_root(&self, input_name: &str) -> Option<&LockNode> {
self.nodes.get(self.resolve_node_name_via_root(input_name)?)
}
pub(crate) fn update_node_via_root(&self, input_name: &str, new_node: &LockNode) -> Self {
let node_name = self.resolve_node_name_via_root(input_name).unwrap();
let mut modified = self.clone();
_ = modified
.nodes
.insert(node_name.to_string(), new_node.clone());
modified
}
fn resolve_node_name_via_root(&self, input_name: &str) -> Option<&str> {
let Input::Direct(node_name) = self
.nodes
.get(&self.root)
.unwrap()
.inputs
.as_ref()
.unwrap()
.get(input_name)?
else {
return None;
};
Some(node_name)
}
pub(crate) fn get_inputs(&self) -> Vec<String> {
self.nodes
.get(&self.root)
.unwrap()
.inputs
.as_ref()
.unwrap()
.keys()
.cloned()
.collect()
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub(crate) struct LockNode {
#[serde(skip_serializing_if = "Option::is_none")]
inputs: Option<HashMap<String, Input>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) locked: Option<LockNodeLocked>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) original: Option<LockNodeOriginal>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
enum Input {
Direct(String),
Indirect(Vec<String>),
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "lowercase", tag = "type")]
pub(crate) enum LockNodeLocked {
Git(LockNodeLockedGit),
GitHub(LockNodeLockedGitHub),
File(LockNodeLockedFile),
}
impl LockNodeLocked {
pub(crate) fn last_modified(&self) -> Option<usize> {
match self {
LockNodeLocked::Git(locked) => Some(locked.last_modified),
LockNodeLocked::GitHub(locked) => Some(locked.last_modified),
LockNodeLocked::File(_locked) => None,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct LockNodeLockedGitHub {
last_modified: usize,
nar_hash: String,
owner: String,
repo: String,
rev: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct LockNodeLockedGit {
last_modified: usize,
nar_hash: String,
r#ref: String,
rev: String,
rev_count: usize,
url: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct LockNodeLockedFile {
nar_hash: String,
url: String,
}
#[derive(Deserialize, Clone, Serialize, PartialEq)]
#[serde(rename_all = "lowercase", tag = "type")]
pub(crate) enum LockNodeOriginal {
Git(LockNodeOriginalGit),
GitHub(LockNodeOriginalGitHub),
Indirect(LockNodeOriginalIndirect),
File(LockNodeOriginalFile),
}
impl Debug for LockNodeOriginal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LockNodeOriginal::Git(git) => write!(f, "{git:?}"),
LockNodeOriginal::GitHub(github) => write!(f, "{github:?}"),
LockNodeOriginal::Indirect(indirect) => write!(f, "{indirect:?}"),
LockNodeOriginal::File(file) => write!(f, "{file:?}"),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub(crate) struct LockNodeOriginalGit {
url: String,
#[serde(skip_serializing_if = "Option::is_none")]
rev: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
r#ref: Option<String>,
}
#[derive(Clone, Deserialize, Serialize)]
pub(crate) struct LockNodeOriginalGitHub {
owner: String,
repo: String,
#[serde(skip_serializing_if = "Option::is_none")]
rev: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
r#ref: Option<String>,
}
impl std::fmt::Debug for LockNodeOriginalGitHub {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "github:{}/{}", self.owner, self.repo)?;
match (&self.rev, &self.r#ref) {
(Some(rev), None) => write!(f, "/{rev}")?,
(None, Some(r#ref)) => write!(f, "/{ref}")?,
(Some(rev), Some(r#ref)) => write!(f, "/{rev}?ref={ref}")?,
(None, None) => {}
}
Ok(())
}
}
impl PartialEq for LockNodeOriginalGitHub {
fn eq(&self, other: &Self) -> bool {
self.owner.eq_ignore_ascii_case(&other.owner)
&& self.repo.eq_ignore_ascii_case(&other.repo)
&& self.rev == other.rev
&& self.r#ref == other.r#ref
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub(crate) struct LockNodeOriginalIndirect {
id: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub(crate) struct LockNodeOriginalFile {
url: String,
}