1use std::collections::HashSet;
3use std::ops::Deref;
4
5#[cfg(feature = "tables")]
6use crate::Table;
7use crate::api::authorization::ListGrantsRequest;
8use crate::catalog_types::{
9 CommitSelector, CommitTarget, CreateBranchRequest, IdempotencyKey, NamespacePath,
10 NamespaceSegment, PageSize, ProjectBranchId, ProjectBranchName, ProjectCommitId, ProjectId,
11 ProjectResourceState, ReadJournalStateRequest, ResolveJournalPathRequest, ResourceAtSnapshot,
12};
13use crate::commit::{Commit, FrozenCommit, SavedCommit};
14use crate::resources::project::{
15 Grant, GrantRoleRequest, GrantRoleResponse, Presentation, PresentationKey, Principal,
16 Project as ProjectInfo, SavePresentation,
17};
18#[cfg(feature = "tables")]
19use crate::table::CatalogBase;
20use crate::{Client, Error, List};
21
22#[derive(Clone)]
24pub struct Project {
25 pub(crate) client: Client,
26 pub(crate) id: ProjectId,
27}
28impl Client {
29 pub fn project(&self, id: impl AsRef<str>) -> Result<Project, Error> {
31 Ok(Project {
32 client: self.clone(),
33 id: ProjectId::new(id.as_ref()).map_err(Error::invalid)?,
34 })
35 }
36}
37impl Project {
38 pub fn client(&self) -> &Client {
40 &self.client
41 }
42
43 pub fn id(&self) -> &ProjectId {
45 &self.id
46 }
47 pub async fn branch(&self, name: &str) -> Result<BranchHead, Error> {
49 let branch = ProjectBranchName::new(name).map_err(Error::invalid)?;
50 let pinned = self
51 .client
52 .connection
53 .catalog
54 .pin_journal(&self.id, &branch)
55 .await?;
56 pinned.validate().map_err(Error::invalid)?;
57 Ok(BranchHead::new(
58 self.clone(),
59 pinned.head_commit_id,
60 pinned.branch_id,
61 ))
62 }
63 pub async fn create_branch(
65 &self,
66 name: &str,
67 commit_id: ProjectCommitId,
68 ) -> Result<BranchHead, Error> {
69 ProjectCommitId::new(commit_id.0).map_err(Error::invalid)?;
70 let branch = self
71 .client
72 .connection
73 .catalog
74 .create_journal_branch(
75 &self.id,
76 &CreateBranchRequest {
77 name: ProjectBranchName::new(name).map_err(Error::invalid)?,
78 target: CommitSelector::Commit(CommitTarget { commit_id }),
79 idempotency_key: IdempotencyKey::new(format!("branch-{name}-{commit_id}"))
80 .map_err(Error::invalid)?,
81 },
82 )
83 .await?;
84 branch.validate().map_err(Error::invalid)?;
85 Ok(BranchHead::new(
86 self.clone(),
87 branch.head_commit_id,
88 branch.branch_id,
89 ))
90 }
91
92 pub fn snapshot_at(&self, commit_id: ProjectCommitId) -> Snapshot {
94 Snapshot {
95 project: self.clone(),
96 commit_id,
97 branch_id: None,
98 }
99 }
100 pub fn resume_commit(&self, saved: SavedCommit) -> Result<FrozenCommit, Error> {
102 FrozenCommit::resume(self, saved)
103 }
104 #[cfg(feature = "tables")]
106 pub async fn table(&self, branch: &str, path: &str) -> Result<Table, Error> {
107 self.table_session(branch).await?.table(path).await
108 }
109}
110
111#[derive(Clone)]
115pub struct Snapshot {
116 pub(crate) project: Project,
117 pub(crate) commit_id: ProjectCommitId,
118 pub(crate) branch_id: Option<ProjectBranchId>,
119}
120impl Snapshot {
121 pub fn commit_id(&self) -> ProjectCommitId {
123 self.commit_id
124 }
125 pub fn branch_id(&self) -> Option<ProjectBranchId> {
127 self.branch_id
128 }
129 pub fn project(&self) -> &Project {
131 &self.project
132 }
133 pub async fn resources(&self) -> Result<Vec<ProjectResourceState>, Error> {
135 let mut resources = Vec::new();
136 let mut page_token = None;
137 let mut seen = HashSet::new();
138 loop {
139 let page = self
140 .project
141 .client
142 .connection
143 .catalog
144 .read_journal_state(
145 &self.project.id,
146 self.commit_id,
147 &ReadJournalStateRequest {
148 page_token: page_token.clone(),
149 page_size: Some(PageSize::default()),
150 },
151 )
152 .await?;
153 if page.commit_id != self.commit_id {
154 return Err(Error::invalid("Catalog changed the selected commit"));
155 }
156 for resource in &page.resources {
157 resource.validate().map_err(Error::invalid)?;
158 }
159 resources.extend(page.resources);
160 let Some(next) = page.next_page_token else {
161 return Ok(resources);
162 };
163 next.validate().map_err(Error::invalid)?;
164 if !seen.insert(next.clone()) {
165 return Err(Error::invalid("Catalog repeated a page token"));
166 }
167 page_token = Some(next);
168 }
169 }
170 #[cfg(feature = "tables")]
173 pub async fn table(&self, path: &str) -> Result<Table, Error> {
174 self.table_session().table(path).await
175 }
176
177 pub(crate) async fn resource_at_path(
179 &self,
180 path: &NamespacePath,
181 ) -> Result<ResourceAtSnapshot, Error> {
182 let resolved = self
183 .project
184 .client
185 .connection
186 .catalog
187 .resolve_journal_path(
188 &self.project.id,
189 self.commit_id,
190 &ResolveJournalPathRequest { path: path.clone() },
191 )
192 .await?;
193 if resolved.commit_id != self.commit_id {
194 return Err(Error::invalid("Catalog changed the selected commit"));
195 }
196 let resource = resolved.resource.ok_or_else(|| {
197 Error::NotFound(
198 path.segments()
199 .iter()
200 .map(NamespaceSegment::as_str)
201 .collect::<Vec<_>>()
202 .join("/"),
203 )
204 })?;
205 resource.validate().map_err(Error::invalid)?;
206 Ok(resource)
207 }
208
209 #[cfg(feature = "tables")]
210 pub(crate) async fn table_at_path(&self, path: &NamespacePath) -> Result<Table, Error> {
211 let resource = self.resource_at_path(path).await?;
212 if resource.descriptor.kind.0 != "table" || resource.descriptor.service.as_str() != "arbor"
213 {
214 return Err(Error::invalid("resource is not an Arbor table"));
215 }
216 let mut table = self.project.client.table_by_id(
217 resource.descriptor.resource_id.as_uuid(),
218 resource.version_id.as_uuid(),
219 );
220 table.base = Some(CatalogBase {
221 project_id: self.project.id.clone(),
222 commit_id: self.commit_id,
223 branch_id: self.branch_id,
224 version_id: resource.version_id.as_uuid(),
225 address: None,
226 });
227 Ok(table)
228 }
229}
230
231#[derive(Clone)]
236pub struct BranchHead {
237 snapshot: Snapshot,
238 branch_id: ProjectBranchId,
239}
240impl BranchHead {
241 pub(crate) fn new(
242 project: Project,
243 commit_id: ProjectCommitId,
244 branch_id: ProjectBranchId,
245 ) -> Self {
246 Self {
247 snapshot: Snapshot {
248 project,
249 commit_id,
250 branch_id: Some(branch_id),
251 },
252 branch_id,
253 }
254 }
255 #[cfg(feature = "tables")]
257 pub(crate) fn of(snapshot: &Snapshot) -> Option<Self> {
258 Some(Self {
259 snapshot: snapshot.clone(),
260 branch_id: snapshot.branch_id?,
261 })
262 }
263 pub fn branch_id(&self) -> ProjectBranchId {
265 self.branch_id
266 }
267 pub fn snapshot(&self) -> &Snapshot {
269 &self.snapshot
270 }
271 pub fn commit(&self) -> Commit {
273 Commit::new(self.clone())
274 }
275}
276impl Deref for BranchHead {
277 type Target = Snapshot;
278 fn deref(&self) -> &Snapshot {
279 &self.snapshot
280 }
281}
282
283impl Project {
284 pub async fn get(&self) -> Result<ProjectInfo, Error> {
286 Ok(self
287 .client
288 .projects()
289 .read_project(self.id.as_str().into())
290 .await?)
291 }
292 pub async fn delete(&self) -> Result<(), Error> {
294 Ok(self
295 .client
296 .projects()
297 .delete_project(self.id.as_str().into())
298 .await?)
299 }
300 pub fn grants(&self, principal: Option<Principal>) -> List<'_, Grant> {
302 self.client.authorization().list_grants_list(
303 self.id.as_str().into(),
304 ListGrantsRequest {
305 principal,
306 page_size: None,
307 page_token: None,
308 },
309 )
310 }
311 pub async fn grant_role(&self, request: GrantRoleRequest) -> Result<GrantRoleResponse, Error> {
313 Ok(self
314 .client
315 .authorization()
316 .grant_role(self.id.as_str().into(), request)
317 .await?)
318 }
319 pub async fn presentation(&self, key: PresentationKey) -> Result<Presentation, Error> {
321 Ok(self
322 .client
323 .projects()
324 .read_presentation(self.id.as_str().into(), key)
325 .await?)
326 }
327 pub async fn save_presentation(
329 &self,
330 request: SavePresentation,
331 ) -> Result<Presentation, Error> {
332 Ok(self
333 .client
334 .projects()
335 .save_presentation(self.id.as_str().into(), request)
336 .await?)
337 }
338}