use crate::hash::fnv1a_64;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AuthorityMode {
Local,
Remote,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SimulationTier {
Canonical,
Predicted,
PresentationOnly,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AuthorityLevel {
Observer,
Delegate,
Controller,
Author,
Owner,
Steward,
System,
}
impl AuthorityLevel {
pub fn rank(&self) -> u8 {
match self {
Self::Observer => 0,
Self::Delegate => 1,
Self::Controller => 2,
Self::Author => 3,
Self::Owner => 4,
Self::Steward => 5,
Self::System => 6,
}
}
pub fn can_perform(&self, required: AuthorityLevel) -> bool {
self.rank() >= required.rank()
}
}
impl Ord for AuthorityLevel {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.rank().cmp(&other.rank())
}
}
impl PartialOrd for AuthorityLevel {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AuthorPermission {
Export,
Rescind,
Delete,
TransferAuthorship,
ViewRevenue,
WithdrawRevenue,
ListContent,
EditMetadata,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ContentType {
Waymark,
Entity,
Item,
Quest,
Dialogue,
Map,
Script,
Asset,
Tileset,
Lore,
Recipe,
Encounter,
}
impl ContentType {
pub fn label(&self) -> &'static str {
match self {
Self::Waymark => "waymark",
Self::Entity => "entity",
Self::Item => "item",
Self::Quest => "quest",
Self::Dialogue => "dialogue",
Self::Map => "map",
Self::Script => "script",
Self::Asset => "asset",
Self::Tileset => "tileset",
Self::Lore => "lore",
Self::Recipe => "recipe",
Self::Encounter => "encounter",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ContentLicense {
AllRightsReserved,
CreativeCommons,
RevenueShare,
OpenSource,
Custom,
}
impl ContentLicense {
pub fn label(&self) -> &'static str {
match self {
Self::AllRightsReserved => "all-rights-reserved",
Self::CreativeCommons => "creative-commons",
Self::RevenueShare => "revenue-share",
Self::OpenSource => "open-source",
Self::Custom => "custom",
}
}
pub fn allows_redistribution(&self) -> bool {
matches!(self, Self::CreativeCommons | Self::RevenueShare | Self::OpenSource)
}
}
pub fn min_level_for_permission(perm: AuthorPermission) -> AuthorityLevel {
match perm {
AuthorPermission::Export => AuthorityLevel::Author,
AuthorPermission::ListContent => AuthorityLevel::Author,
AuthorPermission::ViewRevenue => AuthorityLevel::Author,
AuthorPermission::Rescind => AuthorityLevel::Author,
AuthorPermission::Delete => AuthorityLevel::Author,
AuthorPermission::EditMetadata => AuthorityLevel::Author,
AuthorPermission::TransferAuthorship => AuthorityLevel::Owner,
AuthorPermission::WithdrawRevenue => AuthorityLevel::Steward,
}
}
pub fn validate_authority(required: AuthorityLevel, actual: AuthorityLevel) -> bool {
actual.can_perform(required)
}
pub fn can_author_action(perm: AuthorPermission, level: AuthorityLevel) -> bool {
validate_authority(min_level_for_permission(perm), level)
}
#[derive(Debug, Clone, PartialEq)]
pub struct GdprExportManifest {
pub author_identity: String,
pub export_timestamp_ms: u64,
pub content_count: u32,
pub digest: String,
}
impl GdprExportManifest {
pub fn compute_digest(&mut self) {
let input = format!(
"gdpr:{}:{}:{}",
self.author_identity, self.export_timestamp_ms, self.content_count,
);
self.digest = format!("{:016x}", fnv1a_64(input.as_bytes()));
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct RevenueShareConfig {
pub author_share_bps: u16,
pub steward_share_bps: u16,
pub platform_share_bps: u16,
}
impl RevenueShareConfig {
pub fn validate(&self) -> Result<(), String> {
if self.author_share_bps > 10000 {
return Err(format!(
"revenue_share_component_overflow:author {} bps exceeds 10000",
self.author_share_bps
));
}
if self.steward_share_bps > 10000 {
return Err(format!(
"revenue_share_component_overflow:steward {} bps exceeds 10000",
self.steward_share_bps
));
}
if self.platform_share_bps > 10000 {
return Err(format!(
"revenue_share_component_overflow:platform {} bps exceeds 10000",
self.platform_share_bps
));
}
let total = self.author_share_bps as u32 + self.steward_share_bps as u32 + self.platform_share_bps as u32;
if total > 10000 {
return Err(format!("revenue_share_overflow:total {} bps exceeds 10000", total));
}
Ok(())
}
pub fn author_pct(&self) -> f64 {
self.author_share_bps as f64 / 100.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn authority_level_rank_values() {
assert_eq!(AuthorityLevel::Observer.rank(), 0);
assert_eq!(AuthorityLevel::Delegate.rank(), 1);
assert_eq!(AuthorityLevel::Controller.rank(), 2);
assert_eq!(AuthorityLevel::Author.rank(), 3);
assert_eq!(AuthorityLevel::Owner.rank(), 4);
assert_eq!(AuthorityLevel::Steward.rank(), 5);
assert_eq!(AuthorityLevel::System.rank(), 6);
}
#[test]
fn authority_level_ord_ascending() {
let levels = [
AuthorityLevel::Observer,
AuthorityLevel::Delegate,
AuthorityLevel::Controller,
AuthorityLevel::Author,
AuthorityLevel::Owner,
AuthorityLevel::Steward,
AuthorityLevel::System,
];
for i in 1..levels.len() {
assert!(
levels[i] > levels[i - 1],
"{:?} should be > {:?}",
levels[i],
levels[i - 1]
);
}
}
#[test]
fn authority_level_can_perform_same() {
assert!(AuthorityLevel::Author.can_perform(AuthorityLevel::Author));
}
#[test]
fn authority_level_can_perform_higher() {
assert!(AuthorityLevel::System.can_perform(AuthorityLevel::Observer));
assert!(AuthorityLevel::Owner.can_perform(AuthorityLevel::Author));
}
#[test]
fn authority_level_cannot_perform_lower() {
assert!(!AuthorityLevel::Observer.can_perform(AuthorityLevel::Author));
assert!(!AuthorityLevel::Delegate.can_perform(AuthorityLevel::Owner));
}
#[test]
fn permission_export_requires_author() {
assert_eq!(
min_level_for_permission(AuthorPermission::Export),
AuthorityLevel::Author
);
}
#[test]
fn permission_list_content_requires_author() {
assert_eq!(
min_level_for_permission(AuthorPermission::ListContent),
AuthorityLevel::Author
);
}
#[test]
fn permission_view_revenue_requires_author() {
assert_eq!(
min_level_for_permission(AuthorPermission::ViewRevenue),
AuthorityLevel::Author
);
}
#[test]
fn permission_rescind_requires_author() {
assert_eq!(
min_level_for_permission(AuthorPermission::Rescind),
AuthorityLevel::Author
);
}
#[test]
fn permission_delete_requires_author() {
assert_eq!(
min_level_for_permission(AuthorPermission::Delete),
AuthorityLevel::Author
);
}
#[test]
fn permission_edit_metadata_requires_author() {
assert_eq!(
min_level_for_permission(AuthorPermission::EditMetadata),
AuthorityLevel::Author
);
}
#[test]
fn permission_transfer_authorship_requires_owner() {
assert_eq!(
min_level_for_permission(AuthorPermission::TransferAuthorship),
AuthorityLevel::Owner,
);
}
#[test]
fn permission_withdraw_revenue_requires_steward() {
assert_eq!(
min_level_for_permission(AuthorPermission::WithdrawRevenue),
AuthorityLevel::Steward,
);
}
#[test]
fn can_author_action_author_exports() {
assert!(can_author_action(AuthorPermission::Export, AuthorityLevel::Author));
assert!(can_author_action(AuthorPermission::Export, AuthorityLevel::Owner));
assert!(can_author_action(AuthorPermission::Export, AuthorityLevel::System));
assert!(!can_author_action(AuthorPermission::Export, AuthorityLevel::Controller));
}
#[test]
fn can_author_action_owner_transfers() {
assert!(can_author_action(
AuthorPermission::TransferAuthorship,
AuthorityLevel::Owner
));
assert!(can_author_action(
AuthorPermission::TransferAuthorship,
AuthorityLevel::System
));
assert!(!can_author_action(
AuthorPermission::TransferAuthorship,
AuthorityLevel::Author
));
}
#[test]
fn can_author_action_steward_withdraws() {
assert!(can_author_action(
AuthorPermission::WithdrawRevenue,
AuthorityLevel::Steward
));
assert!(can_author_action(
AuthorPermission::WithdrawRevenue,
AuthorityLevel::System
));
assert!(!can_author_action(
AuthorPermission::WithdrawRevenue,
AuthorityLevel::Owner
));
}
#[test]
fn validate_authority_function() {
assert!(validate_authority(AuthorityLevel::Author, AuthorityLevel::Owner));
assert!(validate_authority(AuthorityLevel::Observer, AuthorityLevel::Observer));
assert!(!validate_authority(AuthorityLevel::System, AuthorityLevel::Steward));
}
#[test]
fn content_type_labels() {
assert_eq!(ContentType::Waymark.label(), "waymark");
assert_eq!(ContentType::Entity.label(), "entity");
assert_eq!(ContentType::Item.label(), "item");
assert_eq!(ContentType::Quest.label(), "quest");
assert_eq!(ContentType::Dialogue.label(), "dialogue");
assert_eq!(ContentType::Map.label(), "map");
assert_eq!(ContentType::Script.label(), "script");
assert_eq!(ContentType::Asset.label(), "asset");
assert_eq!(ContentType::Tileset.label(), "tileset");
assert_eq!(ContentType::Lore.label(), "lore");
assert_eq!(ContentType::Recipe.label(), "recipe");
assert_eq!(ContentType::Encounter.label(), "encounter");
}
#[test]
fn content_license_labels() {
assert_eq!(ContentLicense::AllRightsReserved.label(), "all-rights-reserved");
assert_eq!(ContentLicense::CreativeCommons.label(), "creative-commons");
assert_eq!(ContentLicense::RevenueShare.label(), "revenue-share");
assert_eq!(ContentLicense::OpenSource.label(), "open-source");
assert_eq!(ContentLicense::Custom.label(), "custom");
}
#[test]
fn content_license_redistribution() {
assert!(!ContentLicense::AllRightsReserved.allows_redistribution());
assert!(ContentLicense::CreativeCommons.allows_redistribution());
assert!(ContentLicense::RevenueShare.allows_redistribution());
assert!(ContentLicense::OpenSource.allows_redistribution());
assert!(!ContentLicense::Custom.allows_redistribution());
}
#[test]
fn revenue_share_valid() {
let cfg = RevenueShareConfig {
author_share_bps: 7000,
steward_share_bps: 2000,
platform_share_bps: 1000,
};
assert!(cfg.validate().is_ok());
}
#[test]
fn revenue_share_overflow() {
let cfg = RevenueShareConfig {
author_share_bps: 8000,
steward_share_bps: 2000,
platform_share_bps: 1000,
};
let err = cfg.validate().unwrap_err();
assert!(err.contains("revenue_share_overflow"));
}
#[test]
fn revenue_share_zero() {
let cfg = RevenueShareConfig {
author_share_bps: 0,
steward_share_bps: 0,
platform_share_bps: 0,
};
assert!(cfg.validate().is_ok());
}
#[test]
fn revenue_share_exact_cap() {
let cfg = RevenueShareConfig {
author_share_bps: 10000,
steward_share_bps: 0,
platform_share_bps: 0,
};
assert!(cfg.validate().is_ok());
}
#[test]
fn revenue_share_author_pct() {
let cfg = RevenueShareConfig {
author_share_bps: 7000,
steward_share_bps: 2000,
platform_share_bps: 1000,
};
assert!((cfg.author_pct() - 70.0).abs() < f64::EPSILON);
}
#[test]
fn revenue_share_author_pct_zero() {
let cfg = RevenueShareConfig {
author_share_bps: 0,
steward_share_bps: 0,
platform_share_bps: 0,
};
assert!((cfg.author_pct() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn gdpr_manifest_digest_deterministic() {
let mut a = GdprExportManifest {
author_identity: "id:abc123".to_string(),
export_timestamp_ms: 1700000000000,
content_count: 42,
digest: String::new(),
};
let mut b = a.clone();
a.compute_digest();
b.compute_digest();
assert_eq!(a.digest, b.digest);
assert!(!a.digest.is_empty());
}
#[test]
fn gdpr_manifest_digest_is_hex() {
let mut m = GdprExportManifest {
author_identity: "id:test".to_string(),
export_timestamp_ms: 1000,
content_count: 5,
digest: String::new(),
};
m.compute_digest();
assert_eq!(m.digest.len(), 16);
assert!(m.digest.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn gdpr_manifest_digest_changes_with_identity() {
let mut a = GdprExportManifest {
author_identity: "id:alice".to_string(),
export_timestamp_ms: 1000,
content_count: 1,
digest: String::new(),
};
let mut b = GdprExportManifest {
author_identity: "id:bob".to_string(),
export_timestamp_ms: 1000,
content_count: 1,
digest: String::new(),
};
a.compute_digest();
b.compute_digest();
assert_ne!(a.digest, b.digest);
}
#[test]
fn authority_mode_eq() {
assert_eq!(AuthorityMode::Local, AuthorityMode::Local);
assert_eq!(AuthorityMode::Remote, AuthorityMode::Remote);
assert_ne!(AuthorityMode::Local, AuthorityMode::Remote);
}
#[test]
fn authority_mode_serde_roundtrip() {
let mode = AuthorityMode::Remote;
let json = serde_json::to_string(&mode).unwrap();
let restored: AuthorityMode = serde_json::from_str(&json).unwrap();
assert_eq!(mode, restored);
}
#[test]
fn authority_mode_debug() {
assert_eq!(format!("{:?}", AuthorityMode::Local), "Local");
assert_eq!(format!("{:?}", AuthorityMode::Remote), "Remote");
}
#[test]
fn simulation_tier_eq() {
assert_eq!(SimulationTier::Canonical, SimulationTier::Canonical);
assert_eq!(SimulationTier::Predicted, SimulationTier::Predicted);
assert_eq!(SimulationTier::PresentationOnly, SimulationTier::PresentationOnly);
assert_ne!(SimulationTier::Canonical, SimulationTier::Predicted);
assert_ne!(SimulationTier::Predicted, SimulationTier::PresentationOnly);
}
#[test]
fn simulation_tier_serde_roundtrip() {
for tier in [
SimulationTier::Canonical,
SimulationTier::Predicted,
SimulationTier::PresentationOnly,
] {
let json = serde_json::to_string(&tier).unwrap();
let restored: SimulationTier = serde_json::from_str(&json).unwrap();
assert_eq!(tier, restored);
}
}
#[test]
fn simulation_tier_debug() {
assert_eq!(format!("{:?}", SimulationTier::Canonical), "Canonical");
assert_eq!(format!("{:?}", SimulationTier::Predicted), "Predicted");
assert_eq!(format!("{:?}", SimulationTier::PresentationOnly), "PresentationOnly");
}
}