harmony/adr/008-score-display-formatting.md
Jean-Gabriel Gill-Couture 1cbf4de2a1 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.
2025-04-05 14:38:58 -04:00

6.5 KiB
Raw Blame History

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 dont 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<UIContext>):
    • 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.