1use syn::{Expr, Item};
2
3use crate::areas::{constant_value, implemented_trait};
4use crate::syntax::{read_file, string_literal, type_last_name};
5use crate::{AreaDefinition, ModelError};
6
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct SignInPaths {
9 pub sign_in: String,
10 pub sign_up: Option<String>,
11}
12
13impl AreaDefinition {
14 pub fn sign_in_paths(&self) -> Result<Option<SignInPaths>, ModelError> {
17 let Some(type_name) = self.type_path.last() else {
18 return Ok(None);
19 };
20 let file = read_file(&self.file)?;
21 let constant = |trait_name: &str, constant: &str| {
22 file.items.iter().find_map(|item| match item {
23 Item::Impl(implementation)
24 if implemented_trait(implementation).as_deref() == Some(trait_name)
25 && type_last_name(&implementation.self_ty).as_ref() == Some(type_name) =>
26 {
27 constant_value(implementation, constant).cloned()
28 }
29 _ => None,
30 })
31 };
32 let Some(sign_in) = constant("Area", "SIGN_IN")
33 .as_ref()
34 .and_then(string_literal)
35 else {
36 return Ok(None);
37 };
38 let sign_up = constant("MemberArea", "SIGN_UP")
39 .as_ref()
40 .and_then(some_literal)
41 .or_else(|| constant("SignUp", "PATH").as_ref().and_then(string_literal));
42 Ok(Some(SignInPaths { sign_in, sign_up }))
43 }
44}
45
46fn some_literal(expression: &Expr) -> Option<String> {
47 let Expr::Call(call) = expression else {
48 return None;
49 };
50 let is_some = matches!(call.func.as_ref(), Expr::Path(path) if path.path.is_ident("Some"));
51 match call.args.first() {
52 Some(argument) if is_some && call.args.len() == 1 => string_literal(argument),
53 _ => None,
54 }
55}
56
57#[cfg(test)]
58mod tests {
59 use crate::testing::{TemporaryDirectory, define_area, module};
60
61 use super::SignInPaths;
62
63 const AREA: &str = "impl Area for Member {\n const NAME: &'static str = \"member\";\n \
64 const SIGN_IN: &'static str = \"/sign-in\";\n}\n";
65
66 #[test]
67 fn reads_sign_in_and_an_opened_sign_up_from_the_areas_file() {
68 let directory = TemporaryDirectory::new("sofie-model-sign-in");
69 let mut order = module("order", directory.path("order"));
70 define_area(&mut order, "member");
71 let definition = &order.defined_areas[0];
72 let member = "impl MemberArea for Member {\n const SIGN_UP: Option<&'static str> = Some(\"/join\");\n}\n";
73 directory.write("order/contract/src/area.rs", &format!("{AREA}{member}"));
74
75 let paths = definition.sign_in_paths().unwrap();
76
77 let expected = SignInPaths {
78 sign_in: "/sign-in".to_owned(),
79 sign_up: Some("/join".to_owned()),
80 };
81 assert_eq!(paths, Some(expected));
82 }
83
84 #[test]
85 fn reads_sign_up_declared_for_a_generated_member_area() {
86 let directory = TemporaryDirectory::new("sofie-model-sign-up");
87 let mut order = module("order", directory.path("order"));
88 define_area(&mut order, "member");
89 let sign_up = "impl SignUp for Member {\n const PATH: &'static str = \"/join\";\n}\n";
90 directory.write("order/contract/src/area.rs", &format!("{AREA}{sign_up}"));
91
92 let paths = order.defined_areas[0].sign_in_paths().unwrap().unwrap();
93
94 assert_eq!(paths.sign_up.as_deref(), Some("/join"));
95 }
96
97 #[test]
98 fn a_member_area_without_sign_up_keeps_it_closed() {
99 let directory = TemporaryDirectory::new("sofie-model-sign-in");
100 let mut order = module("order", directory.path("order"));
101 define_area(&mut order, "member");
102 directory.write("order/contract/src/area.rs", AREA);
103
104 let paths = order.defined_areas[0].sign_in_paths().unwrap().unwrap();
105
106 assert_eq!(paths.sign_up, None);
107 }
108}