Skip to main content

heddle_client/grpc_hosted/
content.rs

1use grpc::heddle::api::v1alpha1::{
2    AnnotationScope, CompareResponse, ContextAnnotationKind, GetBlameRequest, GetBlameResponse,
3    GetCompareRequest, GetContextHistoryRequest, GetContextHistoryResponse, ListContextRequest,
4    ListContextResponse, ListContextSuggestionsRequest, ListContextSuggestionsResponse,
5    ReviseContextRequest, ReviseContextResponse, SetContextRequest, SetContextResponse,
6    SupersedeContextRequest, SupersedeContextResponse,
7};
8use tonic::Request;
9use wire::ProtocolError;
10
11use super::{HostedGrpcClient, helpers::status_to_protocol_error, operation_id::ClientOperationId};
12
13const SET_CONTEXT: &str = "heddle.api.v1alpha1.RepositoryService/SetContext";
14const REVISE_CONTEXT: &str = "heddle.api.v1alpha1.RepositoryService/ReviseContext";
15const SUPERSEDE_CONTEXT: &str = "heddle.api.v1alpha1.RepositoryService/SupersedeContext";
16
17impl HostedGrpcClient {
18    pub async fn get_compare(
19        &mut self,
20        repo_path: &str,
21        from: &str,
22        to: &str,
23        include_semantic: bool,
24    ) -> Result<CompareResponse, ProtocolError> {
25        let mut request = Request::new(GetCompareRequest {
26            repo_path: super::helpers::repository_ref(repo_path),
27            from: from.to_string(),
28            to: to.to_string(),
29            include_semantic,
30        });
31        self.apply_signed_auth(
32            &mut request,
33            "/heddle.api.v1alpha1.RepositoryService/GetCompare",
34        )?;
35        self.content
36            .get_compare(request)
37            .await
38            .map_err(status_to_protocol_error)
39            .map(|response| response.into_inner())
40    }
41
42    pub async fn get_blame(
43        &mut self,
44        repo_path: &str,
45        r#ref: Option<&str>,
46        path: &str,
47    ) -> Result<GetBlameResponse, ProtocolError> {
48        let mut request = Request::new(GetBlameRequest {
49            repo_path: super::helpers::repository_ref(repo_path),
50            r#ref: r#ref.unwrap_or_default().to_string(),
51            path: path.to_string(),
52        });
53        self.apply_signed_auth(
54            &mut request,
55            "/heddle.api.v1alpha1.RepositoryService/GetBlame",
56        )?;
57        self.content
58            .get_blame(request)
59            .await
60            .map_err(status_to_protocol_error)
61            .map(|response| response.into_inner())
62    }
63
64    pub async fn list_context(
65        &mut self,
66        repo_path: &str,
67        r#ref: Option<&str>,
68        prefix: Option<&str>,
69        tag_filter: Option<&str>,
70    ) -> Result<ListContextResponse, ProtocolError> {
71        let mut request = Request::new(ListContextRequest {
72            repo_path: super::helpers::repository_ref(repo_path),
73            r#ref: r#ref.unwrap_or_default().to_string(),
74            prefix: prefix.map(str::to_string),
75            tag_filter: tag_filter.map(str::to_string),
76        });
77        self.apply_signed_auth(
78            &mut request,
79            "/heddle.api.v1alpha1.RepositoryService/ListContext",
80        )?;
81        self.content
82            .list_context(request)
83            .await
84            .map_err(status_to_protocol_error)
85            .map(|response| response.into_inner())
86    }
87
88    #[allow(clippy::too_many_arguments)]
89    pub async fn set_context(
90        &mut self,
91        repo_path: &str,
92        path: &str,
93        target_state_id: Option<&str>,
94        scope: AnnotationScope,
95        kind: ContextAnnotationKind,
96        tags: Vec<String>,
97        content: &str,
98        agent_provider: Option<&str>,
99        agent_model: Option<&str>,
100        client_operation_id: String,
101    ) -> Result<SetContextResponse, ProtocolError> {
102        let operation_id =
103            ClientOperationId::for_required_method(SET_CONTEXT, client_operation_id)?;
104        let mut request = Request::new(SetContextRequest {
105            repo_path: super::helpers::repository_ref(repo_path),
106            path: path.to_string(),
107            scope: Some(scope),
108            tags,
109            content: content.to_string(),
110            agent_provider: agent_provider.unwrap_or_default().to_string(),
111            agent_model: agent_model.unwrap_or_default().to_string(),
112            target_state_id: target_state_id
113                .and_then(|s| objects::object::StateId::parse(s).ok())
114                .and_then(super::helpers::proto_state_id),
115            kind: kind as i32,
116            client_operation_id: operation_id.to_wire(),
117        });
118        self.apply_signed_auth(
119            &mut request,
120            "/heddle.api.v1alpha1.RepositoryService/SetContext",
121        )?;
122        self.content
123            .set_context(request)
124            .await
125            .map_err(status_to_protocol_error)
126            .map(|response| response.into_inner())
127    }
128
129    pub async fn get_context_history(
130        &mut self,
131        repo_path: &str,
132        r#ref: Option<&str>,
133        annotation_id: &str,
134    ) -> Result<GetContextHistoryResponse, ProtocolError> {
135        let mut request = Request::new(GetContextHistoryRequest {
136            repo_path: super::helpers::repository_ref(repo_path),
137            r#ref: r#ref.unwrap_or_default().to_string(),
138            annotation_id: annotation_id.to_string(),
139        });
140        self.apply_signed_auth(
141            &mut request,
142            "/heddle.api.v1alpha1.RepositoryService/GetContextHistory",
143        )?;
144        self.content
145            .get_context_history(request)
146            .await
147            .map_err(status_to_protocol_error)
148            .map(|response| response.into_inner())
149    }
150
151    pub async fn list_context_suggestions(
152        &mut self,
153        repo_path: &str,
154        r#ref: Option<&str>,
155        limit: u32,
156    ) -> Result<ListContextSuggestionsResponse, ProtocolError> {
157        let mut request = Request::new(ListContextSuggestionsRequest {
158            repo_path: super::helpers::repository_ref(repo_path),
159            r#ref: r#ref.unwrap_or_default().to_string(),
160            limit,
161        });
162        self.apply_signed_auth(
163            &mut request,
164            "/heddle.api.v1alpha1.RepositoryService/ListContextSuggestions",
165        )?;
166        self.content
167            .list_context_suggestions(request)
168            .await
169            .map_err(status_to_protocol_error)
170            .map(|response| response.into_inner())
171    }
172
173    #[allow(clippy::too_many_arguments)]
174    pub async fn revise_context(
175        &mut self,
176        repo_path: &str,
177        annotation_id: &str,
178        content: &str,
179        tags: Vec<String>,
180        agent_provider: Option<&str>,
181        agent_model: Option<&str>,
182        kind: ContextAnnotationKind,
183        client_operation_id: String,
184    ) -> Result<ReviseContextResponse, ProtocolError> {
185        let operation_id =
186            ClientOperationId::for_required_method(REVISE_CONTEXT, client_operation_id)?;
187        let mut request = Request::new(revise_context_request(
188            repo_path,
189            annotation_id,
190            content,
191            tags,
192            agent_provider,
193            agent_model,
194            kind,
195            &operation_id,
196        ));
197        self.apply_signed_auth(
198            &mut request,
199            "/heddle.api.v1alpha1.RepositoryService/ReviseContext",
200        )?;
201        self.content
202            .revise_context(request)
203            .await
204            .map_err(status_to_protocol_error)
205            .map(|response| response.into_inner())
206    }
207
208    #[allow(clippy::too_many_arguments)]
209    pub async fn supersede_context(
210        &mut self,
211        repo_path: &str,
212        annotation_id: &str,
213        path: Option<&str>,
214        target_state_id: Option<&str>,
215        scope: AnnotationScope,
216        tags: Vec<String>,
217        content: &str,
218        agent_provider: Option<&str>,
219        agent_model: Option<&str>,
220        kind: ContextAnnotationKind,
221        client_operation_id: String,
222    ) -> Result<SupersedeContextResponse, ProtocolError> {
223        let operation_id =
224            ClientOperationId::for_required_method(SUPERSEDE_CONTEXT, client_operation_id)?;
225        let mut request = Request::new(supersede_context_request(
226            repo_path,
227            annotation_id,
228            path,
229            target_state_id,
230            scope,
231            tags,
232            content,
233            agent_provider,
234            agent_model,
235            kind,
236            &operation_id,
237        ));
238        self.apply_signed_auth(
239            &mut request,
240            "/heddle.api.v1alpha1.RepositoryService/SupersedeContext",
241        )?;
242        self.content
243            .supersede_context(request)
244            .await
245            .map_err(status_to_protocol_error)
246            .map(|response| response.into_inner())
247    }
248}
249
250#[allow(clippy::too_many_arguments)]
251fn revise_context_request(
252    repo_path: &str,
253    annotation_id: &str,
254    content: &str,
255    tags: Vec<String>,
256    agent_provider: Option<&str>,
257    agent_model: Option<&str>,
258    kind: ContextAnnotationKind,
259    operation_id: &ClientOperationId,
260) -> ReviseContextRequest {
261    ReviseContextRequest {
262        repo_path: super::helpers::repository_ref(repo_path),
263        annotation_id: annotation_id.to_string(),
264        content: content.to_string(),
265        tags,
266        agent_provider: agent_provider.unwrap_or_default().to_string(),
267        agent_model: agent_model.unwrap_or_default().to_string(),
268        kind: kind as i32,
269        client_operation_id: operation_id.to_wire(),
270    }
271}
272
273#[allow(clippy::too_many_arguments)]
274fn supersede_context_request(
275    repo_path: &str,
276    annotation_id: &str,
277    path: Option<&str>,
278    target_state_id: Option<&str>,
279    scope: AnnotationScope,
280    tags: Vec<String>,
281    content: &str,
282    agent_provider: Option<&str>,
283    agent_model: Option<&str>,
284    kind: ContextAnnotationKind,
285    operation_id: &ClientOperationId,
286) -> SupersedeContextRequest {
287    SupersedeContextRequest {
288        repo_path: super::helpers::repository_ref(repo_path),
289        annotation_id: annotation_id.to_string(),
290        path: path.unwrap_or_default().to_string(),
291        scope: Some(scope),
292        tags,
293        content: content.to_string(),
294        agent_provider: agent_provider.unwrap_or_default().to_string(),
295        agent_model: agent_model.unwrap_or_default().to_string(),
296        target_state_id: target_state_id
297            .and_then(|s| objects::object::StateId::parse(s).ok())
298            .and_then(super::helpers::proto_state_id),
299        kind: kind as i32,
300        client_operation_id: operation_id.to_wire(),
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    #[test]
309    fn context_revision_retry_request_reuses_the_callers_operation_id() {
310        let operation_id =
311            ClientOperationId::for_required_method(REVISE_CONTEXT, "caller-op-1").unwrap();
312        let build = || {
313            revise_context_request(
314                "acme/widgets",
315                "annotation-1",
316                "updated context",
317                vec!["reviewed".to_string()],
318                None,
319                None,
320                ContextAnnotationKind::Rationale,
321                &operation_id,
322            )
323        };
324        let first = build();
325        let retry = build();
326        assert!(!first.client_operation_id.is_empty());
327        assert_eq!(retry.client_operation_id, first.client_operation_id);
328    }
329
330    #[test]
331    fn context_supersession_retry_request_reuses_the_callers_operation_id() {
332        let operation_id =
333            ClientOperationId::for_required_method(SUPERSEDE_CONTEXT, "caller-op-2").unwrap();
334        let build = || {
335            supersede_context_request(
336                "acme/widgets",
337                "annotation-1",
338                Some("src/lib.rs"),
339                None,
340                AnnotationScope::default(),
341                vec!["replacement".to_string()],
342                "replacement context",
343                None,
344                None,
345                ContextAnnotationKind::Rationale,
346                &operation_id,
347            )
348        };
349        let first = build();
350        let retry = build();
351        assert!(!first.client_operation_id.is_empty());
352        assert_eq!(retry.client_operation_id, first.client_operation_id);
353    }
354
355    #[test]
356    fn context_write_rejects_an_empty_caller_operation_id_before_transport() {
357        for method in [SET_CONTEXT, REVISE_CONTEXT, SUPERSEDE_CONTEXT] {
358            let error = ClientOperationId::for_required_method(method, "")
359                .expect_err("required context writes must fail before transport");
360            assert!(error.to_string().contains("non-empty client operation ID"));
361        }
362    }
363
364    #[test]
365    fn actual_context_rpc_retry_boundary_requires_reusing_the_caller_id() {
366        #[allow(dead_code)]
367        async fn compile_caller_retry(client: &mut HostedGrpcClient, client_operation_id: String) {
368            let _ = client
369                .revise_context(
370                    "acme/widgets",
371                    "annotation-1",
372                    "updated context",
373                    Vec::new(),
374                    None,
375                    None,
376                    ContextAnnotationKind::Rationale,
377                    client_operation_id.clone(),
378                )
379                .await;
380            let _ = client
381                .revise_context(
382                    "acme/widgets",
383                    "annotation-1",
384                    "updated context",
385                    Vec::new(),
386                    None,
387                    None,
388                    ContextAnnotationKind::Rationale,
389                    client_operation_id,
390                )
391                .await;
392        }
393
394        let _ = compile_caller_retry;
395    }
396}