diff --git a/crates/spock-cli/src/main.rs b/crates/spock-cli/src/main.rs index bbf1681..f3d575e 100644 --- a/crates/spock-cli/src/main.rs +++ b/crates/spock-cli/src/main.rs @@ -106,9 +106,9 @@ enum GenTarget { /// SPIKE (uhura#29): provider tables from an app declaration + the contract. Provider { file: PathBuf, - /// The app assembly declaration (.wire spike syntax). + /// The app's provider declaration (`.provider` spike syntax). #[arg(long)] - app: PathBuf, + declaration: PathBuf, /// The Uhura client project whose `uhura.toml` carries the package identity. #[arg(long)] uhura: PathBuf, @@ -221,14 +221,14 @@ fn execute(command: Command) -> ExitCode { program.contract(), GenerationTarget::GraphqlSchema, ), - GenTarget::Provider { app, uhura, .. } => { - spock_cli::provider_gen::generate_from_contract( - program.contract(), - &app, - &uhura, - ) - .map_err(anyhow::Error::msg) - } + GenTarget::Provider { + declaration, uhura, .. + } => spock_cli::provider_gen::generate_from_contract( + program.contract(), + &declaration, + &uhura, + ) + .map_err(anyhow::Error::msg), }; match artifact { Ok(content) => emit(out, content), diff --git a/crates/spock-cli/src/provider_gen/mod.rs b/crates/spock-cli/src/provider_gen/mod.rs index 0af21b1..f1cd193 100644 --- a/crates/spock-cli/src/provider_gen/mod.rs +++ b/crates/spock-cli/src/provider_gen/mod.rs @@ -1,5 +1,5 @@ -//! Provider generation spike (uhura#29): .wire — a projection/contract language -//! over the Spock schema. Parses a .wire declaration, validates it against the +//! Provider generation spike (uhura#29): .provider — a projection/contract language +//! over the Spock schema. Parses a .provider declaration, validates it against the //! compiler-emitted contract, and generates the provider artifacts: contract //! types, view types, snapshot query, dispatch, refusals, assets, and the module. @@ -8,13 +8,13 @@ use pest_derive::Parser; use serde_json::Value; #[derive(Parser)] -#[grammar = "provider_gen/wire.pest"] -struct WireParser; +#[grammar = "provider_gen/provider.pest"] +struct ProviderParser; #[derive(Debug, PartialEq)] pub struct Field { pub name: String, - /// Original `.wire` type token (e.g. `post.id`) — used for schema validation. + /// Original `.provider` type token (e.g. `post.id`) — used for schema validation. pub source: String, pub ty: String, } @@ -104,7 +104,7 @@ pub struct SnapshotRead { } #[derive(Debug, Default)] -pub struct WireFile { +pub struct ProviderFile { pub app: Option, /// Explicit video mappings from the fixtures block (no manifest source to derive from) pub videos: Vec<(String, String)>, @@ -125,7 +125,7 @@ impl std::fmt::Display for ParseError { } impl std::error::Error for ParseError {} -/// `.wire` field type → machine-side type name. +/// `.provider` field type → machine-side type name. /// `.id` projects to `
Id`; scalars use the 0.4 prelude names. fn machine_type(ty: &str) -> Result { match ty { @@ -213,10 +213,11 @@ fn asset_type(class: &str) -> Option<&'static str> { } } -pub fn parse(source: &str) -> Result { - let mut pairs = WireParser::parse(Rule::file, source).map_err(|e| ParseError(e.to_string()))?; +pub fn parse(source: &str) -> Result { + let mut pairs = + ProviderParser::parse(Rule::file, source).map_err(|e| ParseError(e.to_string()))?; let file = pairs.next().expect("file rule"); - let mut out = WireFile::default(); + let mut out = ProviderFile::default(); for item in file.into_inner() { match item.as_rule() { @@ -449,7 +450,7 @@ pub fn policy_calls(policy: &str) -> Vec { /// Mutation → backend routing table (JSON). The runtime decides branches, RPC /// arguments, and refusal routes from this table alone — it owns no logic. -pub fn generate_routing(file: &WireFile) -> String { +pub fn generate_routing(file: &ProviderFile) -> String { let mut out = String::from("{\n"); for (i, m) in file.mutations.iter().enumerate() { let kind = m.op.clone().unwrap_or_else(|| snake(&m.name)); @@ -506,9 +507,9 @@ pub fn generate_routing(file: &WireFile) -> String { out } -/// Schema validation: every table/fn/error a .wire file references must exist +/// Schema validation: every table/fn/error a `.provider` file references must exist /// in the contract. Violations come back as a readable list (no silent passes). -pub fn validate_against(file: &WireFile, schema: &SpockSchema) -> Vec { +pub fn validate_against(file: &ProviderFile, schema: &SpockSchema) -> Vec { let mut problems = Vec::new(); for read in &file.snapshot_reads { if !schema.has_table(&read.table) { @@ -598,7 +599,7 @@ fn screaming(name: &str) -> String { /// Mutation surface → the adapter's `toBackendOperation` switch body (TS text). /// kind = `op` override or snake_case(name); field mapping is derived from types. -pub fn generate_dispatch(file: &WireFile) -> Result> { +pub fn generate_dispatch(file: &ProviderFile) -> Result> { let mut problems = Vec::new(); let Some(app) = &file.app else { return Err(vec!["missing `app \"Name\";` declaration".to_string()]); @@ -649,7 +650,7 @@ pub fn generate_dispatch(file: &WireFile) -> Result> { } /// Route key → kebab-case refusal list. Duplicate routes are an error. -pub fn generate_refusals(file: &WireFile) -> Result)>, Vec> { +pub fn generate_refusals(file: &ProviderFile) -> Result)>, Vec> { let mut problems = Vec::new(); let mut out: Vec<(String, Vec)> = Vec::new(); for m in &file.mutations { @@ -676,7 +677,7 @@ pub fn generate_refusals(file: &WireFile) -> Result)>, /// under. Key tags are app data, so they are generated here rather than supplied by /// the runtime, which must stay app-independent. pub fn generate_provider_module( - file: &WireFile, + file: &ProviderFile, schema: &SpockSchema, module: &str, ) -> Result> { @@ -763,7 +764,7 @@ pub fn parse_manifest(source: &str) -> Result, Vec } } -/// Manifest entries + explicit .wire video mappings → Play logical-asset table (TS text). +/// Manifest entries + explicit .provider video mappings → Play logical-asset table (TS text). /// Name collisions are an error. pub fn generate_play_assets( entries: &[(String, String)], @@ -812,10 +813,10 @@ pub fn default_alias(table: &str) -> String { pluralize(&camel(table)) } -/// .wire snapshot declaration + schema columns → the adapter's GraphQL snapshot document. +/// .provider snapshot declaration + schema columns → the adapter's GraphQL snapshot document. /// FK and storage_object columns project as `name { id }`; the rest stay bare. pub fn generate_snapshot_query( - file: &WireFile, + file: &ProviderFile, schema: &SpockSchema, ) -> Result> { let mut problems = Vec::new(); @@ -868,7 +869,10 @@ fn pascal(name: &str) -> String { /// View declaration → machine-side record types. If type derivation disagrees with /// the schema, return a problem list instead of generating (no speculative output). -pub fn generate_view_types(file: &WireFile, schema: &SpockSchema) -> Result> { +pub fn generate_view_types( + file: &ProviderFile, + schema: &SpockSchema, +) -> Result> { let mut problems = Vec::new(); let mut out = String::new(); for (i, view) in file.views.iter().enumerate() { @@ -1072,7 +1076,7 @@ fn emit_variant(out: &mut String, name: &str, fields: &[Field]) { /// Generate the machine-side contract declarations. Settlement's Accepted/Refused are /// fixed by the 0.4 result vocabulary, so they are enforced; only extras follow the file. -pub fn generate_machine_types(file: &WireFile) -> String { +pub fn generate_machine_types(file: &ProviderFile) -> String { let mut out = String::from("pub enum Mutation {\n"); for m in &file.mutations { emit_variant(&mut out, &m.name, &m.fields); @@ -1298,14 +1302,14 @@ pub fn read_module_identity(uhura_project: &std::path::Path) -> Result( contract: &C, - app_declaration: &std::path::Path, + declaration: &std::path::Path, uhura_project: &std::path::Path, ) -> Result { let json = serde_json::to_string(contract) .map_err(|e| format!("contract serialization failed: {e}"))?; let schema = extract_contract(&json).map_err(|p| p.join("\n"))?; - let source = std::fs::read_to_string(app_declaration) - .map_err(|e| format!("cannot read {}: {e}", app_declaration.display()))?; + let source = std::fs::read_to_string(declaration) + .map_err(|e| format!("cannot read {}: {e}", declaration.display()))?; let file = parse(&source).map_err(|e| e.to_string())?; let problems = validate_against(&file, &schema); if !problems.is_empty() { diff --git a/crates/spock-cli/src/provider_gen/wire.pest b/crates/spock-cli/src/provider_gen/provider.pest similarity index 100% rename from crates/spock-cli/src/provider_gen/wire.pest rename to crates/spock-cli/src/provider_gen/provider.pest diff --git a/crates/spock-cli/tests/provider_fixtures/instagram.wire b/crates/spock-cli/tests/provider_fixtures/instagram.provider similarity index 100% rename from crates/spock-cli/tests/provider_fixtures/instagram.wire rename to crates/spock-cli/tests/provider_fixtures/instagram.provider diff --git a/crates/spock-cli/tests/provider_gen.rs b/crates/spock-cli/tests/provider_gen.rs index f234769..1c3bea9 100644 --- a/crates/spock-cli/tests/provider_gen.rs +++ b/crates/spock-cli/tests/provider_gen.rs @@ -5,7 +5,7 @@ use spock_cli::provider_gen as pg; const CONTRACT: &str = include_str!("provider_fixtures/contract.json"); -const WIRE: &str = include_str!("provider_fixtures/instagram.wire"); +const DECLARATION: &str = include_str!("provider_fixtures/instagram.provider"); const PROVIDER_TS: &str = include_str!("provider_fixtures/spock-provider.ts"); const MANIFEST: &str = include_str!("provider_fixtures/manifest.toml"); @@ -22,14 +22,14 @@ fn contract_exposes_the_storage_object_system_table() { #[test] fn declaration_validates_clean_against_the_contract() { - let file = pg::parse(WIRE).expect("parse"); + let file = pg::parse(DECLARATION).expect("parse"); let problems = pg::validate_against(&file, &schema()); assert!(problems.is_empty(), "{problems:?}"); } #[test] fn machine_contract_types_match_the_handwritten_machine_uhura() { - let file = pg::parse(WIRE).expect("parse"); + let file = pg::parse(DECLARATION).expect("parse"); let generated = pg::generate_machine_types(&file); let golden = include_str!("provider_fixtures/machine-types.uhura"); assert_eq!(generated.trim(), golden.trim()); @@ -37,7 +37,7 @@ fn machine_contract_types_match_the_handwritten_machine_uhura() { #[test] fn view_types_match_the_handwritten_machine_uhura() { - let file = pg::parse(WIRE).expect("parse"); + let file = pg::parse(DECLARATION).expect("parse"); let generated = pg::generate_view_types(&file, &schema()).expect("views validate"); let golden = include_str!("provider_fixtures/view-types.uhura"); assert_eq!(generated.trim(), golden.trim()); @@ -45,7 +45,7 @@ fn view_types_match_the_handwritten_machine_uhura() { #[test] fn snapshot_query_matches_the_handwritten_adapter() { - let file = pg::parse(WIRE).expect("parse"); + let file = pg::parse(DECLARATION).expect("parse"); let generated = pg::generate_snapshot_query(&file, &schema()).expect("generates"); let golden = include_str!("provider_fixtures/snapshot-query.graphql"); assert_eq!(generated, golden); @@ -53,7 +53,7 @@ fn snapshot_query_matches_the_handwritten_adapter() { #[test] fn dispatch_switch_matches_the_handwritten_adapter() { - let file = pg::parse(WIRE).expect("parse"); + let file = pg::parse(DECLARATION).expect("parse"); let generated = pg::generate_dispatch(&file).expect("generates"); let golden = include_str!("provider_fixtures/dispatch-switch.ts"); assert_eq!(generated, golden); @@ -61,7 +61,7 @@ fn dispatch_switch_matches_the_handwritten_adapter() { #[test] fn refusal_whitelist_semantically_matches_the_handwritten_table() { - let file = pg::parse(WIRE).expect("parse"); + let file = pg::parse(DECLARATION).expect("parse"); let mut generated = pg::generate_refusals(&file).expect("generates"); for (_, list) in generated.iter_mut() { list.sort(); @@ -96,7 +96,7 @@ fn refusal_whitelist_semantically_matches_the_handwritten_table() { #[test] fn play_assets_match_the_handwritten_adapter() { let entries = pg::parse_manifest(MANIFEST).expect("manifest parses"); - let file = pg::parse(WIRE).expect("parse"); + let file = pg::parse(DECLARATION).expect("parse"); let generated = pg::generate_play_assets(&entries, &file.videos).expect("no collisions"); let golden = include_str!("provider_fixtures/play-assets.ts"); assert_eq!(generated, golden.trim_end()); @@ -133,7 +133,7 @@ const MODULE: &str = "app.instagram@1"; /// module constants, and the runtime contract must stay the four generic helpers. #[test] fn key_tags_are_generated_data_and_helpers_stay_app_independent() { - let file = pg::parse(WIRE).expect("parse"); + let file = pg::parse(DECLARATION).expect("parse"); let module = pg::generate_provider_module(&file, &schema(), MODULE).expect("generates"); assert!( @@ -160,7 +160,7 @@ fn key_tags_are_generated_data_and_helpers_stay_app_independent() { /// One generator keeps them from drifting apart. #[test] fn generated_key_tags_agree_with_the_generated_machine_types() { - let file = pg::parse(WIRE).expect("parse"); + let file = pg::parse(DECLARATION).expect("parse"); let machine = pg::generate_machine_types(&file); let module = pg::generate_provider_module(&file, &schema(), MODULE).expect("generates"); for ty in ["PostId", "UserId", "StoryId"] { @@ -202,7 +202,7 @@ fn run_node(script: &str, args: &[&std::path::Path]) -> String { /// `pnpm -C uhura/web build:provider`, so nothing here is optional. #[test] fn generated_module_drives_the_shared_runtime_under_node() { - let file = pg::parse(WIRE).expect("parse"); + let file = pg::parse(DECLARATION).expect("parse"); let module = pg::generate_provider_module(&file, &schema(), MODULE).expect("generates"); let path = write_module("runtime-tables.mjs", module); let stdout = run_node("runtime-unit.mjs", &[&path]); @@ -213,7 +213,7 @@ fn generated_module_drives_the_shared_runtime_under_node() { /// the Uhura type the generated tables declare. #[test] fn the_runtime_is_app_independent_and_checks_key_types() { - let file = pg::parse(WIRE).expect("parse"); + let file = pg::parse(DECLARATION).expect("parse"); let module = pg::generate_provider_module(&file, &schema(), MODULE).expect("generates"); let path = write_module("genericity-tables.mjs", module); let source = std::path::PathBuf::from(concat!( @@ -242,7 +242,7 @@ fn a_second_app_drives_the_same_runtime() { { "name": "retract_article", "errors": ["not_authorized"] } ] }"#; - const BLOG_WIRE: &str = r#" + const BLOG_DECLARATION: &str = r#" app "Blog"; snapshot app { cap 50 per table; read article; } mutation SetPublished { article: article.id, published: bool } @@ -251,7 +251,7 @@ fn a_second_app_drives_the_same_runtime() { "#; let schema = pg::extract_contract(BLOG_CONTRACT).expect("contract parses"); - let file = pg::parse(BLOG_WIRE).expect("parse"); + let file = pg::parse(BLOG_DECLARATION).expect("parse"); assert!(pg::validate_against(&file, &schema).is_empty()); let module = pg::generate_provider_module(&file, &schema, "app.blog@2").expect("generates"); assert!(module.contains("const ARTICLE_ID_TYPE = `${MODULE}::ArticleId`;"));