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}