bounds-cli 0.5.1

Add a .gitignore and LICENSE to a repository
use std::{collections::HashMap, time::Duration};

use anyhow::{Context, Result};
use serde::Deserialize;
use ureq::Agent;

use crate::core::model::GitignoreTemplate;

const API_URL: &str = "https://www.toptal.com/developers/gitignore/api";

#[derive(Clone)]
pub struct GitignoreClient {
    agent: Agent,
}

#[derive(Deserialize)]
struct ApiTemplate {
    key: String,
    name: String,
    contents: String,
}

impl GitignoreClient {
    pub fn new() -> Self {
        let config = Agent::config_builder()
            .timeout_global(Some(Duration::from_secs(15)))
            .build();

        Self {
            agent: config.into(),
        }
    }

    pub fn templates(&self) -> Result<Vec<GitignoreTemplate>> {
        let url = format!("{API_URL}/list?format=json");
        let mut response = self
            .agent
            .get(&url)
            .header("Accept", "application/json")
            .header("User-Agent", concat!("bounds/", env!("CARGO_PKG_VERSION")))
            .call()
            .with_context(|| format!(".gitignore template request failed: {url}"))?;

        let templates = response
            .body_mut()
            .read_json::<HashMap<String, ApiTemplate>>()
            .with_context(|| format!(".gitignore template service returned invalid data: {url}"))?;

        let mut templates = templates
            .into_values()
            .map(|template| GitignoreTemplate {
                key: template.key,
                name: template.name,
                source: template.contents,
            })
            .collect::<Vec<_>>();

        templates.sort_unstable_by_key(|template| template.name.to_lowercase());

        Ok(templates)
    }
}