1use std::collections::BTreeMap;
9
10use pointlock_ir::{ActionName, CanonicalVerb, Channel, FeatureId, JsonSchemaDocument};
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13
14#[derive(
16 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
17)]
18#[serde(rename_all = "camelCase")]
19pub enum PlatformKind {
20 Android,
22 Ios,
24 Web,
26 HarmonyOs,
28 MacOs,
30 Windows,
32 Linux,
34 Rdp,
36}
37
38#[derive(
43 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
44)]
45#[serde(rename_all = "camelCase")]
46pub enum ActionProtection {
47 Standard,
49 Protected,
51}
52
53#[derive(
55 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
56)]
57#[serde(rename_all = "camelCase")]
58pub enum ChannelRole {
59 Act,
61 Verify,
63 Both,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
70#[serde(rename_all = "camelCase", deny_unknown_fields)]
71pub struct ProtocolRange {
72 pub major: u64,
74 pub min_minor: u64,
76 pub max_minor: u64,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
83#[serde(rename_all = "camelCase", deny_unknown_fields)]
84pub struct ConditionalFeature {
85 pub feature: FeatureId,
87 #[serde(skip_serializing_if = "Option::is_none")]
90 pub requires_platform: Option<Vec<PlatformKind>>,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
97#[serde(rename_all = "camelCase", deny_unknown_fields)]
98pub struct FeatureDeclarations {
99 pub guaranteed: Vec<FeatureId>,
101 pub conditional: Vec<ConditionalFeature>,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
108#[serde(rename_all = "camelCase", deny_unknown_fields)]
109pub struct VerbBinding {
110 pub verb: CanonicalVerb,
112 pub action_name: ActionName,
114 #[serde(skip_serializing_if = "Option::is_none")]
117 pub requires_feature: Option<FeatureId>,
118 pub arg_map: BTreeMap<String, String>,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
126#[serde(rename_all = "camelCase", deny_unknown_fields)]
127pub struct ChannelSupport {
128 pub channel: Channel,
130 pub role: ChannelRole,
132 #[serde(skip_serializing_if = "Option::is_none")]
135 pub requires_feature: Option<FeatureId>,
136 #[serde(skip_serializing_if = "Option::is_none")]
138 pub requires_platform: Option<Vec<PlatformKind>>,
139}
140
141#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
145#[serde(rename_all = "camelCase", deny_unknown_fields)]
146pub struct ActionDefinitionStatic {
147 pub name: ActionName,
149 pub input_schema: JsonSchemaDocument,
152 #[serde(skip_serializing_if = "Option::is_none")]
155 pub output_schema: Option<JsonSchemaDocument>,
156 pub protection: ActionProtection,
158 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
172 pub synthetic: bool,
173}
174
175#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
179#[serde(rename_all = "camelCase", deny_unknown_fields)]
180pub struct ProviderManifest {
181 pub name: String,
183 pub version: String,
185 pub protocol: ProtocolRange,
187 pub features: FeatureDeclarations,
189 pub verb_bindings: Vec<VerbBinding>,
191 pub channels: Vec<ChannelSupport>,
193 pub known_actions: Vec<ActionDefinitionStatic>,
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202 use serde_json::json;
203
204 fn sample_manifest() -> ProviderManifest {
205 ProviderManifest {
206 name: "devicerail".to_owned(),
207 version: "0.1.0".to_owned(),
208 protocol: ProtocolRange {
209 major: 1,
210 min_minor: 5,
211 max_minor: 5,
212 },
213 features: FeatureDeclarations {
214 guaranteed: vec![FeatureId::new("device.semanticActions.v1").unwrap()],
215 conditional: vec![ConditionalFeature {
216 feature: FeatureId::new("observation.uiSnapshot.v1").unwrap(),
217 requires_platform: Some(vec![PlatformKind::Android, PlatformKind::HarmonyOs]),
218 }],
219 },
220 verb_bindings: vec![VerbBinding {
221 verb: CanonicalVerb::Tap,
222 action_name: ActionName::new("tapElement").unwrap(),
223 requires_feature: Some(FeatureId::new("device.semanticActions.v1").unwrap()),
224 arg_map: BTreeMap::from([("element".to_owned(), "element".to_owned())]),
225 }],
226 channels: vec![ChannelSupport {
227 channel: Channel::UiTree,
228 role: ChannelRole::Both,
229 requires_feature: Some(FeatureId::new("observation.uiSnapshot.v1").unwrap()),
230 requires_platform: None,
231 }],
232 known_actions: vec![ActionDefinitionStatic {
233 name: ActionName::new("tapElement").unwrap(),
234 input_schema: JsonSchemaDocument::new(json!(true)).unwrap(),
235 output_schema: None,
236 protection: ActionProtection::Standard,
237 synthetic: false,
238 }],
239 }
240 }
241
242 #[test]
243 fn manifest_wire_shape_is_camel_case() {
244 let wire = serde_json::to_value(sample_manifest()).expect("serialize");
245 assert_eq!(wire["protocol"]["minMinor"], 5);
246 assert_eq!(wire["verbBindings"][0]["verb"], "tap");
247 assert_eq!(wire["verbBindings"][0]["actionName"], "tapElement");
248 assert_eq!(wire["verbBindings"][0]["argMap"]["element"], "element");
249 assert_eq!(
250 wire["features"]["conditional"][0]["requiresPlatform"],
251 json!(["android", "harmonyOs"])
252 );
253 assert_eq!(wire["channels"][0]["role"], "both");
254 assert_eq!(wire["knownActions"][0]["protection"], "standard");
255 let back: ProviderManifest = serde_json::from_value(wire).expect("deserialize");
256 assert_eq!(back, sample_manifest());
257 }
258
259 #[test]
260 fn manifest_rejects_unknown_fields() {
261 let mut wire = serde_json::to_value(sample_manifest()).expect("serialize");
262 wire["surprise"] = json!(1);
263 assert!(serde_json::from_value::<ProviderManifest>(wire).is_err());
264 }
265
266 #[test]
267 fn platform_kind_wire_literals() {
268 for (kind, literal) in [
269 (PlatformKind::Android, "android"),
270 (PlatformKind::Ios, "ios"),
271 (PlatformKind::Web, "web"),
272 (PlatformKind::HarmonyOs, "harmonyOs"),
273 (PlatformKind::MacOs, "macOs"),
274 (PlatformKind::Windows, "windows"),
275 (PlatformKind::Linux, "linux"),
276 (PlatformKind::Rdp, "rdp"),
277 ] {
278 assert_eq!(serde_json::to_value(kind).unwrap(), json!(literal));
279 }
280 }
281}