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}