use std::collections::BTreeMap;
use pointlock_ir::{ActionName, CanonicalVerb, Channel, FeatureId, JsonSchemaDocument};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "camelCase")]
pub enum PlatformKind {
Android,
Ios,
Web,
HarmonyOs,
MacOs,
Windows,
Linux,
Rdp,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "camelCase")]
pub enum ActionProtection {
Standard,
Protected,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "camelCase")]
pub enum ChannelRole {
Act,
Verify,
Both,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ProtocolRange {
pub major: u64,
pub min_minor: u64,
pub max_minor: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ConditionalFeature {
pub feature: FeatureId,
#[serde(skip_serializing_if = "Option::is_none")]
pub requires_platform: Option<Vec<PlatformKind>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct FeatureDeclarations {
pub guaranteed: Vec<FeatureId>,
pub conditional: Vec<ConditionalFeature>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct VerbBinding {
pub verb: CanonicalVerb,
pub action_name: ActionName,
#[serde(skip_serializing_if = "Option::is_none")]
pub requires_feature: Option<FeatureId>,
pub arg_map: BTreeMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ChannelSupport {
pub channel: Channel,
pub role: ChannelRole,
#[serde(skip_serializing_if = "Option::is_none")]
pub requires_feature: Option<FeatureId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub requires_platform: Option<Vec<PlatformKind>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ActionDefinitionStatic {
pub name: ActionName,
pub input_schema: JsonSchemaDocument,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_schema: Option<JsonSchemaDocument>,
pub protection: ActionProtection,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub synthetic: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ProviderManifest {
pub name: String,
pub version: String,
pub protocol: ProtocolRange,
pub features: FeatureDeclarations,
pub verb_bindings: Vec<VerbBinding>,
pub channels: Vec<ChannelSupport>,
pub known_actions: Vec<ActionDefinitionStatic>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn sample_manifest() -> ProviderManifest {
ProviderManifest {
name: "devicerail".to_owned(),
version: "0.1.0".to_owned(),
protocol: ProtocolRange {
major: 1,
min_minor: 5,
max_minor: 5,
},
features: FeatureDeclarations {
guaranteed: vec![FeatureId::new("device.semanticActions.v1").unwrap()],
conditional: vec![ConditionalFeature {
feature: FeatureId::new("observation.uiSnapshot.v1").unwrap(),
requires_platform: Some(vec![PlatformKind::Android, PlatformKind::HarmonyOs]),
}],
},
verb_bindings: vec![VerbBinding {
verb: CanonicalVerb::Tap,
action_name: ActionName::new("tapElement").unwrap(),
requires_feature: Some(FeatureId::new("device.semanticActions.v1").unwrap()),
arg_map: BTreeMap::from([("element".to_owned(), "element".to_owned())]),
}],
channels: vec![ChannelSupport {
channel: Channel::UiTree,
role: ChannelRole::Both,
requires_feature: Some(FeatureId::new("observation.uiSnapshot.v1").unwrap()),
requires_platform: None,
}],
known_actions: vec![ActionDefinitionStatic {
name: ActionName::new("tapElement").unwrap(),
input_schema: JsonSchemaDocument::new(json!(true)).unwrap(),
output_schema: None,
protection: ActionProtection::Standard,
synthetic: false,
}],
}
}
#[test]
fn manifest_wire_shape_is_camel_case() {
let wire = serde_json::to_value(sample_manifest()).expect("serialize");
assert_eq!(wire["protocol"]["minMinor"], 5);
assert_eq!(wire["verbBindings"][0]["verb"], "tap");
assert_eq!(wire["verbBindings"][0]["actionName"], "tapElement");
assert_eq!(wire["verbBindings"][0]["argMap"]["element"], "element");
assert_eq!(
wire["features"]["conditional"][0]["requiresPlatform"],
json!(["android", "harmonyOs"])
);
assert_eq!(wire["channels"][0]["role"], "both");
assert_eq!(wire["knownActions"][0]["protection"], "standard");
let back: ProviderManifest = serde_json::from_value(wire).expect("deserialize");
assert_eq!(back, sample_manifest());
}
#[test]
fn manifest_rejects_unknown_fields() {
let mut wire = serde_json::to_value(sample_manifest()).expect("serialize");
wire["surprise"] = json!(1);
assert!(serde_json::from_value::<ProviderManifest>(wire).is_err());
}
#[test]
fn platform_kind_wire_literals() {
for (kind, literal) in [
(PlatformKind::Android, "android"),
(PlatformKind::Ios, "ios"),
(PlatformKind::Web, "web"),
(PlatformKind::HarmonyOs, "harmonyOs"),
(PlatformKind::MacOs, "macOs"),
(PlatformKind::Windows, "windows"),
(PlatformKind::Linux, "linux"),
(PlatformKind::Rdp, "rdp"),
] {
assert_eq!(serde_json::to_value(kind).unwrap(), json!(literal));
}
}
}