From b317d0b23cf35e287de50dde7be9c68b947740e7 Mon Sep 17 00:00:00 2001 From: yongrean Date: Mon, 27 Jul 2026 17:58:08 +0900 Subject: [PATCH] refactor(provider-gen): name the declaration after what it declares MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "wire" described nothing about this file's role, and the issue that proposed the spike already said the name should go. Measuring what the generator actually replaces settled what to call it: the declaration names the app's provider tables — the snapshot reads, the mutation-to-fn routing, and the refusals a user may see — so it is a `.provider` declaration. Renamed with no behavior change: `wire.pest` to `provider.pest`, `instagram.wire` to `instagram.provider`, `WireParser` to `ProviderParser`, and `WireFile` to `ProviderFile`. The CLI flag becomes `--declaration`, which also removes the odd `--app instagram.provider` reading. Uhura's wire protocol keeps its name. `WireValue` and the "Uhura wire values" the runtime checks are a different concept and are untouched; only the declaration language is renamed. Verified: fmt, clippy -D warnings, `cargo test --locked --workspace`, and the doc snippets all exit 0; `provider_gen` reports 14 passed, 0 ignored; and `spock gen provider --declaration instagram.provider --uhura ` still emits `export const MODULE = "app.instagram@1"`. --- crates/spock-cli/src/main.rs | 20 +++---- crates/spock-cli/src/provider_gen/mod.rs | 52 ++++++++++--------- .../provider_gen/{wire.pest => provider.pest} | 0 .../{instagram.wire => instagram.provider} | 0 crates/spock-cli/tests/provider_gen.rs | 28 +++++----- 5 files changed, 52 insertions(+), 48 deletions(-) rename crates/spock-cli/src/provider_gen/{wire.pest => provider.pest} (100%) rename crates/spock-cli/tests/provider_fixtures/{instagram.wire => instagram.provider} (100%) 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`;"));