//! This module contains structs for working with the github templates use log::{debug, trace}; use reqwest::blocking::Client; use reqwest::header::{ACCEPT, CONTENT_TYPE, USER_AGENT}; use serde::{Deserialize, Serialize}; use std::fs::OpenOptions; use std::io::{BufWriter, Write}; use std::path::PathBuf; use crate::errors::*; /// Default user agent string used for api requests pub const DEFAULT_USER_AGENT: &str = "gitig"; /// Response objects from github api #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[allow(clippy::missing_docs_in_private_items)] pub struct Template { pub name: String, pub path: String, pub sha: String, pub size: i64, pub url: String, #[serde(rename = "html_url")] pub html_url: String, #[serde(rename = "git_url")] pub git_url: String, #[serde(rename = "download_url")] pub download_url: Option, #[serde(rename = "type")] pub type_field: String, #[serde(rename = "_links")] pub links: Links, pub content: Option, } /// Part of github api response #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[allow(clippy::missing_docs_in_private_items)] pub struct Links { #[serde(rename = "self")] pub self_field: String, pub git: String, pub html: String, } impl Template { /// Checks if this template file is a gitignore template file pub fn is_gitignore_template(&self) -> bool { self.name.ends_with(".gitignore") } /// Returns the name of the file without the .gitignore ending /// /// if `!self.is_gitignore_template()` the whole `self.name` is returned pub fn pretty_name(&self) -> &str { if let Some(dot_index) = self.name.rfind('.') { return &self.name.get(0..dot_index).unwrap_or_else(|| self.name.as_str()); } self.name.as_str() } /// Loads the template content from github pub fn load_content(&mut self) -> Result<()> { let url = self .download_url .as_ref() .ok_or_else(|| ErrorKind::TemplateNoDownloadUrl(self.pretty_name().into()))?; debug!("Loading template content for {} from {}", self.pretty_name(), url); let client = Client::new(); let res = client .get(url) .header(ACCEPT, "text/html") .header(USER_AGENT, format!("{} {}", DEFAULT_USER_AGENT, env!("CARGO_PKG_VERSION"))) .send() .chain_err(|| { format!("Error while getting content of template {}", self.pretty_name()) })?; debug!( "Got a response from the server ({} B)", res.content_length().map_or_else(|| "?".to_string(), |v| v.to_string()) ); let body: String = res.text().chain_err(|| "Error while parsing body from template response")?; self.content = Some(body); debug!("Set content for template {}", self.pretty_name()); Ok(()) } /// Writes the content of this template to the given path /// /// Creates the file if it does not exist or trunceates an already existing one. /// /// # Errors /// /// Returns `ErrorKind::TemplateNoContent` if this templates has no content (i.e. /// `load_content` has not yet been called. /// Other reasons for `Err` can be io related errors. #[allow(clippy::option_expect_used)] pub fn write_to(&self, path: &PathBuf, append: bool) -> Result<()> { debug!( "Writing contents of {} to {} (append: {})", self.pretty_name(), path.to_string_lossy(), append ); if self.content.is_none() { return Err(ErrorKind::TemplateNoContent.into()); } let file = OpenOptions::new() .write(true) .append(append) .create(true) .open(path) .chain_err(|| "Error while opening gitignore file to write template")?; let mut writer = BufWriter::new(file); writer.write_all(b"# template downloaded with gitig (https://git.schneider-hosting.de/schneider/gitig) from https://github.com/github/gitignore\n")?; writer.write_all(self.content.as_ref().expect("checked before to be some").as_bytes())?; trace!("Wrote all content"); Ok(()) } } /// This struct holds the information about the templates available at github #[derive(Serialize, Deserialize)] pub struct GithubTemplates { /// The templates templates: Vec