Skip to main content

cf_system_sdk_directory/
api.rs

1//! Directory API - contract for service discovery and instance resolution
2//!
3//! This gear defines the core traits and types for the directory service API.
4
5use anyhow::Result;
6use async_trait::async_trait;
7
8/// Represents an endpoint where a service can be reached
9#[derive(Clone, Debug, PartialEq, Eq, Hash)]
10pub struct ServiceEndpoint {
11    pub uri: String,
12}
13
14impl ServiceEndpoint {
15    pub fn new(uri: impl Into<String>) -> Self {
16        Self { uri: uri.into() }
17    }
18
19    #[must_use]
20    pub fn http(host: &str, port: u16) -> Self {
21        Self {
22            uri: format!("{}://{}:{}", "http", host, port),
23        }
24    }
25
26    #[must_use]
27    pub fn https(host: &str, port: u16) -> Self {
28        Self {
29            uri: format!("https://{host}:{port}"),
30        }
31    }
32
33    pub fn uds(path: impl AsRef<std::path::Path>) -> Self {
34        Self {
35            uri: format!("unix://{}", path.as_ref().display()),
36        }
37    }
38}
39
40/// Information about a service instance
41#[derive(Debug, Clone)]
42pub struct ServiceInstanceInfo {
43    /// Gear name this instance belongs to
44    pub gear: String,
45    /// Unique instance identifier
46    pub instance_id: String,
47    /// Primary endpoint for the instance
48    pub endpoint: ServiceEndpoint,
49    /// Optional version string
50    pub version: Option<String>,
51    /// Optional REST endpoint (HTTP base URL) for this instance.
52    /// Not all gears expose a REST API.
53    pub rest_endpoint: Option<ServiceEndpoint>,
54}
55
56/// Information for registering a new gear instance
57#[derive(Debug, Clone)]
58pub struct RegisterInstanceInfo {
59    /// Gear name
60    pub gear: String,
61    /// Unique instance identifier
62    pub instance_id: String,
63    /// Map of gRPC service name to endpoint
64    pub grpc_services: Vec<(String, ServiceEndpoint)>,
65    /// Optional version string
66    pub version: Option<String>,
67    /// Optional REST endpoint (HTTP base URL) exposed by the gear.
68    pub rest_endpoint: Option<ServiceEndpoint>,
69    /// Optional `OpenAPI` spec (JSON) published by the gear.
70    pub openapi_spec: Option<String>,
71}
72
73/// Directory API trait for service discovery and instance management
74///
75/// This trait defines the contract for interacting with the gear directory.
76/// It can be implemented by:
77/// - A local implementation that delegates to `GearManager`
78/// - A gRPC client for out-of-process gears
79#[async_trait]
80pub trait DirectoryClient: Send + Sync {
81    /// Resolve a gRPC service by its logical name to an endpoint
82    async fn resolve_grpc_service(&self, service_name: &str) -> Result<ServiceEndpoint>;
83
84    /// Resolve a REST endpoint (HTTP base URL) for a gear by its name.
85    ///
86    /// Returns the base URL (e.g. `http://billing:8080`) that callers use to
87    /// make REST requests to the resolved gear.
88    async fn resolve_rest_service(&self, gear_name: &str) -> Result<ServiceEndpoint>;
89
90    /// Retrieve the `OpenAPI` spec (JSON) published by a gear.
91    async fn get_openapi_spec(&self, gear_name: &str) -> Result<String>;
92
93    /// List all service instances for a given gear
94    async fn list_instances(&self, gear: &str) -> Result<Vec<ServiceInstanceInfo>>;
95
96    /// Register a new gear instance with the directory
97    async fn register_instance(&self, info: RegisterInstanceInfo) -> Result<()>;
98
99    /// Deregister a gear instance (for graceful shutdown)
100    async fn deregister_instance(&self, gear: &str, instance_id: &str) -> Result<()>;
101
102    /// Send a heartbeat for a gear instance to indicate it's still alive
103    async fn send_heartbeat(&self, gear: &str, instance_id: &str) -> Result<()>;
104}
105
106#[cfg(test)]
107#[cfg_attr(coverage_nightly, coverage(off))]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_service_endpoint_creation() {
113        let http_ep = ServiceEndpoint::http("localhost", 8080);
114        assert_eq!(http_ep.uri, concat!("http", "://localhost:8080"));
115
116        let https_endpoint = ServiceEndpoint::https("localhost", 8443);
117        assert_eq!(https_endpoint.uri, "https://localhost:8443");
118
119        let uds_ep = ServiceEndpoint::uds("/tmp/socket.sock");
120        assert!(uds_ep.uri.starts_with("unix://"));
121        assert!(uds_ep.uri.contains("socket.sock"));
122
123        let custom_ep = ServiceEndpoint::new(concat!("http", "://example.com"));
124        assert_eq!(custom_ep.uri, concat!("http", "://example.com"));
125    }
126
127    #[test]
128    fn test_register_instance_info() {
129        let info = RegisterInstanceInfo {
130            gear: "test_gear".to_owned(),
131            instance_id: "instance1".to_owned(),
132            grpc_services: vec![(
133                "test.Service".to_owned(),
134                ServiceEndpoint::http("127.0.0.1", 8001),
135            )],
136            version: Some("1.0.0".to_owned()),
137            rest_endpoint: None,
138            openapi_spec: None,
139        };
140
141        assert_eq!(info.gear, "test_gear");
142        assert_eq!(info.instance_id, "instance1");
143        assert_eq!(info.grpc_services.len(), 1);
144        assert!(info.rest_endpoint.is_none());
145        assert!(info.openapi_spec.is_none());
146    }
147
148    #[test]
149    fn test_register_instance_info_with_rest() {
150        let info = RegisterInstanceInfo {
151            gear: "billing".to_owned(),
152            instance_id: "instance1".to_owned(),
153            grpc_services: vec![],
154            version: Some("2.0.0".to_owned()),
155            rest_endpoint: Some(ServiceEndpoint::http("billing", 8080)),
156            openapi_spec: Some("{\"openapi\":\"3.1.0\"}".to_owned()),
157        };
158
159        assert_eq!(info.gear, "billing");
160        assert_eq!(
161            info.rest_endpoint.as_ref().unwrap().uri,
162            concat!("http", "://billing:8080")
163        );
164        assert!(info.openapi_spec.is_some());
165    }
166}