use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use smol_str::SmolStr;
pub const SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackageIndex {
pub schema_version: u32,
pub package: SmolStr,
pub version: SmolStr,
pub lib_path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub r_version: Option<SmolStr>,
pub harvested_at: u64,
pub symbols: Vec<SymbolEntry>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SymbolEntry {
pub name: SmolStr,
pub kind: SymbolKind,
pub exported: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub formals: Option<Vec<Formal>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub help: Option<HelpDoc>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SymbolKind {
Function,
Data,
Other,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Formal {
pub name: SmolStr,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct HelpDoc {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usage: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub arguments: Vec<HelpArg>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HelpArg {
pub name: String,
pub description: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct IndexMeta {
pub schema_version: u32,
pub packages: BTreeMap<SmolStr, SmolStr>,
}
impl IndexMeta {
pub fn new() -> Self {
Self {
schema_version: SCHEMA_VERSION,
packages: BTreeMap::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> PackageIndex {
PackageIndex {
schema_version: SCHEMA_VERSION,
package: SmolStr::new("magrittr"),
version: SmolStr::new("2.0.4"),
lib_path: "/lib".to_string(),
r_version: Some(SmolStr::new("4.5.3")),
harvested_at: 0,
symbols: vec![
SymbolEntry {
name: SmolStr::new("%>%"),
kind: SymbolKind::Function,
exported: true,
formals: Some(vec![
Formal {
name: SmolStr::new("lhs"),
default: None,
},
Formal {
name: SmolStr::new("rhs"),
default: None,
},
]),
help: Some(HelpDoc {
title: Some("Pipe operator".to_string()),
..Default::default()
}),
},
SymbolEntry {
name: SmolStr::new("debug_pipe"),
kind: SymbolKind::Function,
exported: true,
formals: None,
help: None,
},
],
}
}
#[test]
fn round_trips_through_json() {
let idx = sample();
let json = serde_json::to_string(&idx).unwrap();
let back: PackageIndex = serde_json::from_str(&json).unwrap();
assert_eq!(idx, back);
}
#[test]
fn omits_empty_optional_fields() {
let idx = sample();
let json = serde_json::to_string(&idx).unwrap();
assert!(json.contains("debug_pipe"));
assert!(!json.contains("\"arguments\""));
assert!(json.contains("r_version"));
}
#[test]
fn meta_round_trips() {
let mut meta = IndexMeta::new();
meta.packages
.insert(SmolStr::new("magrittr"), SmolStr::new("2.0.4"));
let json = serde_json::to_string(&meta).unwrap();
let back: IndexMeta = serde_json::from_str(&json).unwrap();
assert_eq!(meta, back);
}
}