flake_edit/
api.rs

1use std::collections::HashMap;
2use std::process::Command;
3
4use reqwest::blocking::Client;
5use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, USER_AGENT};
6use semver::Version;
7use serde::Deserialize;
8
9#[derive(Deserialize, Debug)]
10pub struct IntermediaryTags(Vec<IntermediaryTag>);
11
12#[derive(Debug)]
13pub struct Tags {
14    versions: Vec<Version>,
15    prefix: String,
16}
17
18impl Tags {
19    pub fn get_latest_tag(&mut self) -> Option<String> {
20        self.sort();
21        let mut buf = String::new();
22        buf.push_str(&self.prefix);
23        let latest_version = &self.versions.iter().last()?;
24        buf.push_str(&latest_version.to_string());
25        Some(buf)
26    }
27    pub fn sort(&mut self) {
28        self.versions.sort_by(Version::cmp_precedence);
29    }
30}
31
32#[derive(Deserialize, Debug)]
33pub struct IntermediaryTag {
34    name: String,
35}
36
37// TODO: actual error handling
38pub fn get_tags(repo: &str, owner: &str) -> Result<Tags, ()> {
39    let tags = query_tags(repo, owner).unwrap();
40    Ok(tags.into())
41}
42
43#[derive(Deserialize, Debug, Clone)]
44struct NixConfig {
45    #[serde(rename = "access-tokens")]
46    access_tokens: Option<AccessTokens>,
47}
48
49impl NixConfig {
50    fn gh_token(&self) -> Option<String> {
51        self.access_tokens
52            .clone()
53            .unwrap()
54            .value
55            .get("github.com")
56            .cloned()
57    }
58}
59
60#[derive(Deserialize, Debug, Clone)]
61struct AccessTokens {
62    value: HashMap<String, String>,
63}
64
65// Try to query gh access tokens
66pub fn get_gh_token() -> Option<String> {
67    let command = Command::new("nix")
68        .arg("config")
69        .arg("show")
70        .arg("--json")
71        .output()
72        .unwrap();
73    let stdout = String::from_utf8(command.stdout).unwrap();
74    let output: NixConfig = serde_json::from_str(&stdout).unwrap();
75
76    if let Some(token) = output.gh_token() {
77        return Some(token);
78    };
79    if let Ok(token) = std::env::var("GITHUB_TOKEN") {
80        return Some(token);
81    };
82
83    None
84}
85
86// https://api.github.com/repos/{OWNER}/{REPO}/tags
87// Query tags for github currently.
88// TODO: support other forges.
89fn query_tags(repo: &str, owner: &str) -> Result<IntermediaryTags, ()> {
90    let client = Client::new();
91    let mut headers = HeaderMap::new();
92    headers.insert(USER_AGENT, HeaderValue::from_str("flake-edit").unwrap());
93    if let Some(token) = get_gh_token() {
94        tracing::debug!("Found github token.");
95        headers.insert(
96            AUTHORIZATION,
97            HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
98        );
99        tracing::debug!("Settings github token.");
100    }
101    let body = client
102        .get(format!(
103            "https://api.github.com/repos/{}/{}/tags",
104            repo, owner
105        ))
106        .headers(headers)
107        .send()
108        .unwrap()
109        .text()
110        .unwrap();
111
112    tracing::debug!("Body from api: {body}");
113
114    match serde_json::from_str::<IntermediaryTags>(&body) {
115        Ok(tags) => Ok(tags),
116        Err(e) => {
117            tracing::error!("Error from api: {e}");
118            Err(())
119        }
120    }
121}
122
123impl From<IntermediaryTags> for Tags {
124    fn from(value: IntermediaryTags) -> Self {
125        fn strip_until_char(s: &str, c: char) -> Option<(String, String)> {
126            s.find(c).map(|index| {
127                let prefix = s[..index].to_string();
128                let remaining = s[index + 1..].to_string();
129                (prefix, remaining)
130            })
131        }
132        let mut versions = vec![];
133        let mut prefix = String::new();
134        for itag in value.0 {
135            let mut tag = itag.name;
136            // TODO: implement a generic way to find the version prefixes
137            if let Some(new_tag) = tag.strip_prefix('v') {
138                tag = new_tag.to_string();
139                prefix = "v".to_string();
140            }
141
142            if let Some((new_prefix, new_tag)) = strip_until_char(&tag, '-') {
143                tag = new_tag;
144                prefix = format!("{new_prefix}-").to_string();
145            }
146
147            match Version::parse(&tag) {
148                Ok(semver) => {
149                    versions.push(semver);
150                }
151                Err(e) => {
152                    tracing::error!("Could not parse version {:?}", e);
153                }
154            }
155        }
156        Tags { versions, prefix }
157    }
158}