1use crate::hash::fnv1a_64;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub enum AuthorityMode {
15 Local,
17 Remote,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
27pub enum SimulationTier {
28 Canonical,
30 Predicted,
32 PresentationOnly,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub enum AuthorityLevel {
42 Observer,
43 Delegate,
44 Controller,
45 Author,
46 Owner,
47 Steward,
48 System,
49}
50
51impl AuthorityLevel {
52 pub fn rank(&self) -> u8 {
53 match self {
54 Self::Observer => 0,
55 Self::Delegate => 1,
56 Self::Controller => 2,
57 Self::Author => 3,
58 Self::Owner => 4,
59 Self::Steward => 5,
60 Self::System => 6,
61 }
62 }
63
64 pub fn can_perform(&self, required: AuthorityLevel) -> bool {
65 self.rank() >= required.rank()
66 }
67}
68
69impl Ord for AuthorityLevel {
70 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
71 self.rank().cmp(&other.rank())
72 }
73}
74
75impl PartialOrd for AuthorityLevel {
76 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
77 Some(self.cmp(other))
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
86pub enum AuthorPermission {
87 Export,
88 Rescind,
89 Delete,
90 TransferAuthorship,
91 ViewRevenue,
92 WithdrawRevenue,
93 ListContent,
94 EditMetadata,
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
102pub enum ContentType {
103 Waymark,
104 Entity,
105 Item,
106 Quest,
107 Dialogue,
108 Map,
109 Script,
110 Asset,
111 Tileset,
112 Lore,
113 Recipe,
114 Encounter,
115}
116
117impl ContentType {
118 pub fn label(&self) -> &'static str {
119 match self {
120 Self::Waymark => "waymark",
121 Self::Entity => "entity",
122 Self::Item => "item",
123 Self::Quest => "quest",
124 Self::Dialogue => "dialogue",
125 Self::Map => "map",
126 Self::Script => "script",
127 Self::Asset => "asset",
128 Self::Tileset => "tileset",
129 Self::Lore => "lore",
130 Self::Recipe => "recipe",
131 Self::Encounter => "encounter",
132 }
133 }
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
141pub enum ContentLicense {
142 AllRightsReserved,
143 CreativeCommons,
144 RevenueShare,
145 OpenSource,
146 Custom,
147}
148
149impl ContentLicense {
150 pub fn label(&self) -> &'static str {
151 match self {
152 Self::AllRightsReserved => "all-rights-reserved",
153 Self::CreativeCommons => "creative-commons",
154 Self::RevenueShare => "revenue-share",
155 Self::OpenSource => "open-source",
156 Self::Custom => "custom",
157 }
158 }
159
160 pub fn allows_redistribution(&self) -> bool {
161 matches!(self, Self::CreativeCommons | Self::RevenueShare | Self::OpenSource)
162 }
163}
164
165pub fn min_level_for_permission(perm: AuthorPermission) -> AuthorityLevel {
170 match perm {
171 AuthorPermission::Export => AuthorityLevel::Author,
172 AuthorPermission::ListContent => AuthorityLevel::Author,
173 AuthorPermission::ViewRevenue => AuthorityLevel::Author,
174 AuthorPermission::Rescind => AuthorityLevel::Author,
175 AuthorPermission::Delete => AuthorityLevel::Author,
176 AuthorPermission::EditMetadata => AuthorityLevel::Author,
177 AuthorPermission::TransferAuthorship => AuthorityLevel::Owner,
178 AuthorPermission::WithdrawRevenue => AuthorityLevel::Steward,
179 }
180}
181
182pub fn validate_authority(required: AuthorityLevel, actual: AuthorityLevel) -> bool {
183 actual.can_perform(required)
184}
185
186pub fn can_author_action(perm: AuthorPermission, level: AuthorityLevel) -> bool {
187 validate_authority(min_level_for_permission(perm), level)
188}
189
190#[derive(Debug, Clone, PartialEq)]
195pub struct GdprExportManifest {
196 pub author_identity: String,
197 pub export_timestamp_ms: u64,
198 pub content_count: u32,
199 pub digest: String,
200}
201
202impl GdprExportManifest {
203 pub fn compute_digest(&mut self) {
204 let input = format!(
205 "gdpr:{}:{}:{}",
206 self.author_identity, self.export_timestamp_ms, self.content_count,
207 );
208 self.digest = format!("{:016x}", fnv1a_64(input.as_bytes()));
209 }
210}
211
212#[derive(Debug, Clone, PartialEq)]
217pub struct RevenueShareConfig {
218 pub author_share_bps: u16,
219 pub steward_share_bps: u16,
220 pub platform_share_bps: u16,
221}
222
223impl RevenueShareConfig {
224 pub fn validate(&self) -> Result<(), String> {
225 if self.author_share_bps > 10000 {
226 return Err(format!(
227 "revenue_share_component_overflow:author {} bps exceeds 10000",
228 self.author_share_bps
229 ));
230 }
231 if self.steward_share_bps > 10000 {
232 return Err(format!(
233 "revenue_share_component_overflow:steward {} bps exceeds 10000",
234 self.steward_share_bps
235 ));
236 }
237 if self.platform_share_bps > 10000 {
238 return Err(format!(
239 "revenue_share_component_overflow:platform {} bps exceeds 10000",
240 self.platform_share_bps
241 ));
242 }
243 let total = self.author_share_bps as u32 + self.steward_share_bps as u32 + self.platform_share_bps as u32;
244 if total > 10000 {
245 return Err(format!("revenue_share_overflow:total {} bps exceeds 10000", total));
246 }
247 Ok(())
248 }
249
250 pub fn author_pct(&self) -> f64 {
251 self.author_share_bps as f64 / 100.0
252 }
253}
254
255#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[test]
266 fn authority_level_rank_values() {
267 assert_eq!(AuthorityLevel::Observer.rank(), 0);
268 assert_eq!(AuthorityLevel::Delegate.rank(), 1);
269 assert_eq!(AuthorityLevel::Controller.rank(), 2);
270 assert_eq!(AuthorityLevel::Author.rank(), 3);
271 assert_eq!(AuthorityLevel::Owner.rank(), 4);
272 assert_eq!(AuthorityLevel::Steward.rank(), 5);
273 assert_eq!(AuthorityLevel::System.rank(), 6);
274 }
275
276 #[test]
277 fn authority_level_ord_ascending() {
278 let levels = [
279 AuthorityLevel::Observer,
280 AuthorityLevel::Delegate,
281 AuthorityLevel::Controller,
282 AuthorityLevel::Author,
283 AuthorityLevel::Owner,
284 AuthorityLevel::Steward,
285 AuthorityLevel::System,
286 ];
287 for i in 1..levels.len() {
288 assert!(
289 levels[i] > levels[i - 1],
290 "{:?} should be > {:?}",
291 levels[i],
292 levels[i - 1]
293 );
294 }
295 }
296
297 #[test]
298 fn authority_level_can_perform_same() {
299 assert!(AuthorityLevel::Author.can_perform(AuthorityLevel::Author));
300 }
301
302 #[test]
303 fn authority_level_can_perform_higher() {
304 assert!(AuthorityLevel::System.can_perform(AuthorityLevel::Observer));
305 assert!(AuthorityLevel::Owner.can_perform(AuthorityLevel::Author));
306 }
307
308 #[test]
309 fn authority_level_cannot_perform_lower() {
310 assert!(!AuthorityLevel::Observer.can_perform(AuthorityLevel::Author));
311 assert!(!AuthorityLevel::Delegate.can_perform(AuthorityLevel::Owner));
312 }
313
314 #[test]
317 fn permission_export_requires_author() {
318 assert_eq!(
319 min_level_for_permission(AuthorPermission::Export),
320 AuthorityLevel::Author
321 );
322 }
323
324 #[test]
325 fn permission_list_content_requires_author() {
326 assert_eq!(
327 min_level_for_permission(AuthorPermission::ListContent),
328 AuthorityLevel::Author
329 );
330 }
331
332 #[test]
333 fn permission_view_revenue_requires_author() {
334 assert_eq!(
335 min_level_for_permission(AuthorPermission::ViewRevenue),
336 AuthorityLevel::Author
337 );
338 }
339
340 #[test]
341 fn permission_rescind_requires_author() {
342 assert_eq!(
343 min_level_for_permission(AuthorPermission::Rescind),
344 AuthorityLevel::Author
345 );
346 }
347
348 #[test]
349 fn permission_delete_requires_author() {
350 assert_eq!(
351 min_level_for_permission(AuthorPermission::Delete),
352 AuthorityLevel::Author
353 );
354 }
355
356 #[test]
357 fn permission_edit_metadata_requires_author() {
358 assert_eq!(
359 min_level_for_permission(AuthorPermission::EditMetadata),
360 AuthorityLevel::Author
361 );
362 }
363
364 #[test]
365 fn permission_transfer_authorship_requires_owner() {
366 assert_eq!(
367 min_level_for_permission(AuthorPermission::TransferAuthorship),
368 AuthorityLevel::Owner,
369 );
370 }
371
372 #[test]
373 fn permission_withdraw_revenue_requires_steward() {
374 assert_eq!(
375 min_level_for_permission(AuthorPermission::WithdrawRevenue),
376 AuthorityLevel::Steward,
377 );
378 }
379
380 #[test]
381 fn can_author_action_author_exports() {
382 assert!(can_author_action(AuthorPermission::Export, AuthorityLevel::Author));
383 assert!(can_author_action(AuthorPermission::Export, AuthorityLevel::Owner));
384 assert!(can_author_action(AuthorPermission::Export, AuthorityLevel::System));
385 assert!(!can_author_action(AuthorPermission::Export, AuthorityLevel::Controller));
386 }
387
388 #[test]
389 fn can_author_action_owner_transfers() {
390 assert!(can_author_action(
391 AuthorPermission::TransferAuthorship,
392 AuthorityLevel::Owner
393 ));
394 assert!(can_author_action(
395 AuthorPermission::TransferAuthorship,
396 AuthorityLevel::System
397 ));
398 assert!(!can_author_action(
399 AuthorPermission::TransferAuthorship,
400 AuthorityLevel::Author
401 ));
402 }
403
404 #[test]
405 fn can_author_action_steward_withdraws() {
406 assert!(can_author_action(
407 AuthorPermission::WithdrawRevenue,
408 AuthorityLevel::Steward
409 ));
410 assert!(can_author_action(
411 AuthorPermission::WithdrawRevenue,
412 AuthorityLevel::System
413 ));
414 assert!(!can_author_action(
415 AuthorPermission::WithdrawRevenue,
416 AuthorityLevel::Owner
417 ));
418 }
419
420 #[test]
421 fn validate_authority_function() {
422 assert!(validate_authority(AuthorityLevel::Author, AuthorityLevel::Owner));
423 assert!(validate_authority(AuthorityLevel::Observer, AuthorityLevel::Observer));
424 assert!(!validate_authority(AuthorityLevel::System, AuthorityLevel::Steward));
425 }
426
427 #[test]
430 fn content_type_labels() {
431 assert_eq!(ContentType::Waymark.label(), "waymark");
432 assert_eq!(ContentType::Entity.label(), "entity");
433 assert_eq!(ContentType::Item.label(), "item");
434 assert_eq!(ContentType::Quest.label(), "quest");
435 assert_eq!(ContentType::Dialogue.label(), "dialogue");
436 assert_eq!(ContentType::Map.label(), "map");
437 assert_eq!(ContentType::Script.label(), "script");
438 assert_eq!(ContentType::Asset.label(), "asset");
439 assert_eq!(ContentType::Tileset.label(), "tileset");
440 assert_eq!(ContentType::Lore.label(), "lore");
441 assert_eq!(ContentType::Recipe.label(), "recipe");
442 assert_eq!(ContentType::Encounter.label(), "encounter");
443 }
444
445 #[test]
448 fn content_license_labels() {
449 assert_eq!(ContentLicense::AllRightsReserved.label(), "all-rights-reserved");
450 assert_eq!(ContentLicense::CreativeCommons.label(), "creative-commons");
451 assert_eq!(ContentLicense::RevenueShare.label(), "revenue-share");
452 assert_eq!(ContentLicense::OpenSource.label(), "open-source");
453 assert_eq!(ContentLicense::Custom.label(), "custom");
454 }
455
456 #[test]
457 fn content_license_redistribution() {
458 assert!(!ContentLicense::AllRightsReserved.allows_redistribution());
459 assert!(ContentLicense::CreativeCommons.allows_redistribution());
460 assert!(ContentLicense::RevenueShare.allows_redistribution());
461 assert!(ContentLicense::OpenSource.allows_redistribution());
462 assert!(!ContentLicense::Custom.allows_redistribution());
463 }
464
465 #[test]
468 fn revenue_share_valid() {
469 let cfg = RevenueShareConfig {
470 author_share_bps: 7000,
471 steward_share_bps: 2000,
472 platform_share_bps: 1000,
473 };
474 assert!(cfg.validate().is_ok());
475 }
476
477 #[test]
478 fn revenue_share_overflow() {
479 let cfg = RevenueShareConfig {
480 author_share_bps: 8000,
481 steward_share_bps: 2000,
482 platform_share_bps: 1000,
483 };
484 let err = cfg.validate().unwrap_err();
485 assert!(err.contains("revenue_share_overflow"));
486 }
487
488 #[test]
489 fn revenue_share_zero() {
490 let cfg = RevenueShareConfig {
491 author_share_bps: 0,
492 steward_share_bps: 0,
493 platform_share_bps: 0,
494 };
495 assert!(cfg.validate().is_ok());
496 }
497
498 #[test]
499 fn revenue_share_exact_cap() {
500 let cfg = RevenueShareConfig {
501 author_share_bps: 10000,
502 steward_share_bps: 0,
503 platform_share_bps: 0,
504 };
505 assert!(cfg.validate().is_ok());
506 }
507
508 #[test]
509 fn revenue_share_author_pct() {
510 let cfg = RevenueShareConfig {
511 author_share_bps: 7000,
512 steward_share_bps: 2000,
513 platform_share_bps: 1000,
514 };
515 assert!((cfg.author_pct() - 70.0).abs() < f64::EPSILON);
516 }
517
518 #[test]
519 fn revenue_share_author_pct_zero() {
520 let cfg = RevenueShareConfig {
521 author_share_bps: 0,
522 steward_share_bps: 0,
523 platform_share_bps: 0,
524 };
525 assert!((cfg.author_pct() - 0.0).abs() < f64::EPSILON);
526 }
527
528 #[test]
531 fn gdpr_manifest_digest_deterministic() {
532 let mut a = GdprExportManifest {
533 author_identity: "id:abc123".to_string(),
534 export_timestamp_ms: 1700000000000,
535 content_count: 42,
536 digest: String::new(),
537 };
538 let mut b = a.clone();
539 a.compute_digest();
540 b.compute_digest();
541 assert_eq!(a.digest, b.digest);
542 assert!(!a.digest.is_empty());
543 }
544
545 #[test]
546 fn gdpr_manifest_digest_is_hex() {
547 let mut m = GdprExportManifest {
548 author_identity: "id:test".to_string(),
549 export_timestamp_ms: 1000,
550 content_count: 5,
551 digest: String::new(),
552 };
553 m.compute_digest();
554 assert_eq!(m.digest.len(), 16);
555 assert!(m.digest.chars().all(|c| c.is_ascii_hexdigit()));
556 }
557
558 #[test]
559 fn gdpr_manifest_digest_changes_with_identity() {
560 let mut a = GdprExportManifest {
561 author_identity: "id:alice".to_string(),
562 export_timestamp_ms: 1000,
563 content_count: 1,
564 digest: String::new(),
565 };
566 let mut b = GdprExportManifest {
567 author_identity: "id:bob".to_string(),
568 export_timestamp_ms: 1000,
569 content_count: 1,
570 digest: String::new(),
571 };
572 a.compute_digest();
573 b.compute_digest();
574 assert_ne!(a.digest, b.digest);
575 }
576
577 #[test]
580 fn authority_mode_eq() {
581 assert_eq!(AuthorityMode::Local, AuthorityMode::Local);
582 assert_eq!(AuthorityMode::Remote, AuthorityMode::Remote);
583 assert_ne!(AuthorityMode::Local, AuthorityMode::Remote);
584 }
585
586 #[test]
587 fn authority_mode_serde_roundtrip() {
588 let mode = AuthorityMode::Remote;
589 let json = serde_json::to_string(&mode).unwrap();
590 let restored: AuthorityMode = serde_json::from_str(&json).unwrap();
591 assert_eq!(mode, restored);
592 }
593
594 #[test]
595 fn authority_mode_debug() {
596 assert_eq!(format!("{:?}", AuthorityMode::Local), "Local");
597 assert_eq!(format!("{:?}", AuthorityMode::Remote), "Remote");
598 }
599
600 #[test]
603 fn simulation_tier_eq() {
604 assert_eq!(SimulationTier::Canonical, SimulationTier::Canonical);
605 assert_eq!(SimulationTier::Predicted, SimulationTier::Predicted);
606 assert_eq!(SimulationTier::PresentationOnly, SimulationTier::PresentationOnly);
607 assert_ne!(SimulationTier::Canonical, SimulationTier::Predicted);
608 assert_ne!(SimulationTier::Predicted, SimulationTier::PresentationOnly);
609 }
610
611 #[test]
612 fn simulation_tier_serde_roundtrip() {
613 for tier in [
614 SimulationTier::Canonical,
615 SimulationTier::Predicted,
616 SimulationTier::PresentationOnly,
617 ] {
618 let json = serde_json::to_string(&tier).unwrap();
619 let restored: SimulationTier = serde_json::from_str(&json).unwrap();
620 assert_eq!(tier, restored);
621 }
622 }
623
624 #[test]
625 fn simulation_tier_debug() {
626 assert_eq!(format!("{:?}", SimulationTier::Canonical), "Canonical");
627 assert_eq!(format!("{:?}", SimulationTier::Predicted), "Predicted");
628 assert_eq!(format!("{:?}", SimulationTier::PresentationOnly), "PresentationOnly");
629 }
630}