embassy_github/
provider.rs1use async_trait::async_trait;
11use converge_pack::FactPayload;
12use embassy_pack::{CallContext, Observation, content_hash};
13use serde::{Deserialize, Serialize};
14
15use crate::error::GithubError;
16use crate::types::{OrgSlug, Organization};
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19#[serde(tag = "kind", rename_all = "snake_case")]
20pub enum GithubRequest {
21 Lookup { identifier: OrgSlug },
22}
23
24impl FactPayload for GithubRequest {
25 const FAMILY: &'static str = "embassy.github.request";
26 const VERSION: u16 = 1;
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct GithubResponse {
31 pub records: Vec<Observation<Organization>>,
32}
33
34#[async_trait]
35pub trait GithubProvider: Send + Sync {
36 fn name(&self) -> &str;
37
38 async fn fetch(
39 &self,
40 request: &GithubRequest,
41 ctx: &CallContext,
42 ) -> Result<GithubResponse, GithubError>;
43}
44
45#[derive(Debug, Clone, Default)]
46pub struct StubGithubProvider;
47
48#[async_trait]
49impl GithubProvider for StubGithubProvider {
50 fn name(&self) -> &'static str {
51 "stub_github"
52 }
53
54 async fn fetch(
55 &self,
56 request: &GithubRequest,
57 _ctx: &CallContext,
58 ) -> Result<GithubResponse, GithubError> {
59 let hash_input = serde_json::to_string(request)
60 .map_err(|e| GithubError::InvalidRequest(format!("non-serializable request: {e}")))?;
61 let request_hash = content_hash(&hash_input);
62
63 let GithubRequest::Lookup { identifier } = request;
64 let entity = Organization {
65 login: identifier.clone(),
66 html_url: "Stub Organization".to_string(),
67 };
68
69 let obs = Observation {
70 observation_id: format!("obs:github:{request_hash}"),
71 request_hash,
72 vendor: "stub_github".to_string(),
73 model: "stub".to_string(),
74 latency_ms: 5,
75 cost_estimate: None,
76 tokens: None,
77 content: entity,
78 raw_response: None,
79 };
80
81 Ok(GithubResponse { records: vec![obs] })
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[tokio::test]
90 async fn stub_request_hash_matches_content_hash() {
91 let provider = StubGithubProvider;
95 let req = GithubRequest::Lookup {
96 identifier: OrgSlug::parse("STUB-001").unwrap(),
97 };
98 let resp = provider.fetch(&req, &CallContext::default()).await.unwrap();
99 let expected = content_hash(&serde_json::to_string(&req).unwrap());
100 assert_eq!(resp.records[0].request_hash, expected);
101 }
102
103 #[tokio::test]
104 async fn stub_returns_one_observation() {
105 let provider = StubGithubProvider;
106 let req = GithubRequest::Lookup {
107 identifier: OrgSlug::parse("STUB-001").unwrap(),
108 };
109 let resp = provider.fetch(&req, &CallContext::default()).await.unwrap();
110 assert_eq!(resp.records.len(), 1);
111 }
112}