Skip to main content

anvil_api/
lib.rs

1//! Generated Rust types for the Anvil 0.5 gRPC API.
2
3pub mod v1 {
4    tonic::include_proto!("anvil.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/anvil.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("_anvil/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 listindexes(listindexesrequest)",
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 generated_personaldb_client_is_publicly_exposed() {
133        let _: Option<
134            super::v1::personal_db_service_client::PersonalDbServiceClient<
135                tonic::transport::Channel,
136            >,
137        > = None;
138
139        let schema = include_str!("../proto/personaldb.proto").to_ascii_lowercase();
140        for rpc in [
141            "rpc creategroup(",
142            "rpc describegroup(",
143            "rpc listgroups(",
144            "rpc grantgrouprole(",
145            "rpc revokegrouprole(",
146            "rpc appendentry(",
147            "rpc materializeprojection(",
148            "rpc catchup(",
149            "rpc registersnapshot(",
150            "rpc getsnapshot(",
151        ] {
152            assert!(schema.contains(rpc), "PersonalDB schema is missing `{rpc}`");
153        }
154    }
155
156    #[test]
157    fn object_surface_has_only_explicit_typed_mutations() {
158        use super::v1::{
159            BulkOperation, BulkPutIfVersionRequest, CreateBucketRequest, DeleteIfVersionRequest,
160            DeleteRequest, DeleteVersionRequest, DeleteVersionResponse, Durability,
161            ListObjectsRequest, ListObjectsResponse, ObjectAddress, ObjectVersioning, PutHeader,
162            PutIfVersionOperation, PutRequest, PutToken, bulk_operation, put_header,
163        };
164
165        let address = Some(ObjectAddress {
166            tenant: "acme".into(),
167            bucket: "objects".into(),
168            path: "one".into(),
169        });
170        let header = PutHeader {
171            address: address.clone(),
172            content_type: "application/json".into(),
173            command_id: "command-1".into(),
174            durability: Durability::Local as i32,
175            operation: Some(put_header::Operation::PutIfVersion(PutIfVersionOperation {
176                expected_version: 8,
177            })),
178        };
179        let frame = PutRequest {
180            token: Some(PutToken {
181                value: b"opaque".to_vec(),
182                expires_at: None,
183            }),
184            chunk: Vec::new(),
185        };
186        assert!(matches!(
187            header.operation,
188            Some(put_header::Operation::PutIfVersion(_))
189        ));
190        assert!(frame.chunk.is_empty());
191
192        let operations = [
193            bulk_operation::Operation::Put(Default::default()),
194            bulk_operation::Operation::PutIfAbsent(Default::default()),
195            bulk_operation::Operation::PutIfVersion(BulkPutIfVersionRequest::default()),
196            bulk_operation::Operation::PutImmutable(Default::default()),
197            bulk_operation::Operation::Delete(DeleteRequest {
198                address: address.clone(),
199                ..Default::default()
200            }),
201            bulk_operation::Operation::DeleteIfVersion(DeleteIfVersionRequest {
202                address: address.clone(),
203                expected_version: 8,
204                ..Default::default()
205            }),
206        ];
207        assert_eq!(
208            operations
209                .into_iter()
210                .map(|operation| BulkOperation {
211                    operation: Some(operation),
212                })
213                .count(),
214            6
215        );
216
217        let current_head_delete = DeleteIfVersionRequest {
218            address: address.clone(),
219            expected_version: 8,
220            ..Default::default()
221        };
222        let retained_version_delete = DeleteVersionRequest {
223            address,
224            version: 7,
225            ..Default::default()
226        };
227        assert_eq!(current_head_delete.expected_version, 8);
228        assert_eq!(retained_version_delete.version, 7);
229        let replaced_current = DeleteVersionResponse {
230            deleted: true,
231            replacement_tombstone_version: Some(9),
232        };
233        assert_eq!(replaced_current.replacement_tombstone_version, Some(9));
234        assert_eq!(
235            CreateBucketRequest::default().versioning,
236            ObjectVersioning::Unversioned as i32
237        );
238
239        let list_request = ListObjectsRequest {
240            tenant: "acme".into(),
241            bucket: "objects".into(),
242            prefix: "reports/".into(),
243            start_after: Some("reports/2025.json".into()),
244            limit: 100,
245        };
246        let list_response = ListObjectsResponse {
247            paths: vec!["reports/2026.json".into()],
248            has_more: false,
249        };
250        assert_eq!(
251            list_request.start_after.as_deref(),
252            Some("reports/2025.json")
253        );
254        assert_eq!(list_response.paths, vec!["reports/2026.json".to_owned()]);
255    }
256
257    #[test]
258    fn authorization_wire_types_preserve_scope_and_typed_unions() {
259        use super::v1::{
260            AnyUsersetSelector, AtLeastRevision, AuthzConsistency, AuthzScope, DirectRelation,
261            InheritRule, NamespaceDefinition, ObjectRef, Permission, PermissionRule,
262            PutSchemaRequest, RelationDefinition, SubjectSelector, Userset, authz_consistency,
263            object_ref, permission_rule, relation_definition, subject, subject_selector,
264        };
265
266        let account = ObjectRef {
267            namespace: "account".into(),
268            id: Some(object_ref::Id::OpaqueId("acme".into())),
269        };
270        let members = Userset {
271            object: Some(account),
272            relation: "member".into(),
273        };
274        let schema = NamespaceDefinition {
275            name: "ledger".into(),
276            relations: vec![
277                RelationDefinition {
278                    name: "reader".into(),
279                    kind: Some(relation_definition::Kind::Direct(DirectRelation {
280                        allowed_subjects: vec![SubjectSelector {
281                            selector: Some(subject_selector::Selector::AnyUserset(
282                                AnyUsersetSelector {
283                                    namespace: "account".into(),
284                                    relation: "member".into(),
285                                },
286                            )),
287                        }],
288                    })),
289                },
290                RelationDefinition {
291                    name: "read".into(),
292                    kind: Some(relation_definition::Kind::Permission(Permission {
293                        rules: vec![PermissionRule {
294                            rule: Some(permission_rule::Rule::Inherit(InheritRule {
295                                relation: "reader".into(),
296                            })),
297                        }],
298                    })),
299                },
300            ],
301        };
302        let subject = super::v1::Subject {
303            kind: Some(subject::Kind::Userset(members)),
304        };
305        let publication = PutSchemaRequest {
306            schema_id: "acme".into(),
307            namespaces: vec![schema],
308        };
309        let consistency = AuthzConsistency {
310            requirement: Some(authz_consistency::Requirement::AtLeast(AtLeastRevision {
311                revision: 42,
312            })),
313        };
314        let system_scope = AuthzScope {
315            storage_tenant: "anvil-internal".into(),
316            realm: "_anvil/system".into(),
317        };
318
319        assert_eq!(publication.namespaces[0].relations.len(), 2);
320        assert!(matches!(subject.kind, Some(subject::Kind::Userset(_))));
321        assert!(matches!(
322            consistency.requirement,
323            Some(authz_consistency::Requirement::AtLeast(AtLeastRevision {
324                revision: 42
325            }))
326        ));
327        assert_eq!(system_scope.realm, "_anvil/system");
328    }
329
330    #[test]
331    fn tuple_mutation_and_batch_checks_share_request_scope() {
332        use super::v1::{
333            AuthzScope, CheckPermissionsRequest, MutateTuplesRequest, ObjectRef, PermissionCheck,
334            RelationTuple, Subject, TupleMutation, authz_consistency, object_ref, subject,
335            tuple_mutation,
336        };
337
338        let scope = AuthzScope {
339            storage_tenant: "acme".into(),
340            realm: "default".into(),
341        };
342        let ledger = ObjectRef {
343            namespace: "ledger".into(),
344            id: Some(object_ref::Id::OpaqueId("main".into())),
345        };
346        let alice = Subject {
347            kind: Some(subject::Kind::Object(ObjectRef {
348                namespace: "user".into(),
349                id: Some(object_ref::Id::OpaqueId("alice".into())),
350            })),
351        };
352        let tuple = RelationTuple {
353            object: Some(ledger.clone()),
354            relation: "reader".into(),
355            subject: Some(alice.clone()),
356        };
357        let mutation = MutateTuplesRequest {
358            scope: Some(scope.clone()),
359            operation_id: "grant-alice".into(),
360            expected_revision: Some(41),
361            mutations: vec![TupleMutation {
362                operation: Some(tuple_mutation::Operation::Add(tuple)),
363            }],
364        };
365        let checks = CheckPermissionsRequest {
366            scope: Some(scope),
367            checks: vec![PermissionCheck {
368                subject: Some(alice),
369                object: Some(ledger),
370                relation: "read".into(),
371            }],
372            consistency: Some(super::v1::AuthzConsistency {
373                requirement: Some(authz_consistency::Requirement::Exact(
374                    super::v1::ExactRevision { revision: 42 },
375                )),
376            }),
377        };
378
379        assert_eq!(mutation.expected_revision, Some(41));
380        assert_eq!(mutation.mutations.len(), 1);
381        assert_eq!(checks.checks.len(), 1);
382        assert!(matches!(
383            checks
384                .consistency
385                .and_then(|consistency| consistency.requirement),
386            Some(authz_consistency::Requirement::Exact(_))
387        ));
388    }
389}