cratefield_tables_api/declared.rs
1//! The declared tables, as a venture's generated module carries them
2//! (issue #153).
3//!
4//! `TableApi` is a runtime value with `String`s and `Vec`s in it, so a
5//! generated module cannot hold one in a `const`. It holds the
6//! declaration as JSON text instead and parses it once at boot.
7//!
8//! # Why text rather than emitted Rust
9//!
10//! Emitting `TableDef` literals would make a malformed declaration a
11//! compile error instead of a boot-time one, which is strictly better.
12//! It is also a great deal of generated code — every `FieldKind`'s
13//! bounds, every foreign key — and the text comes from the same manifest
14//! `fz build` has already validated, so the failure it would catch is a
15//! bug in the generator rather than anything an author can write.
16//!
17//! The cost is that the failure surfaces at boot. So it surfaces in
18//! `validate_config`, which the harness calls while composing: a
19//! deployment whose declaration will not parse refuses to start rather
20//! than starting and serving an empty table list.
21
22use cratefield_manifest::{AccessMap, TablePrivacy, TablePrivacyMap};
23use cratefield_tables::Schema;
24use serde::{Deserialize, Serialize};
25
26use crate::access::TableApi;
27
28/// A venture's `[tables]` section, in the three parts the API needs.
29#[derive(Debug, Clone, Default, Serialize, Deserialize)]
30pub struct Declared {
31 /// The tables themselves.
32 pub tables: Schema,
33 /// Who may reach each one.
34 pub access: AccessMap,
35 /// What each one holds — read for the subject column, which is what
36 /// `owner` matches a caller against.
37 pub privacy: TablePrivacyMap,
38}
39
40impl Declared {
41 /// The declared tables, ready to serve.
42 ///
43 /// A table with no access level is **left out** rather than given
44 /// one. `fz build` refuses a manifest that omits one (#359), so this
45 /// cannot happen from a generated module; if it somehow does, a table
46 /// nobody can reach is the only safe answer, and a `404` is what a
47 /// caller gets. Defaulting to `public-read` here would publish it and
48 /// defaulting to `admin` would hide a real bug behind a plausible
49 /// refusal.
50 #[must_use]
51 pub fn into_apis(self) -> Vec<TableApi> {
52 let Self {
53 tables,
54 access,
55 privacy,
56 } = self;
57 tables
58 .tables
59 .into_iter()
60 .filter_map(|table| {
61 let level = *access.get(&table.name)?;
62 let subject = match privacy.get(&table.name) {
63 Some(TablePrivacy::Personal { subject, .. }) => Some(subject.clone()),
64 _ => None,
65 };
66 Some(TableApi {
67 table,
68 access: level,
69 subject,
70 })
71 })
72 .collect()
73 }
74}
75
76/// Parses the JSON a generated module carries.
77///
78/// # Errors
79///
80/// A message naming what could not be read. It is for an operator: this
81/// text is generated, so a failure is a bug in the generator and the
82/// message has to be enough to find it.
83pub fn parse(json: &str) -> Result<Vec<TableApi>, String> {
84 serde_json::from_str::<Declared>(json)
85 .map(Declared::into_apis)
86 .map_err(|err| format!("the generated table declaration could not be read: {err}"))
87}