soothfast_spec/lib.rs
1//! Soothfast spec engine, working in both directions.
2//!
3//! **Checking.** Anything that declares an external surface — an OpenAPI
4//! spec, AsyncAPI channels, a GraphQL SDL schema, MCP tool schemas — parses
5//! into a common [`DeclaredOp`] set ([`providers`]) and reconciles against
6//! `#[route]` annotations with one mechanism ([`reconcile`]). The spec is
7//! hand-authored and the code proves it, which is the only order that works
8//! for a contract someone else serves.
9//!
10//! **Generating.** The same four dialects are also emitted *from* the code:
11//! [`schema`] turns rustdoc JSON into JSON Schema, and one of [`openapi`],
12//! [`asyncapi`], [`graphql`] or [`mcp`] renders a document from it. A
13//! generated spec cannot disagree with the code, so what is left worth
14//! gating is whether it broke a consumer — [`compat`], through each
15//! dialect's own diff.
16//!
17//! [`SpecKind`] names the dialect in both directions, and [`dialect`] is
18//! where the generation half dispatches on it.
19
20pub mod asyncapi;
21pub mod compat;
22pub mod dialect;
23pub mod graphql;
24pub mod mcp;
25pub mod openapi;
26pub mod probe;
27pub mod proto;
28pub mod providers;
29pub mod reconcile;
30pub mod schema;
31pub mod serialize;
32
33/// One operation a spec declares.
34#[derive(Debug, Clone, PartialEq)]
35pub struct DeclaredOp {
36 /// Identity: operationId / GraphQL field / MCP tool / channel+verb.
37 pub operation: String,
38 /// HTTP verb, or QUERY/MUTATION/SUBSCRIPTION/PUBLISH/SUBSCRIBE/TOOL.
39 pub method: String,
40 /// Route path / field / tool / channel.
41 pub path: String,
42}
43
44/// One `#[soothfast::route]` annotation from the code.
45#[derive(Debug, Clone)]
46pub struct RouteDecl {
47 pub id: String,
48 pub operation: String,
49 pub method: String,
50 pub path: String,
51}
52
53/// Spec dialects soothfast reads and writes.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum SpecKind {
56 OpenApi,
57 AsyncApi,
58 GraphQl,
59 McpTools,
60}
61
62/// Sniff the dialect from filename + content.
63pub fn sniff_kind(filename: &str, text: &str) -> Option<SpecKind> {
64 if filename.ends_with(".graphql") || filename.ends_with(".sdl") {
65 return Some(SpecKind::GraphQl);
66 }
67 let head: String = text.chars().take(2000).collect();
68 if head.contains("asyncapi") {
69 Some(SpecKind::AsyncApi)
70 } else if head.contains("openapi") || head.contains("swagger") {
71 Some(SpecKind::OpenApi)
72 } else if head.contains("\"tools\"") {
73 Some(SpecKind::McpTools)
74 } else if head.contains("type Query") || head.contains("type Mutation") {
75 Some(SpecKind::GraphQl)
76 } else {
77 None
78 }
79}