Skip to main content

asc_hap/
profile.rs

1//! The profiles `module.json` names for pages: the page list
2//! (`"pages": "$profile:main_pages"`) and the navigation route table
3//! (`"routerMap": "$profile:router_map"`), both JSON files under
4//! `resources/base/profile/`.
5
6use crate::HapError;
7use crate::record;
8use crate::value::{self, Findings, Line, Note, Obj};
9
10/// A parsed page list: `{"src": ["pages/Index", …]}`.
11#[derive(Debug, Clone, Default, PartialEq, Eq)]
12pub struct PageList {
13    /// Page paths relative to the module's `ets/` directory, without
14    /// extension, in file order.
15    pub pages: Vec<String>,
16    /// Fields that were present but wrongly typed, and so skipped.
17    pub notes: Vec<Note>,
18}
19
20impl PageList {
21    /// Read a page list.
22    ///
23    /// # Errors
24    ///
25    /// Only when `bytes` is not JSON or its root is not an object.
26    pub fn parse(bytes: &[u8]) -> Result<Self, HapError> {
27        let map = value::parse_object(bytes)?;
28        let mut findings = Findings::default();
29        let mut root = Obj::root(&map, &mut findings);
30        let pages = root.strings("src");
31        // What else the file holds (`window`, …) says nothing about code.
32        Ok(PageList { pages, notes: findings.notes })
33    }
34
35    /// The `srcEntry` spelling of `page`, which
36    /// [`ModuleJson::record_names`](crate::ModuleJson::record_names) takes:
37    /// `./ets/pages/Index` for `pages/Index`.
38    pub fn src_entry(page: &str) -> String {
39        format!("./ets/{}", page.trim_start_matches("./").trim_start_matches('/'))
40    }
41
42    /// The summary line of `page`: `page pages/Index`.
43    pub fn line(page: &str) -> String {
44        Line::new("page").word(page).end()
45    }
46}
47
48/// A parsed route table: `{"routerMap": [{…}, …]}`.
49#[derive(Debug, Clone, Default, PartialEq, Eq)]
50pub struct RouterMap {
51    /// In file order.
52    pub routes: Vec<Route>,
53    /// Fields that were present but wrongly typed, and so skipped.
54    pub notes: Vec<Note>,
55}
56
57/// One route: a name the navigation stack is pushed with, and the builder
58/// function that makes its page.
59#[derive(Debug, Clone, Default, PartialEq, Eq)]
60pub struct Route {
61    pub name: Option<String>,
62    /// The source file of the page, relative to the root of the package
63    /// that has it: `src/main/ets/pages/Detail.ets`.
64    pub page_source_file: Option<String>,
65    /// The `@Builder` function of that file the system calls.
66    pub build_function: Option<String>,
67    /// The module request of the page's record, as the build wrote it:
68    /// `@normalized:N&<module>&<bundle>&<package>/<path>&<version>`.
69    pub ohmurl: Option<String>,
70    /// The package that has the page, when it is not the module itself.
71    pub package_name: Option<String>,
72    pub module_name: Option<String>,
73    pub bundle_name: Option<String>,
74}
75
76impl Route {
77    /// The record [`Route::ohmurl`] names: `<bundle>&<package>/<path>&<version>`
78    /// of a `@normalized:N&<module>&<bundle>&<package>/<path>&<version>`
79    /// request. `None` for any other form (a native library, a system module).
80    pub fn ohmurl_record(&self) -> Option<&str> {
81        let rest = self.ohmurl.as_deref()?.strip_prefix("@normalized:N&")?;
82        let (_module, record) = rest.split_once('&')?;
83        // `<bundle>&<path>&<version>`: two more `&`, and a path between them.
84        let (_bundle, tail) = record.split_once('&')?;
85        let (path, _version) = tail.rsplit_once('&')?;
86        (!path.is_empty()).then_some(record)
87    }
88
89    /// The summary line of the route:
90    /// `route NAME buildFunction=F source=FILE`, with `package=NAME` when
91    /// the page is in another package than the module.
92    pub fn line(&self) -> String {
93        Line::new("route")
94            .word(self.name.as_deref().unwrap_or("-"))
95            .opt("buildFunction", self.build_function.as_deref())
96            .opt("source", self.page_source_file.as_deref())
97            .opt_present("package", self.package_name.as_deref())
98            .end()
99    }
100
101    /// The module [`Route::ohmurl`] says the record is in; empty for the
102    /// module that has the route table.
103    pub fn ohmurl_module(&self) -> Option<&str> {
104        let rest = self.ohmurl.as_deref()?.strip_prefix("@normalized:N&")?;
105        rest.split_once('&').map(|(module, _)| module)
106    }
107
108    /// [`Route::page_source_file`] without its extension, behind the package
109    /// that has it (`default_package` when the route names none):
110    /// `<package>/src/main/ets/pages/Detail`, the part of a normalized
111    /// record name between its `&`.
112    pub fn record_path(&self, default_package: Option<&str>) -> Option<String> {
113        let source = self.page_source_file.as_deref()?.trim_start_matches("./").trim_start_matches('/');
114        let package = self.package_name.as_deref().or(default_package)?;
115        Some(format!("{package}/{}", record::strip_source_extension(source)))
116    }
117}
118
119impl RouterMap {
120    /// Read a route table.
121    ///
122    /// # Errors
123    ///
124    /// Only when `bytes` is not JSON or its root is not an object.
125    pub fn parse(bytes: &[u8]) -> Result<Self, HapError> {
126        let map = value::parse_object(bytes)?;
127        let mut findings = Findings::default();
128        let mut root = Obj::root(&map, &mut findings);
129        let routes = root.objects("routerMap", |route| Route {
130            name: route.str("name"),
131            page_source_file: route.str("pageSourceFile"),
132            build_function: route.str("buildFunction"),
133            ohmurl: route.str("ohmurl"),
134            package_name: route.str("packageName"),
135            module_name: route.str("moduleName"),
136            bundle_name: route.str("bundleName"),
137        });
138        // `data` / `customData` of a route are the application's own.
139        Ok(RouterMap { routes, notes: findings.notes })
140    }
141}