mirror of
https://github.com/tauri-apps/tauri.git
synced 2024-12-01 03:02:28 +03:00
feat: improve deserialization errors by using serde-untagged on enums (#9952)
* Use serde-untagged instead of derive trait for capabilities structs * Update core/tauri-utils/Cargo.toml * improve errors for other untagged enums * clippy * add tests, fix deserialization * use schemars attribute instead --------- Co-authored-by: Amr Bashir <amr.bashir2015@gmail.com> Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
This commit is contained in:
parent
878198777e
commit
167b51a8de
28
Cargo.lock
generated
28
Cargo.lock
generated
@ -810,6 +810,16 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "erased-serde"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"typeid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.9"
|
||||
@ -3198,6 +3208,17 @@ dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-untagged"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2676ba99bd82f75cae5cbd2c8eda6fa0b8760f18978ea840e980dd5567b5c5b6"
|
||||
dependencies = [
|
||||
"erased-serde",
|
||||
"serde",
|
||||
"typeid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.203"
|
||||
@ -3859,6 +3880,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde-untagged",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"serialize-to-javascript",
|
||||
@ -4222,6 +4244,12 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "059d83cc991e7a42fc37bd50941885db0888e34209f8cfd9aab07ddec03bc9cf"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.17.0"
|
||||
|
@ -42,6 +42,7 @@ infer = "0.15"
|
||||
dunce = "1"
|
||||
log = "0.4.21"
|
||||
cargo_metadata = { version = "0.18", optional = true }
|
||||
serde-untagged = "0.1"
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
swift-rs = { version = "1.0.6", optional = true, features = [ "build" ] }
|
||||
|
@ -7,15 +7,19 @@
|
||||
use std::{path::Path, str::FromStr};
|
||||
|
||||
use crate::{acl::Identifier, platform::Target};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{
|
||||
de::{Error, IntoDeserializer},
|
||||
Deserialize, Deserializer, Serialize,
|
||||
};
|
||||
use serde_untagged::UntaggedEnumVisitor;
|
||||
|
||||
use super::Scopes;
|
||||
|
||||
/// An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`]
|
||||
/// or an object that references a permission and extends its scope.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum PermissionEntry {
|
||||
/// Reference a permission or permission set by identifier.
|
||||
PermissionRef(Identifier),
|
||||
@ -42,6 +46,34 @@ impl PermissionEntry {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for PermissionEntry {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct ExtendedPermissionStruct {
|
||||
identifier: Identifier,
|
||||
#[serde(default, flatten)]
|
||||
scope: Scopes,
|
||||
}
|
||||
|
||||
UntaggedEnumVisitor::new()
|
||||
.string(|string| {
|
||||
let de = string.into_deserializer();
|
||||
Identifier::deserialize(de).map(Self::PermissionRef)
|
||||
})
|
||||
.map(|map| {
|
||||
let ext_perm = map.deserialize::<ExtendedPermissionStruct>()?;
|
||||
Ok(Self::ExtendedPermission {
|
||||
identifier: ext_perm.identifier,
|
||||
scope: ext_perm.scope,
|
||||
})
|
||||
})
|
||||
.deserialize(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
/// A grouping and boundary mechanism developers can use to isolate access to the IPC layer.
|
||||
///
|
||||
/// It controls application windows fine grained access to the Tauri core, application, or plugin commands.
|
||||
@ -204,9 +236,9 @@ pub struct CapabilityRemote {
|
||||
}
|
||||
|
||||
/// Capability formats accepted in a capability file.
|
||||
#[derive(Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
#[cfg_attr(feature = "schema", schemars(untagged))]
|
||||
#[cfg_attr(test, derive(Debug, PartialEq))]
|
||||
pub enum CapabilityFile {
|
||||
/// A single capability.
|
||||
Capability(Capability),
|
||||
@ -234,6 +266,36 @@ impl CapabilityFile {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for CapabilityFile {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
UntaggedEnumVisitor::new()
|
||||
.seq(|seq| seq.deserialize::<Vec<Capability>>().map(Self::List))
|
||||
.map(|map| {
|
||||
#[derive(Deserialize)]
|
||||
struct CapabilityNamedList {
|
||||
capabilities: Vec<Capability>,
|
||||
}
|
||||
|
||||
let value: serde_json::Map<String, serde_json::Value> = map.deserialize()?;
|
||||
if value.contains_key("capabilities") {
|
||||
serde_json::from_value::<CapabilityNamedList>(value.into())
|
||||
.map(|named| Self::NamedList {
|
||||
capabilities: named.capabilities,
|
||||
})
|
||||
.map_err(|e| serde_untagged::de::Error::custom(e.to_string()))
|
||||
} else {
|
||||
serde_json::from_value::<Capability>(value.into())
|
||||
.map(Self::Capability)
|
||||
.map_err(|e| serde_untagged::de::Error::custom(e.to_string()))
|
||||
}
|
||||
})
|
||||
.deserialize(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for CapabilityFile {
|
||||
type Err = super::Error;
|
||||
|
||||
@ -309,3 +371,71 @@ mod build {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::acl::{Identifier, Scopes};
|
||||
|
||||
use super::{Capability, CapabilityFile, PermissionEntry};
|
||||
|
||||
#[test]
|
||||
fn permission_entry_de() {
|
||||
let identifier = Identifier::try_from("plugin:perm".to_string()).unwrap();
|
||||
let identifier_json = serde_json::to_string(&identifier).unwrap();
|
||||
assert_eq!(
|
||||
serde_json::from_str::<PermissionEntry>(&identifier_json).unwrap(),
|
||||
PermissionEntry::PermissionRef(identifier.clone())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::from_value::<PermissionEntry>(serde_json::json!({
|
||||
"identifier": identifier,
|
||||
"allow": [],
|
||||
"deny": null
|
||||
}))
|
||||
.unwrap(),
|
||||
PermissionEntry::ExtendedPermission {
|
||||
identifier,
|
||||
scope: Scopes {
|
||||
allow: Some(vec![]),
|
||||
deny: None
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capability_file_de() {
|
||||
let capability = Capability {
|
||||
identifier: "test".into(),
|
||||
description: "".into(),
|
||||
remote: None,
|
||||
local: true,
|
||||
windows: vec![],
|
||||
webviews: vec![],
|
||||
permissions: vec![],
|
||||
platforms: None,
|
||||
};
|
||||
let capability_json = serde_json::to_string(&capability).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
serde_json::from_str::<CapabilityFile>(&capability_json).unwrap(),
|
||||
CapabilityFile::Capability(capability.clone())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::from_str::<CapabilityFile>(&format!("[{capability_json}]")).unwrap(),
|
||||
CapabilityFile::List(vec![capability.clone()])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::from_str::<CapabilityFile>(&format!(
|
||||
"{{ \"capabilities\": [{capability_json}] }}"
|
||||
))
|
||||
.unwrap(),
|
||||
CapabilityFile::NamedList {
|
||||
capabilities: vec![capability.clone()]
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ use serde::{
|
||||
Deserialize, Serialize, Serializer,
|
||||
};
|
||||
use serde_json::Value as JsonValue;
|
||||
use serde_untagged::UntaggedEnumVisitor;
|
||||
use serde_with::skip_serializing_none;
|
||||
use url::Url;
|
||||
|
||||
@ -270,7 +271,14 @@ impl<'de> Deserialize<'de> for BundleTarget {
|
||||
|
||||
match BundleTargetInner::deserialize(deserializer)? {
|
||||
BundleTargetInner::All(s) if s.to_lowercase() == "all" => Ok(Self::All),
|
||||
BundleTargetInner::All(t) => Err(DeError::custom(format!("invalid bundle type {t}"))),
|
||||
BundleTargetInner::All(t) => Err(DeError::custom(format!(
|
||||
"invalid bundle type {t}, expected one of `all`, {}",
|
||||
BundleType::all()
|
||||
.iter()
|
||||
.map(|b| format!("`{b}`"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
))),
|
||||
BundleTargetInner::List(l) => Ok(Self::List(l)),
|
||||
BundleTargetInner::One(t) => Ok(Self::One(t)),
|
||||
}
|
||||
@ -1708,9 +1716,9 @@ pub struct SecurityConfig {
|
||||
}
|
||||
|
||||
/// A capability entry which can be either an inlined capability or a reference to a capability defined on its own file.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(JsonSchema))]
|
||||
#[serde(rename_all = "camelCase", untagged)]
|
||||
#[serde(untagged)]
|
||||
pub enum CapabilityEntry {
|
||||
/// An inlined capability.
|
||||
Inlined(Capability),
|
||||
@ -1718,6 +1726,18 @@ pub enum CapabilityEntry {
|
||||
Reference(String),
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for CapabilityEntry {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
UntaggedEnumVisitor::new()
|
||||
.string(|string| Ok(Self::Reference(string.to_owned())))
|
||||
.map(|map| map.deserialize::<Capability>().map(Self::Inlined))
|
||||
.deserialize(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
/// The application pattern.
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
|
18
tooling/cli/Cargo.lock
generated
18
tooling/cli/Cargo.lock
generated
@ -4255,6 +4255,17 @@ dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-untagged"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2676ba99bd82f75cae5cbd2c8eda6fa0b8760f18978ea840e980dd5567b5c5b6"
|
||||
dependencies = [
|
||||
"erased-serde",
|
||||
"serde",
|
||||
"typeid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-value"
|
||||
version = "0.7.0"
|
||||
@ -5012,6 +5023,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde-untagged",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"serialize-to-javascript",
|
||||
@ -5423,6 +5435,12 @@ dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "059d83cc991e7a42fc37bd50941885db0888e34209f8cfd9aab07ddec03bc9cf"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.17.0"
|
||||
|
Loading…
Reference in New Issue
Block a user