| |
}
|
| |
}
|
| |
|
| + |
/// The version number of the identity document.
|
| + |
///
|
| + |
/// It is used to ensure compatibility when parsing identity documents.
|
| + |
///
|
| + |
/// If an invalid version is found – either the `0` version, or an unrecognized
|
| + |
/// future version – the parsing of a version will fail.
|
| + |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
| + |
pub struct Version(NonZeroU32);
|
| + |
|
| + |
impl Version {
|
| + |
/// Construct a [`Version`].
|
| + |
///
|
| + |
/// # Errors
|
| + |
///
|
| + |
/// - `n` is 0
|
| + |
/// - `n` is greater than the latest version, specified by
|
| + |
/// [`IDENTITY_VERSION`].
|
| + |
pub fn new(n: u32) -> Result<Version, VersionError> {
|
| + |
match NonZeroU32::new(n) {
|
| + |
None => Err(VersionError::ZeroVersion),
|
| + |
Some(n) if n > IDENTITY_VERSION.into() => Err(VersionError::UnkownVersion(n)),
|
| + |
Some(n) => Ok(Version(n)),
|
| + |
}
|
| + |
}
|
| + |
|
| + |
/// Return the underlying [`NonZeroU32`] number of the `Version`.
|
| + |
pub fn number(&self) -> NonZeroU32 {
|
| + |
self.0
|
| + |
}
|
| + |
|
| + |
/// Check if the provided version is part of the set of accepted versions.
|
| + |
pub fn is_valid_version(v: &u32) -> bool {
|
| + |
0 < *v && *v <= IDENTITY_VERSION.into()
|
| + |
}
|
| + |
|
| + |
/// Helper for skipping the serialization of the version if `version <= 1`.
|
| + |
///
|
| + |
/// Note that we shouldn't allow `version: 0`, but there is no harm in
|
| + |
/// skipping it anyway.
|
| + |
fn skip_serializing(&self) -> bool {
|
| + |
u32::from(*self) <= 1
|
| + |
}
|
| + |
}
|
| + |
|
| + |
impl From<Version> for NonZeroU32 {
|
| + |
fn from(Version(n): Version) -> Self {
|
| + |
n
|
| + |
}
|
| + |
}
|
| + |
|
| + |
impl From<Version> for u32 {
|
| + |
fn from(Version(n): Version) -> Self {
|
| + |
n.into()
|
| + |
}
|
| + |
}
|
| + |
|
| + |
#[derive(Debug, Error)]
|
| + |
#[non_exhaustive]
|
| + |
pub enum VersionError {
|
| + |
#[error("the version 0 is not supported")]
|
| + |
ZeroVersion,
|
| + |
#[error("unknown identity document version {0}, only version {IDENTITY_VERSION} is supported")]
|
| + |
UnkownVersion(NonZeroU32),
|
| + |
}
|
| + |
|
| + |
impl VersionError {
|
| + |
/// Provide a verbose error.
|
| + |
///
|
| + |
/// This will give a user more information on how to upgrade to a newer
|
| + |
/// version of an identity document, if there is one.
|
| + |
pub fn verbose(&self) -> String {
|
| + |
const UNKOWN_VERSION_ERROR: &str = r#"
|
| + |
Perhaps a new version of the identity document is released which is not supported by the current client.
|
| + |
See https://radicle.xyz for the latest versions of Radicle.
|
| + |
The CLI command `rad id migrate` will help to migrate to an up-to-date versions."#;
|
| + |
|
| + |
match self {
|
| + |
err @ Self::ZeroVersion => err.to_string(),
|
| + |
err @ Self::UnkownVersion(_) => format!("{err}{UNKOWN_VERSION_ERROR}"),
|
| + |
}
|
| + |
}
|
| + |
}
|
| + |
|
| + |
impl TryFrom<u32> for Version {
|
| + |
type Error = VersionError;
|
| + |
|
| + |
fn try_from(n: u32) -> Result<Self, Self::Error> {
|
| + |
Version::new(n)
|
| + |
}
|
| + |
}
|
| + |
|
| + |
impl fmt::Display for Version {
|
| + |
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
| + |
write!(f, "{}", self.0)
|
| + |
}
|
| + |
}
|
| + |
|
| + |
impl<'de> Deserialize<'de> for Version {
|
| + |
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
| + |
where
|
| + |
D: serde::Deserializer<'de>,
|
| + |
{
|
| + |
u32::deserialize(deserializer)
|
| + |
.and_then(|v| Version::new(v).map_err(|e| de::Error::custom(e.to_string())))
|
| + |
}
|
| + |
}
|
| + |
|
| + |
/// Used for [`Deserialize`] of a [`Version`] in [`RawDoc`], so that
|
| + |
/// deserializing a missing version results in `Version(1)`.
|
| + |
fn missing_version() -> Version {
|
| + |
// N.B. the default version is `1` which is non-zero so unsafe is fine here
|
| + |
unsafe { Version(NonZeroU32::new_unchecked(1)) }
|
| + |
}
|
| + |
|
| |
/// Identifies an identity document payload type.
|
| |
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
| |
#[serde(transparent)]
|
| |
}
|
| |
|
| |
#[test]
|
| + |
fn test_is_valid_version() {
|
| + |
// 0 is not a valid version
|
| + |
assert!(!Version::is_valid_version(&0));
|
| + |
|
| + |
// Ensures that the latest version is always valid
|
| + |
let current = IDENTITY_VERSION.number();
|
| + |
assert!(Version::is_valid_version(¤t.into()));
|
| + |
|
| + |
// Ensures that the next version is not valid because we have not
|
| + |
// defined it yet
|
| + |
let next = current.checked_add(1).unwrap();
|
| + |
assert!(!Version::is_valid_version(&next.into()));
|
| + |
}
|
| + |
|
| + |
#[test]
|
| + |
fn test_future_version_error() {
|
| + |
let v = Version(NonZeroU32::MAX).to_string();
|
| + |
assert_eq!(
|
| + |
serde_json::from_str::<Version>(&v)
|
| + |
.expect_err("should fail to deserialize")
|
| + |
.to_string(),
|
| + |
VersionError::UnkownVersion(NonZeroU32::MAX).to_string(),
|
| + |
)
|
| + |
}
|
| + |
|
| + |
#[test]
|
| + |
fn test_parse_version() {
|
| + |
// Original document before introducing the version field
|
| + |
let v1 = json!(
|
| + |
{
|
| + |
"payload": {
|
| + |
"xyz.radicle.project": {
|
| + |
"defaultBranch": "master",
|
| + |
"description": "Radicle Heartwood Protocol & Stack",
|
| + |
"name": "heartwood"
|
| + |
}
|
| + |
},
|
| + |
"delegates": [
|
| + |
"did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
|
| + |
"did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW",
|
| + |
"did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM"
|
| + |
],
|
| + |
"threshold": 1
|
| + |
}
|
| + |
);
|
| + |
|
| + |
// Deserializing the `RawDoc` should not fail and should include the
|
| + |
// `IDENTITY_VERSION`.
|
| + |
let doc = serde_json::from_str::<RawDoc>(&v1.to_string()).unwrap();
|
| + |
let payload = [(
|
| + |
PayloadId::project(),
|
| + |
Payload {
|
| + |
value: json!({
|
| + |
"name": "heartwood",
|
| + |
"description": "Radicle Heartwood Protocol & Stack",
|
| + |
"defaultBranch": "master",
|
| + |
}),
|
| + |
},
|
| + |
)]
|
| + |
.into_iter()
|
| + |
.collect::<BTreeMap<_, _>>();
|
| + |
let delegates = vec![
|
| + |
"did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT"
|
| + |
.parse::<Did>()
|
| + |
.unwrap(),
|
| + |
"did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW"
|
| + |
.parse::<Did>()
|
| + |
.unwrap(),
|
| + |
"did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM"
|
| + |
.parse::<Did>()
|
| + |
.unwrap(),
|
| + |
];
|
| + |
// And this is the expected outcome of the deserialization
|
| + |
assert_eq!(
|
| + |
doc,
|
| + |
RawDoc {
|
| + |
version: IDENTITY_VERSION,
|
| + |
payload: payload.clone(),
|
| + |
delegates: delegates.clone(),
|
| + |
threshold: 1,
|
| + |
visibility: Visibility::Public,
|
| + |
}
|
| + |
);
|
| + |
|
| + |
// Deserializing into `Doc` should also succeed.
|
| + |
let verified = serde_json::from_str::<Doc>(&v1.to_string()).unwrap();
|
| + |
let delegates = Delegates(NonEmpty::from_vec(delegates).unwrap());
|
| + |
assert_eq!(
|
| + |
verified,
|
| + |
Doc {
|
| + |
version: IDENTITY_VERSION,
|
| + |
threshold: Threshold::new(1, &delegates).unwrap(),
|
| + |
payload: payload.clone(),
|
| + |
delegates,
|
| + |
visibility: Visibility::Public,
|
| + |
}
|
| + |
);
|
| + |
}
|
| + |
|
| + |
#[test]
|
| |
fn test_canonical_example() {
|
| |
let tempdir = tempfile::tempdir().unwrap();
|
| |
let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
|