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 serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9
10pub const CAPABILITIES_PATH: &str = "/agdx/capabilities";
12pub const QUERY_PATH: &str = "/agdx/query";
14pub const PROJECTIONS_PATH: &str = "/agdx/projections";
16pub const BINDINGS_PATH: &str = "/agdx/bindings";
18pub const SCHEMAS_PATH: &str = "/agdx/schemas";
20pub const KV_PATH: &str = "/agdx/kv";
22pub const FORKS_PATH: &str = "/agdx/forks";
24pub const GRAPHS_PATH: &str = "/agdx/graphs";
26pub const CLIENTS_PATH: &str = "/agdx/clients";
30pub const RUNS_PATH: &str = "/agdx/runs";
34pub const AUTHZ_WHOAMI_PATH: &str = "/agdx/authz/whoami";
36pub const AUTHZ_ROLES_PATH: &str = "/agdx/authz/roles";
38
39pub fn authz_role_path(name: &str) -> String {
41 format!("{AUTHZ_ROLES_PATH}/{name}")
42}
43
44pub fn authz_user_roles_path(user_id: u32) -> String {
46 format!("/agdx/authz/users/{user_id}/roles")
47}
48
49pub fn graph_path(id: &str) -> String {
51 format!("{GRAPHS_PATH}/{id}")
52}
53
54pub fn graph_query_path(name: &str) -> String {
56 format!("/agdx/graph/{name}/query")
57}
58
59pub fn graph_neighbors_path(name: &str, node: &str) -> String {
61 format!("/agdx/graph/{name}/neighbors/{node}")
62}
63
64pub fn projection_path(id: &str) -> String {
66 format!("{PROJECTIONS_PATH}/{id}")
67}
68
69pub fn schema_path(id: u32) -> String {
71 format!("{SCHEMAS_PATH}/{id}")
72}
73
74pub fn schema_decode_path(id: u32) -> String {
76 format!("{SCHEMAS_PATH}/{id}/decode")
77}
78
79pub fn kv_namespace_path(namespace: &str) -> String {
81 format!("{KV_PATH}/{namespace}")
82}
83
84pub fn kv_entry_path(namespace: &str, key_b64: &str) -> String {
88 format!("{KV_PATH}/{namespace}/{key_b64}")
89}
90
91pub fn kv_cas_path(namespace: &str, key_b64: &str) -> String {
98 format!("{KV_PATH}/{namespace}/{key_b64}/cas")
99}
100
101pub fn fork_path(id: &str) -> String {
103 format!("{FORKS_PATH}/{id}")
104}
105
106pub fn fork_promote_path(id: &str) -> String {
108 format!("{FORKS_PATH}/{id}/promote")
109}
110
111pub fn fork_rows_path(id: &str) -> String {
113 format!("{FORKS_PATH}/{id}/rows")
114}
115
116pub fn run_path(id: &str) -> String {
118 format!("{RUNS_PATH}/{id}")
119}
120
121pub fn run_cancel_path(id: &str) -> String {
123 format!("{RUNS_PATH}/{id}/cancel")
124}
125
126#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
133#[non_exhaustive]
134pub struct Capabilities {
135 pub managed: bool,
139 pub query: QueryCapsView,
142 pub kv: KvCapsView,
144 #[serde(default)]
148 pub graph: bool,
149 pub fork: bool,
151 #[serde(default)]
154 pub agent_workflow: bool,
155 #[serde(default)]
159 pub watch: bool,
160 #[serde(default)]
163 pub authz: bool,
164 pub versions: OpVersions,
165 #[serde(default, skip_serializing_if = "Vec::is_empty")]
170 pub backends: Vec<BackendDescriptor>,
171}
172
173#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
176pub struct QueryCapsView {
177 pub available: bool,
179 pub projections: bool,
181 pub schemas: bool,
183 #[serde(default)]
187 pub consistency: Consistency,
188 #[serde(default)]
192 pub keyword: bool,
193}
194
195#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
197pub struct KvCapsView {
198 pub available: bool,
200 #[serde(default)]
204 pub cas: bool,
205 #[serde(default)]
209 pub cas_fenced: bool,
210}
211
212impl Capabilities {
213 pub fn new(enabled: bool, versions: OpVersions) -> Self {
220 Self {
221 managed: enabled,
222 query: QueryCapsView {
223 available: enabled,
224 projections: enabled,
225 schemas: enabled,
226 consistency: Consistency::Eventual,
227 keyword: false,
228 },
229 kv: KvCapsView {
230 available: enabled,
231 cas: false,
232 cas_fenced: false,
233 },
234 graph: false,
235 fork: enabled,
236 agent_workflow: false,
237 watch: false,
238 authz: false,
239 versions,
240 backends: Vec::new(),
241 }
242 }
243
244 #[must_use]
247 pub fn with_graph(mut self, value: bool) -> Self {
248 self.graph = value;
249 self
250 }
251
252 #[must_use]
255 pub fn with_agent_workflow(mut self, value: bool) -> Self {
256 self.agent_workflow = value;
257 self
258 }
259
260 #[must_use]
262 pub fn with_query_keyword(mut self, value: bool) -> Self {
263 self.query.keyword = value;
264 self
265 }
266
267 #[must_use]
270 pub fn with_watch(mut self, value: bool) -> Self {
271 self.watch = value;
272 self
273 }
274
275 #[must_use]
277 pub fn with_authz(mut self, value: bool) -> Self {
278 self.authz = value;
279 self
280 }
281
282 #[must_use]
285 pub fn with_backends(mut self, backends: Vec<BackendDescriptor>) -> Self {
286 self.backends = backends;
287 self
288 }
289
290 #[must_use]
293 pub fn with_kv_cas(mut self, on: bool) -> Self {
294 self.kv.cas = on;
295 self
296 }
297
298 #[must_use]
301 pub fn with_kv_cas_fenced(mut self, on: bool) -> Self {
302 self.kv.cas_fenced = on;
303 self
304 }
305
306 #[must_use]
308 pub fn with_query_consistency(mut self, level: Consistency) -> Self {
309 self.query.consistency = level;
310 self
311 }
312
313 pub fn from_versions(enabled: bool, versions: OpVersions) -> Self {
319 use crate::hello::feature;
320 let consistency = if versions.has_feature(feature::STRONG_CONSISTENCY) {
321 Consistency::Strong
322 } else if versions.has_feature(feature::READ_YOUR_WRITES) {
323 Consistency::ReadYourWrites
324 } else {
325 Consistency::Eventual
326 };
327 Self::new(enabled, versions)
328 .with_kv_cas(versions.has_feature(feature::KV_CAS))
329 .with_kv_cas_fenced(versions.has_feature(feature::KV_CAS_FENCED))
330 .with_agent_workflow(versions.has_feature(feature::AGENT_WORKFLOW))
331 .with_query_keyword(versions.has_feature(feature::KEYWORD_SEARCH))
332 .with_watch(versions.has_feature(feature::WATCH))
333 .with_authz(versions.has_feature(feature::AUTHZ))
334 .with_query_consistency(consistency)
335 .with_graph(enabled && versions.graph > 0)
338 }
339}
340
341#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
345pub struct KvEntryView {
346 pub key: String,
347 pub value: String,
348 pub expires_at_micros: Option<u64>,
349 #[serde(default, skip_serializing_if = "Option::is_none")]
354 pub scope: Option<crate::kv::MemoryRowScope>,
355}
356
357#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
359pub struct KvPageView {
360 pub entries: Vec<KvEntryView>,
361 pub cursor: Option<String>,
362}
363
364#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
369pub struct RunPageView {
370 pub runs: Vec<crate::agent_workflow::AgentRunInfo>,
371 pub cursor: Option<String>,
372}
373
374#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
376pub struct DeletedManyView {
377 pub deleted: usize,
378}
379
380#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
385pub struct ClientMetadataView {
386 pub client_id: u32,
387 pub user_id: Option<u32>,
388 pub transport: u8,
389 pub address: String,
390 pub consumer_groups_count: u32,
391 #[serde(default, skip_serializing_if = "Option::is_none")]
392 pub metadata: Option<String>,
393}
394
395#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
398pub struct ClientMetadataListView {
399 pub clients: Vec<ClientMetadataView>,
400 #[serde(default, skip_serializing_if = "Option::is_none")]
401 pub next_cursor: Option<u32>,
402}
403
404#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
408pub struct ClientsQuery {
409 #[serde(default, skip_serializing_if = "core::ops::Not::not")]
411 pub with_metadata_only: bool,
412 #[serde(default, skip_serializing_if = "Option::is_none")]
414 pub user_id: Option<u32>,
415 #[serde(default, skip_serializing_if = "Option::is_none")]
417 pub after: Option<u32>,
418 #[serde(default, skip_serializing_if = "Option::is_none")]
420 pub limit: Option<u32>,
421}
422
423#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
425pub struct PromotedView {
426 pub rows: usize,
427}
428
429#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
433pub struct GraphNodeView {
434 pub id: String,
435 #[serde(default, skip_serializing_if = "Vec::is_empty")]
436 pub labels: Vec<String>,
437 #[serde(default, skip_serializing_if = "Vec::is_empty")]
438 pub attrs: Vec<(String, String)>,
439 #[serde(default, skip_serializing_if = "Option::is_none")]
441 pub source: Option<SourceRef>,
442}
443
444#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
447pub struct GraphEdgeView {
448 pub id: String,
449 pub from: String,
450 pub to: String,
451 pub edge_type: String,
452 pub weight: f32,
453 #[serde(default, skip_serializing_if = "Option::is_none")]
455 pub valid_from: Option<u64>,
456 #[serde(default, skip_serializing_if = "Option::is_none")]
457 pub valid_to: Option<u64>,
458 #[serde(default, skip_serializing_if = "Option::is_none")]
461 pub source: Option<SourceRef>,
462}
463
464#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
467pub struct GraphResultView {
468 #[serde(default, skip_serializing_if = "Vec::is_empty")]
469 pub nodes: Vec<GraphNodeView>,
470 #[serde(default, skip_serializing_if = "Vec::is_empty")]
471 pub edges: Vec<GraphEdgeView>,
472 #[serde(default, skip_serializing_if = "Vec::is_empty")]
473 pub paths: Vec<PathView>,
474}
475
476#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
479pub struct PathView {
480 pub nodes: Vec<String>,
481 pub edges: Vec<String>,
482}
483
484#[derive(Clone, Debug, Serialize, Deserialize)]
488pub struct RegisterSchemaBody {
489 pub source: crate::control::SchemaSource,
490 #[serde(default, skip_serializing_if = "Option::is_none")]
491 pub name: Option<String>,
492 #[serde(default, skip_serializing_if = "Option::is_none")]
493 pub version: Option<u32>,
494}
495
496#[derive(Clone, Debug, Serialize, Deserialize)]
499pub struct DecodeRecordBody {
500 pub payload: String,
501}
502
503#[derive(Clone, Debug, Serialize, Deserialize)]
505pub struct ForkCreateBody {
506 pub fork_id: String,
507 #[serde(default, skip_serializing_if = "Option::is_none")]
508 pub parent: Option<String>,
509 #[serde(default)]
510 pub kind: ForkKind,
511 #[serde(default, skip_serializing_if = "Vec::is_empty")]
512 pub tables: Vec<String>,
513}
514
515#[derive(Clone, Debug, Serialize, Deserialize)]
518pub struct ForkPutBody {
519 pub table: String,
520 pub partition_id: u32,
521 pub offset: u64,
522 #[serde(default)]
523 pub projection_id: String,
524 #[serde(default)]
525 pub projection_version: u32,
526 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
527 pub fields: BTreeMap<String, String>,
528 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
529 pub metadata: BTreeMap<String, String>,
530 #[serde(default, skip_serializing_if = "Option::is_none")]
531 pub payload_b64: Option<String>,
532 #[serde(default, skip_serializing_if = "Option::is_none")]
533 pub embedding: Option<String>,
534 #[serde(default)]
535 pub tombstone: bool,
536}
537
538#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
543pub struct RemoveBindingBody {
544 pub stream: String,
545 pub topic: String,
546 #[serde(default, skip_serializing_if = "Option::is_none")]
547 pub projection_ref: Option<String>,
548}
549
550#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
559pub struct ErrorBody {
560 pub code: ResultCode,
561 pub message: String,
562 #[serde(default, skip_serializing_if = "Option::is_none")]
563 pub detail: Option<serde_json::Value>,
564}
565
566impl ErrorBody {
567 pub fn new(code: ResultCode, message: impl Into<String>) -> Self {
569 Self {
570 code,
571 message: message.into(),
572 detail: None,
573 }
574 }
575
576 #[must_use]
578 pub fn with_detail(mut self, detail: serde_json::Value) -> Self {
579 self.detail = Some(detail);
580 self
581 }
582
583 pub fn http_status(&self) -> u16 {
586 self.code.http_status()
587 }
588}
589
590impl From<&QueryError> for ErrorBody {
591 fn from(error: &QueryError) -> Self {
592 Self::new(ResultCode::from(error), error.to_string())
593 }
594}
595
596impl From<&KvError> for ErrorBody {
597 fn from(error: &KvError) -> Self {
598 Self::new(ResultCode::from(error), error.to_string())
599 }
600}
601
602impl From<&ForkError> for ErrorBody {
603 fn from(error: &ForkError) -> Self {
604 Self::new(ResultCode::from(error), error.to_string())
605 }
606}
607
608impl From<&crate::agent_workflow::AgentError> for ErrorBody {
609 fn from(error: &crate::agent_workflow::AgentError) -> Self {
610 Self::new(ResultCode::from(error), error.to_string())
611 }
612}
613
614pub const PARAM_TOPIC: &str = "topic";
616pub const PARAM_NAME_CONTAINS: &str = "name_contains";
618pub const PARAM_ID_PREFIX: &str = "id_prefix";
620pub const PARAM_SEARCH: &str = "search";
625pub const PARAM_PREFIX: &str = "prefix";
627pub const PARAM_START: &str = "start";
629pub const PARAM_END: &str = "end";
631pub const PARAM_KEY_CONTAINS: &str = "key_contains";
633pub const PARAM_LIMIT: &str = "limit";
635pub const PARAM_CURSOR: &str = "cursor";
637pub const PARAM_EXPIRES_AT_MICROS: &str = "expires_at_micros";
639pub const PARAM_EXPECT_VERSION: &str = "expect_version";
642pub const PARAM_EXPECT_ABSENT: &str = "expect_absent";
645
646pub const KV_EXPIRES_AT_MICROS_HEADER: &str = "agdx-expires-at-micros";
652
653#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
658pub struct ProjectionListQuery {
659 #[serde(default, skip_serializing_if = "Option::is_none")]
660 pub topic: Option<String>,
661 #[serde(default, skip_serializing_if = "Option::is_none")]
662 pub name_contains: Option<String>,
663 #[serde(default, skip_serializing_if = "Option::is_none")]
664 pub id_prefix: Option<String>,
665 #[serde(default, skip_serializing_if = "Option::is_none")]
668 pub search: Option<String>,
669}
670
671#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
675pub struct SchemaListQuery {
676 #[serde(default, skip_serializing_if = "Option::is_none")]
677 pub name_contains: Option<String>,
678}
679
680#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
684pub struct KvScanQuery {
685 #[serde(default, skip_serializing_if = "Option::is_none")]
686 pub prefix: Option<String>,
687 #[serde(default, skip_serializing_if = "Option::is_none")]
688 pub start: Option<String>,
689 #[serde(default, skip_serializing_if = "Option::is_none")]
690 pub end: Option<String>,
691 #[serde(default, skip_serializing_if = "Option::is_none")]
692 pub key_contains: Option<String>,
693 #[serde(default, skip_serializing_if = "Option::is_none")]
698 pub conversation: Option<String>,
699 #[serde(default, skip_serializing_if = "Option::is_none")]
700 pub limit: Option<usize>,
701 #[serde(default, skip_serializing_if = "Option::is_none")]
702 pub cursor: Option<String>,
703}
704
705#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
707pub struct KvPutQuery {
708 #[serde(default, skip_serializing_if = "Option::is_none")]
709 pub expires_at_micros: Option<u64>,
710}
711
712#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
718pub struct GraphNeighborsQuery {
719 #[serde(default, skip_serializing_if = "Option::is_none")]
720 pub dir: Option<String>,
721 #[serde(default, skip_serializing_if = "Option::is_none")]
722 pub edge_type: Option<String>,
723 #[serde(default, skip_serializing_if = "Option::is_none")]
724 pub depth: Option<u32>,
725 #[serde(default, skip_serializing_if = "Option::is_none")]
726 pub limit: Option<usize>,
727 #[serde(default, skip_serializing_if = "Option::is_none")]
729 pub as_of: Option<u64>,
730 #[serde(default, skip_serializing_if = "Option::is_none")]
734 pub conversation: Option<String>,
735}
736
737#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
743pub struct RunsQuery {
744 #[serde(default, skip_serializing_if = "Option::is_none")]
745 pub agent_id: Option<String>,
746 #[serde(default, skip_serializing_if = "Option::is_none")]
747 pub state: Option<String>,
748 #[serde(default, skip_serializing_if = "Option::is_none")]
749 pub limit: Option<u32>,
750 #[serde(default, skip_serializing_if = "Option::is_none")]
751 pub cursor: Option<String>,
752}
753
754#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
759pub struct KvCasQuery {
760 #[serde(default, skip_serializing_if = "Option::is_none")]
761 pub expect_version: Option<u64>,
762 #[serde(default, skip_serializing_if = "Option::is_none")]
763 pub expect_absent: Option<bool>,
764 #[serde(default, skip_serializing_if = "Option::is_none")]
765 pub expires_at_micros: Option<u64>,
766}
767
768#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
771pub struct CasCommittedView {
772 pub version: u64,
773}
774
775#[cfg(test)]
776mod tests {
777 use super::*;
778
779 #[test]
780 fn given_path_builders_when_rendered_then_should_match_the_router() {
781 assert_eq!(CAPABILITIES_PATH, "/agdx/capabilities");
782 assert_eq!(projection_path("order.v1"), "/agdx/projections/order.v1");
783 assert_eq!(schema_path(7), "/agdx/schemas/7");
784 assert_eq!(schema_decode_path(7), "/agdx/schemas/7/decode");
785 assert_eq!(kv_namespace_path("sessions"), "/agdx/kv/sessions");
786 assert_eq!(
787 kv_entry_path("sessions", "dXNlcjo0Mg"),
788 "/agdx/kv/sessions/dXNlcjo0Mg"
789 );
790 assert_eq!(
791 kv_cas_path("sessions", "dXNlcjo0Mg"),
792 "/agdx/kv/sessions/dXNlcjo0Mg/cas"
793 );
794 assert_eq!(fork_path("f1"), "/agdx/forks/f1");
795 assert_eq!(fork_promote_path("f1"), "/agdx/forks/f1/promote");
796 assert_eq!(fork_rows_path("f1"), "/agdx/forks/f1/rows");
797 }
798
799 #[test]
800 fn given_capabilities_when_constructed_then_extended_features_default_off() {
801 let caps = Capabilities::new(true, OpVersions::new(1, 1, 1, 1));
802 assert!(
803 caps.query.available && caps.kv.available && caps.fork,
804 "core surfaces track enabled"
805 );
806 assert!(
807 !caps.kv.cas && caps.query.consistency == Consistency::Eventual,
808 "sub-features must be opt-in, never on by default"
809 );
810 let opted = caps
811 .with_kv_cas(true)
812 .with_query_consistency(Consistency::ReadYourWrites);
813 assert!(opted.kv.cas && opted.query.consistency == Consistency::ReadYourWrites);
814 }
815
816 #[test]
817 fn given_capabilities_backends_when_json_round_tripped_then_should_preserve_and_omit_empty() {
818 use crate::hello::BackendDescriptor;
819 let caps = Capabilities::new(true, OpVersions::new(1, 1, 1, 1)).with_backends(vec![
820 BackendDescriptor::new("embedded", "embedded"),
821 BackendDescriptor::new("warehouse", "columnar"),
822 ]);
823 let json = serde_json::to_string(&caps).expect("serializes");
824 let back: Capabilities = serde_json::from_str(&json).expect("deserializes");
825 assert_eq!(back.backends.len(), 2);
826 assert_eq!(back.backends[1].id, "warehouse");
827 assert_eq!(back.backends[1].kind, "columnar");
828
829 let plain = Capabilities::new(true, OpVersions::new(1, 1, 1, 1));
832 let json = serde_json::to_string(&plain).expect("json");
833 assert!(!json.contains("backends"), "empty backends omitted: {json}");
834 }
835
836 #[test]
837 fn given_a_typed_error_when_made_into_a_body_then_should_carry_code_and_message() {
838 let body = ErrorBody::from(&QueryError::IndexNotFound("orders".to_owned()));
839 assert_eq!(body.code, ResultCode::NotFound);
840 assert_eq!(body.http_status(), 404);
841 assert!(body.message.contains("orders"));
842 let json = serde_json::to_string(&body).expect("serializes");
844 let back: ErrorBody = serde_json::from_str(&json).expect("deserializes");
845 assert_eq!(back, body);
846 }
847
848 #[test]
849 #[cfg(feature = "http-client")]
850 fn given_scan_filters_when_url_encoded_then_should_omit_absent_fields() {
851 let query = KvScanQuery {
852 prefix: Some("dXNlcjo".to_owned()),
853 limit: Some(50),
854 ..Default::default()
855 };
856 let encoded = serde_urlencoded::to_string(&query).expect("encodes");
857 assert_eq!(encoded, "prefix=dXNlcjo&limit=50");
858 assert!(encoded.contains(&format!("{PARAM_PREFIX}=")));
860 assert!(encoded.contains(&format!("{PARAM_LIMIT}=")));
861 }
862
863 #[test]
864 #[cfg(feature = "http-client")]
865 fn given_list_filters_when_url_encoded_then_field_names_match_the_param_consts() {
866 let projections = ProjectionListQuery {
867 name_contains: Some("order".to_owned()),
868 id_prefix: Some("order.".to_owned()),
869 ..Default::default()
870 };
871 let encoded = serde_urlencoded::to_string(&projections).expect("encodes");
872 assert_eq!(encoded, "name_contains=order&id_prefix=order.");
873 assert!(encoded.contains(&format!("{PARAM_NAME_CONTAINS}=")));
874 assert!(encoded.contains(&format!("{PARAM_ID_PREFIX}=")));
875
876 let schemas = SchemaListQuery {
877 name_contains: Some("Order".to_owned()),
878 };
879 assert_eq!(
880 serde_urlencoded::to_string(&schemas).expect("encodes"),
881 "name_contains=Order"
882 );
883 }
884}