use chrono::Utc;
use kimun_core::NoteVault;
use kimun_core::nfs::NoteEntryData;
use kimun_core::nfs::VaultPath;
use kimun_core::note::{NoteContentData, NoteHeading, NoteMetadata, PropertyEntry};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct JsonHeader {
pub level: u32,
pub text: String,
}
impl From<NoteHeading> for JsonHeader {
fn from(heading: NoteHeading) -> Self {
Self {
level: heading.level.into(),
text: heading.text,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct JsonOutputMetadata {
pub workspace: String,
pub workspace_path: String,
pub total_results: usize,
pub query: Option<String>,
pub is_listing: bool,
pub generated_at: String,
}
#[derive(Debug, Default, Clone, PartialEq)]
pub struct JsonProperties(pub Vec<PropertyEntry>);
impl Serialize for JsonProperties {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(Some(self.0.len()))?;
for (key, value) in &self.0 {
map.serialize_entry(key, value)?;
}
map.end()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct JsonNoteMetadata {
pub tags: Vec<String>,
pub links: Vec<String>,
pub headers: Vec<JsonHeader>,
#[serde(skip_deserializing, default)]
pub properties: JsonProperties,
}
impl JsonNoteMetadata {
pub fn from_content(content: &str) -> Self {
let meta = NoteMetadata::of(content);
Self {
tags: meta.tags,
links: meta.links,
headers: meta.headings.into_iter().map(JsonHeader::from).collect(),
properties: JsonProperties(meta.properties),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct JsonNoteEntry {
pub path: String,
pub title: String,
pub content: String,
pub size: u64,
pub modified: u64,
pub created: u64,
pub hash: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub journal_date: Option<String>,
pub metadata: JsonNoteMetadata,
#[serde(skip_serializing_if = "Option::is_none")]
pub backlinks: Option<Vec<String>>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct JsonOutput {
pub metadata: JsonOutputMetadata,
pub notes: Vec<JsonNoteEntry>,
}
pub fn format_notes_with_content_as_json(
vault: &NoteVault,
entries: &[(NoteEntryData, NoteContentData)],
content_map: &[(VaultPath, String)],
workspace_name: &str,
workspace_path: &str,
query: Option<&str>,
is_listing: bool,
) -> Result<String, Box<dyn std::error::Error>> {
let output_metadata = JsonOutputMetadata {
workspace: workspace_name.to_string(),
workspace_path: workspace_path.to_string(),
total_results: entries.len(),
query: query.map(|q| q.to_string()),
is_listing,
generated_at: Utc::now().to_rfc3339(),
};
let content_lookup: HashMap<String, &str> = content_map
.iter()
.map(|(p, c)| (p.to_string(), c.as_str()))
.collect();
let notes = entries
.iter()
.map(|(entry_data, content_data)| {
let path_str = entry_data.path.to_string();
let path_with_ext = entry_data.path.to_string_with_ext();
let content: &str = content_lookup.get(&path_str).copied().unwrap_or("");
let journal_date = vault
.journal_date(&entry_data.path)
.map(|d| d.format("%Y-%m-%d").to_string());
let created = entry_data.modified_secs;
JsonNoteEntry {
path: path_with_ext,
title: content_data.title.clone(),
content: content.to_owned(),
size: entry_data.size,
modified: entry_data.modified_secs,
created,
hash: format!("{:x}", content_data.hash),
journal_date,
metadata: JsonNoteMetadata::from_content(content),
backlinks: None,
}
})
.collect();
let output = JsonOutput {
metadata: output_metadata,
notes,
};
Ok(serde_json::to_string(&output)?)
}
pub async fn format_notes_as_json(
vault: &NoteVault,
entries: &[(NoteEntryData, NoteContentData)],
workspace_name: &str,
query: Option<&str>,
is_listing: bool,
) -> Result<String, Box<dyn std::error::Error>> {
let workspace_path = vault.workspace_path().to_string();
const JSON_FETCH_CONCURRENCY: usize = 32;
use futures::stream::StreamExt;
let content_results: Vec<_> = futures::stream::iter(entries.iter().map(|(entry_data, _)| {
let path = entry_data.path.clone();
async move {
match vault.get_note_text(&path).await {
Ok(content) => Some((path, content)),
Err(_) => None,
}
}
}))
.buffered(JSON_FETCH_CONCURRENCY)
.collect()
.await;
let content_map: Vec<_> = content_results.into_iter().flatten().collect();
format_notes_with_content_as_json(
vault,
entries,
&content_map,
workspace_name,
&workspace_path,
query,
is_listing,
)
}