use std::collections::HashMap; use std::fmt; use std::marker::PhantomData; use std::{path::PathBuf, str::FromStr}; use clap::arg_enum; use log::warn; use serde::de::{self, MapAccess, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::Value; use super::pandoc::PandocExt; use super::prettyprint::print_value; use super::utility::{Flatten, OneOrMany}; /// Holds information about a specific derivation #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "entry_type", rename_all = "lowercase")] pub enum FlakeEntry { /// A package as it may be defined in a flake /// /// Note: As flakes do not enforce any particular structure to be necessarily /// present, the data represented is an idealization that _should_ match in /// most cases and is open to extension. Package { attribute_name: String, name: String, version: String, platforms: Vec, outputs: Vec, default_output: String, description: Option, license: Option>>, }, /// An "application" that can be called using nix run <..> App { bin: Option, attribute_name: String, platforms: Vec, app_type: Option, }, /// an option defined in a module of a flake Option(NixOption), } /// The representation of an option that is part of some module and can be used /// in some nixos configuration #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct NixOption { /// Location of the defining module(s) pub declarations: Vec, pub description: Option, pub name: String, #[serde(rename = "type")] /// Nix generated description of the options type pub option_type: Option, #[serde(deserialize_with = "optional_field", default)] #[serde(skip_serializing_if = "Option::is_none")] pub default: Option, #[serde(deserialize_with = "optional_field", default)] #[serde(skip_serializing_if = "Option::is_none")] pub example: Option, /// If defined in a flake, contains defining flake and optionally a module pub flake: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(untagged)] pub enum ModulePath { /// A module taken from .nixosModule /// JSON representation is a list, therefore use a 1-Tuple as representation DefaultModule((String,)), /// A module taken from .nixosModules. NamedModule((String, String)), } #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(untagged)] pub enum DocString { DocFormat(DocFormat), String(String), } #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(tag = "_type", content = "text")] pub enum DocFormat { #[serde(rename = "mdDoc")] MarkdownDoc(String), } #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(untagged)] pub enum DocValue { Literal(Literal), Value(Value), } #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(tag = "_type", content = "text")] pub enum Literal { #[serde(rename = "literalExpression", alias = "literalExample")] LiteralExpression(String), #[serde(rename = "literalDocBook")] LiteralDocBook(String), #[serde(rename = "literalMD")] LiteralMarkdown(String), } impl Serialize for DocString { fn serialize(&self, serializer: S) -> Result where S: Serializer, { match self { DocString::String(db) => { serializer.serialize_str(&db.render_docbook().unwrap_or_else(|e| { warn!("Could not render DocBook content: {}", e); db.to_owned() })) } DocString::DocFormat(DocFormat::MarkdownDoc(md)) => { serializer.serialize_str(&md.render_markdown().unwrap_or_else(|e| { warn!("Could not render Markdown content: {}", e); md.to_owned() })) } } } } impl Serialize for DocValue { fn serialize(&self, serializer: S) -> Result where S: Serializer, { match self { DocValue::Literal(Literal::LiteralExpression(s)) => serializer.serialize_str(&s), DocValue::Literal(Literal::LiteralDocBook(db)) => { serializer.serialize_str(&db.render_docbook().unwrap_or_else(|e| { warn!("Could not render DocBook content: {}", e); db.to_owned() })) } DocValue::Literal(Literal::LiteralMarkdown(md)) => { serializer.serialize_str(&md.render_markdown().unwrap_or_else(|e| { warn!("Could not render Markdown content: {}", e); md.to_owned() })) } DocValue::Value(v) => serializer.serialize_str(&print_value(v.to_owned())), } } } /// Package as defined in nixpkgs /// These packages usually have a "more" homogenic structure that is given by /// nixpkgs /// note: This is the parsing module that deals with nested input. A flattened, /// unified representation can be found in [crate::data::export::Derivation] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Package { pub pname: String, pub version: String, #[serde(default)] pub outputs: HashMap>, #[serde(rename = "outputName", default)] pub default_output: Option, pub system: String, #[serde(default)] pub meta: Meta, } /// The nixpkgs output lists attribute names as keys of a map. /// Name and Package definition are combined using this struct #[derive(Debug, Clone)] pub enum NixpkgsEntry { Derivation { attribute: String, package: Package }, Option(NixOption), } /// Most information about packages in nixpkgs is contained in the meta key /// This struct represents a subset of that metadata #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] pub struct Meta { pub license: Option>>, pub maintainers: Option>, pub homepage: Option>, pub platforms: Option, #[serde(rename = "badPlatforms")] pub bad_platforms: Option, pub position: Option, pub description: Option, #[serde(rename = "longDescription")] pub long_description: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(untagged)] pub enum Maintainer { Full { name: Option, github: Option, email: Option, }, Simple(String), } arg_enum! { /// The type of derivation (placed in packages. or apps.) /// Used to command the extraction script #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum Kind { App, Package, Option, All, } } impl AsRef for Kind { fn as_ref(&self) -> &str { match self { Kind::App => "apps", Kind::Package => "packages", Kind::Option => "options", Kind::All => "all", } } } impl Default for Kind { fn default() -> Self { Kind::All } } #[derive(Debug, Clone, PartialEq, Serialize)] pub struct StringOrStruct(pub T); impl<'de, T> Deserialize<'de> for StringOrStruct where T: Deserialize<'de> + FromStr, { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { Ok(StringOrStruct(string_or_struct(deserializer)?)) } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(untagged)] pub enum Platform { System(String), Pattern {}, // TODO how should those be displayed? } #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] pub struct Platforms(Flatten); impl Platforms { // A bit of abstract nonsense: what we really want is // into_iter : Platforms → ∃ (I : Iterator). I // however Rust makes this annoying to write: we would either have to pick a // concrete iterator type or use something like Box>. // Instead, we can use the dual Church-encoded form of that existential type: // ? : Platforms → ∀ B. (∀ (I : Iterator). I → B) → B // ...which is exactly the type of collect! (think about what FromIterator means) pub fn collect>(self) -> B { self.0 .flatten() .into_iter() .flat_map(|p| match p { Platform::System(s) => Some(s), _ => None, }) .collect() } } /// Different representations of the licence attribute #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(untagged)] pub enum License { None { #[serde(skip_serializing)] license: (), }, Simple { license: String, }, #[allow(non_snake_case)] Full { fullName: Option, shortName: Option, url: Option, }, } impl Default for License { fn default() -> Self { License::None { license: () } } } impl FromStr for License { // This implementation of `from_str` can never fail, so use the impossible // `Void` type as the error type. type Err = anyhow::Error; fn from_str(s: &str) -> Result { Ok(License::Simple { license: s.to_string(), }) } } /// Deserialization helper that parses an item using either serde or fromString fn string_or_struct<'de, T, D>(deserializer: D) -> Result where T: Deserialize<'de> + FromStr, D: Deserializer<'de>, { // This is a Visitor that forwards string types to T's `FromStr` impl and // forwards map types to T's `Deserialize` impl. The `PhantomData` is to // keep the compiler from complaining about T being an unused generic type // parameter. We need T in order to know the Value type for the Visitor // impl. struct StringOrStruct(PhantomData T>); impl<'de, T> Visitor<'de> for StringOrStruct where T: Deserialize<'de> + FromStr, { type Value = T; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("string or map") } fn visit_str(self, value: &str) -> Result where E: de::Error, { Ok(FromStr::from_str(value).unwrap()) } fn visit_map(self, map: M) -> Result where M: MapAccess<'de>, { // `MapAccessDeserializer` is a wrapper that turns a `MapAccess` // into a `Deserializer`, allowing it to be used as the input to T's // `Deserialize` implementation. T then deserializes itself using // the entries from the map visitor. Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)) } } deserializer.deserialize_any(StringOrStruct(PhantomData)) } /// Deserializes an Option by passing `null` along to T's deserializer instead /// of treating it as a missing field fn optional_field<'de, T, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, T: Deserialize<'de>, { Ok(Some(T::deserialize(deserializer)?)) } #[cfg(test)] mod tests { use std::collections::HashMap; use super::*; #[test] fn test_nixpkgs_deserialize() { let json = r#" { "nixpkgs-unstable._0verkill": { "name": "0verkill-unstable-2011-01-13", "pname": "0verkill-unstable", "version": "2011-01-13", "system": "x86_64-darwin", "meta": { "available": true, "broken": false, "description": "ASCII-ART bloody 2D action deathmatch-like game", "homepage": "https://github.com/hackndev/0verkill", "insecure": false, "license": { "fullName": "GNU General Public License v2.0 only", "shortName": "gpl2Only", "spdxId": "GPL-2.0-only", "url": "https://spdx.org/licenses/GPL-2.0-only.html" }, "maintainers": [ { "email": "torres.anderson.85@protonmail.com", "github": "AndersonTorres", "githubId": 5954806, "name": "Anderson Torres" }, "Fred Flintstone" ], "name": "0verkill-unstable-2011-01-13", "outputsToInstall": [ "out" ], "platforms": [ "powerpc64-linux", "powerpc64le-linux", "riscv32-linux", "riscv64-linux", {} ], "position": "/nix/store/97lxf2n6zip41j5flbv6b0928mxv9za8-nixpkgs-unstable-21.03pre268853.d9c6f13e13f/nixpkgs-unstable/pkgs/games/0verkill/default.nix:34", "unfree": false, "unsupported": false } } } "#; let map: HashMap = serde_json::from_str(json).unwrap(); let _: Vec = map .into_iter() .map(|(attribute, package)| NixpkgsEntry::Derivation { attribute, package }) .collect(); } #[test] fn test_flake_option() { let json = r#" { "declarations": [], "name": "test-option", "flake": ["flake", "module"] } "#; serde_json::from_str::(json).unwrap(); } #[test] fn test_flake_option_default_module() { let json = r#" { "declarations": [], "name": "test-option", "flake": ["flake"] } "#; serde_json::from_str::(json).unwrap(); } #[test] fn test_option_parsing() {} }