1use crate::{
2 ProtocolVersion, PublicName,
3 domain::{DomainSelector, DomainSummary},
4 pagination::PageRequest,
5 public_error::{ProtocolError, ValidationIssue, ValidationIssueCode},
6 refs::{DomainRef, SessionSelector, TagSummary, VariableRef, VocabularyRef},
7 search::{PredicateSet, SearchCursorBinding, TagPredicate, TextPredicate},
8};
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(tag = "kind", rename_all = "snake_case")]
13pub enum VariableSelector {
14 Id {
15 variable: VariableRef,
16 #[serde(default, skip_serializing_if = "Option::is_none")]
17 domain: Option<DomainSelector>,
18 },
19 Name {
20 domain: DomainSelector,
21 name: PublicName,
22 },
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26pub struct VocabularySummary {
27 pub vocabulary: VocabularyRef,
28 pub domain: DomainRef,
29 pub name: String,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub item_count: Option<usize>,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub description: Option<String>,
35 #[serde(default, skip_serializing_if = "Vec::is_empty")]
36 pub tags: Vec<TagSummary>,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct VocabularyItemSummary {
41 pub value: i32,
42 pub code: String,
43 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub description: Option<String>,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub struct VocabularyDetail {
49 #[serde(default)]
50 pub items_withheld: bool,
51 pub summary: VocabularySummary,
52 #[serde(default)]
53 pub items: Vec<VocabularyItemSummary>,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57pub struct VariableSummary {
58 pub variable: VariableRef,
59 pub domain: DomainRef,
60 pub name: String,
61 pub value_type: String,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub vocabulary: Option<VocabularySummary>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub key_role: Option<String>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub description: Option<String>,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub ontology_class: Option<String>,
70 #[serde(default)]
71 pub tags: Vec<TagSummary>,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75pub struct VariableDetail {
76 #[serde(default)]
77 pub vocabulary_items_withheld: bool,
78 pub summary: VariableSummary,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub value_format: Option<String>,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub note: Option<String>,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub ontology_namespace: Option<String>,
85 #[serde(default)]
86 pub vocabulary_items: Vec<VocabularyItemSummary>,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub domain_detail: Option<DomainSummary>,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92pub struct DictionaryWarnings {
93 #[serde(default, skip_serializing_if = "Vec::is_empty")]
94 pub warnings: Vec<String>,
95 #[serde(default, skip_serializing_if = "Vec::is_empty")]
96 pub validation_errors: Vec<String>,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100pub struct DictionaryMutationIntent {
101 #[serde(default)]
102 pub force_update: bool,
103 #[serde(default)]
104 pub check_dependencies: bool,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
108pub struct ListVariablesRequest {
109 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub session: Option<SessionSelector>,
111 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub domain: Option<DomainSelector>,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub study: Option<crate::study::StudySelector>,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
118pub struct GetVariableRequest {
119 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub session: Option<SessionSelector>,
121 pub variable: VariableSelector,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125pub struct AddVariableRequest {
126 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub session: Option<SessionSelector>,
128 pub domain: DomainSelector,
129 pub name: PublicName,
130 pub value_type: String,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub value_format: Option<String>,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub key_role: Option<String>,
135 #[serde(default, skip_serializing_if = "Option::is_none")]
136 pub description: Option<String>,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub ontology_namespace: Option<String>,
139 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub ontology_class: Option<String>,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
142 pub vocabulary: Option<PublicName>,
143 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub vocabulary_items: Option<String>,
145 pub intent: DictionaryMutationIntent,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149pub struct UpdateVariableRequest {
150 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub session: Option<SessionSelector>,
152 pub variable: VariableSelector,
153 #[serde(default, skip_serializing_if = "Option::is_none")]
154 pub value_type: Option<String>,
155 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub value_format: Option<String>,
157 #[serde(default, skip_serializing_if = "Option::is_none")]
158 pub key_role: Option<String>,
159 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub description: Option<String>,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub ontology_namespace: Option<String>,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub ontology_class: Option<String>,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub vocabulary: Option<PublicName>,
167 pub intent: DictionaryMutationIntent,
168}
169
170#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
171pub struct VariableListResponse {
172 pub variables: Vec<VariableSummary>,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
174 pub warnings: Option<DictionaryWarnings>,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
178pub struct VariableDetailResponse {
179 pub variable: VariableDetail,
180 #[serde(default, skip_serializing_if = "Option::is_none")]
181 pub warnings: Option<DictionaryWarnings>,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
185#[serde(tag = "kind", rename_all = "snake_case")]
186pub enum VocabularySelector {
187 Id {
188 vocabulary: VocabularyRef,
189 #[serde(default, skip_serializing_if = "Option::is_none")]
190 domain: Option<DomainSelector>,
191 },
192 Name {
193 domain: DomainSelector,
194 name: PublicName,
195 },
196}
197
198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
199pub struct ListVocabulariesRequest {
200 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub session: Option<SessionSelector>,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub domain: Option<DomainSelector>,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub study: Option<crate::study::StudySelector>,
206}
207
208#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
209pub struct GetVocabularyRequest {
210 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub session: Option<SessionSelector>,
212 pub vocabulary: VocabularySelector,
213}
214
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
216pub struct AddVocabularyRequest {
217 #[serde(default, skip_serializing_if = "Option::is_none")]
218 pub session: Option<SessionSelector>,
219 pub domain: DomainSelector,
220 pub name: PublicName,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
222 pub description: Option<String>,
223 pub items: String,
224 pub intent: DictionaryMutationIntent,
225}
226
227#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
228pub struct VocabularyListResponse {
229 pub vocabularies: Vec<VocabularySummary>,
230 #[serde(default, skip_serializing_if = "Option::is_none")]
231 pub warnings: Option<DictionaryWarnings>,
232}
233
234#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
235pub struct VocabularyDetailResponse {
236 pub vocabulary: VocabularyDetail,
237 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub warnings: Option<DictionaryWarnings>,
239}
240
241#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
242pub struct SearchVariablesRequest {
243 #[serde(default, skip_serializing_if = "Option::is_none")]
244 pub session: Option<SessionSelector>,
245 pub predicates: PredicateSet<VariablePredicate>,
246 pub page: PageRequest,
247}
248
249#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
250pub struct BrowserVariableSearchRequest {
251 pub datastore_id: crate::PublicUuid,
252 pub search: SearchVariablesRequest,
253}
254
255#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
256pub struct VariableSearchCursorAnchor {
257 pub domain: DomainRef,
258 pub variable: VariableRef,
259 pub name: String,
260}
261
262#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
263pub struct VariableSearchCursorQuery {
264 #[serde(default, skip_serializing_if = "Option::is_none")]
265 pub session: Option<SessionSelector>,
266 pub predicates: PredicateSet<VariablePredicate>,
267}
268
269#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
270pub struct VariableSearchCursor {
271 pub binding: SearchCursorBinding<VariableSearchCursorQuery>,
272 pub after: VariableSearchCursorAnchor,
273}
274
275#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
277pub struct BrowserVariableSearchResponse {
278 pub protocol_version: ProtocolVersion,
279 pub datastore_id: crate::PublicUuid,
280 pub family: String,
281 pub predicates: PredicateSet<VariablePredicate>,
282 #[serde(default)]
283 pub variables: Vec<crate::dictionary_discovery::BrowserDictionaryVariableDiscoveryRecord>,
284 pub returned_count: u16,
285 pub requested_limit: u16,
286 pub ordering: crate::study::BrowserSearchOrdering,
287 pub has_more: bool,
288 #[serde(default, skip_serializing_if = "Option::is_none")]
289 pub next_cursor: Option<String>,
290 #[serde(default, skip_serializing_if = "Option::is_none")]
291 pub empty_state: Option<BrowserVariableSearchEmptyState>,
292}
293
294#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
295pub struct BrowserVariableSearchEmptyState {
296 pub code: String,
297 pub message: String,
298}
299
300#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
301pub struct BrowserVariableSearchCursorQuery {
302 pub datastore_id: crate::PublicUuid,
303 pub family: String,
304 pub predicates: PredicateSet<VariablePredicate>,
305}
306
307#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
308pub struct BrowserVariableSearchCursor {
309 pub binding: SearchCursorBinding<BrowserVariableSearchCursorQuery>,
310 pub after: VariableSearchCursorAnchor,
311}
312
313#[allow(clippy::result_large_err)]
314impl SearchVariablesRequest {
315 pub fn validate(&mut self, protocol_version: &ProtocolVersion) -> Result<(), ProtocolError> {
316 let mut issues = Vec::new();
317 collect_predicate_set_issues("body.predicates", &self.predicates, &mut issues);
318 collect_variable_predicate_issues(&mut self.predicates, &mut issues);
319 collect_page_issues("body.page.limit", self.page.limit, &mut issues);
320 collect_cursor_issues(self, protocol_version, &mut issues);
321
322 if issues.is_empty() {
323 Ok(())
324 } else {
325 Err(ProtocolError::validation(
326 "variable search request is invalid",
327 issues,
328 ))
329 }
330 }
331
332 pub fn decoded_cursor(
333 &self,
334 protocol_version: &ProtocolVersion,
335 ) -> Result<Option<VariableSearchCursorAnchor>, ProtocolError> {
336 match self.page.cursor.as_deref() {
337 None => Ok(None),
338 Some(raw_cursor) => {
339 let cursor: VariableSearchCursor =
340 serde_json::from_str(raw_cursor).map_err(|error| {
341 ProtocolError::validation(
342 "variable search request is invalid",
343 vec![ValidationIssue::new(
344 "body.page.cursor",
345 ValidationIssueCode::InvalidFormat,
346 format!("variable search cursor is invalid: {error}"),
347 )],
348 )
349 })?;
350 let expected = self.cursor_binding(protocol_version);
351 if cursor.binding != expected {
352 return Err(ProtocolError::validation(
353 "variable search request is invalid",
354 vec![ValidationIssue::new(
355 "body.page.cursor",
356 ValidationIssueCode::Conflict,
357 "variable search cursor does not match the request predicates, page limit, or protocol version",
358 )],
359 ));
360 }
361 Ok(Some(cursor.after))
362 }
363 }
364 }
365
366 pub fn encode_cursor(
367 &self,
368 protocol_version: &ProtocolVersion,
369 after: VariableSearchCursorAnchor,
370 ) -> Result<String, ProtocolError> {
371 serde_json::to_string(&VariableSearchCursor {
372 binding: self.cursor_binding(protocol_version),
373 after,
374 })
375 .map_err(|error| {
376 ProtocolError::new(
377 crate::public_error::ProtocolErrorCode::Internal,
378 format!("failed to encode variable search cursor: {error}"),
379 )
380 })
381 }
382
383 fn cursor_binding(
384 &self,
385 protocol_version: &ProtocolVersion,
386 ) -> SearchCursorBinding<VariableSearchCursorQuery> {
387 SearchCursorBinding {
388 query: VariableSearchCursorQuery {
389 session: self.session.clone(),
390 predicates: self.predicates.clone(),
391 },
392 page_limit: self.page.limit,
393 protocol_version: protocol_version.to_string(),
394 }
395 }
396
397 #[allow(clippy::result_large_err)]
398 pub fn validate_for_browser(
399 &mut self,
400 datastore_id: crate::PublicUuid,
401 protocol_version: &ProtocolVersion,
402 ) -> Result<(), ProtocolError> {
403 let mut issues = Vec::new();
404 if self.session.is_some() {
405 issues.push(ValidationIssue::new(
406 "body.session",
407 ValidationIssueCode::Unsupported,
408 "daemon session is not accepted for browser dictionary-variable search",
409 ));
410 }
411 collect_predicate_set_issues("body.predicates", &self.predicates, &mut issues);
412 collect_variable_predicate_issues(&mut self.predicates, &mut issues);
413 collect_page_issues("body.page.limit", self.page.limit, &mut issues);
414 if let Some(raw_cursor) = self.page.cursor.as_deref() {
415 match serde_json::from_str::<BrowserVariableSearchCursor>(raw_cursor) {
416 Ok(cursor)
417 if cursor.binding
418 == self.browser_cursor_binding(datastore_id, protocol_version) => {}
419 Ok(_) => issues.push(ValidationIssue::new(
420 "body.page.cursor",
421 ValidationIssueCode::Conflict,
422 "browser dictionary-variable search cursor does not match the datastore, search family, predicates, page limit, or protocol version",
423 )),
424 Err(_) => issues.push(ValidationIssue::new(
425 "body.page.cursor",
426 ValidationIssueCode::InvalidFormat,
427 "browser dictionary-variable search cursor is invalid",
428 )),
429 }
430 }
431 if issues.is_empty() {
432 Ok(())
433 } else {
434 Err(ProtocolError::validation(
435 "browser dictionary-variable search request is invalid",
436 issues,
437 ))
438 }
439 }
440
441 #[allow(clippy::result_large_err)]
442 pub fn browser_decoded_cursor(
443 &self,
444 datastore_id: crate::PublicUuid,
445 protocol_version: &ProtocolVersion,
446 ) -> Result<Option<VariableSearchCursorAnchor>, ProtocolError> {
447 let Some(raw_cursor) = self.page.cursor.as_deref() else {
448 return Ok(None);
449 };
450 let cursor: BrowserVariableSearchCursor =
451 serde_json::from_str(raw_cursor).map_err(|_| {
452 ProtocolError::validation(
453 "browser dictionary-variable search request is invalid",
454 vec![ValidationIssue::new(
455 "body.page.cursor",
456 ValidationIssueCode::InvalidFormat,
457 "browser dictionary-variable search cursor is invalid",
458 )],
459 )
460 })?;
461 if cursor.binding != self.browser_cursor_binding(datastore_id, protocol_version) {
462 return Err(ProtocolError::validation(
463 "browser dictionary-variable search request is invalid",
464 vec![ValidationIssue::new(
465 "body.page.cursor",
466 ValidationIssueCode::Conflict,
467 "browser dictionary-variable search cursor does not match the datastore, search family, predicates, page limit, or protocol version",
468 )],
469 ));
470 }
471 Ok(Some(cursor.after))
472 }
473
474 #[allow(clippy::result_large_err)]
475 pub fn encode_browser_cursor(
476 &self,
477 datastore_id: crate::PublicUuid,
478 protocol_version: &ProtocolVersion,
479 after: VariableSearchCursorAnchor,
480 ) -> Result<String, ProtocolError> {
481 serde_json::to_string(&BrowserVariableSearchCursor {
482 binding: self.browser_cursor_binding(datastore_id, protocol_version),
483 after,
484 })
485 .map_err(|_| {
486 ProtocolError::new(
487 crate::public_error::ProtocolErrorCode::Internal,
488 "failed to encode browser dictionary-variable search cursor",
489 )
490 })
491 }
492
493 fn browser_cursor_binding(
494 &self,
495 datastore_id: crate::PublicUuid,
496 protocol_version: &ProtocolVersion,
497 ) -> SearchCursorBinding<BrowserVariableSearchCursorQuery> {
498 SearchCursorBinding {
499 query: BrowserVariableSearchCursorQuery {
500 datastore_id,
501 family: "dictionary_variables".to_string(),
502 predicates: self.predicates.clone(),
503 },
504 page_limit: self.page.limit,
505 protocol_version: protocol_version.to_string(),
506 }
507 }
508}
509
510#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
511#[serde(tag = "kind", rename_all = "snake_case")]
512pub enum VariablePredicate {
513 Text(TextPredicate<VariableTextField>),
514 Domain { domain: DomainSelector },
515 ValueType { value_type: String },
516 Vocabulary { vocabulary: VocabularyRef },
517 KeyRole { key_role: String },
518 Tag(TagPredicate),
519}
520
521#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
522#[serde(rename_all = "snake_case")]
523pub enum VariableTextField {
524 Name,
525 Description,
526 ValueType,
527 VocabularyName,
528 OntologyClass,
529}
530
531#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
532pub struct VariableSearchResponse {
533 pub session: crate::session::SessionStatusPayload,
534 pub variables: Vec<VariableSummary>,
535 #[serde(default, skip_serializing_if = "Option::is_none")]
536 pub next_cursor: Option<String>,
537}
538
539fn collect_predicate_set_issues<P>(
540 path: &str,
541 predicates: &PredicateSet<P>,
542 issues: &mut Vec<ValidationIssue>,
543) {
544 if let Err(message) = predicates.validate() {
545 issues.push(ValidationIssue::new(
546 path,
547 ValidationIssueCode::InvalidValue,
548 message,
549 ));
550 }
551}
552
553fn collect_variable_predicate_issues(
554 predicates: &mut PredicateSet<VariablePredicate>,
555 issues: &mut Vec<ValidationIssue>,
556) {
557 for (index, predicate) in predicates.predicates_mut().iter_mut().enumerate() {
558 match predicate {
559 VariablePredicate::Text(text) => {
560 if let Err(message) = text.validate() {
561 issues.push(ValidationIssue::new(
562 format!("body.predicates.predicates[{index}].value"),
563 ValidationIssueCode::InvalidValue,
564 message,
565 ));
566 }
567 }
568 VariablePredicate::ValueType { value_type } => {
569 collect_bounded_string_issue(
570 value_type,
571 format!("body.predicates.predicates[{index}].value_type"),
572 "variable value type",
573 issues,
574 );
575 }
576 VariablePredicate::KeyRole { key_role } => {
577 collect_bounded_string_issue(
578 key_role,
579 format!("body.predicates.predicates[{index}].key_role"),
580 "variable key role",
581 issues,
582 );
583 }
584 VariablePredicate::Domain { .. }
585 | VariablePredicate::Vocabulary { .. }
586 | VariablePredicate::Tag(_) => {}
587 }
588 }
589}
590
591fn collect_bounded_string_issue(
592 value: &mut String,
593 path: String,
594 label: &str,
595 issues: &mut Vec<ValidationIssue>,
596) {
597 *value = value.trim().to_string();
598 if value.is_empty() {
599 issues.push(ValidationIssue::new(
600 path,
601 ValidationIssueCode::TooShort,
602 format!("{label} must not be empty"),
603 ));
604 } else if value.len() > crate::search::MAX_TEXT_VALUE_LENGTH {
605 issues.push(ValidationIssue::new(
606 path,
607 ValidationIssueCode::TooLong,
608 format!(
609 "{label} must be at most {} characters",
610 crate::search::MAX_TEXT_VALUE_LENGTH
611 ),
612 ));
613 }
614}
615
616fn collect_page_issues(path: &str, limit: u16, issues: &mut Vec<ValidationIssue>) {
617 if limit == 0 || limit > crate::search::MAX_PAGE_LIMIT {
618 issues.push(ValidationIssue::new(
619 path,
620 ValidationIssueCode::OutOfRange,
621 if limit == 0 {
622 "page limit must be greater than zero".to_string()
623 } else {
624 format!(
625 "page limit must be at most {}",
626 crate::search::MAX_PAGE_LIMIT
627 )
628 },
629 ));
630 }
631}
632
633fn collect_cursor_issues(
634 request: &SearchVariablesRequest,
635 protocol_version: &ProtocolVersion,
636 issues: &mut Vec<ValidationIssue>,
637) {
638 if let Some(raw_cursor) = request.page.cursor.as_deref() {
639 let cursor: VariableSearchCursor = match serde_json::from_str(raw_cursor) {
640 Ok(cursor) => cursor,
641 Err(error) => {
642 issues.push(ValidationIssue::new(
643 "body.page.cursor",
644 ValidationIssueCode::InvalidFormat,
645 format!("variable search cursor is invalid: {error}"),
646 ));
647 return;
648 }
649 };
650
651 if cursor.binding != request.cursor_binding(protocol_version) {
652 issues.push(ValidationIssue::new(
653 "body.page.cursor",
654 ValidationIssueCode::Conflict,
655 "variable search cursor does not match the request predicates, page limit, or protocol version",
656 ));
657 }
658 }
659}