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