Data Engine: Unterschied zwischen den Versionen
Die Seite wurde neu angelegt: „ == Übersicht == Dieser Artikel dokumentiert ein Rust-Datenmodell, das die zentralen Entity-Konzepte von Drupal (Node, User, Taxonomy Term, Media, Paragraph) als <code>struct</code>/<code>enum</code>-Typen abbildet. Das Modell enthält bewusst '''keine Logik''' (keine <code>impl</code>-Methoden) – es dient als reine Datenrepräsentation. Eine (De-)Serialisierung (z. B. mit [https://serde.rs/ serde] für Drupal-JSON:API-Antworten) ist bewusst noch nich…“ |
Keine Bearbeitungszusammenfassung |
||
| Zeile 1: | Zeile 1: | ||
= Rust-Datenmodell für Drupal-Entities = | |||
== Übersicht == | == Übersicht == | ||
| Zeile 4: | Zeile 5: | ||
Dieser Artikel dokumentiert ein Rust-Datenmodell, das die zentralen | Dieser Artikel dokumentiert ein Rust-Datenmodell, das die zentralen | ||
Entity-Konzepte von Drupal (Node, User, Taxonomy Term, Media, Paragraph) | Entity-Konzepte von Drupal (Node, User, Taxonomy Term, Media, Paragraph) | ||
als < | als <pre>struct</pre>/<pre>enum</pre>-Typen abbildet. Das Modell enthält bewusst '''keine | ||
Logik''' (keine < | Logik''' (keine <pre>impl</pre>-Methoden) – es dient als reine Datenrepräsentation. | ||
Eine (De-)Serialisierung (z. B. mit [https://serde.rs/ serde] für | Eine (De-)Serialisierung (z. B. mit [https://serde.rs/ serde] für | ||
Drupal-JSON:API-Antworten) ist bewusst noch nicht Teil dieses Modells | Drupal-JSON:API-Antworten) ist bewusst noch nicht Teil dieses Modells | ||
| Zeile 13: | Zeile 14: | ||
Drupal modelliert Inhalte über ein generisches Entity-System: | Drupal modelliert Inhalte über ein generisches Entity-System: | ||
* '''Entity-Typen''' (< | * '''Entity-Typen''' (<pre>node</pre>, <pre>user</pre>, <pre>taxonomy_term</pre>, <pre>media</pre>, …) definieren die grundlegende Art eines Objekts. | ||
* '''Bundles''' (bei Nodes „Content Types" genannt, z. B. < | * '''Bundles''' (bei Nodes „Content Types" genannt, z. B. <pre>article</pre>, <pre>page</pre>) spezialisieren einen Entity-Typ. | ||
* '''Felder''' (< | * '''Felder''' (<pre>field_*</pre>) sind pro Bundle konfigurierbar, typisiert und können einfach- oder mehrwertig (Kardinalität) sein. | ||
Um mit einem Drupal-Backend aus Rust heraus zu arbeiten, braucht man | Um mit einem Drupal-Backend aus Rust heraus zu arbeiten, braucht man | ||
| Zeile 23: | Zeile 24: | ||
== Grundtypen == | == Grundtypen == | ||
< | <pre> | ||
#[derive(Debug, Clone, PartialEq, Eq)] | #[derive(Debug, Clone, PartialEq, Eq)] | ||
pub enum EntityType { | pub enum EntityType { | ||
| Zeile 44: | Zeile 45: | ||
pub type LangCode = String; | pub type LangCode = String; | ||
</ | </pre> | ||
< | <pre>EntityType</pre> bildet die von Drupal vordefinierten Entity-Typen ab; der | ||
< | <pre>Custom</pre>-Zweig deckt eigene, per Modul definierte Entity-Typen ab, ohne | ||
das Enum ändern zu müssen. | das Enum ändern zu müssen. | ||
== Feldwerte: < | == Feldwerte: <pre>FieldValue</pre> == | ||
Drupal-Felder sind stark typisiert (Text, Zahl, Datum, Referenz, Bild, …). | Drupal-Felder sind stark typisiert (Text, Zahl, Datum, Referenz, Bild, …). | ||
Dies wird über ein Enum mit Payload pro Variante abgebildet: | Dies wird über ein Enum mit Payload pro Variante abgebildet: | ||
< | <pre> | ||
#[derive(Debug, Clone)] | #[derive(Debug, Clone)] | ||
pub enum FieldValue { | pub enum FieldValue { | ||
| Zeile 78: | Zeile 79: | ||
pub type FieldMap = HashMap<String, FieldValue>; | pub type FieldMap = HashMap<String, FieldValue>; | ||
</ | </pre> | ||
* < | * <pre>List(Vec<FieldValue>)</pre> bildet mehrwertige Felder ab (Kardinalität > 1). | ||
* < | * <pre>FieldMap</pre> ist die generische Ablage für alle <pre>field_*</pre>-Werte eines Bundles, adressiert über den Feldmaschinennamen (z. B. <pre>field_tags</pre>). | ||
== Entity-Typen im Detail == | == Entity-Typen im Detail == | ||
| Zeile 87: | Zeile 88: | ||
=== Gemeinsame Basis === | === Gemeinsame Basis === | ||
< | <pre> | ||
#[derive(Debug, Clone)] | #[derive(Debug, Clone)] | ||
pub struct EntityBase { | pub struct EntityBase { | ||
| Zeile 98: | Zeile 99: | ||
pub changed: DateTime<Utc>, | pub changed: DateTime<Utc>, | ||
} | } | ||
</ | </pre> | ||
< | <pre>EntityBase</pre> fasst Eigenschaften zusammen, die praktisch jede | ||
Content-Entity in Drupal besitzt, und wird per Komposition in konkrete | Content-Entity in Drupal besitzt, und wird per Komposition in konkrete | ||
Entity-Structs eingebettet (siehe < | Entity-Structs eingebettet (siehe <pre>Node</pre>, <pre>Media</pre>). Beim späteren | ||
Ergänzen von serde bietet sich dafür < | Ergänzen von serde bietet sich dafür <pre>#[serde(flatten)]</pre> an, damit die | ||
Felder von < | Felder von <pre>EntityBase</pre> beim (De-)Serialisieren auf derselben Ebene | ||
wie die restlichen Felder erscheinen. | wie die restlichen Felder erscheinen. | ||
=== Node === | === Node === | ||
< | <pre> | ||
#[derive(Debug, Clone)] | #[derive(Debug, Clone)] | ||
pub struct Node { | pub struct Node { | ||
| Zeile 118: | Zeile 119: | ||
pub fields: FieldMap, | pub fields: FieldMap, | ||
} | } | ||
</ | </pre> | ||
=== User === | === User === | ||
< | <pre> | ||
#[derive(Debug, Clone)] | #[derive(Debug, Clone)] | ||
pub struct User { | pub struct User { | ||
| Zeile 134: | Zeile 135: | ||
pub fields: FieldMap, | pub fields: FieldMap, | ||
} | } | ||
</ | </pre> | ||
< | <pre>User</pre> nutzt bewusst kein <pre>EntityBase</pre>, da User-Entities in Drupal kein | ||
< | <pre>bundle</pre>/<pre>langcode</pre> im klassischen Sinn besitzen und stattdessen | ||
< | <pre>roles</pre> und einen einfachen <pre>status: bool</pre> (aktiv/blockiert) haben. | ||
=== TaxonomyTerm === | === TaxonomyTerm === | ||
< | <pre> | ||
#[derive(Debug, Clone)] | #[derive(Debug, Clone)] | ||
pub struct TaxonomyTerm { | pub struct TaxonomyTerm { | ||
| Zeile 154: | Zeile 155: | ||
pub fields: FieldMap, | pub fields: FieldMap, | ||
} | } | ||
</ | </pre> | ||
Bildet Taxonomiebegriffe inkl. hierarchischer Eltern-Referenz | Bildet Taxonomiebegriffe inkl. hierarchischer Eltern-Referenz | ||
(< | (<pre>parent_id</pre>) und Sortiergewicht (<pre>weight</pre>) ab. | ||
=== Media & Paragraph === | === Media & Paragraph === | ||
< | <pre> | ||
#[derive(Debug, Clone)] | #[derive(Debug, Clone)] | ||
pub struct Media { | pub struct Media { | ||
| Zeile 176: | Zeile 177: | ||
pub fields: FieldMap, | pub fields: FieldMap, | ||
} | } | ||
</ | </pre> | ||
< | <pre>Paragraph</pre> ist bewusst schlank gehalten, da Paragraphs in Drupal keine | ||
eigenständigen, direkt aufrufbaren Entities mit URL/Alias sind, sondern | eigenständigen, direkt aufrufbaren Entities mit URL/Alias sind, sondern | ||
stets über eine Host-Entity referenziert werden. | stets über eine Host-Entity referenziert werden. | ||
| Zeile 188: | Zeile 189: | ||
zu spiegeln oder Formulare dynamisch zu generieren: | zu spiegeln oder Formulare dynamisch zu generieren: | ||
< | <pre> | ||
#[derive(Debug, Clone, Copy, PartialEq, Eq)] | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
pub enum Cardinality { | pub enum Cardinality { | ||
| Zeile 211: | Zeile 212: | ||
pub fields: Vec<FieldDefinition>, | pub fields: Vec<FieldDefinition>, | ||
} | } | ||
</ | </pre> | ||
== Design-Entscheidungen == | == Design-Entscheidungen == | ||
| Zeile 219: | Zeile 220: | ||
! Entscheidung !! Begründung | ! Entscheidung !! Begründung | ||
|- | |- | ||
| < | | <pre>FieldValue</pre> als Enum mit Payload pro Variante || Bildet Drupals starke Feldtypisierung typsicher ab, statt alles als <pre>String</pre> zu behandeln. | ||
|- | |- | ||
| < | | <pre>FieldMap = HashMap<String, FieldValue></pre> || Bundles sind zur Compile-Zeit nicht bekannt; generische Map bleibt flexibel für beliebige <pre>field_*</pre>-Namen. | ||
|- | |- | ||
| < | | <pre>EntityBase</pre> als Kompositionsfeld || Vermeidet Duplikation gemeinsamer Properties zwischen Node/Media, ohne Vererbung (die es in Rust nicht gibt). | ||
|- | |- | ||
| Kein < | | Kein <pre>impl</pre>-Block || Modell bleibt reine Datenschicht; Geschäftslogik (Validierung, API-Calls) gehört in separate Module. | ||
|- | |- | ||
| < | | <pre>EntityType::Custom(String)</pre> || Deckt durch Contrib-/Custom-Module definierte Entity-Typen ab, ohne das Enum erweitern zu müssen. | ||
|} | |} | ||
== Offene Erweiterungspunkte == | == Offene Erweiterungspunkte == | ||
* Weitere < | * Weitere <pre>FieldValue</pre>-Varianten je nach genutzten Feldtypen (z. B. <pre>Geolocation</pre>, <pre>Address</pre>, <pre>File</pre> losgelöst von <pre>Image</pre>). | ||
* Übersetzungen (< | * Übersetzungen (<pre>translations: HashMap<LangCode, FieldMap></pre>), falls Drupal-Mehrsprachigkeit (Content Translation) abgebildet werden soll. | ||
* Konkretes Mapping auf die JSON:API-Hüllstruktur (< | * Konkretes Mapping auf die JSON:API-Hüllstruktur (<pre>data</pre>, <pre>attributes</pre>, <pre>relationships</pre>, <pre>included</pre>), falls direkt gegen die Drupal-JSON:API deserialisiert werden soll, statt ein eigenes flaches Modell zu pflegen. | ||
== Verwendete Crates == | == Verwendete Crates == | ||
* [https://crates.io/crates/chrono chrono] – Zeitstempel (< | * [https://crates.io/crates/chrono chrono] – Zeitstempel (<pre>created</pre>, <pre>changed</pre>) | ||
* [https://crates.io/crates/uuid uuid] – Entity-UUIDs | * [https://crates.io/crates/uuid uuid] – Entity-UUIDs | ||
Version vom 12. August 2026, 01:17 Uhr
Rust-Datenmodell für Drupal-Entities
Übersicht
Dieser Artikel dokumentiert ein Rust-Datenmodell, das die zentralen Entity-Konzepte von Drupal (Node, User, Taxonomy Term, Media, Paragraph)
als
struct
/
enum
-Typen abbildet. Das Modell enthält bewusst keine Logik (keine
impl
-Methoden) – es dient als reine Datenrepräsentation.
Eine (De-)Serialisierung (z. B. mit serde für Drupal-JSON:API-Antworten) ist bewusst noch nicht Teil dieses Modells und folgt in einem späteren Schritt.
Motivation
Drupal modelliert Inhalte über ein generisches Entity-System:
- Entity-Typen (
node
,user
,taxonomy_term
,media
, …) definieren die grundlegende Art eines Objekts. - Bundles (bei Nodes „Content Types" genannt, z. B.
article
,page
) spezialisieren einen Entity-Typ. - Felder (
field_*
) sind pro Bundle konfigurierbar, typisiert und können einfach- oder mehrwertig (Kardinalität) sein.
Um mit einem Drupal-Backend aus Rust heraus zu arbeiten, braucht man typisierte Gegenstücke zu diesen Konzepten. Das hier beschriebene Modell bildet diese Struktur 1:1 in Rust ab.
Grundtypen
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EntityType {
Node,
User,
TaxonomyTerm,
Media,
Paragraph,
File,
Menu,
Block,
Custom(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PublishStatus {
Published,
Unpublished,
}
pub type LangCode = String;
EntityType
bildet die von Drupal vordefinierten Entity-Typen ab; der
Custom
-Zweig deckt eigene, per Modul definierte Entity-Typen ab, ohne
das Enum ändern zu müssen.
Feldwerte: FieldValue
Drupal-Felder sind stark typisiert (Text, Zahl, Datum, Referenz, Bild, …). Dies wird über ein Enum mit Payload pro Variante abgebildet:
#[derive(Debug, Clone)]
pub enum FieldValue {
Text(String),
TextLong { value: String, format: Option<String> },
Integer(i64),
Decimal(f64),
Boolean(bool),
DateTime(DateTime<Utc>),
Email(String),
Link { uri: String, title: Option<String> },
EntityReference { target_id: u64, target_type: EntityType },
Image {
target_id: u64,
alt: Option<String>,
title: Option<String>,
width: Option<u32>,
height: Option<u32>,
},
List(Vec<FieldValue>),
}
pub type FieldMap = HashMap<String, FieldValue>;
List(Vec<FieldValue>)
bildet mehrwertige Felder ab (Kardinalität > 1).FieldMap
ist die generische Ablage für allefield_*
-Werte eines Bundles, adressiert über den Feldmaschinennamen (z. B.field_tags
).
Entity-Typen im Detail
Gemeinsame Basis
#[derive(Debug, Clone)]
pub struct EntityBase {
pub id: u64,
pub uuid: Uuid,
pub entity_type: EntityType,
pub bundle: String,
pub langcode: LangCode,
pub created: DateTime<Utc>,
pub changed: DateTime<Utc>,
}
EntityBase
fasst Eigenschaften zusammen, die praktisch jede
Content-Entity in Drupal besitzt, und wird per Komposition in konkrete
Entity-Structs eingebettet (siehe
Node
,
Media
). Beim späteren Ergänzen von serde bietet sich dafür
#[serde(flatten)]
an, damit die Felder von
EntityBase
beim (De-)Serialisieren auf derselben Ebene
wie die restlichen Felder erscheinen.
Node
#[derive(Debug, Clone)]
pub struct Node {
pub base: EntityBase,
pub title: String,
pub status: PublishStatus,
pub author_id: u64,
pub fields: FieldMap,
}
User
#[derive(Debug, Clone)]
pub struct User {
pub id: u64,
pub uuid: Uuid,
pub name: String,
pub mail: String,
pub status: bool,
pub roles: Vec<String>,
pub created: DateTime<Utc>,
pub fields: FieldMap,
}
User
nutzt bewusst kein
EntityBase
, da User-Entities in Drupal kein
bundle
/
langcode
im klassischen Sinn besitzen und stattdessen
roles
und einen einfachen
status: bool
(aktiv/blockiert) haben.
TaxonomyTerm
#[derive(Debug, Clone)]
pub struct TaxonomyTerm {
pub id: u64,
pub uuid: Uuid,
pub vocabulary: String,
pub name: String,
pub description: Option<String>,
pub parent_id: Option<u64>,
pub weight: i32,
pub fields: FieldMap,
}
Bildet Taxonomiebegriffe inkl. hierarchischer Eltern-Referenz
(
parent_id
) und Sortiergewicht (
weight
) ab.
Media & Paragraph
#[derive(Debug, Clone)]
pub struct Media {
pub base: EntityBase,
pub name: String,
pub fields: FieldMap,
}
#[derive(Debug, Clone)]
pub struct Paragraph {
pub id: u64,
pub uuid: Uuid,
pub bundle: String,
pub fields: FieldMap,
}
Paragraph
ist bewusst schlank gehalten, da Paragraphs in Drupal keine
eigenständigen, direkt aufrufbaren Entities mit URL/Alias sind, sondern stets über eine Host-Entity referenziert werden.
Schema-Introspektion (optional)
Für Anwendungsfälle, in denen Feld- und Bundle-Definitionen selbst (nicht nur Werte) abgebildet werden müssen – etwa um ein Drupal-Schema zu spiegeln oder Formulare dynamisch zu generieren:
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Cardinality {
Single,
Multiple,
Unlimited,
}
#[derive(Debug, Clone)]
pub struct FieldDefinition {
pub field_name: String,
pub field_type: String,
pub cardinality: Cardinality,
pub required: bool,
}
#[derive(Debug, Clone)]
pub struct BundleDefinition {
pub entity_type: EntityType,
pub bundle: String,
pub label: String,
pub fields: Vec<FieldDefinition>,
}
Design-Entscheidungen
| Entscheidung | Begründung |
|---|---|
FieldValueals Enum mit Payload pro Variante |
Bildet Drupals starke Feldtypisierung typsicher ab, statt alles als Stringzu behandeln. |
FieldMap = HashMap<String, FieldValue> |
Bundles sind zur Compile-Zeit nicht bekannt; generische Map bleibt flexibel für beliebige field_*-Namen. |
EntityBaseals Kompositionsfeld |
Vermeidet Duplikation gemeinsamer Properties zwischen Node/Media, ohne Vererbung (die es in Rust nicht gibt). |
Kein impl-Block |
Modell bleibt reine Datenschicht; Geschäftslogik (Validierung, API-Calls) gehört in separate Module. |
EntityType::Custom(String) |
Deckt durch Contrib-/Custom-Module definierte Entity-Typen ab, ohne das Enum erweitern zu müssen. |
Offene Erweiterungspunkte
- Weitere
FieldValue
-Varianten je nach genutzten Feldtypen (z. B.Geolocation
,Address
,File
losgelöst vonImage
). - Übersetzungen (
translations: HashMap<LangCode, FieldMap>
), falls Drupal-Mehrsprachigkeit (Content Translation) abgebildet werden soll. - Konkretes Mapping auf die JSON:API-Hüllstruktur (
data
,attributes
,relationships
,included
), falls direkt gegen die Drupal-JSON:API deserialisiert werden soll, statt ein eigenes flaches Modell zu pflegen.
Verwendete Crates
Hinweis: serde (für die (De-)Serialisierung) ist bewusst noch nicht Teil dieses Modells und wird in einem späteren Schritt ergänzt.
Siehe auch
- Drupal-Dokumentation: Entity API
- Drupal-Dokumentation: JSON:API-Modul