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.
6.5 KiB
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:
- Mandatory Serialization: All
Score
structs must implementserde::Serialize
andserde::Deserialize
. They will not be required to implementstd::fmt::Display
or any custom UI-specific display traits (e.g.,WebDisplay
,TuiDisplay
). - 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
). - Data Access via Serialization: UIs will primarily interact with
Score
data through its serialized representation (e.g., JSON obtained viaserde_json
). This provides a standardized interface for UIs to consume the data structure agnostic of the specificScore
type. Alternatively, UIs could potentially use reflection or specific visitor patterns on theScore
struct itself, but serialization is the preferred decoupling mechanism.
Rationale:
- 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. - 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).
- 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. - 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. - 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. - 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; aSecretField
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:
Score
Implementsstd::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.
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.
- Generic Display Trait with Context (
Score
implementsDisplayWithContext<UIContext>
):- Rejected: More flexible than multiple traits, but still requires Score authors to implement potentially complex rendering logic within the
Score
definition itself. TheScore
would still need awareness of different UI contexts, leading to undesirable coupling. Managing context types adds complexity.
- Rejected: More flexible than multiple traits, but still requires Score authors to implement potentially complex rendering logic within the