Skip to content

Commit 59202bc

Browse files
authored
Merge pull request #147 from paritytech/worktree-require-ts-examples
feat(codegen): require a `ts` example on every trait method
2 parents d90f6e2 + a562613 commit 59202bc

4 files changed

Lines changed: 177 additions & 157 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ scripts/codegen.sh Regenerate the TS client from the Rust source
6565

6666
## How it works
6767

68-
1. The protocol is defined as Rust traits in [`rust/crates/truapi/`](rust/crates/truapi/), with each method tagged `#[wire(id = N)]` for a stable byte-level dispatch table.
68+
1. The protocol is defined as Rust traits in [`rust/crates/truapi/`](rust/crates/truapi/), with each method tagged `#[wire(id = N)]` for a stable byte-level dispatch table. Every method's doc comment must carry a ` ```ts ` example, which codegen extracts into the playground's EXAMPLE tab; the build fails if any method is missing one.
6969
2. `truapi-codegen` reads rustdoc JSON for that crate and generates the TypeScript client under git-ignored paths in `js/packages/truapi/`.
7070
3. Higher-level SDKs wrap the typed client; the transport encodes SCALE frames and ships them over `MessagePort` (or `postMessage` in iframe mode) to the host.
7171
4. The host decodes the frame, dispatches to the matching trait method, encodes the response, and ships it back.

rust/crates/truapi-codegen/src/ts.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,8 @@ pub fn generate(
365365
) -> Result<()> {
366366
fs::create_dir_all(output_dir)?;
367367
validate_versioned_wrapper_shapes(api)?;
368+
let wrappers = collect_versioned_wrappers(api);
369+
playground::validate_method_examples(api, &wrappers, target_version)?;
368370

369371
let types_code = generate_types(api, target_version)?;
370372
fs::write(Path::new(output_dir).join("types.ts"), types_code)?;

rust/crates/truapi-codegen/src/ts/playground.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,47 @@ pub(super) fn split_playground_docs(docs: Option<&str>) -> Result<PlaygroundDocs
197197
})
198198
}
199199

200+
/// Fails if any public trait method lacks a valid ` ```ts ` example in its
201+
/// doc comment. Every method renders an EXAMPLE tab in the playground from
202+
/// the extracted `exampleSource`; a missing or mis-fenced example would
203+
/// silently leave that tab empty and dump the snippet into the description.
204+
pub(super) fn validate_method_examples(
205+
api: &ApiDefinition,
206+
wrappers: &HashMap<String, VersionedWrapper>,
207+
target_version: u32,
208+
) -> Result<()> {
209+
for service in public_services(api)? {
210+
let trait_def = service.trait_def;
211+
for method in included_methods(trait_def, wrappers, target_version)? {
212+
validate_example_docs(&trait_def.name, &method.name, method.docs.as_deref())?;
213+
}
214+
}
215+
Ok(())
216+
}
217+
218+
/// Checks a single method's doc comment carries exactly one ` ```ts ` example
219+
/// that `split_playground_docs` can extract. Rejects a missing example, an
220+
/// example fenced with an unrecognized label, and any code fence left behind
221+
/// in the description (which means the example was not extracted).
222+
fn validate_example_docs(trait_name: &str, method_name: &str, docs: Option<&str>) -> Result<()> {
223+
let parsed = split_playground_docs(docs)?;
224+
if let Some(description) = &parsed.description
225+
&& description.contains("```")
226+
{
227+
bail!(
228+
"{trait_name}::{method_name} has a code fence left in its description; \
229+
examples must be fenced with ```ts so they are extracted into exampleSource"
230+
);
231+
}
232+
if parsed.client_example.is_none() {
233+
bail!(
234+
"{trait_name}::{method_name} has no ```ts example in its doc comment; \
235+
every TrUAPI method must carry one so the playground renders an EXAMPLE tab"
236+
);
237+
}
238+
Ok(())
239+
}
240+
200241
pub(super) fn playground_type_name(value: &str) -> String {
201242
value.replace("T.", "")
202243
}
@@ -401,6 +442,35 @@ mod tests {
401442
assert_eq!(data_type_id_from_ts(""), None);
402443
}
403444

445+
#[test]
446+
fn validate_example_docs_accepts_ts_fence() {
447+
let docs = "Summary line.\n\n```ts\nconst x = 1;\n```";
448+
assert!(validate_example_docs("CoinPayment", "create_purse", Some(docs)).is_ok());
449+
}
450+
451+
#[test]
452+
fn validate_example_docs_rejects_missing_example() {
453+
let docs = "Summary line with no example.";
454+
let err = validate_example_docs("CoinPayment", "create_purse", Some(docs)).unwrap_err();
455+
assert!(err.to_string().contains("no ```ts example"));
456+
}
457+
458+
#[test]
459+
fn validate_example_docs_rejects_missing_docs() {
460+
let err = validate_example_docs("CoinPayment", "create_purse", None).unwrap_err();
461+
assert!(err.to_string().contains("no ```ts example"));
462+
}
463+
464+
#[test]
465+
fn validate_example_docs_rejects_unrecognized_label() {
466+
let docs = "Summary.\n\n```truapi-client-example\nconst x = 1;\n```";
467+
let err = validate_example_docs("CoinPayment", "create_purse", Some(docs)).unwrap_err();
468+
assert!(
469+
err.to_string()
470+
.contains("code fence left in its description")
471+
);
472+
}
473+
404474
#[test]
405475
fn data_type_id_kebabs_named_types() {
406476
assert_eq!(

0 commit comments

Comments
 (0)