Skip to main content

keldra_api/
lib.rs

1//! Generated Rust types for the Keldra 0.12 gRPC API.
2
3pub mod v1 {
4    tonic::include_proto!("keldra.v1");
5}
6
7#[cfg(test)]
8mod tests {
9    use super::v1::{DeletedObject, NeverExisted, ObjectHead, PresentObject, object_head};
10
11    #[test]
12    fn exact_path_states_are_distinct() {
13        let states = [
14            ObjectHead {
15                state: Some(object_head::State::Present(PresentObject {
16                    version: 7,
17                    content_hash: vec![7; 32],
18                    content_length: 99,
19                    content_type: String::new(),
20                })),
21            },
22            ObjectHead {
23                state: Some(object_head::State::Deleted(DeletedObject { version: 8 })),
24            },
25            ObjectHead {
26                state: Some(object_head::State::NeverExisted(NeverExisted {})),
27            },
28        ];
29
30        assert!(matches!(
31            &states[0].state,
32            Some(object_head::State::Present(_))
33        ));
34        assert!(matches!(
35            &states[1].state,
36            Some(object_head::State::Deleted(_))
37        ));
38        assert!(matches!(
39            &states[2].state,
40            Some(object_head::State::NeverExisted(_))
41        ));
42    }
43
44    #[test]
45    fn schema_keeps_removed_capabilities_out() {
46        let schema = include_str!("../proto/keldra.proto").to_ascii_lowercase();
47        for forbidden in [
48            "rpc uploadblob",
49            "rpc publishobject",
50            "rpc putobject",
51            "message blobref",
52            "rpc listprefix",
53            "rpc begintransaction",
54            "rpc committransaction",
55            "personaldb",
56        ] {
57            assert!(!schema.contains(forbidden), "schema contains `{forbidden}`");
58        }
59
60        assert!(schema.contains("executor_nomination_log_index"));
61        assert!(schema.contains("commit_log_index"));
62        assert!(schema.contains("immutable_path_prefixes"));
63        assert!(schema.contains("program_only_path_prefixes"));
64        assert!(!schema.contains("rpc registerprogram"));
65        assert!(!schema.contains("message registerprogram"));
66        assert!(schema.contains("objectaddress program"));
67        assert!(schema.contains("_keldra/programs/{name}@{version}"));
68        assert!(schema.contains("rpc startput(putheader) returns (puttoken)"));
69        assert!(schema.contains("rpc put(stream putrequest) returns (puttoken)"));
70        assert!(schema.contains("rpc putend(puttoken) returns (mutationreceipt)"));
71        for rpc in [
72            "rpc createindex(createindexrequest)",
73            "rpc updateindex(updateindexrequest)",
74            "rpc getindex(getindexrequest)",
75            "rpc listindices(listindicesrequest)",
76            "rpc deleteindex(deleteindexrequest)",
77            "rpc queryindex(queryindexrequest)",
78        ] {
79            assert!(schema.contains(rpc), "schema is missing `{rpc}`");
80        }
81        assert!(schema.contains("index_kind_tensor"));
82        assert!(schema.contains("tensorindexspec tensor"));
83        assert!(schema.contains("tensorindexquery tensor"));
84
85        for rpc in [
86            "rpc exchangeclientcredentials",
87            "rpc provisiontenant",
88            "rpc createapplication",
89            "rpc rotateapplicationcredential",
90            "rpc disableapplicationcredential",
91            "rpc createbucket",
92            "rpc grantapplicationrole",
93            "rpc revokeapplicationrole",
94            "rpc putschema",
95            "rpc bindschema",
96            "rpc getbinding",
97            "rpc getschema",
98            "rpc mutatetuples",
99            "rpc readtuples",
100            "rpc checkpermission",
101            "rpc checkpermissions",
102            "rpc watchprefix",
103            "rpc listobjects",
104            "rpc deleteversion",
105            "rpc listobjectversions",
106            "rpc setbucketversioning",
107        ] {
108            assert!(schema.contains(rpc), "schema is missing `{rpc}`");
109        }
110        for forbidden in [
111            "rpc createrealm",
112            "rpc deleterealm",
113            "rpc applyschema",
114            "zookie",
115            "caveat",
116            "publication_metadata",
117            "insecure_no_auth",
118            "api_token",
119        ] {
120            assert!(!schema.contains(forbidden), "schema contains `{forbidden}`");
121        }
122    }
123
124    #[test]
125    fn generated_index_client_is_publicly_exposed() {
126        let _: Option<
127            super::v1::index_service_client::IndexServiceClient<tonic::transport::Channel>,
128        > = None;
129    }
130
131    #[test]
132    fn typed_json_fields_have_explicit_type_cardinality_and_capabilities() {
133        use super::v1::{
134            IndexField, IndexFieldCapability, IndexFieldCardinality, KeywordIndexField, index_field,
135        };
136
137        let field = IndexField {
138            name: "document_id".into(),
139            json_pointer: "/id".into(),
140            cardinality: IndexFieldCardinality::Single as i32,
141            capabilities: vec![IndexFieldCapability::Exact as i32],
142            field_type: Some(index_field::FieldType::Keyword(KeywordIndexField {})),
143        };
144
145        assert!(matches!(
146            field.field_type,
147            Some(index_field::FieldType::Keyword(_))
148        ));
149        assert_eq!(field.capabilities, [IndexFieldCapability::Exact as i32]);
150
151        let schema = include_str!("../proto/keldra.proto").to_ascii_lowercase();
152        assert!(schema.contains("rpc listindices(listindicesrequest)"));
153        assert!(schema.contains("repeated indexdefinition indices = 1"));
154        assert!(!schema.contains("listindexes"));
155        assert!(!schema.contains("fields_json"));
156        assert!(!schema.contains("bool multi_valued"));
157    }
158
159    #[test]
160    fn typed_json_queries_expose_fielded_text_facets_and_aggregates() {
161        use super::v1::{
162            IndexAggregateOperation, IndexAggregateRequest, IndexAggregateResult, IndexFacetBucket,
163            IndexFacetRequest, IndexFacetResult, IndexPredicate, IndexPredicateOperator,
164            IndexQueryHit, QueryIndexResponse, TypedJsonIndexQuery,
165        };
166
167        let query = TypedJsonIndexQuery {
168            predicates: vec![
169                IndexPredicate {
170                    field: "summary".into(),
171                    operator: IndexPredicateOperator::FullText as i32,
172                    values_json: vec![br#""memory safety""#.to_vec()],
173                },
174                IndexPredicate {
175                    field: "summary".into(),
176                    operator: IndexPredicateOperator::Phrase as i32,
177                    values_json: vec![br#""memory safety""#.to_vec()],
178                },
179            ],
180            order: Vec::new(),
181            facets: vec![IndexFacetRequest {
182                field: "ecosystem".into(),
183                limit: 10,
184            }],
185            aggregates: vec![IndexAggregateRequest {
186                field: "severity".into(),
187                operation: IndexAggregateOperation::Average as i32,
188            }],
189        };
190        let response = QueryIndexResponse {
191            hits: vec![IndexQueryHit {
192                address: None,
193                object_version: 7,
194                score: Some(0.75),
195            }],
196            next_page_token: Vec::new(),
197            freshness: None,
198            facet_results: vec![IndexFacetResult {
199                field: "ecosystem".into(),
200                buckets: vec![IndexFacetBucket {
201                    value_json: br#""cargo""#.to_vec(),
202                    count: 4,
203                }],
204            }],
205            aggregate_results: vec![IndexAggregateResult {
206                field: "severity".into(),
207                operation: IndexAggregateOperation::Average as i32,
208                value_json: Some(b"7.5".to_vec()),
209                contributing_count: 4,
210            }],
211        };
212
213        assert_eq!(query.facets[0].limit, 10);
214        assert_eq!(response.hits[0].object_version, 7);
215        assert_eq!(response.aggregate_results[0].contributing_count, 4);
216    }
217
218    #[test]
219    fn generated_personaldb_client_is_publicly_exposed() {
220        let _: Option<
221            super::v1::personal_db_service_client::PersonalDbServiceClient<
222                tonic::transport::Channel,
223            >,
224        > = None;
225
226        let schema = include_str!("../proto/personaldb.proto").to_ascii_lowercase();
227        for rpc in [
228            "rpc creategroup(",
229            "rpc describegroup(",
230            "rpc listgroups(",
231            "rpc grantgrouprole(",
232            "rpc revokegrouprole(",
233            "rpc appendentry(",
234            "rpc materializeprojection(",
235            "rpc catchup(",
236            "rpc registersnapshot(",
237            "rpc getsnapshot(",
238        ] {
239            assert!(schema.contains(rpc), "PersonalDB schema is missing `{rpc}`");
240        }
241    }
242
243    #[test]
244    fn object_surface_has_only_explicit_typed_mutations() {
245        use super::v1::{
246            BulkOperation, BulkPutIfVersionRequest, CreateBucketRequest, DeleteIfVersionRequest,
247            DeleteRequest, DeleteVersionRequest, DeleteVersionResponse, Durability,
248            ListObjectsRequest, ListObjectsResponse, ObjectAddress, ObjectVersioning, PutHeader,
249            PutIfVersionOperation, PutRequest, PutToken, bulk_operation, put_header,
250        };
251
252        let address = Some(ObjectAddress {
253            tenant: "acme".into(),
254            bucket: "objects".into(),
255            path: "one".into(),
256        });
257        let header = PutHeader {
258            address: address.clone(),
259            content_type: "application/json".into(),
260            command_id: "command-1".into(),
261            durability: Durability::Local as i32,
262            operation: Some(put_header::Operation::PutIfVersion(PutIfVersionOperation {
263                expected_version: 8,
264            })),
265        };
266        let frame = PutRequest {
267            token: Some(PutToken {
268                value: b"opaque".to_vec(),
269                expires_at: None,
270            }),
271            chunk: Vec::new(),
272        };
273        assert!(matches!(
274            header.operation,
275            Some(put_header::Operation::PutIfVersion(_))
276        ));
277        assert!(frame.chunk.is_empty());
278
279        let operations = [
280            bulk_operation::Operation::Put(Default::default()),
281            bulk_operation::Operation::PutIfAbsent(Default::default()),
282            bulk_operation::Operation::PutIfVersion(BulkPutIfVersionRequest::default()),
283            bulk_operation::Operation::PutImmutable(Default::default()),
284            bulk_operation::Operation::Delete(DeleteRequest {
285                address: address.clone(),
286                ..Default::default()
287            }),
288            bulk_operation::Operation::DeleteIfVersion(DeleteIfVersionRequest {
289                address: address.clone(),
290                expected_version: 8,
291                ..Default::default()
292            }),
293        ];
294        assert_eq!(
295            operations
296                .into_iter()
297                .map(|operation| BulkOperation {
298                    operation: Some(operation),
299                })
300                .count(),
301            6
302        );
303
304        let current_head_delete = DeleteIfVersionRequest {
305            address: address.clone(),
306            expected_version: 8,
307            ..Default::default()
308        };
309        let retained_version_delete = DeleteVersionRequest {
310            address,
311            version: 7,
312            ..Default::default()
313        };
314        assert_eq!(current_head_delete.expected_version, 8);
315        assert_eq!(retained_version_delete.version, 7);
316        let replaced_current = DeleteVersionResponse {
317            deleted: true,
318            replacement_tombstone_version: Some(9),
319        };
320        assert_eq!(replaced_current.replacement_tombstone_version, Some(9));
321        assert_eq!(
322            CreateBucketRequest::default().versioning,
323            ObjectVersioning::Unversioned as i32
324        );
325
326        let list_request = ListObjectsRequest {
327            tenant: "acme".into(),
328            bucket: "objects".into(),
329            prefix: "reports/".into(),
330            start_after: Some("reports/2025.json".into()),
331            limit: 100,
332        };
333        let list_response = ListObjectsResponse {
334            paths: vec!["reports/2026.json".into()],
335            has_more: false,
336        };
337        assert_eq!(
338            list_request.start_after.as_deref(),
339            Some("reports/2025.json")
340        );
341        assert_eq!(list_response.paths, vec!["reports/2026.json".to_owned()]);
342    }
343
344    #[test]
345    fn authorization_wire_types_preserve_scope_and_typed_unions() {
346        use super::v1::{
347            AnyUsersetSelector, AtLeastRevision, AuthzConsistency, AuthzScope, DirectRelation,
348            InheritRule, NamespaceDefinition, ObjectRef, Permission, PermissionRule,
349            PutSchemaRequest, RelationDefinition, SubjectSelector, Userset, authz_consistency,
350            object_ref, permission_rule, relation_definition, subject, subject_selector,
351        };
352
353        let account = ObjectRef {
354            namespace: "account".into(),
355            id: Some(object_ref::Id::OpaqueId("acme".into())),
356        };
357        let members = Userset {
358            object: Some(account),
359            relation: "member".into(),
360        };
361        let schema = NamespaceDefinition {
362            name: "ledger".into(),
363            relations: vec![
364                RelationDefinition {
365                    name: "reader".into(),
366                    kind: Some(relation_definition::Kind::Direct(DirectRelation {
367                        allowed_subjects: vec![SubjectSelector {
368                            selector: Some(subject_selector::Selector::AnyUserset(
369                                AnyUsersetSelector {
370                                    namespace: "account".into(),
371                                    relation: "member".into(),
372                                },
373                            )),
374                        }],
375                    })),
376                },
377                RelationDefinition {
378                    name: "read".into(),
379                    kind: Some(relation_definition::Kind::Permission(Permission {
380                        rules: vec![PermissionRule {
381                            rule: Some(permission_rule::Rule::Inherit(InheritRule {
382                                relation: "reader".into(),
383                            })),
384                        }],
385                    })),
386                },
387            ],
388        };
389        let subject = super::v1::Subject {
390            kind: Some(subject::Kind::Userset(members)),
391        };
392        let publication = PutSchemaRequest {
393            schema_id: "acme".into(),
394            namespaces: vec![schema],
395        };
396        let consistency = AuthzConsistency {
397            requirement: Some(authz_consistency::Requirement::AtLeast(AtLeastRevision {
398                revision: 42,
399            })),
400        };
401        let system_scope = AuthzScope {
402            storage_tenant: "keldra-internal".into(),
403            realm: "_keldra/system".into(),
404        };
405
406        assert_eq!(publication.namespaces[0].relations.len(), 2);
407        assert!(matches!(subject.kind, Some(subject::Kind::Userset(_))));
408        assert!(matches!(
409            consistency.requirement,
410            Some(authz_consistency::Requirement::AtLeast(AtLeastRevision {
411                revision: 42
412            }))
413        ));
414        assert_eq!(system_scope.realm, "_keldra/system");
415    }
416
417    #[test]
418    fn tuple_mutation_and_batch_checks_share_request_scope() {
419        use super::v1::{
420            AuthzScope, CheckPermissionsRequest, MutateTuplesRequest, ObjectRef, PermissionCheck,
421            RelationTuple, Subject, TupleMutation, authz_consistency, object_ref, subject,
422            tuple_mutation,
423        };
424
425        let scope = AuthzScope {
426            storage_tenant: "acme".into(),
427            realm: "default".into(),
428        };
429        let ledger = ObjectRef {
430            namespace: "ledger".into(),
431            id: Some(object_ref::Id::OpaqueId("main".into())),
432        };
433        let alice = Subject {
434            kind: Some(subject::Kind::Object(ObjectRef {
435                namespace: "user".into(),
436                id: Some(object_ref::Id::OpaqueId("alice".into())),
437            })),
438        };
439        let tuple = RelationTuple {
440            object: Some(ledger.clone()),
441            relation: "reader".into(),
442            subject: Some(alice.clone()),
443        };
444        let mutation = MutateTuplesRequest {
445            scope: Some(scope.clone()),
446            operation_id: "grant-alice".into(),
447            expected_revision: Some(41),
448            mutations: vec![TupleMutation {
449                operation: Some(tuple_mutation::Operation::Add(tuple)),
450            }],
451        };
452        let checks = CheckPermissionsRequest {
453            scope: Some(scope),
454            checks: vec![PermissionCheck {
455                subject: Some(alice),
456                object: Some(ledger),
457                relation: "read".into(),
458            }],
459            consistency: Some(super::v1::AuthzConsistency {
460                requirement: Some(authz_consistency::Requirement::Exact(
461                    super::v1::ExactRevision { revision: 42 },
462                )),
463            }),
464        };
465
466        assert_eq!(mutation.expected_revision, Some(41));
467        assert_eq!(mutation.mutations.len(), 1);
468        assert_eq!(checks.checks.len(), 1);
469        assert!(matches!(
470            checks
471                .consistency
472                .and_then(|consistency| consistency.requirement),
473            Some(authz_consistency::Requirement::Exact(_))
474        ));
475    }
476}