| Crates.io | hemmer-provider-sdk |
| lib.rs | hemmer-provider-sdk |
| version | 0.3.1 |
| created_at | 2025-11-29 17:17:01.329879+00 |
| updated_at | 2026-01-18 04:53:58.42819+00 |
| description | Rust SDK for building Hemmer providers |
| homepage | |
| repository | https://github.com/hemmer-io/hemmer-provider-sdk |
| max_upload_size | |
| id | 1957082 |
| size | 345,827 |
Rust SDK for building Hemmer providers
The Hemmer Provider SDK provides gRPC protocol types and server helpers for building providers that integrate with Hemmer, the next-generation Infrastructure as Code tool.
🚧 Currently in active development
Add to your Cargo.toml:
[dependencies]
hemmer-provider-sdk = "0.1"
tokio = { version = "1", features = ["full"] }
serde_json = "1"
use hemmer_provider_sdk::{
serve, ProviderService, ProviderError, PlanResult,
schema::{ProviderSchema, Schema, Attribute, Diagnostic},
};
struct MyProvider;
#[hemmer_provider_sdk::async_trait]
impl ProviderService for MyProvider {
fn schema(&self) -> ProviderSchema {
ProviderSchema::new()
.with_provider_config(
Schema::v0()
.with_attribute("api_key", Attribute::required_string().sensitive())
)
.with_resource("mycloud_instance", Schema::v0()
.with_attribute("name", Attribute::required_string())
.with_attribute("size", Attribute::optional_string())
.with_attribute("id", Attribute::computed_string())
)
}
async fn configure(
&self,
config: serde_json::Value,
) -> Result<Vec<Diagnostic>, ProviderError> {
// Initialize provider with credentials
Ok(vec![])
}
async fn plan(
&self,
resource_type: &str,
prior_state: Option<serde_json::Value>,
proposed_state: serde_json::Value,
config: serde_json::Value,
) -> Result<PlanResult, ProviderError> {
Ok(PlanResult::no_change(proposed_state))
}
async fn create(
&self,
resource_type: &str,
planned_state: serde_json::Value,
) -> Result<serde_json::Value, ProviderError> {
// Create the resource and return its state
Ok(planned_state)
}
async fn read(
&self,
resource_type: &str,
current_state: serde_json::Value,
) -> Result<serde_json::Value, ProviderError> {
// Read current state from the API
Ok(current_state)
}
async fn update(
&self,
resource_type: &str,
prior_state: serde_json::Value,
planned_state: serde_json::Value,
) -> Result<serde_json::Value, ProviderError> {
// Update the resource
Ok(planned_state)
}
async fn delete(
&self,
resource_type: &str,
current_state: serde_json::Value,
) -> Result<(), ProviderError> {
// Delete the resource
Ok(())
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
serve(MyProvider).await
}
The SDK provides automatic diff computation to simplify plan implementation. Instead of manually constructing AttributeChange instances, use PlanResult::from_diff():
async fn plan(
&self,
resource_type: &str,
prior_state: Option<serde_json::Value>,
proposed_state: serde_json::Value,
config: serde_json::Value,
) -> Result<PlanResult, ProviderError> {
// Automatically compute all changes between prior and proposed states
Ok(PlanResult::from_diff(prior_state.as_ref(), &proposed_state))
}
The from_diff() method:
"metadata.labels.app")"tags[0]")no_change when states are identicalFor advanced use cases where you need custom logic or want to mark specific changes as requiring replacement, you can still construct plans manually:
use hemmer_provider_sdk::{PlanResult, AttributeChange};
// Manual construction with requires_replace flag
let mut result = PlanResult::from_diff(prior_state.as_ref(), &proposed_state);
// Check if immutable field changed and mark as requiring replacement
if let Some(prior) = prior_state.as_ref() {
if prior.get("ami") != proposed_state.get("ami") {
result.requires_replace = true;
}
}
Ok(result)
The SDK implements a complete provider protocol with the following RPCs:
| RPC | Purpose |
|---|---|
GetMetadata |
Returns provider capabilities and resource/data source names |
GetSchema |
Returns full schema for provider config, resources, and data sources |
ValidateProviderConfig |
Validates provider configuration before use |
Configure |
Configures provider with credentials and settings |
Stop |
Gracefully shuts down the provider |
ValidateResourceConfig |
Validates resource configuration before planning |
UpgradeResourceState |
Migrates state from older schema versions |
Plan |
Calculates required changes to reach desired state |
Create |
Creates a new resource |
Read |
Reads current state of a resource |
Update |
Updates an existing resource |
Delete |
Deletes a resource |
ImportResourceState |
Imports existing infrastructure |
ValidateDataSourceConfig |
Validates data source configuration |
ReadDataSource |
Reads data from external sources |
Define schemas for your resources using the builder pattern:
use hemmer_provider_sdk::schema::*;
let schema = Schema::v0()
// Required string attribute
.with_attribute("name", Attribute::required_string()
.with_description("The name of the resource"))
// Optional with default
.with_attribute("region", Attribute::optional_string()
.with_default(serde_json::json!("us-east-1")))
// Computed (read-only) attribute
.with_attribute("id", Attribute::computed_string())
// Sensitive attribute (hidden in logs)
.with_attribute("password", Attribute::required_string().sensitive())
// Force replacement when changed
.with_attribute("ami", Attribute::required_string().with_force_new())
// Nested blocks
.with_block("network", NestedBlock::list(
Block::new()
.with_attribute("subnet_id", Attribute::required_string())
.with_attribute("security_groups",
Attribute::new(AttributeType::list(AttributeType::String),
AttributeFlags::optional()))
));
The SDK provides built-in validation helpers to validate configuration values against schemas:
use hemmer_provider_sdk::{validate, is_valid, schema::Schema};
use serde_json::json;
let schema = Schema::v0()
.with_attribute("name", Attribute::required_string())
.with_attribute("count", Attribute::optional(AttributeType::Number));
let value = json!({
"name": "my-resource",
"count": 5
});
// Get detailed validation diagnostics
let diagnostics = validate(&schema, &value);
if diagnostics.is_empty() {
println!("Configuration is valid!");
}
// Or use the simple boolean check
if is_valid(&schema, &value) {
println!("Valid!");
}
The SDK provides a comprehensive ProviderError enum that maps to appropriate gRPC status codes:
use hemmer_provider_sdk::ProviderError;
// Resource not found (maps to NOT_FOUND)
return Err(ProviderError::NotFound("instance-123 not found".to_string()));
// Resource already exists (maps to ALREADY_EXISTS)
return Err(ProviderError::AlreadyExists("bucket my-bucket already exists".to_string()));
// Permission/auth errors (maps to PERMISSION_DENIED)
return Err(ProviderError::PermissionDenied("insufficient permissions to delete resource".to_string()));
// Rate limiting or quota errors (maps to RESOURCE_EXHAUSTED)
return Err(ProviderError::ResourceExhausted("API rate limit exceeded".to_string()));
// Temporary service issues (maps to UNAVAILABLE)
return Err(ProviderError::Unavailable("service temporarily unavailable".to_string()));
// Timeout errors (maps to DEADLINE_EXCEEDED)
return Err(ProviderError::DeadlineExceeded("operation timed out after 30s".to_string()));
// State precondition failures (maps to FAILED_PRECONDITION)
return Err(ProviderError::FailedPrecondition("resource must be stopped before deletion".to_string()));
// Unimplemented operations (maps to UNIMPLEMENTED)
return Err(ProviderError::Unimplemented("import not supported for this resource type".to_string()));
// Validation errors (maps to INVALID_ARGUMENT)
return Err(ProviderError::Validation("invalid instance size".to_string()));
// Configuration errors (maps to FAILED_PRECONDITION)
return Err(ProviderError::Configuration("missing required API key".to_string()));
Using the appropriate error variant enables:
Unavailable but not NotFoundThe SDK includes a test harness for provider implementations:
use hemmer_provider_sdk::{ProviderTester, ProviderService};
use serde_json::json;
#[tokio::test]
async fn test_resource_lifecycle() {
let provider = MyProvider::new();
let tester = ProviderTester::new(provider);
// Test complete CRUD lifecycle
let final_state = tester
.lifecycle_crud(
"mycloud_instance",
json!({"name": "test"}), // create config
json!({"name": "test-updated"}), // update config
)
.await
.expect("lifecycle should succeed");
// Or test individual operations with assertions
let plan = tester
.plan("mycloud_instance", None, json!({"name": "test"}))
.await
.unwrap();
tester.assert_plan_creates(&plan);
}
When a provider starts via serve(), it outputs a handshake string to stdout:
HEMMER_PROVIDER|1|127.0.0.1:50051
Format: HEMMER_PROVIDER|<protocol_version>|<address>
This allows Hemmer to spawn the provider as a subprocess and connect via gRPC.
The SDK implements protocol version negotiation to ensure compatibility between Hemmer and providers built with different SDK versions.
use hemmer_provider_sdk::{PROTOCOL_VERSION, MIN_PROTOCOL_VERSION, check_protocol_version};
// Current protocol version (incremented for breaking changes)
assert_eq!(PROTOCOL_VERSION, 1);
// Minimum supported version for backwards compatibility
assert_eq!(MIN_PROTOCOL_VERSION, 1);
During the GetSchema RPC, Hemmer sends its protocol version, and the provider validates compatibility:
// Automatically handled by the SDK
// Providers reject clients with versions below MIN_PROTOCOL_VERSION
check_protocol_version(client_version)?;
PROTOCOL_VERSIONWhen PROTOCOL_VERSION is incremented, consider whether older clients should still be supported by setting MIN_PROTOCOL_VERSION appropriately.
# Clone and setup development environment
git clone https://github.com/hemmer-io/hemmer-provider-sdk
cd hemmer-provider-sdk
./scripts/setup.sh
The setup script will:
# Build
cargo build
# Run tests
cargo test
# Run linter
cargo clippy --all-targets -- -D warnings
# Format code
cargo fmt --all
# View documentation
cargo doc --no-deps --open
Proto types are pre-compiled and committed. To regenerate after changing proto/provider.proto:
# Requires protoc to be installed
cargo build --features regenerate-proto
maincargo test, cargo clippy, cargo fmt --check)Licensed under the Apache License, Version 2.0. See LICENSE for details.