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