use std::{ env, fs, path::{Path, PathBuf}, }; use nana_domain::{ AcquisitionMode, ActionSuggestion, AppInfo, BeatKind, CharacterCard, CharacterJudgmentRule, CharacterStyle, CheckDifficulty, CheckRecord, CheckResult, ClockState, DemoPackSummary, HistoryNodeView, ItemAcquisition, ItemInstance, ItemMechanics, ItemPlacement, ItemSpec, KnowledgeCertainty, KnowledgeRecord, Persona, PlayerItemView, PlayerKnowledgeView, PlayerPromiseView, PlayerView, PlotEvent, PlotModule, PlotOutcome, PlotPressure, Promise, PromiseStatus, PromiseWeight, RelationshipAdjustment, RelationshipAxes, RelationshipBand, RelationshipDimension, RelationshipState, RelationshipView, ResourceBundle, ResourceHeader, ResourceId, ResourceKind, ResourceRef, RuntimeState, SkillValue, StateDelta, StateOp, Story, StoryBinding, StoryNode, TurnFailure, TurnFailureCode, TurnIntent, TurnRequest, TurnResult, ValidationCode, ValidationIssue, ValidationReport, VisualDirective, WorldBook, WorldBookEntry, }; use schemars::{JsonSchema, schema_for}; use serde::Serialize; use sha2::{Digest, Sha256}; use ts_rs::TS; fn main() -> Result<(), Box> { let check = env::args().any(|argument| argument == "--check"); let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); let outputs = generated_outputs(&root)?; if check { let mut stale = Vec::new(); for (path, expected) in outputs { if fs::read(&path).ok().as_deref() != Some(expected.as_slice()) { stale.push(path); } } if stale.is_empty() { return Ok(()); } for path in stale { eprintln!("stale generated contract: {}", path.display()); } return Err("generated contracts are stale".into()); } for (path, bytes) in outputs { if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } fs::write(path, bytes)?; } Ok(()) } fn generated_outputs( root: &Path, ) -> Result)>, Box> { let mut outputs = Vec::new(); let schema_dir = root.join("contracts/schema"); add_schema::(&mut outputs, &schema_dir, "resource-header")?; add_schema::(&mut outputs, &schema_dir, "character-card")?; add_schema::(&mut outputs, &schema_dir, "world-book")?; add_schema::(&mut outputs, &schema_dir, "persona")?; add_schema::(&mut outputs, &schema_dir, "plot-module")?; add_schema::(&mut outputs, &schema_dir, "item-spec")?; add_schema::(&mut outputs, &schema_dir, "resource-bundle")?; add_schema::(&mut outputs, &schema_dir, "story-node")?; add_schema::(&mut outputs, &schema_dir, "runtime-state")?; add_schema::(&mut outputs, &schema_dir, "player-view")?; add_schema::(&mut outputs, &schema_dir, "turn-request")?; add_schema::(&mut outputs, &schema_dir, "turn-result")?; add_schema::(&mut outputs, &schema_dir, "turn-failure")?; add_schema::(&mut outputs, &schema_dir, "app-info")?; add_schema::(&mut outputs, &schema_dir, "demo-pack-summary")?; let declarations = [ ResourceId::decl(), ResourceKind::decl(), ResourceRef::decl(), ResourceHeader::decl(), CharacterStyle::decl(), CharacterJudgmentRule::decl(), SkillValue::decl(), CharacterCard::decl(), WorldBookEntry::decl(), WorldBook::decl(), Persona::decl(), PlotPressure::decl(), PlotOutcome::decl(), PlotEvent::decl(), PlotModule::decl(), ItemMechanics::decl(), ItemSpec::decl(), ResourceBundle::decl(), RelationshipAxes::decl(), RelationshipAdjustment::decl(), RelationshipDimension::decl(), RelationshipBand::decl(), RelationshipView::decl(), PromiseStatus::decl(), PromiseWeight::decl(), Promise::decl(), RelationshipState::decl(), KnowledgeCertainty::decl(), KnowledgeRecord::decl(), ItemPlacement::decl(), AcquisitionMode::decl(), ItemAcquisition::decl(), ItemInstance::decl(), CheckDifficulty::decl(), CheckResult::decl(), CheckRecord::decl(), ClockState::decl(), StoryBinding::decl(), Story::decl(), StateDelta::decl(), StateOp::decl(), StoryNode::decl(), RuntimeState::decl(), BeatKind::decl(), VisualDirective::decl(), PresentationBeat::decl(), ActionSuggestion::decl(), PlayerItemView::decl(), PlayerKnowledgeView::decl(), PlayerPromiseView::decl(), HistoryNodeView::decl(), PlayerView::decl(), TurnIntent::decl(), TurnRequest::decl(), TurnResult::decl(), TurnFailureCode::decl(), TurnFailure::decl(), AppInfo::decl(), DemoPackSummary::decl(), ValidationCode::decl(), ValidationIssue::decl(), ValidationReport::decl(), ] .join("\n\n"); let ts = format!( "// @generated by crates/nana-contracts; do not edit.\n\n{declarations}\n" ); outputs.push(( root.join("contracts/ts/index.ts"), ts.into_bytes(), )); let domain_source = fs::read(root.join("crates/nana-domain/src/lib.rs"))?; let source_hash = format!("{:x}\n", Sha256::digest(domain_source)); outputs.push(( root.join("contracts/.source.sha256"), source_hash.into_bytes(), )); Ok(outputs) } fn add_schema( outputs: &mut Vec<(PathBuf, Vec)>, schema_dir: &Path, name: &str, ) -> Result<(), serde_json::Error> { let mut bytes = serde_json::to_vec_pretty(&schema_for!(T))?; bytes.push(b'\n'); outputs.push((schema_dir.join(format!("{name}.schema.json")), bytes)); Ok(()) }