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:
parent
a21da84122
commit
37cd133e1d
2 changed files with 150 additions and 2 deletions
|
|
@ -28,6 +28,71 @@ const MAX_UPLOAD_SIZE: usize = 100 * 1024 * 1024;
|
||||||
/// Gyldige visibility-verdier (speiler PG enum).
|
/// Gyldige visibility-verdier (speiler PG enum).
|
||||||
const VALID_VISIBILITIES: &[&str] = &["hidden", "discoverable", "readable", "open"];
|
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)]
|
#[derive(Serialize)]
|
||||||
pub struct ErrorResponse {
|
pub struct ErrorResponse {
|
||||||
pub error: String,
|
pub error: String,
|
||||||
|
|
@ -284,6 +349,10 @@ pub async fn create_node(
|
||||||
let metadata = req
|
let metadata = req
|
||||||
.metadata
|
.metadata
|
||||||
.unwrap_or_else(|| serde_json::json!({}));
|
.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();
|
let metadata_str = metadata.to_string();
|
||||||
|
|
||||||
// -- Kontekstbasert identitet (oppgave 8.2) --
|
// -- 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 title = req.title.unwrap_or(existing.title.unwrap_or_default());
|
||||||
let content = req.content.unwrap_or(existing.content.unwrap_or_default());
|
let content = req.content.unwrap_or(existing.content.unwrap_or_default());
|
||||||
let metadata = req.metadata.unwrap_or(existing.metadata);
|
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 metadata_str = metadata.to_string();
|
||||||
|
|
||||||
let node_id_str = req.node_id.to_string();
|
let node_id_str = req.node_id.to_string();
|
||||||
|
|
@ -2709,3 +2782,79 @@ pub async fn close_communication(
|
||||||
status: "closed".to_string(),
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
3
tasks.md
3
tasks.md
|
|
@ -130,8 +130,7 @@ Uavhengige faser kan fortsatt plukkes.
|
||||||
|
|
||||||
## Fase 13: Trait-system
|
## 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`.
|
- [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`.
|
||||||
> Påbegynt: 2026-03-18T00:10
|
|
||||||
- [ ] 13.2 Trait-aware frontend: samlingssider leser `traits` fra metadata og rendrer kun aktive komponenter. Dynamisk komponent-lasting basert på trait-liste.
|
- [ ] 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.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.
|
- [ ] 13.4 Trait-administrasjon: admin-UI for å legge til/fjerne traits på eksisterende samlinger med konfigurasjon per trait.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue