Skip to main content

sofie_model/
identity.rs

1use crate::naming::IDENTITY_AREA;
2use crate::package_routes::{DEFAULT_MOUNT, ListedRoute, PackageRoutes, ServedRoute, served_route};
3use crate::sources::{ConditionalWiring, module_tree};
4use crate::{Code, Identity, ModelError};
5
6pub(crate) const IDENTITY_CRATE: &str = "sofie_identity";
7/// The member routes, which the wiring mounts in every member area a contract defines.
8const MEMBER_MOUNT: &str = DEFAULT_MOUNT;
9
10impl crate::AreaDefinition {
11    /// It implements sofie-identity's `MemberArea`, or `sofie gen` writes one for it.
12    #[must_use]
13    pub fn is_member_area(&self) -> bool {
14        self.implements_member_area || self.needs_generated_member_area()
15    }
16}
17
18impl<'app> PackageRoutes<'app> {
19    /// The wiring mounts the member routes for every member area a contract defines; the
20    /// area-generic ones are where the app's code mounts them.
21    pub fn identity(&self) -> Result<Vec<ServedRoute<'app>>, ModelError> {
22        let Some(identity) = &self.app.identity else {
23            return Ok(Vec::new());
24        };
25        let mut served = self.member_routes(identity)?;
26        let generic = identity
27            .mounts
28            .iter()
29            .filter(|mount| mount.mount != MEMBER_MOUNT);
30        served.extend(self.mounted(IDENTITY_CRATE, &identity.routes, generic));
31        Ok(served)
32    }
33
34    fn member_routes(
35        &self,
36        identity: &'app Identity,
37    ) -> Result<Vec<ServedRoute<'app>>, ModelError> {
38        let routes: Vec<&ListedRoute> = identity
39            .routes
40            .iter()
41            .filter(|route| route.mount == MEMBER_MOUNT)
42            .collect();
43        let mut served = Vec::new();
44        if routes.is_empty() {
45            return Ok(served);
46        }
47        for area in self.areas.iter().filter(|area| area.contract.is_some()) {
48            let Some(definition) = area
49                .definition
50                .filter(|definition| definition.is_member_area())
51            else {
52                continue;
53            };
54            let (Some(prefix), Some(paths)) =
55                (definition.prefix.as_deref(), definition.sign_in_paths()?)
56            else {
57                continue;
58            };
59            for route in &routes {
60                if route.path.contains("{sign_up}") && paths.sign_up.is_none() {
61                    continue;
62                }
63                let path = route
64                    .path
65                    .replace("{sign_in}", &paths.sign_in)
66                    .replace("{sign_up}", paths.sign_up.as_deref().unwrap_or_default())
67                    .replace("{prefix}", prefix);
68                served.push(served_route(
69                    IDENTITY_CRATE,
70                    IDENTITY_AREA,
71                    area.name,
72                    path,
73                    route,
74                ));
75            }
76        }
77        Ok(served)
78    }
79}
80
81impl Identity {
82    /// Read on demand: only `explain` needs sofie-identity's handlers.
83    pub fn code(&self) -> Result<Code, ModelError> {
84        Ok(module_tree(&self.directory, ConditionalWiring::Allowed)?
85            .found
86            .code)
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use crate::testing::{self, TemporaryDirectory};
93
94    #[test]
95    fn signs_members_in_to_no_public_area_a_contract_defines() {
96        let directory = TemporaryDirectory::new("sofie-model-identity-areas");
97        let mut order = testing::module("order", directory.path("order"));
98        testing::define_area(&mut order, "member");
99        let mut docs = order.defined_areas[0].clone();
100        docs.name = "docs".to_owned();
101        docs.type_path[2] = "Docs".to_owned();
102        docs.prefix = Some("/docs".to_owned());
103        docs.principal = vec!["Visitor".to_owned()];
104        docs.implements_member_area = false;
105        order.defined_areas.push(docs);
106        directory.write(
107            "order/contract/src/area.rs",
108            "impl Area for Member {\n    const SIGN_IN: &'static str = \"/sign-in\";\n}\n\
109             impl Area for Docs {\n    const SIGN_IN: &'static str = \"/\";\n}\n",
110        );
111        let mut shop = testing::app(directory.root(), vec![order]);
112        shop.identity = Some(crate::Identity {
113            routes: vec![crate::ListedRoute {
114                mount: "routes".to_owned(),
115                method: "GET".to_owned(),
116                path: "{sign_in}".to_owned(),
117                handler: "sign_in_page".to_owned(),
118                permission: None,
119            }],
120            ..crate::Identity::default()
121        });
122
123        let served = shop.package_routes().identity().unwrap();
124
125        let areas: Vec<(&str, &str)> = served
126            .iter()
127            .map(|route| (route.name.as_str(), route.path.as_str()))
128            .collect();
129        assert_eq!(areas, [("sofie_identity.member.sign_in_page", "/sign-in")]);
130    }
131}