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}