use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use sha2::{Digest, Sha256};
use super::messages::MessageTrust;
use super::tasks::UsageV1;
use super::{canonical_json, require_schema, validate_hex64, validate_id, validate_text};
use crate::{UtcTimestamp, ValidationError};
pub const SWARM_SCHEMA: &str = "leanctx.hive.swarm/v1";
pub const SWARM_SUBTASK_LIST_SCHEMA: &str = "leanctx.hive.swarm-subtask-list/v1";
pub const SWARM_SUBTASK_PROPOSE_SCHEMA: &str = "leanctx.hive.swarm-subtask-propose/v1";
pub const SWARM_CLAIM_REQUEST_SCHEMA: &str = "leanctx.hive.swarm-claim-request/v1";
pub const SWARM_CLAIM_RENEW_SCHEMA: &str = "leanctx.hive.swarm-claim-renew/v1";
pub const SWARM_CLAIM_RELEASE_SCHEMA: &str = "leanctx.hive.swarm-claim-release/v1";
pub const SWARM_CLAIM_SCHEMA: &str = "leanctx.hive.swarm-claim/v1";
pub const SWARM_RESULT_SCHEMA: &str = "leanctx.hive.swarm-result/v1";
pub const SWARM_RESULT_ACK_SCHEMA: &str = "leanctx.hive.swarm-result-ack/v1";
pub const SWARM_VERIFICATION_LIST_SCHEMA: &str = "leanctx.hive.swarm-verification-list/v1";
pub const VERIFICATION_COMMIT_SCHEMA: &str = "leanctx.hive.verification-commit/v1";
pub const VERIFICATION_COMMIT_V2_SCHEMA: &str = "leanctx.hive.verification-commit/v2";
pub const VERIFICATION_REVEAL_SCHEMA: &str = "leanctx.hive.verification-reveal/v1";
pub const VERIFICATION_REVEAL_ACK_SCHEMA: &str = "leanctx.hive.verification-reveal-ack/v1";
pub const MODERATION_REPORT_SCHEMA: &str = "leanctx.hive.moderation-report/v1";
pub const SWARM_SEED_SCHEMA: &str = "leanctx.hive.swarm-seed/v1";
pub const MAX_RESULT_BODY_CHARS: usize = 50_000;
pub const MAX_VALUE_TEXT_CHARS: usize = 2000;
pub const MAX_VALUE_LIST: usize = 1000;
pub const MAX_VALUE_ITEM_CHARS: usize = 500;
pub const MAX_MATERIAL: usize = 64;
pub const MAX_SEED_SUBTASKS: usize = 200;
pub const MIN_CLAIM_MINUTES: u32 = 5;
pub const MAX_CLAIM_MINUTES: u32 = 15;
fn untrusted(trust: MessageTrust) -> Result<(), ValidationError> {
if trust == MessageTrust::Untrusted {
Ok(())
} else {
Err(ValidationError::new("every Swarm payload is untrusted"))
}
}
fn minutes_ok(minutes: u32) -> Result<(), ValidationError> {
if (MIN_CLAIM_MINUTES..=MAX_CLAIM_MINUTES).contains(&minutes) {
Ok(())
} else {
Err(ValidationError::new("a Swarm claim lasts 5-15 minutes"))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct HandleRefV1 {
pub handle: String,
}
impl HandleRefV1 {
fn validate(&self) -> Result<(), ValidationError> {
if super::profiles::handle_ok(&self.handle) {
Ok(())
} else {
Err(ValidationError::new("handle is malformed"))
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SwarmState {
Open,
Working,
Answered,
Closed,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SwarmScope {
Public,
Team,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MaterialV1 {
pub name: String,
pub media_type: String,
pub sha256: String,
pub bytes: u64,
}
impl MaterialV1 {
fn validate(&self) -> Result<(), ValidationError> {
validate_text(&self.name, 200, "material name", false)?;
validate_text(&self.media_type, 100, "media_type", false)?;
validate_hex64(&self.sha256, "material sha256")
}
}
fn validate_material(material: &[MaterialV1]) -> Result<(), ValidationError> {
if material.len() > MAX_MATERIAL {
return Err(ValidationError::new("at most 64 material files"));
}
material.iter().try_for_each(MaterialV1::validate)
}
fn scope_org(scope: SwarmScope, org_id: Option<&str>) -> Result<(), ValidationError> {
match (scope, org_id) {
(SwarmScope::Public, None) => Ok(()),
(SwarmScope::Team, Some(org)) => validate_id(org, "org_id"),
(SwarmScope::Public, Some(_)) => Err(ValidationError::new("a public swarm names no org")),
(SwarmScope::Team, None) => Err(ValidationError::new("a team swarm names its org_id")),
(SwarmScope::Unknown, _) => Err(ValidationError::new("unknown swarm scope")),
}
}
fn reaches(scope: SwarmScope, org_id: Option<&str>, lease_org_id: &str) -> bool {
match scope {
SwarmScope::Public => true,
SwarmScope::Team => org_id == Some(lease_org_id),
SwarmScope::Unknown => false,
}
}
fn licence_ok(licence: &str) -> bool {
(1..=64).contains(&licence.len())
&& licence
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'+' | b'-'))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SwarmV1 {
pub schema: String,
pub swarm_id: String,
pub title: String,
pub body: String,
#[serde(default)]
pub topics: Vec<String>,
pub licence: String,
pub scope: SwarmScope,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub org_id: Option<String>,
pub state: SwarmState,
pub owner: HandleRefV1,
#[serde(default)]
pub material: Vec<MaterialV1>,
pub answer_revision: u64,
pub trust: MessageTrust,
}
impl SwarmV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
require_schema(&self.schema, SWARM_SCHEMA)?;
validate_id(&self.swarm_id, "swarm_id")?;
validate_text(&self.title, 200, "title", false)?;
validate_text(&self.body, 20_000, "body", true)?;
if !licence_ok(&self.licence) {
return Err(ValidationError::new("licence is an SPDX id"));
}
scope_org(self.scope, self.org_id.as_deref())?;
self.owner.validate()?;
validate_material(&self.material)?;
untrusted(self.trust)
}
#[must_use]
pub fn reaches(&self, lease_org_id: &str) -> bool {
reaches(self.scope, self.org_id.as_deref(), lease_org_id)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SwarmSubtaskState {
Proposed,
Open,
Claimed,
Verifying,
Accepted,
Rejected,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExpectedKind {
Number,
List,
Hash,
Text,
#[serde(other)]
Unknown,
}
pub fn validate_value(
kind: Option<ExpectedKind>,
value: Option<&Value>,
) -> Result<(), ValidationError> {
let ok = match (kind, value) {
(None, None) => true,
(Some(ExpectedKind::Number), Some(v)) => v.is_i64() || v.is_u64(),
(Some(ExpectedKind::List), Some(v)) => v.as_array().is_some_and(|items| {
items.len() <= MAX_VALUE_LIST
&& items.iter().all(|item| {
item.is_i64()
|| item.is_u64()
|| item.as_str().is_some_and(|s| {
validate_text(s, MAX_VALUE_ITEM_CHARS, "value", false).is_ok()
})
})
}),
(Some(ExpectedKind::Hash), Some(v)) => v
.as_str()
.is_some_and(|s| validate_hex64(s, "value").is_ok()),
(Some(ExpectedKind::Text), Some(v)) => v
.as_str()
.is_some_and(|s| validate_text(s, MAX_VALUE_TEXT_CHARS, "value", true).is_ok()),
_ => false,
};
if ok {
Ok(())
} else {
Err(ValidationError::new(
"value must match expected_kind and is absent without one",
))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SwarmRefV1 {
pub swarm_id: String,
pub title: String,
pub licence: String,
pub scope: SwarmScope,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub org_id: Option<String>,
}
impl SwarmRefV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
validate_id(&self.swarm_id, "swarm_id")?;
validate_text(&self.title, 200, "title", false)?;
if !licence_ok(&self.licence) {
return Err(ValidationError::new("licence is an SPDX id"));
}
scope_org(self.scope, self.org_id.as_deref())
}
#[must_use]
pub fn reaches(&self, lease_org_id: &str) -> bool {
reaches(self.scope, self.org_id.as_deref(), lease_org_id)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SwarmSubtaskV1 {
pub subtask_id: String,
pub swarm_id: String,
pub number: u64,
pub title: String,
pub acceptance: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expected_kind: Option<ExpectedKind>,
pub state: SwarmSubtaskState,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub claimed_by: Option<HandleRefV1>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub claim_until: Option<UtcTimestamp>,
pub mine: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub swarm: Option<SwarmRefV1>,
}
impl SwarmSubtaskV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
validate_id(&self.subtask_id, "subtask_id")?;
validate_id(&self.swarm_id, "swarm_id")?;
validate_text(&self.title, 200, "title", false)?;
validate_text(&self.acceptance, 2000, "acceptance", true)?;
if let Some(by) = &self.claimed_by {
by.validate()?;
}
if self.claimed_by.is_some() != (self.state == SwarmSubtaskState::Claimed) {
return Err(ValidationError::new(
"claimed_by comes exactly with a claimed subtask",
));
}
if let Some(swarm) = &self.swarm {
swarm.validate()?;
if swarm.swarm_id != self.swarm_id {
return Err(ValidationError::new("swarm does not match swarm_id"));
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SwarmSubtaskListV1 {
pub schema: String,
pub items: Vec<SwarmSubtaskV1>,
pub trust: MessageTrust,
}
impl SwarmSubtaskListV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
require_schema(&self.schema, SWARM_SUBTASK_LIST_SCHEMA)?;
if self.items.len() > 200 {
return Err(ValidationError::new("at most 200 subtasks per page"));
}
self.items.iter().try_for_each(SwarmSubtaskV1::validate)?;
untrusted(self.trust)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SwarmSubtaskProposeV1 {
pub schema: String,
pub title: String,
pub acceptance: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expected_kind: Option<ExpectedKind>,
}
impl SwarmSubtaskProposeV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
require_schema(&self.schema, SWARM_SUBTASK_PROPOSE_SCHEMA)?;
validate_text(&self.title, 200, "title", false)?;
validate_text(&self.acceptance, 2000, "acceptance", true)?;
if self.expected_kind == Some(ExpectedKind::Unknown) {
return Err(ValidationError::new("unknown expected_kind"));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SwarmClaimRequestV1 {
pub schema: String,
pub optin_id: String,
pub minutes: u32,
}
impl SwarmClaimRequestV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
require_schema(&self.schema, SWARM_CLAIM_REQUEST_SCHEMA)?;
validate_id(&self.optin_id, "optin_id")?;
minutes_ok(self.minutes)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SwarmClaimRenewV1 {
pub schema: String,
pub fencing_token: u64,
pub minutes: u32,
}
impl SwarmClaimRenewV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
require_schema(&self.schema, SWARM_CLAIM_RENEW_SCHEMA)?;
minutes_ok(self.minutes)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SwarmClaimReleaseV1 {
pub schema: String,
pub fencing_token: u64,
}
impl SwarmClaimReleaseV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
require_schema(&self.schema, SWARM_CLAIM_RELEASE_SCHEMA)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SwarmClaimV1 {
pub schema: String,
pub subtask_id: String,
pub swarm_id: String,
pub number: u64,
pub fencing_token: u64,
pub until: UtcTimestamp,
pub trust: MessageTrust,
}
impl SwarmClaimV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
require_schema(&self.schema, SWARM_CLAIM_SCHEMA)?;
validate_id(&self.subtask_id, "subtask_id")?;
validate_id(&self.swarm_id, "swarm_id")?;
untrusted(self.trust)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SwarmResultRequestV1 {
pub schema: String,
pub fencing_token: u64,
pub summary: String,
#[serde(default)]
pub body: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub value: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usage: Option<UsageV1>,
}
impl SwarmResultRequestV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
require_schema(&self.schema, SWARM_RESULT_SCHEMA)?;
validate_text(&self.summary, 500, "summary", false)?;
if !self.body.is_empty() {
validate_text(&self.body, MAX_RESULT_BODY_CHARS, "body", true)?;
}
Ok(())
}
pub fn digest(&self) -> Result<String, ValidationError> {
let doc = json!({"summary": self.summary, "body": self.body, "value": self.value});
Ok(hex(&Sha256::digest(canonical_json(&doc)?)))
}
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SwarmResultAckV1 {
pub schema: String,
pub result_id: String,
pub sha256: String,
pub state: SwarmSubtaskState,
pub verifiers_assigned: u32,
pub trust: MessageTrust,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VerificationState {
Assigned,
Committed,
Revealed,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VerifiedResultV1 {
pub summary: String,
pub body: String,
pub by: HandleRefV1,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VerifiedSubtaskV1 {
pub title: String,
pub acceptance: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expected_kind: Option<ExpectedKind>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SwarmVerificationV1 {
pub verification_id: String,
pub state: VerificationState,
pub due_at: UtcTimestamp,
pub result: VerifiedResultV1,
pub subtask: VerifiedSubtaskV1,
pub swarm: SwarmRefV1,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SwarmVerificationListV1 {
pub schema: String,
pub items: Vec<SwarmVerificationV1>,
pub trust: MessageTrust,
}
impl SwarmVerificationListV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
require_schema(&self.schema, SWARM_VERIFICATION_LIST_SCHEMA)?;
for item in &self.items {
validate_id(&item.verification_id, "verification_id")?;
item.result.by.validate()?;
item.swarm.validate()?;
if item.state == VerificationState::Revealed {
return Err(ValidationError::new("revealed assignments are not listed"));
}
}
untrusted(self.trust)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Verdict {
Agree,
Disagree,
#[serde(other)]
Unknown,
}
pub fn verification_commitment(
verdict: Verdict,
value: Option<&Value>,
nonce: &str,
) -> Result<String, ValidationError> {
let doc = json!({"nonce": nonce, "value": value, "verdict": verdict});
Ok(hex(&Sha256::digest(canonical_json(&doc)?)))
}
pub fn verification_commitment_v2(
verification_id: &str,
verdict: Verdict,
value: Option<&Value>,
nonce: &str,
) -> Result<String, ValidationError> {
let doc = json!({
"domain": VERIFICATION_COMMIT_V2_SCHEMA,
"verification_id": verification_id,
"nonce": nonce,
"value": value,
"verdict": verdict,
});
Ok(hex(&Sha256::digest(canonical_json(&doc)?)))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct VerificationCommitV1 {
pub schema: String,
pub sha256: String,
}
impl VerificationCommitV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
if self.schema != VERIFICATION_COMMIT_V2_SCHEMA {
require_schema(&self.schema, VERIFICATION_COMMIT_SCHEMA)?;
}
validate_hex64(&self.sha256, "sha256")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct VerificationRevealV1 {
pub schema: String,
pub verdict: Verdict,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub value: Option<Value>,
pub nonce: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
impl VerificationRevealV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
require_schema(&self.schema, VERIFICATION_REVEAL_SCHEMA)?;
if self.verdict == Verdict::Unknown {
return Err(ValidationError::new("verdict is agree or disagree"));
}
if self.nonce.len() < 24 {
return Err(ValidationError::new("nonce holds at least 16 random bytes"));
}
super::validate_b64_bounded(&self.nonce, 128, "nonce")?;
match (&self.reason, self.verdict) {
(Some(reason), _) => validate_text(reason, 2000, "reason", true),
(None, Verdict::Disagree) => Err(ValidationError::new("disagree needs a reason")),
(None, _) => Ok(()),
}
}
pub fn commitment(&self) -> Result<String, ValidationError> {
verification_commitment(self.verdict, self.value.as_ref(), &self.nonce)
}
pub fn commitment_v2(&self, verification_id: &str) -> Result<String, ValidationError> {
verification_commitment_v2(
verification_id,
self.verdict,
self.value.as_ref(),
&self.nonce,
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ResultOutcome {
Verifying,
Accepted,
Rejected,
Split,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VerificationRevealAckV1 {
pub schema: String,
pub state: VerificationState,
pub result_state: ResultOutcome,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReportTarget {
Swarm,
Subtask,
Result,
Message,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReportReason {
Spam,
Abuse,
SecretOrPii,
Illegal,
Injection,
Other,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ModerationReportV1 {
pub schema: String,
pub target_kind: ReportTarget,
pub target_id: String,
pub reason: ReportReason,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}
impl ModerationReportV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
require_schema(&self.schema, MODERATION_REPORT_SCHEMA)?;
if self.target_kind == ReportTarget::Unknown || self.reason == ReportReason::Unknown {
return Err(ValidationError::new("unknown report target or reason"));
}
validate_id(&self.target_id, "target_id")?;
if let Some(note) = &self.note {
validate_text(note, 1000, "note", true)?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SeedSubtaskV1 {
pub title: String,
pub acceptance: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expected_kind: Option<ExpectedKind>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SwarmSeedV1 {
pub schema: String,
pub title: String,
pub body: String,
#[serde(default)]
pub topics: Vec<String>,
pub licence: String,
pub subtasks: Vec<SeedSubtaskV1>,
#[serde(default)]
pub material: Vec<MaterialV1>,
}
impl SwarmSeedV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
require_schema(&self.schema, SWARM_SEED_SCHEMA)?;
validate_text(&self.title, 200, "title", false)?;
validate_text(&self.body, 20_000, "body", true)?;
if !licence_ok(&self.licence) {
return Err(ValidationError::new("licence is an SPDX id"));
}
if self.subtasks.is_empty() || self.subtasks.len() > MAX_SEED_SUBTASKS {
return Err(ValidationError::new("a seed has 1-200 subtasks"));
}
for subtask in &self.subtasks {
validate_text(&subtask.title, 200, "title", false)?;
validate_text(&subtask.acceptance, 2000, "acceptance", true)?;
}
validate_material(&self.material)
}
pub fn digest(&self) -> Result<String, ValidationError> {
let value = serde_json::to_value(self)
.map_err(|error| ValidationError::new(format!("seed: {error}")))?;
Ok(hex(&Sha256::digest(canonical_json(&value)?)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_team_swarm_names_its_org_and_reaches_only_it() {
let swarm = |scope, org_id: Option<&str>| SwarmRefV1 {
swarm_id: "sw_01jbretry".into(),
title: "Retry budget".into(),
licence: "CC-BY-4.0".into(),
scope,
org_id: org_id.map(str::to_string),
};
let public = swarm(SwarmScope::Public, None);
assert!(
public.validate().is_ok() && public.reaches("org_acme") && public.reaches("org_other")
);
let team = swarm(SwarmScope::Team, Some("org_acme"));
assert!(team.validate().is_ok());
assert!(team.reaches("org_acme") && !team.reaches("org_other"));
assert!(
swarm(SwarmScope::Public, Some("org_acme"))
.validate()
.is_err()
);
assert!(swarm(SwarmScope::Team, None).validate().is_err());
let unknown = swarm(SwarmScope::Unknown, None);
assert!(unknown.validate().is_err() && !unknown.reaches("org_acme"));
}
#[test]
fn a_reveal_reproduces_its_commitment_and_a_changed_verdict_does_not() {
let reveal = VerificationRevealV1 {
schema: VERIFICATION_REVEAL_SCHEMA.into(),
verdict: Verdict::Agree,
value: Some(json!(3)),
nonce: "AAECAwQFBgcICQoLDA0ODxAREhM=".into(),
reason: None,
};
reveal.validate().unwrap();
let commitment = reveal.commitment().unwrap();
assert_eq!(
commitment,
verification_commitment(Verdict::Agree, Some(&json!(3)), &reveal.nonce).unwrap()
);
assert_ne!(
commitment,
verification_commitment(Verdict::Disagree, Some(&json!(3)), &reveal.nonce).unwrap()
);
}
#[test]
fn values_follow_the_expected_kind() {
assert!(validate_value(None, None).is_ok());
assert!(validate_value(Some(ExpectedKind::Number), Some(&json!(5))).is_ok());
assert!(validate_value(Some(ExpectedKind::Number), Some(&json!("5"))).is_err());
assert!(validate_value(None, Some(&json!(5))).is_err());
assert!(validate_value(Some(ExpectedKind::Hash), Some(&json!("A".repeat(64)))).is_err());
let list = Some(ExpectedKind::List);
assert!(validate_value(list, Some(&json!(["retry", 3, "backoff"]))).is_ok());
for bad in [
json!(["ok\u{E0041}\u{E0042}"]),
json!(["a\u{202E}b"]),
json!(["a\u{200B}b"]),
json!(["two\nlines"]),
json!([""]),
json!(["x".repeat(MAX_VALUE_ITEM_CHARS + 1)]),
] {
assert!(validate_value(list, Some(&bad)).is_err(), "{bad}");
}
assert!(validate_value(Some(ExpectedKind::Text), Some(&json!("a\u{E0041}"))).is_err());
}
}