Skip to main content

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}