From 37cd133e1daf92ca1b53f8acf2c9c81ee2ed388b Mon Sep 17 00:00:00 2001 From: vegard Date: Wed, 18 Mar 2026 00:14:21 +0000 Subject: [PATCH] Trait-validering for samlingsnoder (oppgave 13.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maskinrommet validerer nå metadata.traits ved create_node og update_node for collection-noder. Ukjente trait-navn avvises med 400 Bad Request. Lukket katalog med 48 gyldige traits fra docs/primitiver/traits.md. Konfigurasjon per trait er fri JSONB — kun nøkkelnavnene valideres. Noder som ikke er collections valideres ikke (traits ignoreres). Inkluderer 6 unit-tester for valideringsfunksjonen. Co-Authored-By: Claude Opus 4.6 (1M context) --- maskinrommet/src/intentions.rs | 149 +++++++++++++++++++++++++++++++++ tasks.md | 3 +- 2 files changed, 150 insertions(+), 2 deletions(-) diff --git a/maskinrommet/src/intentions.rs b/maskinrommet/src/intentions.rs index 7c549e0..75c206b 100644 --- a/maskinrommet/src/intentions.rs +++ b/maskinrommet/src/intentions.rs @@ -28,6 +28,71 @@ const MAX_UPLOAD_SIZE: usize = 100 * 1024 * 1024; /// Gyldige visibility-verdier (speiler PG enum). const VALID_VISIBILITIES: &[&str] = &["hidden", "discoverable", "readable", "open"]; +/// Gyldige trait-navn for samlingsnoder. +/// Lukket katalog — ref: docs/primitiver/traits.md § "Trait-katalog" +const VALID_TRAITS: &[&str] = &[ + // Innhold & redigering + "editor", "versioning", "collaboration", "translation", "templates", + // Publisering & distribusjon + "publishing", "rss", "newsletter", "custom_domain", "analytics", "embed", "api", + // Lyd & video + "podcast", "recording", "transcription", "tts", "clips", "playlist", + // Kommunikasjon + "chat", "forum", "comments", "guest_input", "announcements", "polls", "qa", + // Organisering + "kanban", "calendar", "timeline", "table", "gallery", "bookmarks", "tags", + // Kunnskap + "knowledge_graph", "wiki", "glossary", "faq", "bibliography", + // Automatisering & AI + "auto_tag", "auto_summarize", "digest", "bridge", "moderation", + // Tilgang & fellesskap + "membership", "roles", "invites", "paywall", "directory", + // Ekstern integrasjon + "webhook", "import", "export", "ical_sync", +]; + +/// Validerer `metadata.traits`-objektet for samlingsnoder. +/// +/// Regler: +/// - Kun samlingsnoder (`node_kind == "collection"`) valideres. +/// - `traits` må være et objekt (ikke array, string, etc.). +/// - Hvert nøkkelnavn må finnes i VALID_TRAITS. +/// - Verdien per trait er fri JSONB (åpen konfigurasjon). +/// +/// Ref: docs/primitiver/traits.md § "Lukket katalog, åpen konfigurasjon" +fn validate_collection_traits( + node_kind: &str, + metadata: &serde_json::Value, +) -> Result<(), String> { + if node_kind != "collection" { + return Ok(()); + } + + let traits = match metadata.get("traits") { + None => return Ok(()), // Ingen traits er OK — samling uten funksjonalitet + Some(t) => t, + }; + + let traits_obj = traits.as_object().ok_or( + "metadata.traits må være et objekt".to_string(), + )?; + + let unknown: Vec<&String> = traits_obj + .keys() + .filter(|k| !VALID_TRAITS.contains(&k.as_str())) + .collect(); + + if !unknown.is_empty() { + let unknown_str: Vec<&str> = unknown.iter().map(|s| s.as_str()).collect(); + return Err(format!( + "Ukjente traits: {:?}. Gyldige traits: se docs/primitiver/traits.md", + unknown_str, + )); + } + + Ok(()) +} + #[derive(Serialize)] pub struct ErrorResponse { pub error: String, @@ -284,6 +349,10 @@ pub async fn create_node( let metadata = req .metadata .unwrap_or_else(|| serde_json::json!({})); + + // -- Valider traits for samlingsnoder (oppgave 13.1) -- + validate_collection_traits(&node_kind, &metadata).map_err(|e| bad_request(&e))?; + let metadata_str = metadata.to_string(); // -- Kontekstbasert identitet (oppgave 8.2) -- @@ -653,6 +722,10 @@ pub async fn update_node( let title = req.title.unwrap_or(existing.title.unwrap_or_default()); let content = req.content.unwrap_or(existing.content.unwrap_or_default()); let metadata = req.metadata.unwrap_or(existing.metadata); + + // -- Valider traits for samlingsnoder (oppgave 13.1) -- + validate_collection_traits(&node_kind, &metadata).map_err(|e| bad_request(&e))?; + let metadata_str = metadata.to_string(); let node_id_str = req.node_id.to_string(); @@ -2709,3 +2782,79 @@ pub async fn close_communication( status: "closed".to_string(), })) } + +// ============================================================================= +// Tester +// ============================================================================= + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn test_validate_traits_ok_empty() { + let meta = json!({}); + assert!(validate_collection_traits("collection", &meta).is_ok()); + } + + #[test] + fn test_validate_traits_ok_known() { + let meta = json!({ + "traits": { + "publishing": { "slug": "test" }, + "rss": { "format": "atom" }, + "editor": { "preset": "longform" } + } + }); + assert!(validate_collection_traits("collection", &meta).is_ok()); + } + + #[test] + fn test_validate_traits_rejects_unknown() { + let meta = json!({ + "traits": { + "publishing": {}, + "banana": {} + } + }); + let err = validate_collection_traits("collection", &meta).unwrap_err(); + assert!(err.contains("banana"), "Feilmelding skal nevne ukjent trait: {err}"); + } + + #[test] + fn test_validate_traits_rejects_non_object() { + let meta = json!({ "traits": ["publishing"] }); + let err = validate_collection_traits("collection", &meta).unwrap_err(); + assert!(err.contains("objekt"), "Feilmelding: {err}"); + } + + #[test] + fn test_validate_traits_skips_non_collection() { + let meta = json!({ "traits": { "totally_invalid": {} } }); + assert!(validate_collection_traits("content", &meta).is_ok()); + assert!(validate_collection_traits("person", &meta).is_ok()); + } + + #[test] + fn test_validate_traits_all_known() { + // Verifiser at alle traits fra katalogen er gyldige + let all_traits = vec![ + "editor", "versioning", "collaboration", "translation", "templates", + "publishing", "rss", "newsletter", "custom_domain", "analytics", "embed", "api", + "podcast", "recording", "transcription", "tts", "clips", "playlist", + "chat", "forum", "comments", "guest_input", "announcements", "polls", "qa", + "kanban", "calendar", "timeline", "table", "gallery", "bookmarks", "tags", + "knowledge_graph", "wiki", "glossary", "faq", "bibliography", + "auto_tag", "auto_summarize", "digest", "bridge", "moderation", + "membership", "roles", "invites", "paywall", "directory", + "webhook", "import", "export", "ical_sync", + ]; + let mut traits_obj = serde_json::Map::new(); + for t in &all_traits { + traits_obj.insert(t.to_string(), json!({})); + } + let meta = json!({ "traits": traits_obj }); + assert!(validate_collection_traits("collection", &meta).is_ok()); + } +} diff --git a/tasks.md b/tasks.md index 3aa7b26..9e1c964 100644 --- a/tasks.md +++ b/tasks.md @@ -130,8 +130,7 @@ Uavhengige faser kan fortsatt plukkes. ## Fase 13: Trait-system -- [~] 13.1 Trait-metadata på samlingsnoder: maskinrommet validerer `metadata.traits`-objektet ved `create_node` og `update_node` for samlingsnoder. Avvis ukjente trait-navn. Ref: `docs/primitiver/traits.md`. - > Påbegynt: 2026-03-18T00:10 +- [x] 13.1 Trait-metadata på samlingsnoder: maskinrommet validerer `metadata.traits`-objektet ved `create_node` og `update_node` for samlingsnoder. Avvis ukjente trait-navn. Ref: `docs/primitiver/traits.md`. - [ ] 13.2 Trait-aware frontend: samlingssider leser `traits` fra metadata og rendrer kun aktive komponenter. Dynamisk komponent-lasting basert på trait-liste. - [ ] 13.3 Pakkevelger: UI for å opprette ny samling med forhåndsdefinert pakke (nettmagasin, podcaststudio, redaksjon osv.) eller manuelt valg av traits. - [ ] 13.4 Trait-administrasjon: admin-UI for å legge til/fjerne traits på eksisterende samlinger med konfigurasjon per trait.