1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
//! A feature's relationship routes (ADR 0016), one [`Relation`] per
//! relationship of its table whose related table has a feature of its own:
//! a to-one relationship is embedded by `GET /{key}?include=<name>`, a
//! has-many one is read by `GET /{key}/<name>`, and a many-to-many one is
//! also synced, attached and detached. Related rows answer through their
//! own feature's response, and that feature's access and owner apply.
use heck::{ToKebabCase, ToUpperCamelCase};
use super::feature::{Endpoint, Feature};
use super::scope::Caller;
use crate::generator::joins::joins;
use crate::generator::relationships::{join_pair, resolve};
use crate::schema::{Column, Table};
use crate::{ApiAccess, ApiConfig};
/// One relationship route set of a feature.
#[derive(Debug, Clone)]
pub struct Relation<'a> {
/// The relationship's name (`[relationships]` renames applied).
pub name: String,
pub kind: Kind<'a>,
/// The related table.
pub target: &'a Table,
/// Who may read the related table: the route needs the stricter of it
/// and the feature's own access.
pub target_access: ApiAccess,
/// The related table's owner column: only the caller's related rows
/// are answered, attached or detached.
pub target_owner: Option<&'a Column>,
/// The tenant column whose type the route's `Tenant` holds, when any
/// data layer call it makes is tenant-scoped.
pub tenant: Option<&'a Column>,
}
/// What a relation's routes read and write.
#[derive(Debug, Clone)]
pub enum Kind<'a> {
/// A to-one relationship, embedded by `?include=` through the data
/// layer's joined read.
ToOne {
/// Whether the related row may be absent.
optional: bool,
/// The joined read's field holding the row read.
row_field: String,
/// Whether the joined read takes the tenant.
tenant: bool,
},
/// A to-many relationship, read through the data layer's loader.
ToMany {
/// The column of the feature's rows the loader keys its map by.
key: &'a Column,
/// Whether the loader takes the tenant.
tenant: bool,
/// A many-to-many relationship's writes.
writes: Option<Writes<'a>>,
},
}
/// The writes of a many-to-many relationship, through its join table.
#[derive(Debug, Clone)]
pub struct Writes<'a> {
/// The join table.
pub join: &'a Table,
/// The join table's column holding the feature's key, and the one
/// holding the related row's.
pub own: &'a Column,
pub other: &'a Column,
/// Whether the join table's `attach` and `sync` take the feature's key
/// first (its pair's `near` column).
pub near: bool,
/// Whether the join table has `attach` and `sync`: they insert only the
/// pair.
pub inserts: bool,
}
impl<'a> Relation<'a> {
/// Who the routes act for: the stricter of the feature's and the related
/// table's access, and the caller's id when either table is owned.
pub fn caller(&self, feature: &Feature<'_>) -> Caller {
fn rank(access: ApiAccess) -> u8 {
match access {
ApiAccess::Public => 0,
ApiAccess::User => 1,
ApiAccess::Admin => 2,
}
}
let access = if rank(self.target_access) > rank(feature.access) {
self.target_access
} else {
feature.access
};
Caller {
access,
user: feature.owner.is_some() || self.target_owner.is_some(),
}
}
/// The related feature's response, such as `AccountResponse`.
pub fn response(&self) -> String {
format!("{}Response", self.target.struct_name)
}
/// The related table's row, such as `Account`.
pub fn row(&self) -> &str {
&self.target.struct_name
}
/// The related table's key type, such as `LabelId`.
pub fn target_key(&self) -> String {
crate::generator::keys::key_type(self.target)
}
/// The route segment, such as `labels`.
pub fn segment(&self) -> String {
self.name.to_kebab_case()
}
/// The to-one embedding's response, such as `TaskWithOwnerResponse`.
pub fn with_response(&self, feature: &Feature<'_>) -> String {
format!(
"{}With{}Response",
feature.row(),
self.name.to_upper_camel_case()
)
}
/// The `?include=` variant, such as `Owner`.
pub fn variant(&self) -> String {
self.name.to_upper_camel_case()
}
/// Whether the relation is many-to-many with writes.
pub fn writes(&self) -> Option<&Writes<'a>> {
match &self.kind {
Kind::ToMany { writes, .. } => writes.as_ref(),
Kind::ToOne { .. } => None,
}
}
/// Whether `PUT /{key}/<name>` replaces the pairs: only from the join
/// table's `near` side, where the data layer has `sync`, and only when
/// the related rows aren't owned (a sync would drop other owners'
/// pairs).
pub fn syncs(&self) -> bool {
self.writes()
.is_some_and(|writes| writes.near && writes.inserts && self.target_owner.is_none())
}
/// Whether `PUT /{key}/<name>/{other}` attaches.
pub fn attaches(&self) -> bool {
self.writes().is_some_and(|writes| writes.inserts)
}
/// The handlers of a to-many relation, in the order `handlers.rs` holds
/// them: its list, then a many-to-many's sync, attach and detach where
/// served. None for a to-one relation, which `GET /{key}` embeds.
pub fn actions(&self) -> Vec<Action> {
if !matches!(self.kind, Kind::ToMany { .. }) {
return Vec::new();
}
let mut actions = vec![Action::List];
if self.writes().is_some() {
if self.syncs() {
actions.push(Action::Sync);
}
if self.attaches() {
actions.push(Action::Attach);
}
actions.push(Action::Detach);
}
actions
}
/// The name of `action`'s handler, which is also its operation id, such
/// as `list_labels`.
pub fn handler(&self, action: Action) -> String {
format!("{}_{}", action.name(), self.name)
}
/// `action`'s route under the feature's route base, such as
/// `/{id}/labels/{other}`.
pub fn path(&self, feature: &Feature<'_>, action: Action) -> String {
let path = format!("{}/{}", feature.key_path(), self.segment());
if action.names_other() {
format!("{path}/{{other}}")
} else {
path
}
}
}
/// A handler of a to-many relation.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
/// `GET /{key}/<name>`: the related rows.
List,
/// `PUT /{key}/<name>`: pairs the row with exactly the rows named.
Sync,
/// `PUT /{key}/<name>/{other}`: pairs the row with another.
Attach,
/// `DELETE /{key}/<name>/{other}`: unpairs them.
Detach,
}
impl Action {
/// The handler's name before the relation's: `list`, `sync`, `attach`
/// or `detach`.
pub fn name(self) -> &'static str {
match self {
Action::List => "list",
Action::Sync => "sync",
Action::Attach => "attach",
Action::Detach => "detach",
}
}
/// The HTTP method, lowercase.
pub fn method(self) -> &'static str {
match self {
Action::List => "get",
Action::Sync | Action::Attach => "put",
Action::Detach => "delete",
}
}
/// Whether the route names the related row too, as `{other}`.
pub fn names_other(self) -> bool {
matches!(self, Action::Attach | Action::Detach)
}
}
impl<'a> Feature<'a> {
/// The to-one relations, which `GET /{key}?include=` embeds.
pub fn includes(&self) -> Vec<&Relation<'a>> {
self.relations
.iter()
.filter(|relation| matches!(relation.kind, Kind::ToOne { .. }))
.collect()
}
}
/// The relations of `feature`, among `features` (each with its table in
/// `tables`): none when `[api.skip]` skips them.
pub fn relations<'a>(
feature: &Feature<'a>,
features: &[Feature<'a>],
tables: &'a [Table],
api: &ApiConfig,
) -> Vec<Relation<'a>> {
let table = feature.table;
if Feature::skips_relationships(api, table) || feature.key.is_none() {
return Vec::new();
}
let joined = joins(table, tables);
table
.relationships
.iter()
.filter_map(|relationship| {
let resolved = resolve(table, relationship, tables);
let target = features
.iter()
.find(|other| other.table.module_name == resolved.target.module_name)?;
let target_tenant = target.table.tenant();
let (kind, tenants) = if relationship.kind.is_to_many() {
let writes = resolved.join.as_ref().and_then(|join| {
let pair = join_pair(join.table)?;
let single = |table: &Table| matches!(table.key_columns().as_slice(), [key] if key.is_copy());
if !single(table) || !single(target.table) {
return None;
}
Some(Writes {
join: join.table,
own: join.column,
other: join.target_column,
near: pair.near.sql_name == join.column.sql_name,
inserts: join_inserts(join.table, pair.near, pair.far),
})
});
let join_tenant = resolved.join.as_ref().and_then(|join| join.table.tenant());
let loader_tenant = target_tenant.or(join_tenant);
let mut tenants = vec![loader_tenant, target_tenant];
if let Some(writes) = &writes {
tenants.push(writes.join.tenant());
}
let kind = Kind::ToMany {
key: resolved.column,
tenant: loader_tenant.is_some(),
writes,
};
(kind, tenants)
} else {
if !feature.serves(Endpoint::GetOne) {
return None;
}
let join = joined.iter().find(|join| join.name() == relationship.name)?;
let kind = Kind::ToOne {
optional: join.optional(),
row_field: join.row_field(),
tenant: join.tenant().is_some(),
};
(kind, vec![join.tenant(), target_tenant])
};
let tenant = one_tenant(feature.tenant, &tenants)?;
Some(Relation {
name: relationship.name.clone(),
kind,
target: target.table,
target_access: target.access,
target_owner: target.owner,
tenant,
})
})
.collect()
}
/// The one tenant column of a route's calls: the feature's, or the first
/// of `others`. `None` (no route) when two of them hold different types,
/// or a type a `Tenant` can't hand to several calls by value.
#[allow(
clippy::option_option,
reason = "`Some(None)`: a route without a tenant"
)]
fn one_tenant<'a>(
own: Option<&'a Column>,
others: &[Option<&'a Column>],
) -> Option<Option<&'a Column>> {
let mut all = own.into_iter().chain(others.iter().flatten().copied());
let Some(first) = all.next() else {
return Some(None);
};
let same = all.all(|other| other.value_type() == first.value_type());
(same && first.is_copy()).then_some(Some(first))
}
/// Whether a join table's `attach` and `sync` exist: every insertable
/// column but the pair can be left out.
fn join_inserts(join: &Table, near: &Column, far: &Column) -> bool {
join.insert_columns().iter().all(|column| {
column.sql_name == near.sql_name
|| column.sql_name == far.sql_name
|| column.has_default
|| column.nullable
})
}