use std::{ env, fs, path::{Path, PathBuf}, }; use nana_domain::{ AcquisitionMode, ActionSuggestion, AppInfo, BeatKind, BranchList, BranchSummary, CharacterCard, CharacterJudgmentRule, CharacterStyle, CheckDifficulty, CheckRecord, CheckResult, ClockState, DemoPackSummary, ForkBranchRequest, ForkBranchResult, HistoryNodeView, ItemAcquisition, ItemInstance, ItemMechanics, ItemPlacement, ItemSpec, KnowledgeCertainty, KnowledgeRecord, LappConnectionTestResult, LappMode, LappModelOption, LappSettings, Persona, PlayerItemView, PlayerKnowledgeView, PlayerPromiseView, PlayerView, PlotEvent, PlotModule, PlotOutcome, PlotPressure, PresentationBeat, PresentationCharacter, PresentationScene, PresentationSnapshot, Promise, PromiseStatus, PromiseWeight, RelationshipAdjustment, RelationshipAxes, RelationshipBand, RelationshipDimension, RelationshipState, RelationshipView, RenameBranchRequest, ResourceBundle, ResourceHeader, ResourceId, ResourceKind, ResourceRef, RuntimeState, SkillValue, StateDelta, StateOp, Story, StoryBinding, StoryNode, SwitchBranchRequest, SwitchBranchResult, TurnFailure, TurnFailureCode, TurnIntent, TurnRequest, TurnResult, UpdateLappSettingsRequest, ValidationCode, ValidationIssue, ValidationReport, VisualDirective, WorldBook, WorldBookEntry, }; use schemars::{JsonSchema, schema_for}; use serde::Serialize; use sha2::{Digest, Sha256}; use ts_rs::TS; type GeneratedOutput = (PathBuf, Vec); type GeneratedOutputs = Vec; 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 { let matches = fs::read(&path).ok().is_some_and(|actual| { normalize_line_endings(&actual) == normalize_line_endings(&expected) }); if !matches { 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 normalize_line_endings(bytes: &[u8]) -> Vec { let mut normalized = Vec::with_capacity(bytes.len()); let mut index = 0; while index < bytes.len() { if bytes[index] == b'\r' && bytes.get(index + 1) == Some(&b'\n') { normalized.push(b'\n'); index += 2; } else { normalized.push(bytes[index]); index += 1; } } normalized } fn generated_outputs(root: &Path) -> Result> { 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, "presentation-snapshot")?; 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, "fork-branch-request")?; add_schema::(&mut outputs, &schema_dir, "fork-branch-result")?; add_schema::(&mut outputs, &schema_dir, "branch-list")?; add_schema::(&mut outputs, &schema_dir, "switch-branch-request")?; add_schema::(&mut outputs, &schema_dir, "switch-branch-result")?; add_schema::(&mut outputs, &schema_dir, "rename-branch-request")?; add_schema::(&mut outputs, &schema_dir, "lapp-settings")?; add_schema::( &mut outputs, &schema_dir, "update-lapp-settings-request", )?; add_schema::( &mut outputs, &schema_dir, "lapp-connection-test-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 = generated_declarations(); 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(normalize_line_endings(&domain_source)) ); outputs.push(( root.join("contracts/.source.sha256"), source_hash.into_bytes(), )); Ok(outputs) } fn generated_declarations() -> String { [ 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(), PresentationScene::decl(), PresentationCharacter::decl(), PresentationSnapshot::decl(), PlayerItemView::decl(), PlayerKnowledgeView::decl(), PlayerPromiseView::decl(), HistoryNodeView::decl(), PlayerView::decl(), TurnIntent::decl(), TurnRequest::decl(), TurnResult::decl(), ForkBranchRequest::decl(), ForkBranchResult::decl(), BranchSummary::decl(), BranchList::decl(), SwitchBranchRequest::decl(), SwitchBranchResult::decl(), RenameBranchRequest::decl(), LappModelOption::decl(), LappMode::decl(), LappSettings::decl(), UpdateLappSettingsRequest::decl(), LappConnectionTestResult::decl(), TurnFailureCode::decl(), TurnFailure::decl(), AppInfo::decl(), DemoPackSummary::decl(), ValidationCode::decl(), ValidationIssue::decl(), ValidationReport::decl(), ] .into_iter() .map(|declaration| format!("export {declaration}")) .collect::>() .join("\n\n") } fn add_schema( outputs: &mut GeneratedOutputs, 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(()) } #[cfg(test)] mod tests { use super::normalize_line_endings; #[test] fn source_hash_input_is_independent_of_checkout_line_endings() { assert_eq!( normalize_line_endings(b"first\r\nsecond\nthird\r"), b"first\nsecond\nthird\r" ); assert_eq!( normalize_line_endings(b"first\nsecond\nthird\r"), b"first\nsecond\nthird\r" ); } }