1use crate::checkpoint::{
2 CheckpointMutationResult, CheckpointReadConsistency, CheckpointRequestEnvelope,
3 DestinationCheckpointStatus, PreparedAttemptSummary, RetentionGap,
4};
5use crate::destination::{
6 DestinationId, DestinationOperationId, MaterializationDestination, QueryRoute,
7};
8use crate::fork::{ForkError, ForkKind};
9use crate::graph::SourceRef;
10use crate::hello::{BackendDescriptor, FilterAnnounce, OpVersions};
11use crate::kv::KvError;
12use crate::query::{Consistency, QueryError, QueryExecutionId, QueryExecutionStatus, QueryResult};
13use crate::result::ResultCode;
14use crate::schema::{Digest32, LogicalSchema, TypedValue, UuidValue};
15use crate::topology::WireTopology;
16use serde::{Deserialize, Serialize};
17use std::collections::BTreeMap;
18
19pub const CAPABILITIES_PATH: &str = "/agdx/capabilities";
21pub const QUERY_PATH: &str = "/agdx/query";
23pub const DESTINATIONS_PATH: &str = "/agdx/destinations";
25pub const QUERY_ROUTES_PATH: &str = "/agdx/query-routes";
27pub const PROJECTIONS_PATH: &str = "/agdx/projections";
29pub const BINDINGS_PATH: &str = "/agdx/bindings";
31pub const SCHEMAS_PATH: &str = "/agdx/schemas";
33pub const KV_PATH: &str = "/agdx/kv";
35pub const FORKS_PATH: &str = "/agdx/forks";
37pub const GRAPHS_PATH: &str = "/agdx/graphs";
39pub const GRAPH_PATH: &str = "/agdx/graph";
41pub const CLIENTS_PATH: &str = "/agdx/clients";
45pub const AUTHZ_WHOAMI_PATH: &str = "/agdx/authz/whoami";
47pub const AUTHZ_ROLES_PATH: &str = "/agdx/authz/roles";
49pub const FILTERS_PATH: &str = "/agdx/filters";
51pub const FILTER_METRICS_PATH: &str = "/agdx/filters/metrics";
52pub const FILTER_MUTATIONS_PATH: &str = "/agdx/filters/mutations";
54pub const FILTER_VALIDATE_PATH: &str = "/agdx/filters/validate";
56pub const FILTER_TEST_PATH: &str = "/agdx/filters/test";
58pub const FILTER_PREVIEW_PATH: &str = "/agdx/filters/preview";
60pub const FILTER_BINDINGS_PATH: &str = "/agdx/filter-bindings";
62pub const GROUP_POLICIES_PATH: &str = "/agdx/group-policies";
64pub const SESSIONS_PATH: &str = "/agdx/sessions";
66
67pub fn sessions_path(stream: &str) -> String {
68 format!("{SESSIONS_PATH}/{}", path_segment(stream))
69}
70
71pub fn session_changes_path(stream: &str) -> String {
72 format!("{}/changes", sessions_path(stream))
73}
74
75pub fn session_path(stream: &str, id: crate::agent::ConversationId) -> String {
76 format!(
77 "{}/{}",
78 sessions_path(stream),
79 path_segment(&id.to_string())
80 )
81}
82
83pub fn session_events_path(stream: &str, id: crate::agent::ConversationId) -> String {
84 format!("{}/events", session_path(stream, id))
85}
86
87pub fn session_state_path(stream: &str, id: crate::agent::ConversationId) -> String {
88 format!("{}/state", session_path(stream, id))
89}
90
91pub fn session_links_path(stream: &str, id: crate::agent::ConversationId) -> String {
92 format!("{}/links", session_path(stream, id))
93}
94
95pub fn session_sources_path(stream: &str, id: crate::agent::ConversationId) -> String {
96 format!("{}/sources", session_path(stream, id))
97}
98
99pub fn filter_path(id: u32) -> String {
101 format!("{FILTERS_PATH}/{id}")
102}
103
104pub fn filter_revisions_path(id: u32) -> String {
106 format!("{FILTERS_PATH}/{id}/revisions")
107}
108
109pub fn filter_operation_path(operation_id: u128) -> String {
111 format!("{FILTERS_PATH}/operations/{operation_id}")
112}
113
114pub fn filter_binding_path(stream: &str, topic: &str, group: &str) -> String {
118 format!(
119 "{FILTER_BINDINGS_PATH}/{}/{}/{}",
120 path_segment(stream),
121 path_segment(topic),
122 path_segment(group)
123 )
124}
125
126pub fn group_policy_path(stream: &str, topic: &str, group: &str) -> String {
127 format!(
128 "{GROUP_POLICIES_PATH}/{}/{}/{}",
129 path_segment(stream),
130 path_segment(topic),
131 path_segment(group)
132 )
133}
134
135pub fn path_segment(text: &str) -> String {
138 let mut out = String::with_capacity(text.len());
139 for byte in text.bytes() {
140 if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
141 out.push(char::from(byte));
142 } else {
143 out.push_str(&format!("%{byte:02X}"));
144 }
145 }
146 out
147}
148
149pub fn destination_path(id: DestinationId) -> String {
150 format!("{DESTINATIONS_PATH}/{id}")
151}
152
153pub fn destination_enable_path(id: DestinationId) -> String {
154 format!("{}/{id}/enable", DESTINATIONS_PATH)
155}
156
157pub fn destination_disable_path(id: DestinationId) -> String {
158 format!("{}/{id}/disable", DESTINATIONS_PATH)
159}
160
161pub fn destination_status_path(id: DestinationId) -> String {
162 format!("{}/{id}/status", DESTINATIONS_PATH)
163}
164
165pub fn destination_checkpoint_path(id: DestinationId) -> String {
166 format!("{}/{id}/checkpoint", DESTINATIONS_PATH)
167}
168
169pub fn destination_retention_gap_path(id: DestinationId) -> String {
170 format!("{}/{id}/retention-gap", DESTINATIONS_PATH)
171}
172
173pub fn destination_prepared_attempt_path(id: DestinationId) -> String {
174 format!("{}/{id}/prepared-attempt", DESTINATIONS_PATH)
175}
176
177pub fn destination_table_path(id: DestinationId) -> String {
178 format!("{}/{id}/table", DESTINATIONS_PATH)
179}
180
181pub fn destination_table_schema_path(id: DestinationId) -> String {
182 format!("{}/{id}/table/schema", DESTINATIONS_PATH)
183}
184
185pub fn destination_snapshots_path(id: DestinationId) -> String {
186 format!("{}/{id}/table/snapshots", DESTINATIONS_PATH)
187}
188
189pub fn destination_current_snapshot_path(id: DestinationId) -> String {
190 format!("{}/{id}/table/current-snapshot", DESTINATIONS_PATH)
191}
192
193pub fn destination_snapshot_path(id: DestinationId, snapshot_id: i64) -> String {
194 format!("{}/{id}/table/snapshots/{snapshot_id}", DESTINATIONS_PATH)
195}
196
197pub fn destination_files_path(id: DestinationId) -> String {
198 format!("{}/{id}/table/files", DESTINATIONS_PATH)
199}
200
201pub fn destination_metrics_path(id: DestinationId) -> String {
202 format!("{}/{id}/table/metrics", DESTINATIONS_PATH)
203}
204
205pub fn query_execution_path(id: QueryExecutionId) -> String {
206 format!("{QUERY_PATH}/{id}")
207}
208
209pub fn query_page_path(id: QueryExecutionId) -> String {
210 format!("{QUERY_PATH}/{id}/pages")
211}
212
213pub fn query_cancel_path(id: QueryExecutionId) -> String {
214 format!("{QUERY_PATH}/{id}/cancel")
215}
216
217pub fn destination_operation_path(id: DestinationOperationId) -> String {
218 format!("{DESTINATIONS_PATH}/operations/{id}")
219}
220
221#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
222pub struct CheckpointReadQuery {
223 pub consistency: CheckpointReadConsistency,
224}
225
226impl CheckpointReadQuery {
227 pub const fn new(consistency: CheckpointReadConsistency) -> Self {
228 Self { consistency }
229 }
230}
231
232#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
233pub struct DestinationListQuery {
234 #[serde(default, skip_serializing_if = "Option::is_none")]
235 pub source_stream: Option<String>,
236 #[serde(default, skip_serializing_if = "Option::is_none")]
237 pub source_topic: Option<String>,
238 #[serde(default, skip_serializing_if = "Option::is_none")]
239 pub name_contains: Option<String>,
240 #[serde(default, skip_serializing_if = "Option::is_none")]
241 pub after: Option<String>,
242 #[serde(default, skip_serializing_if = "Option::is_none")]
243 pub limit: Option<usize>,
244 pub consistency: CheckpointReadConsistency,
245}
246
247#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
248pub struct DestinationPageView {
249 pub destinations: Vec<DestinationView>,
250 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub next_cursor: Option<String>,
252 pub global_state_revision: u64,
253 pub consistency: crate::checkpoint::CheckpointReadConsistency,
254}
255
256#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
257pub struct DestinationView {
258 pub destination: MaterializationDestination,
259 pub status: DestinationCheckpointStatus,
260}
261
262#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
263pub struct DestinationMutationBody {
264 pub checkpoint: CheckpointRequestEnvelope,
265}
266
267#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
268pub struct AcceptedOperationView {
269 pub operation_id: DestinationOperationId,
270 pub request_id: crate::checkpoint::CheckpointRequestId,
271 pub state: OperationState,
272 pub submitted_at_micros: u64,
273 #[serde(default, skip_serializing_if = "Option::is_none")]
274 pub completed_at_micros: Option<u64>,
275 #[serde(default, skip_serializing_if = "Option::is_none")]
276 pub error: Option<OperationErrorView>,
277 #[serde(default, skip_serializing_if = "Option::is_none")]
278 pub result: Option<CheckpointMutationResult>,
279}
280
281#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
282pub struct OperationErrorView {
283 pub code: ResultCode,
284 pub message: String,
285}
286
287#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
288#[serde(rename_all = "snake_case")]
289#[non_exhaustive]
290pub enum OperationState {
291 Accepted,
292 Running,
293 Succeeded,
294 Failed,
295 Cancelled,
296}
297
298#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
299pub struct QueryRouteListQuery {
300 #[serde(default, skip_serializing_if = "Option::is_none")]
301 pub name_contains: Option<String>,
302 #[serde(default, skip_serializing_if = "Option::is_none")]
303 pub after: Option<String>,
304 #[serde(default, skip_serializing_if = "Option::is_none")]
305 pub limit: Option<usize>,
306 pub consistency: CheckpointReadConsistency,
307}
308
309#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
310pub struct QueryRoutePageView {
311 pub routes: Vec<QueryRoute>,
312 #[serde(default, skip_serializing_if = "Option::is_none")]
313 pub next_cursor: Option<String>,
314 pub definition_revision: u64,
315 pub global_state_revision: u64,
316 pub consistency: CheckpointReadConsistency,
317}
318
319#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
320pub struct TableView {
321 pub table_uuid: UuidValue,
322 pub destination_id: DestinationId,
323 pub destination_generation: u64,
324 pub namespace: Vec<String>,
325 pub table: String,
326 pub current_snapshot_id: i64,
327 pub current_schema_id: i32,
328 pub current_partition_spec_id: i32,
329 pub metadata_identity: String,
330 pub properties: BTreeMap<String, String>,
331}
332
333#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
334pub struct TableSchemaView {
335 pub table_uuid: UuidValue,
336 pub iceberg_schema_id: i32,
337 pub logical_schema: LogicalSchema,
338}
339
340#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
341pub struct SnapshotListQuery {
342 #[serde(default, skip_serializing_if = "Option::is_none")]
343 pub before_snapshot_id: Option<i64>,
344 #[serde(default, skip_serializing_if = "Option::is_none")]
345 pub limit: Option<usize>,
346}
347
348#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
349pub struct SnapshotPageView {
350 pub snapshots: Vec<TableSnapshotView>,
351 #[serde(default, skip_serializing_if = "Option::is_none")]
352 pub next_before_snapshot_id: Option<i64>,
353}
354
355#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
356pub struct TableSnapshotView {
357 pub snapshot_id: i64,
358 #[serde(default, skip_serializing_if = "Option::is_none")]
359 pub parent_snapshot_id: Option<i64>,
360 pub sequence_number: i64,
361 pub committed_at_micros: u64,
362 pub schema_id: i32,
363 pub partition_spec_id: i32,
364 pub materialization_boundary_digest: Digest32,
365 pub checkpoint_revision: u64,
366 pub summary: BTreeMap<String, String>,
367}
368
369#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
370pub struct TableFileListQuery {
371 #[serde(default, skip_serializing_if = "Option::is_none")]
372 pub snapshot_id: Option<i64>,
373 #[serde(default, skip_serializing_if = "Option::is_none")]
374 pub after: Option<String>,
375 #[serde(default, skip_serializing_if = "Option::is_none")]
376 pub limit: Option<usize>,
377}
378
379#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
380pub struct TableFilePageView {
381 pub files: Vec<TableFileView>,
382 #[serde(default, skip_serializing_if = "Option::is_none")]
383 pub next_cursor: Option<String>,
384}
385
386#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
387pub struct TableFileView {
388 pub object_identity: String,
389 pub file_size_bytes: u64,
390 pub row_count: u64,
391 pub partition: BTreeMap<String, TypedValue>,
392 pub lower_bounds: BTreeMap<u32, TypedValue>,
393 pub upper_bounds: BTreeMap<u32, TypedValue>,
394 pub null_value_counts: BTreeMap<u32, u64>,
395}
396
397#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
398pub struct TableMetricsView {
399 pub snapshot_id: i64,
400 pub data_file_count: u64,
401 pub delete_file_count: u64,
402 pub total_rows: u64,
403 pub total_bytes: u64,
404 pub partition_count: u64,
405}
406
407#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
408pub struct QueryPageBody {
409 pub cursor: String,
410 pub deadline_micros: u64,
411}
412
413#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
414pub struct QueryExecutionView {
415 pub status: QueryExecutionStatus,
416 #[serde(default, skip_serializing_if = "Option::is_none")]
417 pub result: Option<QueryResult>,
418}
419
420#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
421pub struct DestinationIssueView {
422 #[serde(default, skip_serializing_if = "Option::is_none")]
423 pub retention_gap: Option<RetentionGap>,
424 #[serde(default, skip_serializing_if = "Option::is_none")]
425 pub prepared_attempt: Option<PreparedAttemptSummary>,
426 #[serde(default, skip_serializing_if = "Option::is_none")]
427 pub block: Option<crate::checkpoint::DestinationBlock>,
428 pub global_state_revision: u64,
429 pub checkpoint_revision: u64,
430 pub consistency: CheckpointReadConsistency,
431}
432
433#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
435#[serde(rename_all = "snake_case")]
436#[non_exhaustive]
437pub enum DestinationHttpOperation {
438 ListDestinations,
439 GetDestination,
440 CreateDestination,
441 EnableDestination,
442 DisableDestination,
443 GetOperation,
444 GetStatus,
445 GetCheckpoint,
446 GetRetentionGap,
447 GetPreparedAttempt,
448 ListQueryRoutes,
449 GetTable,
450 GetTableSchema,
451 GetCurrentSnapshot,
452 ListSnapshots,
453 GetSnapshot,
454 ListFiles,
455 GetMetrics,
456}
457
458#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
464pub struct DestinationHttpRequest {
465 pub v: u32,
466 pub operation: DestinationHttpOperation,
467 pub parameters: Vec<String>,
468 pub query: Vec<u8>,
469 pub body: Vec<u8>,
470}
471
472#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
474pub struct DestinationHttpReply {
475 pub status: u16,
476 pub body: Vec<u8>,
477}
478
479pub fn authz_role_path(name: &str) -> String {
481 format!("{AUTHZ_ROLES_PATH}/{name}")
482}
483
484pub fn authz_user_roles_path(user_id: u32) -> String {
486 format!("/agdx/authz/users/{user_id}/roles")
487}
488
489pub fn graph_path(id: &str) -> String {
493 format!("{GRAPHS_PATH}/{}", path_segment(id))
494}
495
496pub fn graph_query_path(name: &str) -> String {
498 format!("{GRAPH_PATH}/{}/query", path_segment(name))
499}
500
501pub fn graph_neighbors_path(name: &str, node: &str) -> String {
503 format!(
504 "{GRAPH_PATH}/{}/neighbors/{}",
505 path_segment(name),
506 path_segment(node)
507 )
508}
509
510pub fn projection_path(id: &str) -> String {
512 format!("{PROJECTIONS_PATH}/{}", path_segment(id))
513}
514
515pub fn schema_path(id: u32) -> String {
517 format!("{SCHEMAS_PATH}/{id}")
518}
519
520pub fn schema_decode_path(id: u32) -> String {
522 format!("{SCHEMAS_PATH}/{id}/decode")
523}
524
525pub fn kv_namespace_path(namespace: &str) -> String {
527 format!("{KV_PATH}/{}", path_segment(namespace))
528}
529
530pub fn kv_entry_path(namespace: &str, key_b64: &str) -> String {
534 format!("{}/{}", kv_namespace_path(namespace), path_segment(key_b64))
535}
536
537pub fn kv_cas_path(namespace: &str, key_b64: &str) -> String {
544 format!("{}/cas", kv_entry_path(namespace, key_b64))
545}
546
547pub fn fork_path(id: &str) -> String {
549 format!("{FORKS_PATH}/{}", path_segment(id))
550}
551
552pub fn fork_promote_path(id: &str) -> String {
554 format!("{}/promote", fork_path(id))
555}
556
557pub fn fork_rows_path(id: &str) -> String {
559 format!("{}/rows", fork_path(id))
560}
561
562#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
569#[non_exhaustive]
570pub struct Capabilities {
571 pub managed: bool,
575 pub query: QueryCapsView,
578 pub destinations: DestinationCapsView,
580 pub kv: KvCapsView,
582 #[serde(default)]
586 pub graph: bool,
587 pub fork: bool,
589 #[serde(default)]
591 pub sessions: bool,
592 #[serde(default)]
596 pub watch: bool,
597 #[serde(default)]
600 pub authz: bool,
601 #[serde(default)]
604 pub filters: FilterCapsView,
605 pub versions: OpVersions,
606 #[serde(default, skip_serializing_if = "Vec::is_empty")]
611 pub backends: Vec<BackendDescriptor>,
612 #[serde(default, skip_serializing_if = "Option::is_none")]
618 pub topology: Option<WireTopology>,
619}
620
621#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
624pub struct QueryCapsView {
625 pub available: bool,
627 pub projections: bool,
629 pub schemas: bool,
631 #[serde(default)]
635 pub consistency: Consistency,
636 #[serde(default)]
640 pub keyword: bool,
641 #[serde(default)]
643 pub cursor_paging: bool,
644 #[serde(default)]
646 pub cancellation: bool,
647 #[serde(default)]
649 pub execution_status: bool,
650}
651
652#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
653pub struct DestinationCapsView {
654 pub available: bool,
655 #[serde(default)]
656 pub lifecycle: bool,
657 #[serde(default)]
658 pub checkpoint_status: bool,
659 #[serde(default)]
660 pub query_routes: bool,
661 #[serde(default)]
662 pub table_schema: bool,
663 #[serde(default)]
664 pub snapshots: bool,
665 #[serde(default)]
666 pub files: bool,
667 #[serde(default)]
668 pub metrics: bool,
669 #[serde(default)]
670 pub strongest_consistency: crate::checkpoint::CheckpointReadConsistency,
671}
672
673#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
677pub struct FilterCapsView {
678 #[serde(default)]
680 pub native: bool,
681 #[serde(default)]
683 pub group_policy_reads: bool,
684 #[serde(default)]
686 pub preview: bool,
687 #[serde(default)]
689 pub primary_routing: bool,
690 #[serde(default)]
692 pub catalog: bool,
693 #[serde(default)]
695 pub managed_groups: bool,
696 #[serde(default)]
698 pub evaluator_version: u32,
699 #[serde(default, skip_serializing_if = "Vec::is_empty")]
701 pub codecs: Vec<crate::filter::FilterCodec>,
702}
703
704impl FilterCapsView {
705 pub fn served(native: bool, catalog: bool) -> Self {
707 let evaluation = native.then(FilterAnnounce::served);
708 Self {
709 native,
710 group_policy_reads: false,
711 preview: native,
712 primary_routing: native,
713 catalog,
714 managed_groups: catalog,
715 evaluator_version: evaluation
716 .as_ref()
717 .map_or(0, |evaluation| evaluation.evaluator_version),
718 codecs: evaluation
719 .map(|evaluation| evaluation.codecs)
720 .unwrap_or_default(),
721 }
722 }
723
724 #[must_use]
725 pub const fn with_group_policy_reads(mut self, served: bool) -> Self {
726 self.group_policy_reads = served;
727 self
728 }
729
730 #[must_use]
733 pub fn with_evaluation(mut self, evaluation: FilterAnnounce) -> Self {
734 if self.native {
735 self.evaluator_version = evaluation.evaluator_version;
736 self.codecs = evaluation.codecs;
737 }
738 self
739 }
740}
741
742#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
744pub struct KvCapsView {
745 pub available: bool,
747 #[serde(default)]
751 pub cas: bool,
752 #[serde(default)]
756 pub cas_fenced: bool,
757 #[serde(default)]
760 pub fenced_leases: bool,
761}
762
763impl Capabilities {
764 pub fn new(enabled: bool, versions: OpVersions) -> Self {
771 Self {
772 managed: enabled,
773 query: QueryCapsView {
774 available: enabled,
775 projections: enabled,
776 schemas: enabled,
777 consistency: Consistency::Eventual,
778 keyword: false,
779 cursor_paging: false,
780 cancellation: false,
781 execution_status: false,
782 },
783 destinations: DestinationCapsView {
784 available: false,
785 lifecycle: false,
786 checkpoint_status: false,
787 query_routes: false,
788 table_schema: false,
789 snapshots: false,
790 files: false,
791 metrics: false,
792 strongest_consistency:
793 crate::checkpoint::CheckpointReadConsistency::PotentiallyStale,
794 },
795 kv: KvCapsView {
796 available: enabled,
797 cas: false,
798 cas_fenced: false,
799 fenced_leases: false,
800 },
801 graph: false,
802 fork: enabled,
803 sessions: false,
804 watch: false,
805 authz: false,
806 filters: FilterCapsView::default(),
807 versions,
808 backends: Vec::new(),
809 topology: None,
810 }
811 }
812
813 #[must_use]
815 pub fn with_filters(mut self, filters: FilterCapsView) -> Self {
816 self.filters = filters;
817 self
818 }
819
820 #[must_use]
823 pub fn with_graph(mut self, value: bool) -> Self {
824 self.graph = value;
825 self
826 }
827
828 #[must_use]
830 pub fn with_query_keyword(mut self, value: bool) -> Self {
831 self.query.keyword = value;
832 self
833 }
834
835 #[must_use]
836 pub fn with_query_execution(mut self, paging: bool, cancellation: bool, status: bool) -> Self {
837 self.query.cursor_paging = paging;
838 self.query.cancellation = cancellation;
839 self.query.execution_status = status;
840 self
841 }
842
843 #[must_use]
844 pub fn with_destinations(mut self, destinations: DestinationCapsView) -> Self {
845 self.destinations = destinations;
846 self
847 }
848
849 #[must_use]
851 pub fn with_sessions(mut self, value: bool) -> Self {
852 self.sessions = value;
853 self
854 }
855
856 #[must_use]
859 pub fn with_watch(mut self, value: bool) -> Self {
860 self.watch = value;
861 self
862 }
863
864 #[must_use]
866 pub fn with_authz(mut self, value: bool) -> Self {
867 self.authz = value;
868 self
869 }
870
871 #[must_use]
874 pub fn with_backends(mut self, backends: Vec<BackendDescriptor>) -> Self {
875 self.backends = backends;
876 self
877 }
878
879 #[must_use]
882 pub fn with_topology(mut self, topology: WireTopology) -> Self {
883 self.topology = Some(topology);
884 self
885 }
886
887 #[must_use]
890 pub fn with_kv_cas(mut self, on: bool) -> Self {
891 self.kv.cas = on;
892 self
893 }
894
895 #[must_use]
898 pub fn with_kv_cas_fenced(mut self, on: bool) -> Self {
899 self.kv.cas_fenced = on;
900 self
901 }
902
903 #[must_use]
906 pub fn with_kv_fenced_leases(mut self, on: bool) -> Self {
907 self.kv.fenced_leases = on;
908 self.kv.cas_fenced |= on;
909 self
910 }
911
912 #[must_use]
914 pub fn with_query_consistency(mut self, level: Consistency) -> Self {
915 self.query.consistency = level;
916 self
917 }
918
919 pub fn from_versions(enabled: bool, versions: OpVersions) -> Self {
925 use crate::hello::feature;
926 let consistency = if versions.has_feature(feature::STRONG_CONSISTENCY) {
927 Consistency::Strong
928 } else if versions.has_feature(feature::READ_YOUR_WRITES) {
929 Consistency::ReadYourWrites
930 } else {
931 Consistency::Eventual
932 };
933 let destinations_available =
934 enabled && versions.checkpoint > 0 && versions.has_feature(feature::DESTINATIONS);
935 Self::new(enabled, versions)
936 .with_kv_cas(versions.has_feature(feature::KV_CAS))
937 .with_kv_cas_fenced(versions.has_feature(feature::KV_CAS_FENCED))
938 .with_kv_fenced_leases(versions.has_feature(feature::KV_FENCED_LEASES))
939 .with_query_keyword(versions.has_feature(feature::KEYWORD_SEARCH))
940 .with_watch(versions.has_feature(feature::WATCH))
941 .with_sessions(enabled && versions.has_feature(feature::SESSIONS))
942 .with_authz(versions.has_feature(feature::AUTHZ))
943 .with_query_consistency(consistency)
944 .with_destinations(DestinationCapsView {
945 available: destinations_available,
946 lifecycle: destinations_available,
947 checkpoint_status: destinations_available,
948 query_routes: destinations_available,
949 table_schema: false,
950 snapshots: false,
951 files: false,
952 metrics: false,
953 strongest_consistency:
954 crate::checkpoint::CheckpointReadConsistency::PotentiallyStale,
955 })
956 .with_graph(enabled && versions.graph > 0)
959 .with_filters(
960 FilterCapsView::served(
961 versions.has_feature(feature::CONSUMER_FILTERS),
962 enabled && versions.filter > 0,
963 )
964 .with_group_policy_reads(versions.has_feature(feature::GROUP_POLICY_READS)),
965 )
966 }
967}
968
969#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
973pub struct KvEntryView {
974 pub key: String,
975 pub value: String,
976 pub expires_at_micros: Option<u64>,
977 #[serde(default, skip_serializing_if = "Option::is_none")]
982 pub scope: Option<crate::kv::MemoryRowScope>,
983 #[serde(default, skip_serializing_if = "Option::is_none")]
988 pub source: Option<crate::graph::SourceRef>,
989}
990
991#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
993pub struct KvPageView {
994 pub entries: Vec<KvEntryView>,
995 pub cursor: Option<String>,
996}
997
998#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1000pub struct DeletedManyView {
1001 pub deleted: usize,
1002}
1003
1004#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1009pub struct ClientMetadataView {
1010 pub client_id: u32,
1011 pub user_id: Option<u32>,
1012 pub transport: u8,
1013 pub address: String,
1014 pub consumer_groups_count: u32,
1015 #[serde(default, skip_serializing_if = "Option::is_none")]
1016 pub metadata: Option<String>,
1017}
1018
1019#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1022pub struct ClientMetadataListView {
1023 pub clients: Vec<ClientMetadataView>,
1024 #[serde(default, skip_serializing_if = "Option::is_none")]
1025 pub next_cursor: Option<u32>,
1026}
1027
1028#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1032pub struct ClientsQuery {
1033 #[serde(default, skip_serializing_if = "core::ops::Not::not")]
1035 pub with_metadata_only: bool,
1036 #[serde(default, skip_serializing_if = "Option::is_none")]
1038 pub user_id: Option<u32>,
1039 #[serde(default, skip_serializing_if = "Option::is_none")]
1041 pub after: Option<u32>,
1042 #[serde(default, skip_serializing_if = "Option::is_none")]
1044 pub limit: Option<u32>,
1045}
1046
1047#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1049pub struct PromotedView {
1050 pub rows: usize,
1051}
1052
1053#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1057pub struct GraphNodeView {
1058 pub id: String,
1059 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1060 pub labels: Vec<String>,
1061 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1062 pub attrs: Vec<(String, String)>,
1063 #[serde(default, skip_serializing_if = "Option::is_none")]
1065 pub source: Option<SourceRef>,
1066}
1067
1068#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1071pub struct GraphEdgeView {
1072 pub id: String,
1073 pub from: String,
1074 pub to: String,
1075 pub edge_type: String,
1076 pub weight: f32,
1077 #[serde(default, skip_serializing_if = "Option::is_none")]
1079 pub valid_from: Option<u64>,
1080 #[serde(default, skip_serializing_if = "Option::is_none")]
1081 pub valid_to: Option<u64>,
1082 #[serde(default, skip_serializing_if = "Option::is_none")]
1085 pub source: Option<SourceRef>,
1086}
1087
1088#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1091pub struct GraphResultView {
1092 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1093 pub nodes: Vec<GraphNodeView>,
1094 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1095 pub edges: Vec<GraphEdgeView>,
1096 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1097 pub paths: Vec<PathView>,
1098}
1099
1100#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1103pub struct PathView {
1104 pub nodes: Vec<String>,
1105 pub edges: Vec<String>,
1106}
1107
1108#[derive(Clone, Debug, Serialize, Deserialize)]
1112pub struct RegisterSchemaBody {
1113 pub source: crate::control::SchemaSource,
1114 #[serde(default, skip_serializing_if = "Option::is_none")]
1115 pub name: Option<String>,
1116 #[serde(default, skip_serializing_if = "Option::is_none")]
1117 pub version: Option<u32>,
1118 #[serde(default, skip_serializing_if = "Option::is_none")]
1121 pub stream: Option<String>,
1122}
1123
1124#[derive(Clone, Debug, Serialize, Deserialize)]
1127pub struct DecodeRecordBody {
1128 pub payload: String,
1129}
1130
1131#[derive(Clone, Debug, Serialize, Deserialize)]
1133pub struct ForkCreateBody {
1134 pub fork_id: String,
1135 #[serde(default, skip_serializing_if = "Option::is_none")]
1136 pub parent: Option<String>,
1137 #[serde(default)]
1138 pub kind: ForkKind,
1139 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1140 pub tables: Vec<String>,
1141}
1142
1143#[derive(Clone, Debug, Serialize, Deserialize)]
1146pub struct ForkPutBody {
1147 pub table: String,
1148 pub partition_id: u32,
1149 pub offset: u64,
1150 #[serde(default)]
1151 pub projection_id: String,
1152 #[serde(default)]
1153 pub projection_version: u32,
1154 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1155 pub fields: BTreeMap<String, String>,
1156 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1157 pub metadata: BTreeMap<String, String>,
1158 #[serde(default, skip_serializing_if = "Option::is_none")]
1159 pub payload_b64: Option<String>,
1160 #[serde(default, skip_serializing_if = "Option::is_none")]
1161 pub embedding: Option<String>,
1162 #[serde(default)]
1163 pub tombstone: bool,
1164}
1165
1166#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1171pub struct RemoveBindingBody {
1172 pub stream: String,
1173 pub topic: String,
1174 #[serde(default, skip_serializing_if = "Option::is_none")]
1175 pub projection_ref: Option<String>,
1176}
1177
1178#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1187pub struct ErrorBody {
1188 pub code: ResultCode,
1189 pub message: String,
1190 #[serde(default, skip_serializing_if = "Option::is_none")]
1191 pub detail: Option<serde_json::Value>,
1192}
1193
1194impl ErrorBody {
1195 pub fn new(code: ResultCode, message: impl Into<String>) -> Self {
1197 Self {
1198 code,
1199 message: message.into(),
1200 detail: None,
1201 }
1202 }
1203
1204 #[must_use]
1206 pub fn with_detail(mut self, detail: serde_json::Value) -> Self {
1207 self.detail = Some(detail);
1208 self
1209 }
1210
1211 pub fn http_status(&self) -> u16 {
1214 self.code.http_status()
1215 }
1216}
1217
1218impl From<&QueryError> for ErrorBody {
1219 fn from(error: &QueryError) -> Self {
1220 Self::new(ResultCode::from(error), error.to_string())
1221 }
1222}
1223
1224impl From<&crate::filter::FilterError> for ErrorBody {
1225 fn from(error: &crate::filter::FilterError) -> Self {
1226 Self::new(error.code, error.message.clone())
1227 .with_detail(serde_json::json!({ "reason": error.reason }))
1228 }
1229}
1230
1231impl From<&KvError> for ErrorBody {
1232 fn from(error: &KvError) -> Self {
1233 Self::new(ResultCode::from(error), error.to_string())
1234 }
1235}
1236
1237impl From<&ForkError> for ErrorBody {
1238 fn from(error: &ForkError) -> Self {
1239 Self::new(ResultCode::from(error), error.to_string())
1240 }
1241}
1242
1243impl From<&crate::authz::AuthzError> for ErrorBody {
1244 fn from(error: &crate::authz::AuthzError) -> Self {
1245 Self::new(ResultCode::from(error), error.to_string())
1246 }
1247}
1248
1249impl From<&crate::session::SessionError> for ErrorBody {
1250 fn from(error: &crate::session::SessionError) -> Self {
1251 Self::new(ResultCode::from(error), error.to_string())
1252 }
1253}
1254
1255pub const PARAM_TOPIC: &str = "topic";
1257pub const PARAM_NAME_CONTAINS: &str = "name_contains";
1259pub const PARAM_ID_PREFIX: &str = "id_prefix";
1261pub const PARAM_STREAM: &str = "stream";
1264pub const PARAM_SEARCH: &str = "search";
1269pub const PARAM_PREFIX: &str = "prefix";
1271pub const PARAM_START: &str = "start";
1273pub const PARAM_END: &str = "end";
1275pub const PARAM_KEY_CONTAINS: &str = "key_contains";
1277pub const PARAM_LIMIT: &str = "limit";
1279pub const PARAM_CURSOR: &str = "cursor";
1281pub const PARAM_STATUS: &str = "status";
1283pub const PARAM_AGENT: &str = "agent";
1285pub const PARAM_TEXT: &str = "text";
1287pub const PARAM_ROOT: &str = "root";
1289pub const PARAM_LABEL_PREFIX: &str = "label_prefix";
1291pub const PARAM_SURFACE: &str = "surface";
1293pub const PARAM_AFTER: &str = "after";
1295pub const PARAM_TOTAL: &str = "total";
1297pub const PARAM_FIXED_FRONTIER: &str = "fixed_frontier";
1300pub const PARAM_HISTORY_LIMIT: &str = "history_limit";
1302pub const PARAM_EXPIRES_AT_MICROS: &str = "expires_at_micros";
1304pub const PARAM_EXPECT_VERSION: &str = "expect_version";
1307pub const PARAM_EXPECT_ABSENT: &str = "expect_absent";
1310
1311pub const KV_EXPIRES_AT_MICROS_HEADER: &str = "agdx-expires-at-micros";
1317
1318#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1323pub struct ProjectionListQuery {
1324 #[serde(default, skip_serializing_if = "Option::is_none")]
1325 pub topic: Option<String>,
1326 #[serde(default, skip_serializing_if = "Option::is_none")]
1327 pub name_contains: Option<String>,
1328 #[serde(default, skip_serializing_if = "Option::is_none")]
1329 pub id_prefix: Option<String>,
1330 #[serde(default, skip_serializing_if = "Option::is_none")]
1333 pub search: Option<String>,
1334}
1335
1336#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1340pub struct SchemaListQuery {
1341 #[serde(default, skip_serializing_if = "Option::is_none")]
1342 pub name_contains: Option<String>,
1343 #[serde(default, skip_serializing_if = "Option::is_none")]
1346 pub stream: Option<String>,
1347}
1348
1349#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1353pub struct SchemaQuery {
1354 #[serde(default, skip_serializing_if = "Option::is_none")]
1355 pub stream: Option<String>,
1356}
1357
1358#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1360pub struct FilterListQuery {
1361 #[serde(default, skip_serializing_if = "Option::is_none")]
1362 pub name_contains: Option<String>,
1363 #[serde(default, skip_serializing_if = "Option::is_none")]
1364 pub state: Option<crate::filter::FilterState>,
1365 #[serde(default, skip_serializing_if = "Option::is_none")]
1367 pub before_id: Option<u32>,
1368 #[serde(default, skip_serializing_if = "Option::is_none")]
1369 pub page: Option<u32>,
1370 #[serde(default, skip_serializing_if = "Option::is_none")]
1371 pub page_size: Option<u32>,
1372}
1373
1374#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1376pub struct FilterRevisionListQuery {
1377 #[serde(default, skip_serializing_if = "Option::is_none")]
1378 pub page: Option<u32>,
1379 #[serde(default, skip_serializing_if = "Option::is_none")]
1380 pub page_size: Option<u32>,
1381}
1382
1383#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1385pub struct FilterBindingListQuery {
1386 #[serde(default, skip_serializing_if = "Option::is_none")]
1387 pub filter_id: Option<u32>,
1388 #[serde(default, skip_serializing_if = "Option::is_none")]
1389 pub stream: Option<String>,
1390 #[serde(default, skip_serializing_if = "Option::is_none")]
1391 pub topic: Option<String>,
1392 #[serde(default, skip_serializing_if = "Option::is_none")]
1393 pub page: Option<u32>,
1394 #[serde(default, skip_serializing_if = "Option::is_none")]
1395 pub page_size: Option<u32>,
1396}
1397
1398#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1401pub struct FilterTestBody {
1402 pub filter: crate::filter::FilterRef,
1403 #[serde(default, skip_serializing_if = "Option::is_none")]
1404 pub payload_base64: Option<String>,
1405 #[serde(default, skip_serializing_if = "Option::is_none")]
1406 pub payload_text: Option<String>,
1407 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1408 pub headers: Vec<crate::filter::FilterHeader>,
1409}
1410
1411#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1415pub struct KvScanQuery {
1416 #[serde(default, skip_serializing_if = "Option::is_none")]
1417 pub prefix: Option<String>,
1418 #[serde(default, skip_serializing_if = "Option::is_none")]
1419 pub start: Option<String>,
1420 #[serde(default, skip_serializing_if = "Option::is_none")]
1421 pub end: Option<String>,
1422 #[serde(default, skip_serializing_if = "Option::is_none")]
1423 pub key_contains: Option<String>,
1424 #[serde(default, skip_serializing_if = "Option::is_none")]
1429 pub conversation: Option<String>,
1430 #[serde(default, skip_serializing_if = "Option::is_none")]
1431 pub limit: Option<usize>,
1432 #[serde(default, skip_serializing_if = "Option::is_none")]
1433 pub cursor: Option<String>,
1434}
1435
1436#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1438pub struct KvPutQuery {
1439 #[serde(default, skip_serializing_if = "Option::is_none")]
1440 pub expires_at_micros: Option<u64>,
1441}
1442
1443#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1449pub struct GraphNeighborsQuery {
1450 #[serde(default, skip_serializing_if = "Option::is_none")]
1451 pub dir: Option<String>,
1452 #[serde(default, skip_serializing_if = "Option::is_none")]
1453 pub edge_type: Option<String>,
1454 #[serde(default, skip_serializing_if = "Option::is_none")]
1455 pub depth: Option<u32>,
1456 #[serde(default, skip_serializing_if = "Option::is_none")]
1457 pub limit: Option<usize>,
1458 #[serde(default, skip_serializing_if = "Option::is_none")]
1460 pub as_of: Option<u64>,
1461 #[serde(default, skip_serializing_if = "Option::is_none")]
1465 pub conversation: Option<String>,
1466}
1467
1468#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1471pub struct SessionsQuery {
1472 #[serde(default, skip_serializing_if = "Option::is_none")]
1473 pub status: Option<String>,
1474 #[serde(default, skip_serializing_if = "Option::is_none")]
1475 pub root: Option<String>,
1476 #[serde(default, skip_serializing_if = "Option::is_none")]
1477 pub label_prefix: Option<String>,
1478 #[serde(default, skip_serializing_if = "Option::is_none")]
1479 pub agent: Option<String>,
1480 #[serde(default, skip_serializing_if = "Option::is_none")]
1481 pub text: Option<String>,
1482 #[serde(default, skip_serializing_if = "Option::is_none")]
1483 pub cursor: Option<String>,
1484 #[serde(default, skip_serializing_if = "Option::is_none")]
1485 pub limit: Option<u32>,
1486 #[serde(default, rename = "total")]
1487 pub want_total: bool,
1488}
1489
1490#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1493pub struct SessionEventsQuery {
1494 #[serde(default, skip_serializing_if = "Option::is_none")]
1495 pub cursor: Option<String>,
1496 #[serde(default, skip_serializing_if = "Option::is_none")]
1497 pub limit: Option<u32>,
1498 #[serde(default)]
1499 pub fixed_frontier: bool,
1500}
1501
1502#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1505pub struct SessionStateQuery {
1506 #[serde(default, skip_serializing_if = "Option::is_none")]
1507 pub history_limit: Option<u32>,
1508}
1509
1510#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1512pub struct SessionLinksQuery {
1513 #[serde(default, skip_serializing_if = "Option::is_none")]
1514 pub surface: Option<String>,
1515}
1516
1517#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1520pub struct SessionChangesQuery {
1521 #[serde(default)]
1522 pub after: u64,
1523 #[serde(default, skip_serializing_if = "Option::is_none")]
1524 pub limit: Option<u32>,
1525}
1526
1527#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1532pub struct KvCasQuery {
1533 #[serde(default, skip_serializing_if = "Option::is_none")]
1534 pub expect_version: Option<u64>,
1535 #[serde(default, skip_serializing_if = "Option::is_none")]
1536 pub expect_absent: Option<bool>,
1537 #[serde(default, skip_serializing_if = "Option::is_none")]
1538 pub expires_at_micros: Option<u64>,
1539}
1540
1541#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1544pub struct CasCommittedView {
1545 pub version: u64,
1546}
1547
1548#[cfg(test)]
1549mod tests {
1550 use super::*;
1551
1552 #[test]
1553 fn given_path_builders_when_rendered_then_should_match_the_router() {
1554 assert_eq!(CAPABILITIES_PATH, "/agdx/capabilities");
1555 assert_eq!(
1556 projection_path("reading.v1"),
1557 "/agdx/projections/reading.v1"
1558 );
1559 assert_eq!(schema_path(7), "/agdx/schemas/7");
1560 assert_eq!(schema_decode_path(7), "/agdx/schemas/7/decode");
1561 assert_eq!(kv_namespace_path("sessions"), "/agdx/kv/sessions");
1562 assert_eq!(
1563 kv_entry_path("sessions", "dXNlcjo0Mg"),
1564 "/agdx/kv/sessions/dXNlcjo0Mg"
1565 );
1566 assert_eq!(
1567 kv_cas_path("sessions", "dXNlcjo0Mg"),
1568 "/agdx/kv/sessions/dXNlcjo0Mg/cas"
1569 );
1570 assert_eq!(fork_path("f1"), "/agdx/forks/f1");
1571 assert_eq!(fork_promote_path("f1"), "/agdx/forks/f1/promote");
1572 assert_eq!(fork_rows_path("f1"), "/agdx/forks/f1/rows");
1573 }
1574
1575 #[test]
1576 fn given_stream_scoped_names_when_rendered_then_should_encode_each_as_one_segment() {
1577 assert_eq!(
1578 kv_namespace_path("stream:acme/support"),
1579 "/agdx/kv/stream%3Aacme%2Fsupport"
1580 );
1581 assert_eq!(
1582 kv_cas_path("stream:acme/support", "dXNlcjo0Mg"),
1583 "/agdx/kv/stream%3Aacme%2Fsupport/dXNlcjo0Mg/cas"
1584 );
1585 assert_eq!(
1586 graph_neighbors_path("stream:acme/kg", "a/b"),
1587 "/agdx/graph/stream%3Aacme%2Fkg/neighbors/a%2Fb"
1588 );
1589 assert_eq!(
1590 graph_query_path("stream:acme/kg"),
1591 "/agdx/graph/stream%3Aacme%2Fkg/query"
1592 );
1593 assert_eq!(
1594 graph_path("stream:acme/kg"),
1595 "/agdx/graphs/stream%3Aacme%2Fkg"
1596 );
1597 assert_eq!(
1598 projection_path("stream:acme/readings"),
1599 "/agdx/projections/stream%3Aacme%2Freadings"
1600 );
1601 }
1602
1603 #[test]
1604 fn given_the_sessions_feature_when_built_from_versions_then_should_advertise_sessions() {
1605 let mut versions = OpVersions::new(1, 1, 1, 1);
1606 assert!(!Capabilities::from_versions(true, versions).sessions);
1607 versions.features |= crate::hello::feature::SESSIONS;
1608 assert!(Capabilities::from_versions(true, versions).sessions);
1609 assert!(!Capabilities::from_versions(false, versions).sessions);
1610 }
1611
1612 #[test]
1613 fn given_capabilities_when_constructed_then_extended_features_default_off() {
1614 let caps = Capabilities::new(true, OpVersions::new(1, 1, 1, 1));
1615 assert!(
1616 caps.query.available && caps.kv.available && caps.fork,
1617 "core surfaces track enabled"
1618 );
1619 assert!(
1620 !caps.kv.cas
1621 && !caps.kv.cas_fenced
1622 && !caps.kv.fenced_leases
1623 && caps.query.consistency == Consistency::Eventual,
1624 "sub-features must be opt-in, never on by default"
1625 );
1626 let opted = caps
1627 .with_kv_cas(true)
1628 .with_query_consistency(Consistency::ReadYourWrites);
1629 assert!(opted.kv.cas && opted.query.consistency == Consistency::ReadYourWrites);
1630 }
1631
1632 #[test]
1633 fn given_the_fenced_lease_bit_when_building_http_capabilities_then_should_advertise_it() {
1634 let versions =
1635 OpVersions::new(1, 1, 1, 1).with_features(crate::hello::feature::KV_FENCED_LEASES);
1636 let caps = Capabilities::from_versions(true, versions);
1637 assert!(caps.kv.fenced_leases);
1638 assert!(
1639 caps.kv.cas_fenced,
1640 "the full fenced-lease contract subsumes fenced CAS"
1641 );
1642 }
1643
1644 #[test]
1645 fn given_group_reads_without_evaluation_when_building_http_capabilities_then_should_keep_catalog_available()
1646 {
1647 let versions = OpVersions::new(1, 1, 1, 1)
1648 .with_filter(crate::codes::FILTER_OP_VERSION)
1649 .with_features(crate::hello::feature::GROUP_POLICY_READS);
1650 let caps = Capabilities::from_versions(true, versions);
1651 assert!(caps.filters.group_policy_reads);
1652 assert!(caps.filters.catalog && caps.filters.managed_groups);
1653 assert!(!caps.filters.native && !caps.filters.preview);
1654 assert_eq!(caps.filters.evaluator_version, 0);
1655 assert!(caps.filters.codecs.is_empty());
1656 let absent = Capabilities::from_versions(false, OpVersions::new(1, 1, 1, 1));
1657 assert!(!absent.filters.group_policy_reads && !absent.filters.catalog);
1658 }
1659
1660 #[test]
1661 fn given_destination_versions_when_building_capabilities_then_the_core_surface_is_available() {
1662 use crate::codes::CHECKPOINT_OP_VERSION;
1663 use crate::hello::feature;
1664
1665 let versions = OpVersions::new(2, 2, 1, 1)
1666 .with_checkpoint(CHECKPOINT_OP_VERSION)
1667 .with_features(feature::DESTINATIONS);
1668 let caps = Capabilities::from_versions(true, versions);
1669
1670 assert!(caps.destinations.available);
1671 assert!(caps.destinations.lifecycle);
1672 assert!(caps.destinations.checkpoint_status);
1673 assert!(caps.destinations.query_routes);
1674 assert!(!caps.destinations.table_schema);
1675 }
1676
1677 #[test]
1678 fn given_capabilities_backends_when_json_round_tripped_then_should_preserve_and_omit_empty() {
1679 use crate::destination::BackendResourceId;
1680 use crate::hello::{BackendDescriptor, BackendImplementation, BackendMode};
1681 let caps = Capabilities::new(true, OpVersions::new(1, 1, 1, 1)).with_backends(vec![
1682 BackendDescriptor::new(
1683 BackendResourceId::from_u128(1),
1684 BackendMode::Operational,
1685 "Embedded",
1686 BackendImplementation {
1687 kind: "embedded".to_owned(),
1688 version: "1.0.0".to_owned(),
1689 },
1690 1,
1691 1,
1692 ),
1693 BackendDescriptor::new(
1694 BackendResourceId::from_u128(2),
1695 BackendMode::Lakehouse,
1696 "Warehouse",
1697 BackendImplementation {
1698 kind: "columnar".to_owned(),
1699 version: "1.0.0".to_owned(),
1700 },
1701 1,
1702 1,
1703 ),
1704 ]);
1705 let json = serde_json::to_string(&caps).expect("serializes");
1706 let back: Capabilities = serde_json::from_str(&json).expect("deserializes");
1707 assert_eq!(back.backends.len(), 2);
1708 assert_eq!(
1709 back.backends[1].resource_id,
1710 BackendResourceId::from_u128(2)
1711 );
1712 assert_eq!(back.backends[1].implementation.kind, "columnar");
1713
1714 let plain = Capabilities::new(true, OpVersions::new(1, 1, 1, 1));
1717 let json = serde_json::to_string(&plain).expect("json");
1718 assert!(!json.contains("backends"), "empty backends omitted: {json}");
1719 }
1720
1721 #[test]
1722 fn given_capabilities_topology_when_json_round_tripped_then_should_preserve_and_omit_absent() {
1723 let custom = WireTopology {
1724 ops_stream: "custom-ops".to_owned(),
1725 ..WireTopology::default()
1726 };
1727 let caps =
1728 Capabilities::new(true, OpVersions::new(1, 1, 1, 1)).with_topology(custom.clone());
1729 let json = serde_json::to_string(&caps).expect("serializes");
1730 let back: Capabilities = serde_json::from_str(&json).expect("deserializes");
1731 assert_eq!(back.topology, Some(custom));
1732
1733 let plain = Capabilities::new(true, OpVersions::new(1, 1, 1, 1));
1736 let json = serde_json::to_string(&plain).expect("json");
1737 assert!(
1738 !json.contains("topology"),
1739 "absent topology omitted: {json}"
1740 );
1741 }
1742
1743 #[test]
1744 fn given_a_typed_error_when_made_into_a_body_then_should_carry_code_and_message() {
1745 let body = ErrorBody::from(&QueryError::IndexNotFound("readings".to_owned()));
1746 assert_eq!(body.code, ResultCode::NotFound);
1747 assert_eq!(body.http_status(), 404);
1748 assert!(body.message.contains("readings"));
1749 let json = serde_json::to_string(&body).expect("serializes");
1751 let back: ErrorBody = serde_json::from_str(&json).expect("deserializes");
1752 assert_eq!(back, body);
1753 }
1754
1755 #[test]
1756 #[cfg(feature = "http-client")]
1757 fn given_scan_filters_when_url_encoded_then_should_omit_absent_fields() {
1758 let query = KvScanQuery {
1759 prefix: Some("dXNlcjo".to_owned()),
1760 limit: Some(50),
1761 ..Default::default()
1762 };
1763 let encoded = serde_urlencoded::to_string(&query).expect("encodes");
1764 assert_eq!(encoded, "prefix=dXNlcjo&limit=50");
1765 assert!(encoded.contains(&format!("{PARAM_PREFIX}=")));
1767 assert!(encoded.contains(&format!("{PARAM_LIMIT}=")));
1768 }
1769
1770 #[test]
1771 #[cfg(feature = "http-client")]
1772 fn given_names_with_reserved_characters_when_building_a_binding_path_then_should_encode_each_segment()
1773 {
1774 assert_eq!(
1775 filter_binding_path("orbit", "fleet/changes", "desk?x#1"),
1776 "/agdx/filter-bindings/orbit/fleet%2Fchanges/desk%3Fx%231"
1777 );
1778 }
1779
1780 #[cfg(feature = "http-client")]
1781 #[test]
1782 fn given_list_filters_when_url_encoded_then_field_names_match_the_param_consts() {
1783 let projections = ProjectionListQuery {
1784 name_contains: Some("reading".to_owned()),
1785 id_prefix: Some("reading.".to_owned()),
1786 ..Default::default()
1787 };
1788 let encoded = serde_urlencoded::to_string(&projections).expect("encodes");
1789 assert_eq!(encoded, "name_contains=reading&id_prefix=reading.");
1790 assert!(encoded.contains(&format!("{PARAM_NAME_CONTAINS}=")));
1791 assert!(encoded.contains(&format!("{PARAM_ID_PREFIX}=")));
1792
1793 let schemas = SchemaListQuery {
1794 name_contains: Some("Reading".to_owned()),
1795 stream: None,
1796 };
1797 assert_eq!(
1798 serde_urlencoded::to_string(&schemas).expect("encodes"),
1799 "name_contains=Reading"
1800 );
1801 let scoped = SchemaListQuery {
1802 name_contains: None,
1803 stream: Some("acme".to_owned()),
1804 };
1805 assert_eq!(
1806 serde_urlencoded::to_string(&scoped).expect("encodes"),
1807 format!("{PARAM_STREAM}=acme")
1808 );
1809 assert_eq!(
1810 serde_urlencoded::to_string(SchemaQuery::default()).expect("encodes"),
1811 ""
1812 );
1813 assert_eq!(
1814 serde_urlencoded::to_string(SchemaQuery {
1815 stream: Some("acme".to_owned()),
1816 })
1817 .expect("encodes"),
1818 format!("{PARAM_STREAM}=acme")
1819 );
1820 }
1821
1822 #[test]
1823 fn given_a_register_schema_body_without_a_stream_when_encoded_then_should_omit_it() {
1824 let body = RegisterSchemaBody {
1825 source: crate::control::SchemaSource::JsonSchema {
1826 schema: "{}".to_owned(),
1827 },
1828 name: None,
1829 version: None,
1830 stream: None,
1831 };
1832 let json = serde_json::to_string(&body).expect("encodes");
1833 assert!(
1834 !json.contains("stream"),
1835 "an unset stream is omitted: {json}"
1836 );
1837 let scoped: RegisterSchemaBody = serde_json::from_str(
1838 r#"{"source":{"kind":"json_schema","schema":"{}"},"stream":"acme"}"#,
1839 )
1840 .expect("decodes");
1841 assert_eq!(scoped.stream.as_deref(), Some("acme"));
1842 }
1843}