Skip to main content

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}