loftspec 2.0.1

The Loft Specification Format: document model, identity, canonical bytes, hashing and conversion
Documentation
//! The lock record a publish writes to `loft.lock.json` beside `spec/` (RQ-19, AC-666): the
//! format and algorithm, the document key, the state, content and binding roots, the publish
//! sequence and time, and for a document derived from a Loft v1 export the source state (RQ-20).

use std::num::NonZeroU64;

use serde::{Deserialize, Serialize};

use crate::FORMAT;
use crate::canonical::Digest;
use crate::document::DerivedFrom;
use crate::identity::Key;
use crate::ids::{AuthoredStateId, DocumentId};
use crate::manifest::{ALGORITHM, ManifestError, check_identifiers};
use crate::publish::time::{PublishedAt, TimeError};

/// The lock's file name, in the directory that holds `spec/`.
pub const LOCK: &str = "loft.lock.json";

/// What a publish pins.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Lock {
    pub document_key: DocumentId,
    pub state_root: Digest,
    pub content_root: Digest,
    pub binding_root: Digest,
    pub publish_sequence: NonZeroU64,
    pub published_at: PublishedAt,
    pub derived_from: Option<DerivedFrom>,
}

/// Why bytes are not a valid lock record.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum LockError {
    #[error("{0}")]
    Shape(String),
    #[error(transparent)]
    Identifier(#[from] ManifestError),
    #[error(transparent)]
    Time(#[from] TimeError),
}

/// The lock as it is written: its fields in the Format's order, no other field.
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct Wire {
    format: String,
    algorithm: String,
    document_key: Key,
    state_root: Digest,
    content_root: Digest,
    binding_root: Digest,
    publish_sequence: NonZeroU64,
    published_at: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    derived_from: Option<WireDerivedFrom>,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct WireDerivedFrom {
    authored_state_id: Key,
    content_address: Digest,
    publish_sequence: u64,
}

impl Lock {
    /// Reads a lock record, refusing one that lacks a lock field, holds another field, names a
    /// format or an algorithm this implementation does not implement, or holds a malformed
    /// value.
    ///
    /// # Errors
    ///
    /// The bytes are not a valid lock record.
    pub fn read(bytes: &[u8]) -> Result<Self, LockError> {
        let wire: Wire =
            serde_json::from_slice(bytes).map_err(|error| LockError::Shape(error.to_string()))?;
        check_identifiers(&wire.format, &wire.algorithm)?;
        Ok(Self {
            document_key: DocumentId::new(wire.document_key),
            state_root: wire.state_root,
            content_root: wire.content_root,
            binding_root: wire.binding_root,
            publish_sequence: wire.publish_sequence,
            published_at: wire.published_at.parse()?,
            derived_from: wire.derived_from.map(|derived| DerivedFrom {
                authored_state_id: AuthoredStateId::new(derived.authored_state_id),
                content_address: derived.content_address,
                publish_sequence: derived.publish_sequence,
            }),
        })
    }

    /// The lock's bytes: a JSON object with its fields in the Format's order, indented by two
    /// spaces and ending with one newline.
    #[must_use]
    pub fn to_bytes(&self) -> Vec<u8> {
        let wire = Wire {
            format: FORMAT.to_owned(),
            algorithm: ALGORITHM.to_owned(),
            document_key: self.document_key.key(),
            state_root: self.state_root,
            content_root: self.content_root,
            binding_root: self.binding_root,
            publish_sequence: self.publish_sequence,
            published_at: self.published_at.to_string(),
            derived_from: self.derived_from.as_ref().map(|derived| WireDerivedFrom {
                authored_state_id: derived.authored_state_id.key(),
                content_address: derived.content_address,
                publish_sequence: derived.publish_sequence,
            }),
        };
        let mut bytes = serde_json::to_vec_pretty(&wire).expect("a lock serialises");
        bytes.push(b'\n');
        bytes
    }
}