|
|
| Zeile 1: |
Zeile 1: |
|
| |
|
| * [[Rust-Datenmodell für Drupal-Entities]] | | * [[Rust-Datenmodell für Drupal-Entities]] |
| = 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 <pre>struct</pre>/<pre>enum</pre>-Typen abbildet. Das Modell enthält bewusst '''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
| |
| 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''' (<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. <pre>article</pre>, <pre>page</pre>) spezialisieren einen Entity-Typ.
| |
| * '''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
| |
| typisierte Gegenstücke zu diesen Konzepten. Das hier beschriebene
| |
| Modell bildet diese Struktur 1:1 in Rust ab.
| |
|
| |
| == Grundtypen ==
| |
|
| |
| <pre>
| |
| #[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;
| |
| </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.
| |
|
| |
| == Feldwerte: <pre>FieldValue</pre> ==
| |
|
| |
| Drupal-Felder sind stark typisiert (Text, Zahl, Datum, Referenz, Bild, …).
| |
| Dies wird über ein Enum mit Payload pro Variante abgebildet:
| |
|
| |
| <pre>
| |
| #[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>;
| |
| </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 ==
| |
|
| |
| === Gemeinsame Basis ===
| |
|
| |
| <pre>
| |
| #[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>,
| |
| }
| |
| </pre>
| |
|
| |
| <pre>EntityBase</pre> fasst Eigenschaften zusammen, die praktisch jede
| |
| Content-Entity in Drupal besitzt, und wird per Komposition in konkrete
| |
| Entity-Structs eingebettet (siehe <pre>Node</pre>, <pre>Media</pre>). Beim späteren
| |
| Ergänzen von serde bietet sich dafür <pre>#[serde(flatten)]</pre> an, damit die
| |
| Felder von <pre>EntityBase</pre> beim (De-)Serialisieren auf derselben Ebene
| |
| wie die restlichen Felder erscheinen.
| |
|
| |
| === Node ===
| |
|
| |
| <pre>
| |
| #[derive(Debug, Clone)]
| |
| pub struct Node {
| |
| pub base: EntityBase,
| |
| pub title: String,
| |
| pub status: PublishStatus,
| |
| pub author_id: u64,
| |
| pub fields: FieldMap,
| |
| }
| |
| </pre>
| |
|
| |
| === User ===
| |
|
| |
| <pre>
| |
| #[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,
| |
| }
| |
| </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 ===
| |
|
| |
| <pre>
| |
| #[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,
| |
| }
| |
| </pre>
| |
|
| |
| Bildet Taxonomiebegriffe inkl. hierarchischer Eltern-Referenz
| |
| (<pre>parent_id</pre>) und Sortiergewicht (<pre>weight</pre>) ab.
| |
|
| |
| === Media & Paragraph ===
| |
|
| |
| <pre>
| |
| #[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,
| |
| }
| |
| </pre>
| |
|
| |
| <pre>Paragraph</pre> 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:
| |
|
| |
| <pre>
| |
| #[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>,
| |
| }
| |
| </pre>
| |
|
| |
| == Design-Entscheidungen ==
| |
|
| |
| {| class="wikitable"
| |
| |-
| |
| ! 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 <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 ==
| |
|
| |
| * 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 (<pre>translations: HashMap<LangCode, FieldMap></pre>), falls Drupal-Mehrsprachigkeit (Content Translation) abgebildet werden soll.
| |
| * 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 ==
| |
|
| |
| * [https://crates.io/crates/chrono chrono] – Zeitstempel (<pre>created</pre>, <pre>changed</pre>)
| |
| * [https://crates.io/crates/uuid uuid] – Entity-UUIDs
| |
|
| |
| Hinweis: [https://serde.rs/ 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: [https://www.drupal.org/docs/drupal-apis/entity-api Entity API]
| |
| * Drupal-Dokumentation: [https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module JSON:API-Modul]
| |
|
| |
| [[Kategorie:Rust]]
| |
| [[Kategorie:Drupal]]
| |