/// This module defines the unified putput format as expected by the elastic search /// Additionally, we implement converseions from the two possible input formats, i.e. /// Flakes, or Nixpkgs. use std::{ convert::{TryFrom, TryInto}, path::PathBuf, }; use super::{ import::{DocValue, ModulePath}, pandoc::PandocExt, }; use crate::data::import::NixOption; use anyhow::Context; use serde::{Deserialize, Serialize}; use super::{ import, system::System, utility::{AttributeQuery, Flatten, OneOrMany, Reverse}, }; use lazy_static::lazy_static; lazy_static! { static ref FILTERS_PATH: PathBuf = std::env::var("NIXPKGS_PANDOC_FILTERS_PATH") .unwrap_or("".into()) .into(); } type Flake = super::Flake; #[allow(non_snake_case)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct License { url: Option, fullName: String, } impl From for License { #[allow(non_snake_case)] fn from(license: import::License) -> Self { match license { import::License::None { .. } => License { url: None, fullName: "No License Specified".to_string(), }, import::License::Simple { license } => License { url: None, fullName: license, }, import::License::Full { fullName, url, .. } => License { url, fullName }, import::License::Url { url } => License { url: Some(url), fullName: "No Name".into(), }, } } } // ----- Unified derivation representation #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type")] pub enum Derivation { #[serde(rename = "package")] #[allow(non_snake_case)] Package { package_attr_name: String, package_attr_name_reverse: Reverse, package_attr_name_query: AttributeQuery, package_attr_name_query_reverse: Reverse, package_attr_set: String, package_attr_set_reverse: Reverse, package_pname: String, package_pname_reverse: Reverse, package_pversion: String, package_platforms: Vec, package_outputs: Vec, package_default_output: Option, package_license: Vec, package_license_set: Vec, package_maintainers: Vec, package_maintainers_set: Vec, package_description: Option, package_description_reverse: Option>, package_longDescription: Option, package_longDescription_reverse: Option>, package_hydra: (), package_system: String, package_homepage: Vec, package_position: Option, }, #[serde(rename = "app")] App { app_attr_name: String, app_platforms: Vec, app_type: Option, app_bin: Option, }, #[serde(rename = "option")] Option { option_source: Option, option_name: String, option_name_reverse: Reverse, option_name_query: AttributeQuery, option_name_query_reverse: Reverse, option_description: Option, option_description_reverse: Option>, option_type: Option, option_default: Option, option_example: Option, option_flake: Option, }, } // ----- Conversions impl TryFrom<(import::FlakeEntry, super::Flake)> for Derivation { type Error = anyhow::Error; fn try_from((d, f): (import::FlakeEntry, super::Flake)) -> Result { Ok(match d { import::FlakeEntry::Package { attribute_name, name, version, platforms, outputs, default_output, description, license, } => { let package_attr_set: Vec<_> = attribute_name.split(".").collect(); let package_attr_set: String = (if package_attr_set.len() > 1 { package_attr_set[0] } else { "No package set" }) .into(); let package_attr_set_reverse = Reverse(package_attr_set.clone()); let package_license: Vec = vec![license.into()]; let package_license_set: Vec = package_license .iter() .clone() .map(|l| l.fullName.to_owned()) .collect(); let maintainer: Maintainer = f.into(); Derivation::Package { package_attr_name_query: AttributeQuery::new(&attribute_name), package_attr_name_query_reverse: Reverse(AttributeQuery::new(&attribute_name)), package_attr_name: attribute_name.clone(), package_attr_name_reverse: Reverse(attribute_name), package_attr_set, package_attr_set_reverse, package_pname: name.clone(), package_pname_reverse: Reverse(name), package_pversion: version, package_platforms: platforms, package_outputs: outputs, package_default_output: Some(default_output), package_license, package_license_set, package_description: description.clone(), package_maintainers: vec![maintainer.clone()], package_maintainers_set: maintainer.name.map_or(vec![], |n| vec![n]), package_description_reverse: description.map(Reverse), package_longDescription: None, package_longDescription_reverse: None, package_hydra: (), package_system: String::new(), package_homepage: Vec::new(), package_position: None, } } import::FlakeEntry::App { bin, attribute_name, platforms, app_type, } => Derivation::App { app_attr_name: attribute_name, app_platforms: platforms, app_bin: bin, app_type, }, import::FlakeEntry::Option(option) => option.try_into()?, }) } } impl TryFrom for Derivation { type Error = anyhow::Error; fn try_from(entry: import::NixpkgsEntry) -> Result { Ok(match entry { import::NixpkgsEntry::Derivation { attribute, package } => { let package_attr_set: Vec<_> = attribute.split(".").collect(); let package_attr_set: String = (if package_attr_set.len() > 1 { package_attr_set[0] } else { "No package set" }) .into(); let package_attr_set_reverse = Reverse(package_attr_set.clone()); let package_license: Vec<_> = package .meta .license .map(OneOrMany::into_list) .unwrap_or_default() .into_iter() .map(|sos| sos.0.into()) .collect(); let package_license_set = package_license .iter() .map(|l: &License| l.fullName.to_owned()) .collect(); let package_maintainers: Vec = package .meta .maintainers .map_or(Default::default(), Flatten::flatten) .into_iter() .map(Into::into) .collect(); let package_maintainers_set = package_maintainers .iter() .flat_map(|m| m.name.to_owned()) .collect(); let position: Option = package.meta.position.map(|p| { if p.starts_with("/nix/store") { p.split("/").skip(4).collect::>().join("/") } else { p } }); Derivation::Package { package_attr_name: attribute.clone(), package_attr_name_reverse: Reverse(attribute.clone()), package_attr_name_query: AttributeQuery::new(&attribute), package_attr_name_query_reverse: Reverse(AttributeQuery::new(&attribute)), package_attr_set, package_attr_set_reverse, package_pname: package.pname.clone(), package_pname_reverse: Reverse(package.pname), package_pversion: package.version, package_platforms: package .meta .platforms .map(Flatten::flatten) .unwrap_or_default(), package_outputs: package.outputs.into_keys().collect(), package_default_output: package.default_output, package_license, package_license_set, package_maintainers, package_maintainers_set, package_description: package.meta.description.clone(), package_description_reverse: package.meta.description.map(Reverse), package_longDescription: package.meta.long_description.clone(), package_longDescription_reverse: package.meta.long_description.map(Reverse), package_hydra: (), package_system: package.system, package_homepage: package .meta .homepage .map_or(Default::default(), OneOrMany::into_list), package_position: position, } } import::NixpkgsEntry::Option(option) => option.try_into()?, }) } } impl TryFrom for Derivation { type Error = anyhow::Error; fn try_from( NixOption { declarations, description, name, option_type, default, example, flake, }: import::NixOption, ) -> Result { let description = description .as_ref() .map(PandocExt::render) .transpose() .with_context(|| format!("While rendering the description for option `{}`", name))?; let option_default = default; // .map(TryInto::try_into) // .transpose() // .with_context(|| format!("While rendering the default for option `{}`", name))?; let option_example = example; // .map(TryInto::try_into) // .transpose() // .with_context(|| format!("While rendering the example for option `{}`", name))?; let option_type = option_type; // .map(TryInto::try_into) // .transpose() // .with_context(|| format!("While rendering the type for option `{}`", name))?; Ok(Derivation::Option { option_source: declarations.get(0).map(Clone::clone), option_name: name.clone(), option_name_reverse: Reverse(name.clone()), option_description: description.clone(), option_description_reverse: description.map(Reverse), option_default, option_example, option_flake: flake, option_type, option_name_query: AttributeQuery::new(&name), option_name_query_reverse: Reverse(AttributeQuery::new(&name)), }) } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Maintainer { name: Option, github: Option, email: Option, } impl From for Maintainer { fn from(import: import::Maintainer) -> Self { match import { import::Maintainer::Full { name, github, email, } => Maintainer { name, github, email, }, import::Maintainer::Simple(name) => Maintainer { name: Some(name), github: None, email: None, }, } } } impl From for Maintainer { fn from(flake: super::Flake) -> Self { let github = flake .source .and_then(|source| match source { super::Source::Github { owner, .. } => Some(owner), _ => None, }) .unwrap_or_else(|| "Maintainer Unknown".to_string()); Maintainer { github: Some(github), email: None, name: None, } } } // ----- output type /// Export type that brings together derivation and optional flake info #[derive(Debug, Clone, PartialEq, Serialize)] pub struct Export { #[serde(flatten)] flake: Option, #[serde(flatten)] item: Derivation, } impl Export { /// Construct Export from Flake and Flake entry pub fn flake(flake: Flake, item: import::FlakeEntry) -> anyhow::Result { Ok(Self { flake: Some(flake.clone()), item: Derivation::try_from((item, flake))?, }) } /// Construct Export from NixpkgsEntry pub fn nixpkgs(item: import::NixpkgsEntry) -> anyhow::Result { Ok(Self { flake: None, item: Derivation::try_from(item)?, }) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_option() { let option: NixOption = serde_json::from_str(r#" { "declarations":["/nix/store/s1q1238ahiks5a4g6j6qhhfb3rlmamvz-source/nixos/modules/system/boot/luksroot.nix"], "default": {"one": 1, "two" : { "three": "tree", "four": []}}, "description":"Commands that should be run right after we have mounted our LUKS device.\n", "example":null, "internal":false, "loc":["boot","initrd","luks","devices","","postOpenCommands"], "name":"boot.initrd.luks.devices..postOpenCommands", "readOnly":false, "type": "boolean", "visible":true }"#).unwrap(); let option: Derivation = option.try_into().unwrap(); println!("{}", serde_json::to_string_pretty(&option).unwrap()); } }