1use sofie_model::naming::pascal_case;
2use sofie_model::{App, Crate};
3
4use super::edits::module_file;
5use super::route::{NewRoute, file_names, new_route};
6use super::{Edits, checked_name, checked_type_name, find_module};
7use crate::gen_command::GenError;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum AreaAccess {
12 Public,
13 Owned,
14 Permitted,
15}
16
17impl AreaAccess {
18 pub const ALL: [Self; 3] = [Self::Public, Self::Owned, Self::Permitted];
19
20 #[must_use]
21 pub fn parse(name: &str) -> Option<Self> {
22 Self::ALL.into_iter().find(|access| access.name() == name)
23 }
24
25 fn parts(self) -> (&'static str, &'static str, &'static str) {
27 match self {
28 Self::Public => ("public", "sofie::http", "Visitor"),
29 Self::Owned => ("owned", "sofie_identity", "Account"),
30 Self::Permitted => ("permitted", "sofie_identity", "Permitted"),
31 }
32 }
33
34 #[must_use]
35 pub fn name(self) -> &'static str {
36 self.parts().0
37 }
38
39 fn signs_in(self) -> bool {
40 self != Self::Public
41 }
42}
43
44pub(super) fn new_area(
46 edits: &mut Edits,
47 app: &App,
48 module: &str,
49 name: &str,
50 access: AreaAccess,
51) -> Result<(), GenError> {
52 let name = checked_name(name)?;
53 let type_name = pascal_case(name);
54 checked_type_name(&type_name)?;
55 if app.area(name).is_some() {
56 return Err(GenError::Usage(format!(
57 "the app has an area `{name}` already; choose another name"
58 )));
59 }
60 let defining = find_module(app, module)?;
61 let contract = defining.contract.as_ref().ok_or_else(|| {
62 GenError::Usage(format!(
63 "module `{module}` has no contract to define the area in; add `contract/` as \
64 `sofie new module` writes it"
65 ))
66 })?;
67 refuse_taken_prefix(app, name)?;
68 refuse_taken_type_name(app, contract, name, &type_name)?;
69 write_definition(edits, contract, name, &type_name, access)?;
70 depend_on_principal(edits, app, contract, access)?;
71 let reloaded = sofie_model::load(&app.root).map_err(GenError::Model)?;
72 new_route(
73 edits,
74 &reloaded,
75 &NewRoute {
76 module: module.to_owned(),
77 area: name.to_owned(),
78 method: "get".to_owned(),
79 path: HOME.to_owned(),
80 handler: format!("{name}_home"),
81 screen: Some("home".to_owned()),
82 fields: None,
83 read_model: None,
84 title: None,
85 back: None,
86 },
87 )
88}
89
90const HOME: &str = "/home";
93
94fn prefix(name: &str) -> String {
95 format!("/{}", name.replace('_', "-"))
96}
97
98fn refuse_taken_prefix(app: &App, name: &str) -> Result<(), GenError> {
99 let prefix = prefix(name);
100 let areas = app.areas();
101 let Some(taken) = areas
102 .iter()
103 .find(|area| area.prefix() == Some(prefix.as_str()))
104 else {
105 return Ok(());
106 };
107 Err(GenError::Usage(format!(
108 "area `{}` serves under the prefix `{prefix}` already, which area `{name}` would take; \
109 choose another name",
110 taken.name
111 )))
112}
113
114fn refuse_taken_type_name(
117 app: &App,
118 contract: &Crate,
119 name: &str,
120 type_name: &str,
121) -> Result<(), GenError> {
122 let file = module_file(contract, "area");
123 let in_file = if file.is_file() {
124 file_names(&file)?
125 } else {
126 Vec::new()
127 };
128 let principals = AreaAccess::ALL.map(|access| access.parts().2);
129 if !in_file
130 .iter()
131 .map(String::as_str)
132 .chain(principals)
133 .any(|taken| taken == type_name)
134 {
135 return Ok(());
136 }
137 Err(GenError::Usage(format!(
138 "`{type_name}`, the type area `{name}` would define, is a name {} declares or imports \
139 already; choose another name",
140 file.strip_prefix(&app.root).unwrap_or(&file).display()
141 )))
142}
143
144fn write_definition(
145 edits: &mut Edits,
146 contract: &Crate,
147 name: &str,
148 type_name: &str,
149 access: AreaAccess,
150) -> Result<(), GenError> {
151 let (_, principal_crate, principal) = access.parts();
152 let prefix = prefix(name);
153 let sign_in = if access.signs_in() {
154 format!("{prefix}/sign-in")
155 } else {
156 "/".to_owned()
157 };
158 let definition = format!(
159 "pub struct {type_name};\n\nimpl Area for {type_name} {{\n \
160 const NAME: &'static str = \"{name}\";\n const PREFIX: &'static str = \"{prefix}\";\n \
161 const SIGN_IN: &'static str = \"{sign_in}\";\n type Principal = {principal};\n}}\n"
162 );
163 let file = module_file(contract, "area");
164 let imports = [
165 ("use sofie::http", "Area"),
166 (&format!("use {principal_crate}"), principal),
167 ];
168 edits.append(&file, &imports, &definition)?;
169 edits.declare(contract, &["area"])
170}
171
172fn depend_on_principal(
175 edits: &mut Edits,
176 app: &App,
177 contract: &Crate,
178 access: AreaAccess,
179) -> Result<(), GenError> {
180 edits.depend_on_framework(contract)?;
181 match access {
182 AreaAccess::Public => return Ok(()),
183 AreaAccess::Owned => edits.depend_on_workspace(contract, IDENTITY)?,
184 AreaAccess::Permitted => edits.enable_feature(contract, IDENTITY, "permissions")?,
185 }
186 edits.depend_on_workspace(&app.app, IDENTITY)?;
187 edits.depend(&app.app, contract)
188}
189
190const IDENTITY: &str = "sofie-identity";
191
192#[cfg(test)]
193mod tests {
194 use super::AreaAccess;
195
196 #[test]
197 fn each_name_reads_back_as_its_access() {
198 for access in AreaAccess::ALL {
199 assert_eq!(AreaAccess::parse(access.name()), Some(access));
200 }
201 assert_eq!(AreaAccess::parse("private"), None);
202 }
203}