1use canton_core::{Error, Result};
40use canton_proto::com::daml::ledger::api::v2 as pb;
41
42#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
45#[non_exhaustive]
46pub enum TransactionShape {
47 AcsDelta,
50 #[default]
54 LedgerEffects,
55}
56
57impl TransactionShape {
58 pub(crate) fn as_grpc(self) -> pb::TransactionShape {
59 match self {
60 Self::AcsDelta => pb::TransactionShape::AcsDelta,
61 Self::LedgerEffects => pb::TransactionShape::LedgerEffects,
62 }
63 }
64}
65
66#[derive(Clone, Debug)]
73#[must_use = "a request does nothing until passed to a client method"]
74#[allow(clippy::struct_excessive_bools)]
77pub struct UpdatesRequest {
78 pub(crate) parties: Vec<String>,
79 pub(crate) begin_exclusive: i64,
80 end_inclusive: Option<i64>,
81 shape: TransactionShape,
82 templates: Vec<pb::Identifier>,
84 interfaces: Vec<pb::Identifier>,
86 include_created_event_blobs: bool,
87 include_reassignments: bool,
88 include_topology_events: bool,
89 verbose: bool,
90 any_party: bool,
91 descending: bool,
92}
93
94impl UpdatesRequest {
95 pub(crate) fn validate(&self) -> crate::Result<()> {
103 use canton_core::Error;
104 if self.parties.is_empty() && !self.any_party {
105 return Err(Error::InvalidRequest(
106 "a read needs at least one party (or filters_for_any_party)".to_string(),
107 ));
108 }
109 if self.begin_exclusive < 0 {
110 return Err(Error::InvalidRequest(format!(
111 "begin offset must not be negative, got {}",
112 self.begin_exclusive
113 )));
114 }
115 if let Some(end) = self.end_inclusive {
116 if end < 0 {
117 return Err(Error::InvalidRequest(format!(
118 "end offset must not be negative, got {end}"
119 )));
120 }
121 if !self.descending && end < self.begin_exclusive {
122 return Err(Error::InvalidRequest(format!(
123 "end offset {end} is before the begin offset {}",
124 self.begin_exclusive
125 )));
126 }
127 }
128 Ok(())
129 }
130
131 pub fn new(parties: Vec<String>, begin_exclusive: i64) -> Self {
138 Self {
139 parties,
140 begin_exclusive,
141 end_inclusive: None,
142 shape: TransactionShape::LedgerEffects,
143 templates: Vec::new(),
144 interfaces: Vec::new(),
145 include_created_event_blobs: false,
146 include_reassignments: true,
147 include_topology_events: false,
148 verbose: true,
149 any_party: false,
150 descending: false,
151 }
152 }
153
154 pub fn until(mut self, end_inclusive: i64) -> Self {
158 self.end_inclusive = Some(end_inclusive);
159 self
160 }
161
162 pub fn descending(mut self) -> Self {
166 self.descending = true;
167 self
168 }
169
170 pub fn for_any_party(mut self) -> Self {
176 self.any_party = true;
177 self
178 }
179
180 pub fn with_shape(mut self, shape: TransactionShape) -> Self {
182 self.shape = shape;
183 self
184 }
185
186 pub fn for_templates<I, S>(mut self, template_ids: I) -> Result<Self>
194 where
195 I: IntoIterator<Item = S>,
196 S: AsRef<str>,
197 {
198 for id in template_ids {
199 self.templates.push(parse_identifier(id.as_ref())?);
200 }
201 Ok(self)
202 }
203
204 pub fn for_interfaces<I, S>(mut self, interface_ids: I) -> Result<Self>
211 where
212 I: IntoIterator<Item = S>,
213 S: AsRef<str>,
214 {
215 for id in interface_ids {
216 self.interfaces.push(parse_identifier(id.as_ref())?);
217 }
218 Ok(self)
219 }
220
221 pub fn with_created_event_blobs(mut self) -> Self {
224 self.include_created_event_blobs = true;
225 self
226 }
227
228 pub fn without_reassignments(mut self) -> Self {
230 self.include_reassignments = false;
231 self
232 }
233
234 pub fn with_topology_events(mut self) -> Self {
237 self.include_topology_events = true;
238 self
239 }
240
241 pub fn non_verbose(mut self) -> Self {
244 self.verbose = false;
245 self
246 }
247
248 pub(crate) fn resume_after(&self, offset: i64) -> Self {
250 let mut request = self.clone();
251 request.begin_exclusive = offset;
252 request
253 }
254
255 pub(crate) fn bounds(&self) -> (i64, Option<i64>) {
257 (self.begin_exclusive, self.end_inclusive)
258 }
259
260 pub(crate) fn is_descending(&self) -> bool {
262 self.descending
263 }
264
265 pub(crate) fn update_format(&self) -> pb::UpdateFormat {
267 let filters = build_filters(
268 &self.templates,
269 &self.interfaces,
270 self.include_created_event_blobs,
271 );
272 let event_format = |verbose: bool| pb::EventFormat {
273 filters_by_party: self
274 .parties
275 .iter()
276 .map(|party| (party.clone(), filters.clone()))
277 .collect(),
278 filters_for_any_party: self.any_party.then(|| filters.clone()),
279 verbose,
280 };
281 pb::UpdateFormat {
282 include_transactions: Some(pb::TransactionFormat {
283 event_format: Some(event_format(self.verbose)),
284 transaction_shape: self.shape.as_grpc() as i32,
285 }),
286 include_reassignments: self
289 .include_reassignments
290 .then(|| event_format(self.verbose)),
291 include_topology_events: self.include_topology_events.then(|| pb::TopologyFormat {
292 include_participant_authorization_events: Some(
293 pb::ParticipantAuthorizationTopologyFormat {
294 parties: self.parties.clone(),
295 },
296 ),
297 }),
298 }
299 }
300
301 pub(crate) fn into_grpc(self) -> pb::GetUpdatesRequest {
303 pb::GetUpdatesRequest {
304 begin_exclusive: self.begin_exclusive,
305 end_inclusive: self.end_inclusive,
306 descending_order: self.descending,
307 update_format: Some(self.update_format()),
308 }
309 }
310
311 pub(crate) fn json_body(&self) -> serde_json::Value {
314 let event_format = || {
315 event_format_json(
316 &self.parties,
317 &self.templates,
318 &self.interfaces,
319 self.include_created_event_blobs,
320 self.verbose,
321 self.any_party,
322 )
323 };
324 let shape = match self.shape {
325 TransactionShape::AcsDelta => "TRANSACTION_SHAPE_ACS_DELTA",
326 TransactionShape::LedgerEffects => "TRANSACTION_SHAPE_LEDGER_EFFECTS",
327 };
328 let mut update_format = serde_json::json!({
329 "includeTransactions": {
330 "eventFormat": event_format(),
331 "transactionShape": shape,
332 }
333 });
334 if self.include_reassignments {
335 update_format["includeReassignments"] = event_format();
336 }
337 if self.include_topology_events {
338 update_format["includeTopologyEvents"] = serde_json::json!({
339 "includeParticipantAuthorizationEvents": { "parties": self.parties }
340 });
341 }
342 let mut body = serde_json::json!({
343 "beginExclusive": self.begin_exclusive,
344 "updateFormat": update_format,
345 });
346 if let Some(end) = self.end_inclusive {
347 body["endInclusive"] = serde_json::json!(end);
348 }
349 if self.descending {
350 body["descendingOrder"] = serde_json::json!(true);
351 }
352 body
353 }
354}
355
356#[derive(Clone, Debug)]
366#[must_use = "a request does nothing until passed to a client method"]
367pub struct ActiveContractsRequest {
368 pub(crate) parties: Vec<String>,
369 pub(crate) active_at_offset: i64,
370 templates: Vec<pb::Identifier>,
371 interfaces: Vec<pb::Identifier>,
372 include_created_event_blobs: bool,
373 verbose: bool,
374 any_party: bool,
375}
376
377impl ActiveContractsRequest {
378 pub(crate) fn validate(&self) -> crate::Result<()> {
381 use canton_core::Error;
382 if self.parties.is_empty() && !self.any_party {
383 return Err(Error::InvalidRequest(
384 "an ACS read needs at least one party (or filters_for_any_party)".to_string(),
385 ));
386 }
387 if self.active_at_offset < 0 {
388 return Err(Error::InvalidRequest(format!(
389 "active_at_offset must not be negative, got {}",
390 self.active_at_offset
391 )));
392 }
393 Ok(())
394 }
395
396 pub fn new(parties: Vec<String>, active_at_offset: i64) -> Self {
402 Self {
403 parties,
404 active_at_offset,
405 templates: Vec::new(),
406 interfaces: Vec::new(),
407 include_created_event_blobs: false,
408 verbose: true,
409 any_party: false,
410 }
411 }
412
413 pub fn for_any_party(mut self) -> Self {
419 self.any_party = true;
420 self
421 }
422
423 pub fn for_templates<I, S>(mut self, template_ids: I) -> Result<Self>
429 where
430 I: IntoIterator<Item = S>,
431 S: AsRef<str>,
432 {
433 for id in template_ids {
434 self.templates.push(parse_identifier(id.as_ref())?);
435 }
436 Ok(self)
437 }
438
439 pub fn for_interfaces<I, S>(mut self, interface_ids: I) -> Result<Self>
446 where
447 I: IntoIterator<Item = S>,
448 S: AsRef<str>,
449 {
450 for id in interface_ids {
451 self.interfaces.push(parse_identifier(id.as_ref())?);
452 }
453 Ok(self)
454 }
455
456 pub fn with_created_event_blobs(mut self) -> Self {
459 self.include_created_event_blobs = true;
460 self
461 }
462
463 pub fn non_verbose(mut self) -> Self {
465 self.verbose = false;
466 self
467 }
468
469 pub(crate) fn event_format(&self) -> pb::EventFormat {
471 let filters = build_filters(
472 &self.templates,
473 &self.interfaces,
474 self.include_created_event_blobs,
475 );
476 pb::EventFormat {
477 filters_by_party: self
478 .parties
479 .iter()
480 .map(|party| (party.clone(), filters.clone()))
481 .collect(),
482 filters_for_any_party: self.any_party.then(|| filters.clone()),
483 verbose: self.verbose,
484 }
485 }
486
487 pub(crate) fn json_body(&self) -> serde_json::Value {
491 serde_json::json!({
492 "activeAtOffset": self.active_at_offset,
493 "eventFormat": event_format_json(
494 &self.parties,
495 &self.templates,
496 &self.interfaces,
497 self.include_created_event_blobs,
498 self.verbose,
499 self.any_party,
500 ),
501 })
502 }
503}
504
505#[derive(Clone, Debug)]
510#[must_use = "a request does nothing until passed to a client method"]
511pub struct CompletionsRequest {
512 pub(crate) parties: Vec<String>,
513 pub(crate) begin_exclusive: i64,
514 user_id: Option<String>,
515}
516
517impl CompletionsRequest {
518 pub(crate) fn validate(&self) -> crate::Result<()> {
520 use canton_core::Error;
521 if self.parties.is_empty() {
522 return Err(Error::InvalidRequest(
523 "a completion subscription needs at least one party".to_string(),
524 ));
525 }
526 if self.begin_exclusive < 0 {
527 return Err(Error::InvalidRequest(format!(
528 "begin offset must not be negative, got {}",
529 self.begin_exclusive
530 )));
531 }
532 Ok(())
533 }
534
535 pub fn new(parties: Vec<String>, begin_exclusive: i64) -> Self {
538 Self {
539 parties,
540 begin_exclusive,
541 user_id: None,
542 }
543 }
544
545 pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
552 self.user_id = Some(user_id.into());
553 self
554 }
555
556 pub(crate) fn into_grpc(self) -> pb::CompletionStreamRequest {
558 pb::CompletionStreamRequest {
559 user_id: self.user_id.unwrap_or_default(),
560 parties: self.parties,
561 begin_exclusive: self.begin_exclusive,
562 }
563 }
564
565 #[cfg(feature = "ws")]
569 pub(crate) fn json_body(&self) -> serde_json::Value {
570 let mut body = serde_json::json!({
571 "parties": self.parties,
572 "beginExclusive": self.begin_exclusive,
573 });
574 if let Some(user_id) = &self.user_id {
575 body["userId"] = serde_json::json!(user_id);
576 }
577 body
578 }
579}
580
581fn build_filters(
585 templates: &[pb::Identifier],
586 interfaces: &[pb::Identifier],
587 include_created_event_blobs: bool,
588) -> pb::Filters {
589 use pb::cumulative_filter::IdentifierFilter;
590
591 if templates.is_empty() && interfaces.is_empty() {
592 return pb::Filters {
593 cumulative: vec![pb::CumulativeFilter {
594 identifier_filter: Some(IdentifierFilter::WildcardFilter(pb::WildcardFilter {
595 include_created_event_blob: include_created_event_blobs,
596 })),
597 }],
598 };
599 }
600
601 let template_filters = templates.iter().map(|id| {
602 IdentifierFilter::TemplateFilter(pb::TemplateFilter {
603 template_id: Some(id.clone()),
604 include_created_event_blob: include_created_event_blobs,
605 })
606 });
607 let interface_filters = interfaces.iter().map(|id| {
608 IdentifierFilter::InterfaceFilter(pb::InterfaceFilter {
609 interface_id: Some(id.clone()),
610 include_interface_view: true,
611 include_created_event_blob: include_created_event_blobs,
612 })
613 });
614 pb::Filters {
615 cumulative: template_filters
616 .chain(interface_filters)
617 .map(|filter| pb::CumulativeFilter {
618 identifier_filter: Some(filter),
619 })
620 .collect(),
621 }
622}
623
624fn event_format_json(
630 parties: &[String],
631 templates: &[pb::Identifier],
632 interfaces: &[pb::Identifier],
633 include_created_event_blobs: bool,
634 verbose: bool,
635 any_party: bool,
636) -> serde_json::Value {
637 use serde_json::json;
638
639 let cumulative: Vec<serde_json::Value> = if templates.is_empty() && interfaces.is_empty() {
640 vec![json!({
641 "identifierFilter": {
642 "WildcardFilter": {
643 "value": { "includeCreatedEventBlob": include_created_event_blobs }
644 }
645 }
646 })]
647 } else {
648 let identifier = |id: &pb::Identifier| {
649 format!("{}:{}:{}", id.package_id, id.module_name, id.entity_name)
650 };
651 templates
652 .iter()
653 .map(|id| {
654 json!({
655 "identifierFilter": {
656 "TemplateFilter": {
657 "value": {
658 "templateId": identifier(id),
659 "includeCreatedEventBlob": include_created_event_blobs,
660 }
661 }
662 }
663 })
664 })
665 .chain(interfaces.iter().map(|id| {
666 json!({
667 "identifierFilter": {
668 "InterfaceFilter": {
669 "value": {
670 "interfaceId": identifier(id),
671 "includeInterfaceView": true,
672 "includeCreatedEventBlob": include_created_event_blobs,
673 }
674 }
675 }
676 })
677 }))
678 .collect()
679 };
680 let filters_by_party: serde_json::Map<String, serde_json::Value> = parties
681 .iter()
682 .map(|party| (party.clone(), json!({ "cumulative": cumulative })))
683 .collect();
684 let mut format = json!({ "filtersByParty": filters_by_party, "verbose": verbose });
685 if any_party {
686 format["filtersForAnyParty"] = json!({ "cumulative": cumulative });
687 }
688 format
689}
690
691fn parse_identifier(id: &str) -> Result<pb::Identifier> {
695 let mut parts = id.splitn(3, ':');
696 match (parts.next(), parts.next(), parts.next()) {
697 (Some(package), Some(module), Some(entity))
698 if !package.is_empty() && !module.is_empty() && !entity.is_empty() =>
699 {
700 Ok(pb::Identifier {
701 package_id: package.to_string(),
702 module_name: module.to_string(),
703 entity_name: entity.to_string(),
704 })
705 }
706 _ => Err(Error::InvalidRequest(format!(
707 "malformed identifier `{id}`: expected `package:Module:Entity` \
708 (package id, or `#package-name`)"
709 ))),
710 }
711}
712
713#[cfg(test)]
714#[allow(clippy::unwrap_used)]
715mod tests {
716 use super::*;
717
718 #[test]
719 fn the_default_request_matches_the_plain_updates_call() {
720 let request = UpdatesRequest::new(vec!["alice".to_string()], 7).into_grpc();
723
724 assert_eq!(request.begin_exclusive, 7);
725 assert_eq!(request.end_inclusive, None);
726 assert!(!request.descending_order);
727 let format = request.update_format.unwrap();
728 let transactions = format.include_transactions.unwrap();
729 assert_eq!(
730 transactions.transaction_shape,
731 pb::TransactionShape::LedgerEffects as i32
732 );
733 let events = transactions.event_format.unwrap();
734 assert!(events.verbose);
735 assert!(events.filters_for_any_party.is_none());
736 let filters = &events.filters_by_party["alice"];
737 assert_eq!(filters.cumulative.len(), 1);
738 assert!(matches!(
739 filters.cumulative[0].identifier_filter,
740 Some(pb::cumulative_filter::IdentifierFilter::WildcardFilter(
741 pb::WildcardFilter {
742 include_created_event_blob: false
743 }
744 ))
745 ));
746 assert!(format.include_reassignments.is_some());
747 assert!(format.include_topology_events.is_none());
748 }
749
750 #[test]
751 fn every_builder_knob_reaches_the_wire_request() {
752 let request = UpdatesRequest::new(vec!["alice".to_string()], 0)
753 .until(41)
754 .with_shape(TransactionShape::AcsDelta)
755 .for_templates(["#my-app:My.Mod:Asset"])
756 .unwrap()
757 .for_interfaces(["#my-app:My.Api:IAsset"])
758 .unwrap()
759 .with_created_event_blobs()
760 .without_reassignments()
761 .with_topology_events()
762 .non_verbose()
763 .into_grpc();
764
765 assert_eq!(request.end_inclusive, Some(41));
766 let format = request.update_format.unwrap();
767 assert!(format.include_reassignments.is_none());
768 assert_eq!(
769 format
770 .include_topology_events
771 .unwrap()
772 .include_participant_authorization_events
773 .unwrap()
774 .parties,
775 vec!["alice".to_string()]
776 );
777 let transactions = format.include_transactions.unwrap();
778 assert_eq!(
779 transactions.transaction_shape,
780 pb::TransactionShape::AcsDelta as i32
781 );
782 let events = transactions.event_format.unwrap();
783 assert!(!events.verbose);
784 let filters = &events.filters_by_party["alice"].cumulative;
785 assert_eq!(filters.len(), 2, "one template + one interface filter");
786 let Some(pb::cumulative_filter::IdentifierFilter::TemplateFilter(template)) =
787 &filters[0].identifier_filter
788 else {
789 panic!("expected a template filter first");
790 };
791 assert_eq!(template.template_id.as_ref().unwrap().package_id, "#my-app");
792 assert!(template.include_created_event_blob);
793 let Some(pb::cumulative_filter::IdentifierFilter::InterfaceFilter(interface)) =
794 &filters[1].identifier_filter
795 else {
796 panic!("expected an interface filter second");
797 };
798 assert_eq!(
799 interface.interface_id.as_ref().unwrap().entity_name,
800 "IAsset"
801 );
802 assert!(interface.include_interface_view);
803 }
804
805 #[test]
806 fn identifiers_parse_and_malformed_ones_are_refused() {
807 let id = parse_identifier("#pkg-name:Some.Dotted.Module:Entity").unwrap();
808 assert_eq!(id.package_id, "#pkg-name");
809 assert_eq!(id.module_name, "Some.Dotted.Module");
810 assert_eq!(id.entity_name, "Entity");
811
812 for bad in ["", "nope", "a:b", ":Mod:Ent", "pkg::Ent", "pkg:Mod:"] {
813 assert!(parse_identifier(bad).is_err(), "`{bad}` should be refused");
814 }
815 }
816
817 #[test]
818 fn acs_request_knobs_reach_the_wire_and_json_bodies() {
819 let request = ActiveContractsRequest::new(vec!["alice".to_string()], 42)
820 .for_templates(["#app:Mod:Asset"])
821 .unwrap()
822 .for_interfaces(["#app:Api:IAsset"])
823 .unwrap()
824 .with_created_event_blobs()
825 .non_verbose();
826
827 let format = request.event_format();
829 assert!(!format.verbose);
830 let filters = &format.filters_by_party["alice"].cumulative;
831 assert_eq!(filters.len(), 2);
832 assert!(matches!(
833 &filters[0].identifier_filter,
834 Some(pb::cumulative_filter::IdentifierFilter::TemplateFilter(t))
835 if t.include_created_event_blob
836 ));
837
838 let body = request.json_body();
840 assert_eq!(body["activeAtOffset"], 42);
841 let cumulative = &body["eventFormat"]["filtersByParty"]["alice"]["cumulative"];
842 assert_eq!(
843 cumulative[0]["identifierFilter"]["TemplateFilter"]["value"]["templateId"],
844 "#app:Mod:Asset"
845 );
846 assert_eq!(
847 cumulative[1]["identifierFilter"]["InterfaceFilter"]["value"]["interfaceId"],
848 "#app:Api:IAsset"
849 );
850 assert_eq!(body["eventFormat"]["verbose"], false);
851
852 let plain = ActiveContractsRequest::new(vec!["alice".to_string()], 42).json_body();
854 assert!(plain["eventFormat"]["filtersByParty"]["alice"]["cumulative"][0]
855 ["identifierFilter"]["WildcardFilter"]
856 .is_object());
857 assert_eq!(plain["eventFormat"]["verbose"], true);
858 }
859
860 #[test]
861 fn updates_json_body_mirrors_the_grpc_query() {
862 let body = UpdatesRequest::new(vec!["alice".to_string()], 5)
863 .until(9)
864 .with_shape(TransactionShape::AcsDelta)
865 .for_templates(["#app:Mod:Asset"])
866 .unwrap()
867 .without_reassignments()
868 .with_topology_events()
869 .json_body();
870
871 assert_eq!(body["beginExclusive"], 5);
872 assert_eq!(body["endInclusive"], 9);
873 let format = &body["updateFormat"];
874 assert_eq!(
875 format["includeTransactions"]["transactionShape"],
876 "TRANSACTION_SHAPE_ACS_DELTA"
877 );
878 assert!(format.get("includeReassignments").is_none());
879 assert_eq!(
880 format["includeTopologyEvents"]["includeParticipantAuthorizationEvents"]["parties"][0],
881 "alice"
882 );
883 assert_eq!(
884 format["includeTransactions"]["eventFormat"]["filtersByParty"]["alice"]["cumulative"]
885 [0]["identifierFilter"]["TemplateFilter"]["value"]["templateId"],
886 "#app:Mod:Asset"
887 );
888 }
889
890 #[test]
891 fn descending_and_any_party_reach_both_wire_shapes() {
892 let request = UpdatesRequest::new(vec!["alice".to_string()], 5)
893 .until(9)
894 .descending()
895 .for_any_party();
896
897 let body = request.clone().json_body();
898 assert_eq!(body["descendingOrder"], true);
899 let format = &body["updateFormat"]["includeTransactions"]["eventFormat"];
900 assert!(format["filtersForAnyParty"]["cumulative"].is_array());
901 assert!(format["filtersByParty"]["alice"].is_object());
902
903 let grpc = request.into_grpc();
904 assert!(grpc.descending_order);
905 let Some(format) = grpc
906 .update_format
907 .and_then(|f| f.include_transactions)
908 .and_then(|t| t.event_format)
909 else {
910 panic!("expected an event format");
911 };
912 assert!(format.filters_for_any_party.is_some());
913 assert!(format.filters_by_party.contains_key("alice"));
914
915 let plain = UpdatesRequest::new(vec!["alice".to_string()], 5).json_body();
917 assert!(plain.get("descendingOrder").is_none());
918 assert!(
919 plain["updateFormat"]["includeTransactions"]["eventFormat"]
920 .get("filtersForAnyParty")
921 .is_none()
922 );
923 }
924
925 #[test]
926 fn acs_any_party_reaches_both_wire_shapes() {
927 let request = ActiveContractsRequest::new(vec![], 7).for_any_party();
928
929 let body = request.json_body();
930 assert!(body["eventFormat"]["filtersForAnyParty"]["cumulative"].is_array());
931
932 let format = request.event_format();
933 assert!(format.filters_for_any_party.is_some());
934 assert!(format.filters_by_party.is_empty());
935 }
936
937 #[test]
938 #[cfg(feature = "ws")] fn completions_json_body_carries_the_user_id_only_when_set() {
940 let plain = CompletionsRequest::new(vec!["p".to_string()], 3).json_body();
941 assert!(plain.get("userId").is_none());
942 assert_eq!(plain["beginExclusive"], 3);
943
944 let scoped = CompletionsRequest::new(vec!["p".to_string()], 3)
945 .with_user_id("sync-tool")
946 .json_body();
947 assert_eq!(scoped["userId"], "sync-tool");
948 }
949
950 #[test]
951 fn completions_request_carries_the_user_id() {
952 let plain = CompletionsRequest::new(vec!["p".to_string()], 3).into_grpc();
953 assert_eq!(plain.user_id, "");
954 assert_eq!(plain.begin_exclusive, 3);
955
956 let scoped = CompletionsRequest::new(vec!["p".to_string()], 3)
957 .with_user_id("sync-tool")
958 .into_grpc();
959 assert_eq!(scoped.user_id, "sync-tool");
960 }
961}