use std::{
fs, io,
path::{Path, PathBuf},
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NotionManifest {
pub schema: String,
pub producer: String,
pub producer_version: String,
pub pages: Vec<NotionManifestPage>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NotionManifestPage {
pub document_id: String,
pub path: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NotionPageArtifact {
pub schema: String,
pub producer: String,
pub producer_version: String,
pub document: ArtifactDocument,
pub source: ArtifactSource,
pub target: ArtifactTarget,
pub content: ArtifactContent,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactDocument {
pub schema: String,
pub id: String,
pub title: String,
pub description: String,
pub kind: String,
pub status: String,
pub visibility: String,
#[serde(default)]
pub tags: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactSource {
pub repository: String,
pub path: String,
pub commit: String,
pub content_hash: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactTarget {
pub workspace: String,
pub data_source: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactContent {
pub format: String,
pub markdown: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoadedManifest {
pub path: PathBuf,
pub manifest: NotionManifest,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoadedPageArtifact {
pub path: PathBuf,
pub artifact: NotionPageArtifact,
}
pub fn load_manifest(input_dir: impl AsRef<Path>) -> Result<LoadedManifest, ArtifactError> {
let path = input_dir.as_ref().join("manifest.json");
let source = fs::read_to_string(&path)?;
let manifest: NotionManifest = serde_json::from_str(&source)?;
if manifest.schema != "codexa.notion.manifest@1" {
return Err(ArtifactError::InvalidSchema(manifest.schema));
}
Ok(LoadedManifest { path, manifest })
}
pub fn load_page_artifact(
input_dir: impl AsRef<Path>,
page_path: &str,
) -> Result<LoadedPageArtifact, ArtifactError> {
let path = input_dir.as_ref().join(page_path);
let source = fs::read_to_string(&path)?;
let artifact: NotionPageArtifact = serde_json::from_str(&source)?;
if artifact.schema != "codexa.notion.page@1" {
return Err(ArtifactError::InvalidSchema(artifact.schema));
}
Ok(LoadedPageArtifact { path, artifact })
}
#[derive(Debug)]
pub enum ArtifactError {
Io(io::Error),
Json(serde_json::Error),
InvalidSchema(String),
}
impl std::fmt::Display for ArtifactError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(error) => write!(formatter, "artifact I/O error: {error}"),
Self::Json(error) => write!(formatter, "artifact JSON error: {error}"),
Self::InvalidSchema(schema) => {
write!(formatter, "unsupported artifact schema `{schema}`")
}
}
}
}
impl std::error::Error for ArtifactError {}
impl From<io::Error> for ArtifactError {
fn from(error: io::Error) -> Self {
Self::Io(error)
}
}
impl From<serde_json::Error> for ArtifactError {
fn from(error: serde_json::Error) -> Self {
Self::Json(error)
}
}
#[cfg(test)]
mod tests {
use super::NotionPageArtifact;
#[test]
fn parses_page_artifact_shape() {
let json = r##"
{
"schema": "codexa.notion.page@1",
"producer": "codexa",
"producer_version": "0.0.2",
"document": {
"schema": "codexa.document@1",
"id": "notes.example",
"title": "Example",
"description": "One line.",
"kind": "note",
"status": "active",
"visibility": "private",
"tags": ["test"]
},
"source": {
"repository": "archivora/knowledge",
"path": "notes/example.md",
"commit": "abc123",
"content_hash": "sha256:abc"
},
"target": {
"workspace": "codexa",
"data_source": "documents"
},
"content": {
"format": "markdown",
"markdown": "# Example"
}
}
"##;
let artifact: NotionPageArtifact =
serde_json::from_str(json).expect("artifact should parse");
assert_eq!(artifact.document.id, "notes.example");
assert_eq!(artifact.document.description, "One line.");
assert_eq!(artifact.target.data_source, "documents");
}
}