From 1cbf4de2a1487dc2615be6d15ca85f2c6f3458bc Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Sat, 5 Apr 2025 14:38:52 -0400 Subject: [PATCH] adr: proposal serde for score data representation and UI rendering Decouples score definitions from UI implementations by mandating `serde::Serialize` and `serde::Deserialize` for all `Score` structs. UIs will interact with scores via their serialized representation, enabling scalability and reducing complexity for score authors. This approach: - Scales better with new score types and UI targets. - Simplifies score authoring by removing the need for UI-specific display traits. - Leverages the `serde` ecosystem for robust data handling. Adding new field types requires updates to all UIs, a trade-off acknowledged in the ADR. --- adr/008-score-display-formatting.md | 62 +++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 adr/008-score-display-formatting.md diff --git a/adr/008-score-display-formatting.md b/adr/008-score-display-formatting.md new file mode 100644 index 0000000..7bc0620 --- /dev/null +++ b/adr/008-score-display-formatting.md @@ -0,0 +1,62 @@ +## Architecture Decision Record: Data Representation and UI Rendering for Score Types + +**Status:** Proposed + +**TL;DR:** `Score` types will be serialized (using `serde`) for presentation in UIs. This decouples data definition from presentation, improving scalability and reducing complexity for developers defining `Score` types. New UI types only need to handle existing field types, and new `Score` types don’t require UI changes as long as they use existing field types. Adding a new field type *does* require updates to all UIs. + +**Key benefits:** Scalability, reduced complexity for `Score` authors, decoupling of data and presentation. + +**Key trade-off:** Adding new field types requires updating all UIs. + +--- + +**Context:** + +Harmony is a pure Rust infrastructure orchestrator focused on compile-time safety and providing a developer-friendly, Ansible-module-like experience for defining infrastructure configurations via "Scores". These Scores (e.g., `LAMPScore`) are Rust structs composed of specific, strongly-typed fields (e.g., `VersionField`, `UrlField`, `PathField`) which are validated at compile-time using macros (`Version!`, `Url!`, etc.). + +A key requirement is displaying the configuration defined in these Scores across various user interfaces (Web UI, TUI, potentially Mobile UI, etc.) in a consistent and type-safe manner. As the number of Score types is expected to grow significantly (hundreds or thousands), we need a scalable approach for rendering their data that avoids tightly coupling Score definitions to specific UI implementations. + +The primary challenge is preventing the need for every `Score` struct author to implement multiple display traits (e.g., `Display`, `WebDisplay`, `TuiDisplay`) for every potential UI target. This would create an N x M complexity problem (N Scores * M UI types) and place an unreasonable burden on Score developers, hindering scalability and maintainability. + +**Decision:** + +1. **Mandatory Serialization:** All `Score` structs *must* implement `serde::Serialize` and `serde::Deserialize`. They *will not* be required to implement `std::fmt::Display` or any custom UI-specific display traits (e.g., `WebDisplay`, `TuiDisplay`). +2. **Field-Level Rendering:** Responsibility for rendering data will reside within the UI components. Each UI (Web, TUI, etc.) will implement logic to display *individual field types* (e.g., `UrlField`, `VersionField`, `IpAddressField`, `SecretField`). +3. **Data Access via Serialization:** UIs will primarily interact with `Score` data through its serialized representation (e.g., JSON obtained via `serde_json`). This provides a standardized interface for UIs to consume the data structure agnostic of the specific `Score` type. Alternatively, UIs *could* potentially use reflection or specific visitor patterns on the `Score` struct itself, but serialization is the preferred decoupling mechanism. + +**Rationale:** + +1. **Decoupling Data from Presentation:** This decision cleanly separates the data definition (`Score` structs and their fields) from the presentation logic (UI rendering). `Score` authors can focus solely on defining the data and its structure, while UI developers focus on how to best present known data *types*. +2. **Scalability:** This approach scales significantly better than requiring display trait implementations on Scores: + * Adding a *new Score type* requires *no changes* to existing UI code, provided it uses existing field types. + * Adding a *new UI type* requires implementing rendering logic only for the defined set of *field types*, not for every individual `Score` type. This reduces the N x M complexity to N + M complexity (approximately). +3. **Simplicity for Score Authors:** Requiring only `serde::Serialize + Deserialize` (which can often be derived automatically with `#[derive(Serialize, Deserialize)]`) is a much lower burden than implementing custom rendering logic for multiple, potentially unknown, UI targets. +4. **Leverages Rust Ecosystem Standards:** `serde` is the de facto standard for serialization and deserialization in Rust. Relying on it aligns with common Rust practices and benefits from its robustness, performance, and extensive tooling. +5. **Consistency for UIs:** Serialization provides a consistent, structured format (like JSON) for UIs to consume data, regardless of the underlying `Score` struct's complexity or composition. +6. **Flexibility for UI Implementation:** UIs can choose the best way to render each field type based on their capabilities (e.g., a `UrlField` might be a clickable link in a Web UI, plain text in a TUI; a `SecretField` might be masked). + +**Consequences:** + +**Positive:** + +* Greatly improved scalability for adding new Score types and UI targets. +* Strong separation of concerns between data definition and presentation. +* Reduced implementation burden and complexity for Score authors. +* Consistent mechanism for UIs to access and interpret Score data. +* Aligns well with the Hexagonal Architecture (ADR-002) by treating UIs as adapters interacting with the application core via a defined port (the serialized data contract). + +**Negative:** + +* Adding a *new field type* (e.g., `EmailField`) requires updates to *all* existing UI implementations to support rendering it. +* UI components become dependent on the set of defined field types and need comprehensive logic to handle each one appropriately. +* Potential minor overhead of serialization/deserialization compared to direct function calls (though likely negligible for UI purposes). +* Requires careful design and management of the standard library of field types. + +**Alternatives Considered:** + +1. **`Score` Implements `std::fmt::Display`:** + * _Rejected:_ Too simplistic. Only suitable for basic text rendering, doesn't cater to structured UIs (Web, etc.), and doesn't allow type-specific rendering logic (e.g., masking secrets). Doesn't scale to multiple UI formats. +2. **`Score` Implements Multiple Custom Display Traits (`WebDisplay`, `TuiDisplay`, etc.):** + * _Rejected:_ Leads directly to the N x M complexity problem. Tightly couples Score definitions to specific UI implementations. Places an excessive burden on Score authors, hindering adoption and scalability. +3. **Generic Display Trait with Context (`Score` implements `DisplayWithContext`):** + * _Rejected:_ More flexible than multiple traits, but still requires Score authors to implement potentially complex rendering logic within the `Score` definition itself. The `Score` would still need awareness of different UI contexts, leading to undesirable coupling. Managing context types adds complexity.