Trait-validering for samlingsnoder (oppgave 13.1)

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) <noreply@anthropic.com>
This commit is contained in:
vegard 2026-03-18 00:14:21 +00:00
parent a21da84122
commit 37cd133e1d
2 changed files with 150 additions and 2 deletions

View file

@ -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());
}
}

View file

@ -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.