1use crate::fork::{ForkError, ForkKind};
2use crate::graph::SourceRef;
3use crate::hello::{BackendDescriptor, OpVersions};
4use crate::kv::KvError;
5use crate::query::{Consistency, QueryError};
6use crate::result::ResultCode;
7use crate::topology::WireTopology;
8use serde::{Deserialize, Serialize};
9use std::collections::BTreeMap;
10
11pub const CAPABILITIES_PATH: &str = "/agdx/capabilities";
13pub const QUERY_PATH: &str = "/agdx/query";
15pub const PROJECTIONS_PATH: &str = "/agdx/projections";
17pub const BINDINGS_PATH: &str = "/agdx/bindings";
19pub const SCHEMAS_PATH: &str = "/agdx/schemas";
21pub const KV_PATH: &str = "/agdx/kv";
23pub const FORKS_PATH: &str = "/agdx/forks";
25pub const GRAPHS_PATH: &str = "/agdx/graphs";
27pub const CLIENTS_PATH: &str = "/agdx/clients";
31pub const RUNS_PATH: &str = "/agdx/runs";
35pub const AUTHZ_WHOAMI_PATH: &str = "/agdx/authz/whoami";
37pub const AUTHZ_ROLES_PATH: &str = "/agdx/authz/roles";
39
40pub fn authz_role_path(name: &str) -> String {
42 format!("{AUTHZ_ROLES_PATH}/{name}")
43}
44
45pub fn authz_user_roles_path(user_id: u32) -> String {
47 format!("/agdx/authz/users/{user_id}/roles")
48}
49
50pub fn graph_path(id: &str) -> String {
52 format!("{GRAPHS_PATH}/{id}")
53}
54
55pub fn graph_query_path(name: &str) -> String {
57 format!("/agdx/graph/{name}/query")
58}
59
60pub fn graph_neighbors_path(name: &str, node: &str) -> String {
62 format!("/agdx/graph/{name}/neighbors/{node}")
63}
64
65pub fn projection_path(id: &str) -> String {
67 format!("{PROJECTIONS_PATH}/{id}")
68}
69
70pub fn schema_path(id: u32) -> String {
72 format!("{SCHEMAS_PATH}/{id}")
73}
74
75pub fn schema_decode_path(id: u32) -> String {
77 format!("{SCHEMAS_PATH}/{id}/decode")
78}
79
80pub fn kv_namespace_path(namespace: &str) -> String {
82 format!("{KV_PATH}/{namespace}")
83}
84
85pub fn kv_entry_path(namespace: &str, key_b64: &str) -> String {
89 format!("{KV_PATH}/{namespace}/{key_b64}")
90}
91
92pub fn kv_cas_path(namespace: &str, key_b64: &str) -> String {
99 format!("{KV_PATH}/{namespace}/{key_b64}/cas")
100}
101
102pub fn fork_path(id: &str) -> String {
104 format!("{FORKS_PATH}/{id}")
105}
106
107pub fn fork_promote_path(id: &str) -> String {
109 format!("{FORKS_PATH}/{id}/promote")
110}
111
112pub fn fork_rows_path(id: &str) -> String {
114 format!("{FORKS_PATH}/{id}/rows")
115}
116
117pub fn run_path(id: &str) -> String {
119 format!("{RUNS_PATH}/{id}")
120}
121
122pub fn run_cancel_path(id: &str) -> String {
124 format!("{RUNS_PATH}/{id}/cancel")
125}
126
127#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
134#[non_exhaustive]
135pub struct Capabilities {
136 pub managed: bool,
140 pub query: QueryCapsView,
143 pub kv: KvCapsView,
145 #[serde(default)]
149 pub graph: bool,
150 pub fork: bool,
152 #[serde(default)]
155 pub agent_workflow: bool,
156 #[serde(default)]
160 pub watch: bool,
161 #[serde(default)]
164 pub authz: bool,
165 pub versions: OpVersions,
166 #[serde(default, skip_serializing_if = "Vec::is_empty")]
171 pub backends: Vec<BackendDescriptor>,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub topology: Option<WireTopology>,
179}
180
181#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
184pub struct QueryCapsView {
185 pub available: bool,
187 pub projections: bool,
189 pub schemas: bool,
191 #[serde(default)]
195 pub consistency: Consistency,
196 #[serde(default)]
200 pub keyword: bool,
201}
202
203#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
205pub struct KvCapsView {
206 pub available: bool,
208 #[serde(default)]
212 pub cas: bool,
213 #[serde(default)]
217 pub cas_fenced: bool,
218}
219
220impl Capabilities {
221 pub fn new(enabled: bool, versions: OpVersions) -> Self {
228 Self {
229 managed: enabled,
230 query: QueryCapsView {
231 available: enabled,
232 projections: enabled,
233 schemas: enabled,
234 consistency: Consistency::Eventual,
235 keyword: false,
236 },
237 kv: KvCapsView {
238 available: enabled,
239 cas: false,
240 cas_fenced: false,
241 },
242 graph: false,
243 fork: enabled,
244 agent_workflow: false,
245 watch: false,
246 authz: false,
247 versions,
248 backends: Vec::new(),
249 topology: None,
250 }
251 }
252
253 #[must_use]
256 pub fn with_graph(mut self, value: bool) -> Self {
257 self.graph = value;
258 self
259 }
260
261 #[must_use]
264 pub fn with_agent_workflow(mut self, value: bool) -> Self {
265 self.agent_workflow = value;
266 self
267 }
268
269 #[must_use]
271 pub fn with_query_keyword(mut self, value: bool) -> Self {
272 self.query.keyword = value;
273 self
274 }
275
276 #[must_use]
279 pub fn with_watch(mut self, value: bool) -> Self {
280 self.watch = value;
281 self
282 }
283
284 #[must_use]
286 pub fn with_authz(mut self, value: bool) -> Self {
287 self.authz = value;
288 self
289 }
290
291 #[must_use]
294 pub fn with_backends(mut self, backends: Vec<BackendDescriptor>) -> Self {
295 self.backends = backends;
296 self
297 }
298
299 #[must_use]
302 pub fn with_topology(mut self, topology: WireTopology) -> Self {
303 self.topology = Some(topology);
304 self
305 }
306
307 #[must_use]
310 pub fn with_kv_cas(mut self, on: bool) -> Self {
311 self.kv.cas = on;
312 self
313 }
314
315 #[must_use]
318 pub fn with_kv_cas_fenced(mut self, on: bool) -> Self {
319 self.kv.cas_fenced = on;
320 self
321 }
322
323 #[must_use]
325 pub fn with_query_consistency(mut self, level: Consistency) -> Self {
326 self.query.consistency = level;
327 self
328 }
329
330 pub fn from_versions(enabled: bool, versions: OpVersions) -> Self {
336 use crate::hello::feature;
337 let consistency = if versions.has_feature(feature::STRONG_CONSISTENCY) {
338 Consistency::Strong
339 } else if versions.has_feature(feature::READ_YOUR_WRITES) {
340 Consistency::ReadYourWrites
341 } else {
342 Consistency::Eventual
343 };
344 Self::new(enabled, versions)
345 .with_kv_cas(versions.has_feature(feature::KV_CAS))
346 .with_kv_cas_fenced(versions.has_feature(feature::KV_CAS_FENCED))
347 .with_agent_workflow(versions.has_feature(feature::AGENT_WORKFLOW))
348 .with_query_keyword(versions.has_feature(feature::KEYWORD_SEARCH))
349 .with_watch(versions.has_feature(feature::WATCH))
350 .with_authz(versions.has_feature(feature::AUTHZ))
351 .with_query_consistency(consistency)
352 .with_graph(enabled && versions.graph > 0)
355 }
356}
357
358#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
362pub struct KvEntryView {
363 pub key: String,
364 pub value: String,
365 pub expires_at_micros: Option<u64>,
366 #[serde(default, skip_serializing_if = "Option::is_none")]
371 pub scope: Option<crate::kv::MemoryRowScope>,
372 #[serde(default, skip_serializing_if = "Option::is_none")]
377 pub source: Option<crate::graph::SourceRef>,
378}
379
380#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
382pub struct KvPageView {
383 pub entries: Vec<KvEntryView>,
384 pub cursor: Option<String>,
385}
386
387#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
392pub struct RunPageView {
393 pub runs: Vec<crate::agent_workflow::AgentRunInfo>,
394 pub cursor: Option<String>,
395}
396
397#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
399pub struct DeletedManyView {
400 pub deleted: usize,
401}
402
403#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
408pub struct ClientMetadataView {
409 pub client_id: u32,
410 pub user_id: Option<u32>,
411 pub transport: u8,
412 pub address: String,
413 pub consumer_groups_count: u32,
414 #[serde(default, skip_serializing_if = "Option::is_none")]
415 pub metadata: Option<String>,
416}
417
418#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
421pub struct ClientMetadataListView {
422 pub clients: Vec<ClientMetadataView>,
423 #[serde(default, skip_serializing_if = "Option::is_none")]
424 pub next_cursor: Option<u32>,
425}
426
427#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
431pub struct ClientsQuery {
432 #[serde(default, skip_serializing_if = "core::ops::Not::not")]
434 pub with_metadata_only: bool,
435 #[serde(default, skip_serializing_if = "Option::is_none")]
437 pub user_id: Option<u32>,
438 #[serde(default, skip_serializing_if = "Option::is_none")]
440 pub after: Option<u32>,
441 #[serde(default, skip_serializing_if = "Option::is_none")]
443 pub limit: Option<u32>,
444}
445
446#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
448pub struct PromotedView {
449 pub rows: usize,
450}
451
452#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
456pub struct GraphNodeView {
457 pub id: String,
458 #[serde(default, skip_serializing_if = "Vec::is_empty")]
459 pub labels: Vec<String>,
460 #[serde(default, skip_serializing_if = "Vec::is_empty")]
461 pub attrs: Vec<(String, String)>,
462 #[serde(default, skip_serializing_if = "Option::is_none")]
464 pub source: Option<SourceRef>,
465}
466
467#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
470pub struct GraphEdgeView {
471 pub id: String,
472 pub from: String,
473 pub to: String,
474 pub edge_type: String,
475 pub weight: f32,
476 #[serde(default, skip_serializing_if = "Option::is_none")]
478 pub valid_from: Option<u64>,
479 #[serde(default, skip_serializing_if = "Option::is_none")]
480 pub valid_to: Option<u64>,
481 #[serde(default, skip_serializing_if = "Option::is_none")]
484 pub source: Option<SourceRef>,
485}
486
487#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
490pub struct GraphResultView {
491 #[serde(default, skip_serializing_if = "Vec::is_empty")]
492 pub nodes: Vec<GraphNodeView>,
493 #[serde(default, skip_serializing_if = "Vec::is_empty")]
494 pub edges: Vec<GraphEdgeView>,
495 #[serde(default, skip_serializing_if = "Vec::is_empty")]
496 pub paths: Vec<PathView>,
497}
498
499#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
502pub struct PathView {
503 pub nodes: Vec<String>,
504 pub edges: Vec<String>,
505}
506
507#[derive(Clone, Debug, Serialize, Deserialize)]
511pub struct RegisterSchemaBody {
512 pub source: crate::control::SchemaSource,
513 #[serde(default, skip_serializing_if = "Option::is_none")]
514 pub name: Option<String>,
515 #[serde(default, skip_serializing_if = "Option::is_none")]
516 pub version: Option<u32>,
517}
518
519#[derive(Clone, Debug, Serialize, Deserialize)]
522pub struct DecodeRecordBody {
523 pub payload: String,
524}
525
526#[derive(Clone, Debug, Serialize, Deserialize)]
528pub struct ForkCreateBody {
529 pub fork_id: String,
530 #[serde(default, skip_serializing_if = "Option::is_none")]
531 pub parent: Option<String>,
532 #[serde(default)]
533 pub kind: ForkKind,
534 #[serde(default, skip_serializing_if = "Vec::is_empty")]
535 pub tables: Vec<String>,
536}
537
538#[derive(Clone, Debug, Serialize, Deserialize)]
541pub struct ForkPutBody {
542 pub table: String,
543 pub partition_id: u32,
544 pub offset: u64,
545 #[serde(default)]
546 pub projection_id: String,
547 #[serde(default)]
548 pub projection_version: u32,
549 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
550 pub fields: BTreeMap<String, String>,
551 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
552 pub metadata: BTreeMap<String, String>,
553 #[serde(default, skip_serializing_if = "Option::is_none")]
554 pub payload_b64: Option<String>,
555 #[serde(default, skip_serializing_if = "Option::is_none")]
556 pub embedding: Option<String>,
557 #[serde(default)]
558 pub tombstone: bool,
559}
560
561#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
566pub struct RemoveBindingBody {
567 pub stream: String,
568 pub topic: String,
569 #[serde(default, skip_serializing_if = "Option::is_none")]
570 pub projection_ref: Option<String>,
571}
572
573#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
582pub struct ErrorBody {
583 pub code: ResultCode,
584 pub message: String,
585 #[serde(default, skip_serializing_if = "Option::is_none")]
586 pub detail: Option<serde_json::Value>,
587}
588
589impl ErrorBody {
590 pub fn new(code: ResultCode, message: impl Into<String>) -> Self {
592 Self {
593 code,
594 message: message.into(),
595 detail: None,
596 }
597 }
598
599 #[must_use]
601 pub fn with_detail(mut self, detail: serde_json::Value) -> Self {
602 self.detail = Some(detail);
603 self
604 }
605
606 pub fn http_status(&self) -> u16 {
609 self.code.http_status()
610 }
611}
612
613impl From<&QueryError> for ErrorBody {
614 fn from(error: &QueryError) -> Self {
615 Self::new(ResultCode::from(error), error.to_string())
616 }
617}
618
619impl From<&KvError> for ErrorBody {
620 fn from(error: &KvError) -> Self {
621 Self::new(ResultCode::from(error), error.to_string())
622 }
623}
624
625impl From<&ForkError> for ErrorBody {
626 fn from(error: &ForkError) -> Self {
627 Self::new(ResultCode::from(error), error.to_string())
628 }
629}
630
631impl From<&crate::agent_workflow::AgentError> for ErrorBody {
632 fn from(error: &crate::agent_workflow::AgentError) -> Self {
633 Self::new(ResultCode::from(error), error.to_string())
634 }
635}
636
637pub const PARAM_TOPIC: &str = "topic";
639pub const PARAM_NAME_CONTAINS: &str = "name_contains";
641pub const PARAM_ID_PREFIX: &str = "id_prefix";
643pub const PARAM_SEARCH: &str = "search";
648pub const PARAM_PREFIX: &str = "prefix";
650pub const PARAM_START: &str = "start";
652pub const PARAM_END: &str = "end";
654pub const PARAM_KEY_CONTAINS: &str = "key_contains";
656pub const PARAM_LIMIT: &str = "limit";
658pub const PARAM_CURSOR: &str = "cursor";
660pub const PARAM_EXPIRES_AT_MICROS: &str = "expires_at_micros";
662pub const PARAM_EXPECT_VERSION: &str = "expect_version";
665pub const PARAM_EXPECT_ABSENT: &str = "expect_absent";
668
669pub const KV_EXPIRES_AT_MICROS_HEADER: &str = "agdx-expires-at-micros";
675
676#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
681pub struct ProjectionListQuery {
682 #[serde(default, skip_serializing_if = "Option::is_none")]
683 pub topic: Option<String>,
684 #[serde(default, skip_serializing_if = "Option::is_none")]
685 pub name_contains: Option<String>,
686 #[serde(default, skip_serializing_if = "Option::is_none")]
687 pub id_prefix: Option<String>,
688 #[serde(default, skip_serializing_if = "Option::is_none")]
691 pub search: Option<String>,
692}
693
694#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
698pub struct SchemaListQuery {
699 #[serde(default, skip_serializing_if = "Option::is_none")]
700 pub name_contains: Option<String>,
701}
702
703#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
707pub struct KvScanQuery {
708 #[serde(default, skip_serializing_if = "Option::is_none")]
709 pub prefix: Option<String>,
710 #[serde(default, skip_serializing_if = "Option::is_none")]
711 pub start: Option<String>,
712 #[serde(default, skip_serializing_if = "Option::is_none")]
713 pub end: Option<String>,
714 #[serde(default, skip_serializing_if = "Option::is_none")]
715 pub key_contains: Option<String>,
716 #[serde(default, skip_serializing_if = "Option::is_none")]
721 pub conversation: Option<String>,
722 #[serde(default, skip_serializing_if = "Option::is_none")]
723 pub limit: Option<usize>,
724 #[serde(default, skip_serializing_if = "Option::is_none")]
725 pub cursor: Option<String>,
726}
727
728#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
730pub struct KvPutQuery {
731 #[serde(default, skip_serializing_if = "Option::is_none")]
732 pub expires_at_micros: Option<u64>,
733}
734
735#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
741pub struct GraphNeighborsQuery {
742 #[serde(default, skip_serializing_if = "Option::is_none")]
743 pub dir: Option<String>,
744 #[serde(default, skip_serializing_if = "Option::is_none")]
745 pub edge_type: Option<String>,
746 #[serde(default, skip_serializing_if = "Option::is_none")]
747 pub depth: Option<u32>,
748 #[serde(default, skip_serializing_if = "Option::is_none")]
749 pub limit: Option<usize>,
750 #[serde(default, skip_serializing_if = "Option::is_none")]
752 pub as_of: Option<u64>,
753 #[serde(default, skip_serializing_if = "Option::is_none")]
757 pub conversation: Option<String>,
758}
759
760#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
767pub struct RunsQuery {
768 #[serde(default, skip_serializing_if = "Option::is_none")]
769 pub agent_id: Option<String>,
770 #[serde(default, skip_serializing_if = "Option::is_none")]
771 pub state: Option<String>,
772 #[serde(default, skip_serializing_if = "Option::is_none")]
773 pub limit: Option<u32>,
774 #[serde(default, skip_serializing_if = "Option::is_none")]
775 pub cursor: Option<String>,
776}
777
778#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
783pub struct KvCasQuery {
784 #[serde(default, skip_serializing_if = "Option::is_none")]
785 pub expect_version: Option<u64>,
786 #[serde(default, skip_serializing_if = "Option::is_none")]
787 pub expect_absent: Option<bool>,
788 #[serde(default, skip_serializing_if = "Option::is_none")]
789 pub expires_at_micros: Option<u64>,
790}
791
792#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
795pub struct CasCommittedView {
796 pub version: u64,
797}
798
799#[cfg(test)]
800mod tests {
801 use super::*;
802
803 #[test]
804 fn given_path_builders_when_rendered_then_should_match_the_router() {
805 assert_eq!(CAPABILITIES_PATH, "/agdx/capabilities");
806 assert_eq!(projection_path("order.v1"), "/agdx/projections/order.v1");
807 assert_eq!(schema_path(7), "/agdx/schemas/7");
808 assert_eq!(schema_decode_path(7), "/agdx/schemas/7/decode");
809 assert_eq!(kv_namespace_path("sessions"), "/agdx/kv/sessions");
810 assert_eq!(
811 kv_entry_path("sessions", "dXNlcjo0Mg"),
812 "/agdx/kv/sessions/dXNlcjo0Mg"
813 );
814 assert_eq!(
815 kv_cas_path("sessions", "dXNlcjo0Mg"),
816 "/agdx/kv/sessions/dXNlcjo0Mg/cas"
817 );
818 assert_eq!(fork_path("f1"), "/agdx/forks/f1");
819 assert_eq!(fork_promote_path("f1"), "/agdx/forks/f1/promote");
820 assert_eq!(fork_rows_path("f1"), "/agdx/forks/f1/rows");
821 }
822
823 #[test]
824 fn given_capabilities_when_constructed_then_extended_features_default_off() {
825 let caps = Capabilities::new(true, OpVersions::new(1, 1, 1, 1));
826 assert!(
827 caps.query.available && caps.kv.available && caps.fork,
828 "core surfaces track enabled"
829 );
830 assert!(
831 !caps.kv.cas && caps.query.consistency == Consistency::Eventual,
832 "sub-features must be opt-in, never on by default"
833 );
834 let opted = caps
835 .with_kv_cas(true)
836 .with_query_consistency(Consistency::ReadYourWrites);
837 assert!(opted.kv.cas && opted.query.consistency == Consistency::ReadYourWrites);
838 }
839
840 #[test]
841 fn given_capabilities_backends_when_json_round_tripped_then_should_preserve_and_omit_empty() {
842 use crate::hello::BackendDescriptor;
843 let caps = Capabilities::new(true, OpVersions::new(1, 1, 1, 1)).with_backends(vec![
844 BackendDescriptor::new("embedded", "embedded"),
845 BackendDescriptor::new("warehouse", "columnar"),
846 ]);
847 let json = serde_json::to_string(&caps).expect("serializes");
848 let back: Capabilities = serde_json::from_str(&json).expect("deserializes");
849 assert_eq!(back.backends.len(), 2);
850 assert_eq!(back.backends[1].id, "warehouse");
851 assert_eq!(back.backends[1].kind, "columnar");
852
853 let plain = Capabilities::new(true, OpVersions::new(1, 1, 1, 1));
856 let json = serde_json::to_string(&plain).expect("json");
857 assert!(!json.contains("backends"), "empty backends omitted: {json}");
858 }
859
860 #[test]
861 fn given_capabilities_topology_when_json_round_tripped_then_should_preserve_and_omit_absent() {
862 let custom = WireTopology {
863 ops_stream: "custom-ops".to_owned(),
864 ..WireTopology::default()
865 };
866 let caps =
867 Capabilities::new(true, OpVersions::new(1, 1, 1, 1)).with_topology(custom.clone());
868 let json = serde_json::to_string(&caps).expect("serializes");
869 let back: Capabilities = serde_json::from_str(&json).expect("deserializes");
870 assert_eq!(back.topology, Some(custom));
871
872 let plain = Capabilities::new(true, OpVersions::new(1, 1, 1, 1));
875 let json = serde_json::to_string(&plain).expect("json");
876 assert!(
877 !json.contains("topology"),
878 "absent topology omitted: {json}"
879 );
880 }
881
882 #[test]
883 fn given_a_typed_error_when_made_into_a_body_then_should_carry_code_and_message() {
884 let body = ErrorBody::from(&QueryError::IndexNotFound("orders".to_owned()));
885 assert_eq!(body.code, ResultCode::NotFound);
886 assert_eq!(body.http_status(), 404);
887 assert!(body.message.contains("orders"));
888 let json = serde_json::to_string(&body).expect("serializes");
890 let back: ErrorBody = serde_json::from_str(&json).expect("deserializes");
891 assert_eq!(back, body);
892 }
893
894 #[test]
895 #[cfg(feature = "http-client")]
896 fn given_scan_filters_when_url_encoded_then_should_omit_absent_fields() {
897 let query = KvScanQuery {
898 prefix: Some("dXNlcjo".to_owned()),
899 limit: Some(50),
900 ..Default::default()
901 };
902 let encoded = serde_urlencoded::to_string(&query).expect("encodes");
903 assert_eq!(encoded, "prefix=dXNlcjo&limit=50");
904 assert!(encoded.contains(&format!("{PARAM_PREFIX}=")));
906 assert!(encoded.contains(&format!("{PARAM_LIMIT}=")));
907 }
908
909 #[test]
910 #[cfg(feature = "http-client")]
911 fn given_list_filters_when_url_encoded_then_field_names_match_the_param_consts() {
912 let projections = ProjectionListQuery {
913 name_contains: Some("order".to_owned()),
914 id_prefix: Some("order.".to_owned()),
915 ..Default::default()
916 };
917 let encoded = serde_urlencoded::to_string(&projections).expect("encodes");
918 assert_eq!(encoded, "name_contains=order&id_prefix=order.");
919 assert!(encoded.contains(&format!("{PARAM_NAME_CONTAINS}=")));
920 assert!(encoded.contains(&format!("{PARAM_ID_PREFIX}=")));
921
922 let schemas = SchemaListQuery {
923 name_contains: Some("Order".to_owned()),
924 };
925 assert_eq!(
926 serde_urlencoded::to_string(&schemas).expect("encodes"),
927 "name_contains=Order"
928 );
929 }
930}