use haematite::tree::Hash;
use serde::{Deserialize, Deserializer, Serialize};
use super::protocol::{Change, Version};
use super::storage::Failure;
pub mod attribution;
pub mod envelopes;
mod receipt;
pub use receipt::Receipt;
pub(super) const FINGERPRINT_FORMAT: u32 = 1;
const FINGERPRINT_DOMAIN: &[u8] = b"haem.mutation.fingerprint\0";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "CallerWire")]
pub struct Caller {
issuer: String,
subject: String,
#[serde(skip_serializing_if = "Option::is_none")]
responsible: Option<String>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct CallerWire {
issuer: String,
subject: String,
responsible: Option<String>,
}
impl Caller {
pub fn new(
issuer: impl Into<String>,
subject: impl Into<String>,
responsible: Option<String>,
) -> Result<Self, Failure> {
let caller = Self {
issuer: issuer.into(),
subject: subject.into(),
responsible,
};
identifier(&caller.issuer)?;
identifier(&caller.subject)?;
if let Some(person) = &caller.responsible {
identifier(person)?;
}
Ok(caller)
}
pub fn local(subject: impl Into<String>) -> Result<Self, Failure> {
Self::new("local", subject, None)
}
#[must_use]
pub fn issuer(&self) -> &str {
&self.issuer
}
#[must_use]
pub fn subject(&self) -> &str {
&self.subject
}
#[must_use]
pub fn responsible(&self) -> Option<&str> {
self.responsible.as_deref()
}
}
impl TryFrom<CallerWire> for Caller {
type Error = Failure;
fn try_from(wire: CallerWire) -> Result<Self, Self::Error> {
Self::new(wire.issuer, wire.subject, wire.responsible)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum ReadDependency {
Document {
collection: String,
document_id: String,
},
Collection {
collection: String,
},
SourceInterval {
collection: String,
lower: Option<String>,
upper: Option<String>,
},
}
impl ReadDependency {
fn validate(&self) -> Result<(), Failure> {
match self {
Self::Document {
collection,
document_id,
} => {
identifier(collection)?;
identifier(document_id)
}
Self::Collection { collection } => identifier(collection),
Self::SourceInterval {
collection,
lower,
upper,
} => {
identifier(collection)?;
for bound in [lower, upper].into_iter().flatten() {
if bound.len() > 512 || bound.contains('\0') {
return Err(Failure::new(
"invalid_dependency",
"interval bounds must contain at most 512 UTF-8 bytes and no NUL",
));
}
}
if matches!((lower, upper), (Some(lower), Some(upper)) if lower > upper) {
return Err(Failure::new(
"invalid_dependency",
"interval lower bound exceeds its exclusive upper bound",
));
}
Ok(())
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum Contract {
ExactVersion {
expected: Version,
},
Dependencies {
origin: Version,
reads: Vec<ReadDependency>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContractKind {
ExactVersion,
Dependencies,
}
impl Contract {
#[must_use]
pub const fn origin(&self) -> Version {
match self {
Self::ExactVersion { expected } => *expected,
Self::Dependencies { origin, .. } => *origin,
}
}
#[must_use]
pub const fn kind(&self) -> ContractKind {
match self {
Self::ExactVersion { .. } => ContractKind::ExactVersion,
Self::Dependencies { .. } => ContractKind::Dependencies,
}
}
fn validate(&self) -> Result<(), Failure> {
if let Self::Dependencies { reads, .. } = self {
for read in reads {
read.validate()?;
}
}
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(try_from = "MutationWire")]
pub struct MutationRequest {
request_id: String,
branch: String,
#[serde(skip_serializing_if = "Option::is_none")]
caller: Option<Caller>,
contract: Contract,
change: Change,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct MutationWire {
request_id: String,
branch: String,
#[serde(default, deserialize_with = "present_caller")]
caller: Option<Caller>,
contract: Contract,
change: Change,
}
fn present_caller<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<Caller>, D::Error> {
Caller::deserialize(deserializer).map(Some)
}
impl TryFrom<MutationWire> for MutationRequest {
type Error = Failure;
fn try_from(wire: MutationWire) -> Result<Self, Self::Error> {
identifier(&wire.request_id)?;
identifier(&wire.branch)?;
wire.contract.validate()?;
if let Change::Fork(fork) = &wire.change {
identifier(&fork.name)?;
placeable(&fork.name)?;
}
Ok(Self {
request_id: wire.request_id,
branch: wire.branch,
caller: wire.caller,
contract: wire.contract,
change: wire.change,
})
}
}
impl MutationRequest {
pub fn decode(bytes: &[u8]) -> Result<Self, Failure> {
serde_json::from_slice(bytes)
.map_err(|error| Failure::new("invalid_mutation", error.to_string()))
}
pub fn resolve(self, socket_owner_subject: &str) -> Result<ResolvedMutation, Failure> {
let caller = match self.caller {
Some(caller) => caller,
None => Caller::local(socket_owner_subject)?,
};
Ok(ResolvedMutation {
request_id: self.request_id,
branch: self.branch,
caller,
contract: self.contract,
change: self.change,
})
}
}
#[derive(Debug, Serialize)]
pub struct ResolvedMutation {
request_id: String,
branch: String,
caller: Caller,
contract: Contract,
change: Change,
}
impl ResolvedMutation {
#[must_use]
pub fn request_id(&self) -> &str {
&self.request_id
}
#[must_use]
pub fn branch(&self) -> &str {
&self.branch
}
#[must_use]
pub const fn caller(&self) -> &Caller {
&self.caller
}
#[must_use]
pub const fn contract(&self) -> &Contract {
&self.contract
}
#[must_use]
pub const fn change(&self) -> &Change {
&self.change
}
#[must_use]
pub const fn origin(&self) -> Version {
self.contract.origin()
}
pub fn fingerprint(&self) -> Result<String, Failure> {
let mut bytes = FINGERPRINT_DOMAIN.to_vec();
bytes.extend_from_slice(&FINGERPRINT_FORMAT.to_be_bytes());
serde_json::to_writer(&mut bytes, self).map_err(Failure::storage)?;
Ok(Hash::of(&bytes).to_string())
}
pub(super) fn destination(&self) -> &str {
match &self.change {
Change::Fork(fork) => &fork.name,
_ => &self.branch,
}
}
}
pub(super) fn placeable(name: &str) -> Result<(), Failure> {
if name
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.'))
{
return Ok(());
}
Err(Failure::new(
"branch_name_unplaceable",
"a new branch name may hold only ASCII letters, digits, `_`, `-` and `.`, so Lys can place it",
))
}
pub(super) fn identifier(value: &str) -> Result<(), Failure> {
if value.is_empty() || value.len() > 512 || value.contains('\0') {
return Err(Failure::new(
"invalid_identifier",
"names must contain 1 to 512 UTF-8 bytes and no NUL",
));
}
Ok(())
}
#[cfg(test)]
#[path = "contracts/placeable_tests.rs"]
mod placeable_tests;