# Leveraging Rust's Type System in Regent SDK
This document explores concrete, high-impact ways to extend Rust's type safety across various aspects of the Regent project beyond the current Attributes implementation.
## Table of Contents
1. [State Machine Typing for Compliance Lifecycle](#1-state-machine-typing-for-compliance-lifecycle)
2. [Platform-Specific Attribute Constraints](#2-platform-specific-attribute-constraints)
3. [Typed Secret References with PhantomData](#3-typed-secret-references-with-phantomdata)
4. [Strongly-Typed Host Properties](#4-strongly-typed-host-properties)
5. [Attribute Dependencies as Type Constraints](#5-attribute-dependencies-as-type-constraints)
6. [Typed Timeout Configuration](#6-typed-timeout-configuration)
7. [Typed Connection Method States](#7-typed-connection-method-states)
8. [Typed Secret Provider Capabilities](#8-typed-secret-provider-capabilities)
9. [Attribute Result Typing with GATs](#9-attribute-result-typing-with-gats)
10. [Typed Inventory Groups](#10-typed-inventory-groups)
11. [Typed Validation Rules](#11-typed-validation-rules)
12. [Typed Host Handler Capabilities](#12-typed-host-handler-capabilities)
13. [Prioritization Recommendations](#prioritization-recommendations)
14. [Migration Strategy](#migration-strategy)
---
## 1. State Machine Typing for Compliance Lifecycle
**Current**: Compliance states are likely represented as strings or simple enums without compile-time guarantees about valid transitions.
**Opportunity**: Encode the compliance lifecycle as a **type-state pattern**:
```rust
// Instead of:
pub enum ComplianceStatus { Assessing, Compliant, NonCompliant, Remediating, Failed }
// Use type-state:
pub struct NotAssessed;
pub struct Assessing;
pub struct Compliant;
pub struct NonCompliant<Remediations: IntoIterator<Item = Remediation>>;
pub struct Remediating;
pub struct Failed<Error: std::error::Error>;
// Then make operations only available in valid states:
impl<H: HostHandler> ManagedHost<Assessing> {
pub async fn wait_for_assessment(self) -> ManagedHost<Compliant or NonCompliant> { ... }
}
impl<H: HostHandler> ManagedHost<NonCompliant> {
pub async fn reach_compliance(self) -> ManagedHost<Remediating> { ... }
}
```
**Benefit**: Prevents invalid operations at compile time (e.g., can't `reach_compliance` without first assessing).
---
## 2. Platform-Specific Attribute Constraints
**Current**: Attributes like `Apt` or `YumDnf` can be defined for any host, but they'll fail at runtime on incompatible platforms.
**Opportunity**: Use **trait bounds with associated types** for OS compatibility:
```rust
pub trait Platform {
type PackageManager: PackageAttribute;
type InitSystem: ServiceAttribute;
// ...
}
pub struct Debian;
impl Platform for Debian {
type PackageManager = AptBlockExpectedState;
type InitSystem = SystemdService;
}
pub struct RedHat;
impl Platform for RedHat {
type PackageManager = YumDnfBlockExpectedState;
type InitSystem = SystemdService;
}
// Then make ExpectedState generic:
pub struct ExpectedState<P: Platform> {
attributes: Vec<Attribute<P>>,
}
// Usage:
let expected: ExpectedState<Debian> = ExpectedState::new()
.with_attribute(Attribute::package(AptBlockExpectedState::...));
```
**Benefit**: Compile-time error if someone tries to use `Apt` on a `RedHat` platform.
---
## 3. Typed Secret References with PhantomData
**Current**: Secret references are strings like `"arn:aws:secretsmanager:..."` with runtime validation.
**Opportunity**: Create **type-safe secret references**:
```rust
pub struct SecretRef<P: SecretProviderType> {
reference: String,
_phantom: PhantomData<P>,
}
pub trait SecretProviderType {}
pub struct AwsSecretsManager;
impl SecretProviderType for AwsSecretsManager {}
pub struct GcpSecretManager;
impl SecretProviderType for GcpSecretManager {}
// Usage:
let aws_ref: SecretRef<AwsSecretsManager> = SecretRef::new("arn:aws:...")?;
let gcp_ref: SecretRef<GcpSecretManager> = SecretRef::new("projects/.../secrets/...")?;
// In LineInFile:
pub struct LineInFileBlockExpectedState {
file_path: PathBuf,
line: SecretRef<impl SecretProviderType>, // or generic over P
state: LineState,
}
```
**Benefit**: Secret provider compatibility is enforced at compile time.
---
## 4. Strongly-Typed Host Properties
**Current**: `HostProperties` appears to be a collection of strings/values.
**Opportunity**: Use **newtypes and enums** for validated properties:
```rust
#[derive(Debug, Clone, PartialEq)]
pub enum OsFamily { Debian, RedHat, Arch, Windows, Unknown }
#[derive(Debug, Clone, PartialEq)]
pub enum InitSystem { Systemd, SysV, OpenRC, Unknown }
#[derive(Debug, Clone)]
pub struct HostProperties {
pub os_family: OsFamily,
pub init_system: InitSystem,
pub architecture: Architecture,
pub kernel_version: KernelVersion, // newtype with validation
}
impl TryFrom<&str> for OsFamily {
fn try_from(value: &str) -> Result<Self, String> { ... }
}
```
**Benefit**: No more stringly-typed properties; invalid values are caught at parse time.
---
## 5. Attribute Dependencies as Type Constraints
**Current**: Attribute ordering/dependencies are handled at runtime.
**Opportunity**: Use **trait bounds** to express dependencies:
```rust
pub trait Requires<Dep: Attribute> {}
pub trait Provides<Capability> {}
// Example: User requires Group to exist
impl Requires<GroupAttribute> for UserAttribute {}
// In ExpectedState builder:
pub fn with_attribute<A: Attribute>(self, attr: A) -> Self {
// Check at compile time if all dependencies are satisfied
// Or use a type-level set of provided capabilities
...
}
```
**Alternative**: Use **const generics** or **GATs** for more complex dependency graphs.
---
## 6. Typed Timeout Configuration
**Current**: Timeout is configured via optional fields that can conflict.
**Opportunity**: Use **strongly-typed timeout builders**:
```rust
#[derive(Debug, Clone)]
pub enum TimeoutConfig {
Default,
Custom(Duration),
FromAttribute, // Use attribute's default
}
pub struct TimeoutBuilder {
source: TimeoutSource,
}
pub enum TimeoutSource {
NotSet,
Explicit(Duration),
FromAttribute,
}
impl TimeoutBuilder {
pub fn explicit(mut self, duration: Duration) -> Self {
self.source = TimeoutSource::Explicit(duration);
self
}
}
```
**Benefit**: Eliminates the current `timeout_sec`/`timeout_ms` mutual exclusivity runtime check.
---
## 7. Typed Connection Method States
**Current**: `ConnectionMethod` is likely an enum, but connection lifecycle isn't type-enforced.
**Opportunity**: Use **state pattern with types**:
```rust
pub struct Disconnected;
pub struct Connecting;
pub struct Connected<H: HostHandler>;
pub struct FailedToConnect<E: std::error::Error>;
pub struct ManagedHost<State> {
id: String,
endpoint: String,
state: State,
// ...
}
impl ManagedHost<Disconnected> {
pub async fn connect(self) -> Result<ManagedHost<Connected<H>>, ManagedHost<FailedToConnect<E>>> { ... }
}
impl<H: HostHandler> ManagedHost<Connected<H>> {
pub async fn assess_compliance(&self, state: &ExpectedState) -> ... { ... }
// Can't call connect() again - already connected!
}
```
**Benefit**: Prevents invalid operations like calling `connect()` twice or `assess_compliance()` before connecting.
---
## 8. Typed Secret Provider Capabilities
**Current**: `SecretProvider` is an enum, but capabilities (e.g., supports versioning) are runtime-checked.
**Opportunity**: Use **trait-based capability markers**:
```rust
pub trait SecretProvider: Send + Sync {
fn get_secret(&self, reference: &str) -> Future<Result<String, SecretError>>;
}
pub trait SupportsSecretVersioning: SecretProvider {
fn get_secret_version(&self, reference: &str, version: &str) -> Future<Result<String, SecretError>>;
}
pub trait SupportsSecretRotation: SecretProvider {
fn rotate_secret(&self, reference: &str) -> Future<Result<(), SecretError>>;
}
// AWS supports all:
impl SupportsSecretVersioning for AwsSecretsManagerProvider {}
impl SupportsSecretRotation for AwsSecretsManagerProvider {}
// Environment variables don't:
impl SecretProvider for EnvVarSecretProvider {}
// In code that needs versioning:
fn get_versioned_secret<P: SupportsSecretVersioning>(provider: &P, ref: &str, version: &str) { ... }
```
**Benefit**: Code requiring specific capabilities can only accept providers that support them.
---
## 9. Attribute Result Typing with GATs
**Current**: Compliance results are likely homogeneous.
**Opportunity**: Use **GATs (Generic Associated Types)** for attribute-specific results:
```rust
pub trait Attribute: Send + Sync {
type Assessment: ComplianceAssessment;
type Remediation: RemediationPlan;
async fn assess(&self, host: &mut dyn HostHandler) -> Result<Self::Assessment, RegentError>;
async fn remediate(&self, host: &mut dyn HostHandler) -> Result<Self::Remediation, RegentError>;
}
// Each attribute defines its own result types:
impl Attribute for AptAttribute {
type Assessment = AptComplianceAssessment;
type Remediation = AptRemediationPlan;
}
```
**Benefit**: Type-safe access to attribute-specific result data without downcasting.
---
## 10. Typed Inventory Groups
**Current**: Inventory groups are likely strings or simple structs.
**Opportunity**: Use **type-level group identifiers**:
```rust
pub struct GroupId<Name: 'static> {
_phantom: PhantomData<&'static Name>,
}
pub type WebServers = GroupId<WebServersMarker>;
pub type DatabaseServers = GroupId<DatabaseServersMarker>;
struct WebServersMarker;
struct DatabaseServersMarker;
// Usage:
let web_servers: Inventory<WebServers> = Inventory::from_yaml(yaml)?;
let db_servers: Inventory<DatabaseServers> = Inventory::from_yaml(yaml)?;
// Can't accidentally pass web_servers where db_servers expected
```
---
## 11. Typed Validation Rules
**Current**: Attribute validation happens at runtime.
**Opportunity**: Use **const generics** or **type-level validation**:
```rust
pub struct Validated<T, const MIN: usize, const MAX: usize>(T);
pub type PackageName = Validated<String, 1, 256>;
pub type ServiceName = Validated<String, 1, 64>;
// Or use a validation trait:
pub trait Validate {
fn validate(&self) -> Result<(), ValidationError>;
}
impl<T: Validate> AttributeDetail<T> {
pub fn new(detail: T) -> Result<Self, ValidationError> {
detail.validate()?;
Ok(Self(detail))
}
}
```
**Benefit**: Catch invalid configurations at construction time, not execution time.
---
## 12. Typed Host Handler Capabilities
**Current**: `HostHandler` is a trait with many methods, some of which may not be supported by all implementations.
**Opportunity**: Split into **capability-based traits**:
```rust
pub trait HostHandler: Send + Sync {}
pub trait PackageManagement: HostHandler {
async fn install_package(&mut self, name: &str) -> Result<(), RegentError>;
async fn remove_package(&mut self, name: &str) -> Result<(), RegentError>;
}
pub trait ServiceManagement: HostHandler {
async fn start_service(&mut self, name: &str) -> Result<(), RegentError>;
// ...
}
// SSH handler supports both:
impl PackageManagement for Ssh2HostHandler {}
impl ServiceManagement for Ssh2HostHandler {}
// Local handler might only support package management:
impl PackageManagement for LocalHostHandler {}
// In attribute code:
async fn install_package<A: Attribute, H: PackageManagement>(
attribute: &A,
handler: &mut H,
) -> Result<(), RegentError> {
// Only accepts handlers that support package management
}
```
**Benefit**: Clear compile-time errors when trying to use unsupported operations.
---
## Prioritization Recommendations
| Platform-specific attributes | High | Medium | ★★★★★ |
| State machine for compliance | High | Medium | ★★★★★ |
| Typed host properties | High | Low | ★★★★★ |
| Typed connection states | Medium | Medium | ★★★★☆ |
| Secret provider capabilities | Medium | Medium | ★★★★☆ |
| Typed secret references | Medium | High | ★★★☆☆ |
| Attribute dependencies | High | High | ★★★☆☆ |
| GATs for attribute results | Medium | High | ★★☆☆☆ |
**Start with**: Platform-specific attributes, typed host properties, and compliance state machines. These provide immediate value with manageable complexity.
**Defer**: GATs-based approaches and complex type-level dependencies until you hit specific pain points that justify the complexity.
---
## Migration Strategy
1. **Add, don't replace**: Introduce new typed APIs alongside existing ones
2. **Feature flags**: Use cargo features to enable advanced type safety
3. **Macros**: Consider procedural macros to reduce boilerplate for common patterns
4. **Documentation**: Clearly document the type safety guarantees
---
## Current Type System Strengths in Regent
The Regent SDK already demonstrates excellent use of Rust's type system in several areas:
- **`AttributeDetail` enum**: Provides type safety for different attribute types with exhaustive pattern matching
- **Builder pattern**: Used extensively for complex configurations (e.g., `AptBlockExpectedState::builder()`)
- **Error handling**: Strong use of `thiserror` for comprehensive error variants
- **Serialization traits**: Proper use of `serde` for YAML/JSON serialization with `deny_unknown_fields`
- **SecretProvider enum**: Type-safe representation of different secret backend types
- **Trait-based polymorphism**: Use of traits like `HostHandler`, `AssessCompliance`, `ReachCompliance` for shared behavior
These patterns form a solid foundation that the recommendations above seek to build upon.