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 proto;
27pub mod providers;
28pub mod reconcile;
29pub mod schema;
30pub mod serialize;
31
32/// One operation a spec declares.
33#[derive(Debug, Clone, PartialEq)]
34pub struct DeclaredOp {
35    /// Identity: operationId / GraphQL field / MCP tool / channel+verb.
36    pub operation: String,
37    /// HTTP verb, or QUERY/MUTATION/SUBSCRIPTION/PUBLISH/SUBSCRIBE/TOOL.
38    pub method: String,
39    /// Route path / field / tool / channel.
40    pub path: String,
41}
42
43/// One `#[soothfast::route]` annotation from the code.
44#[derive(Debug, Clone)]
45pub struct RouteDecl {
46    pub id: String,
47    pub operation: String,
48    pub method: String,
49    pub path: String,
50}
51
52/// Spec dialects soothfast reads and writes.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum SpecKind {
55    OpenApi,
56    AsyncApi,
57    GraphQl,
58    McpTools,
59}
60
61/// Sniff the dialect from filename + content.
62pub fn sniff_kind(filename: &str, text: &str) -> Option<SpecKind> {
63    if filename.ends_with(".graphql") || filename.ends_with(".sdl") {
64        return Some(SpecKind::GraphQl);
65    }
66    let head: String = text.chars().take(2000).collect();
67    if head.contains("asyncapi") {
68        Some(SpecKind::AsyncApi)
69    } else if head.contains("openapi") || head.contains("swagger") {
70        Some(SpecKind::OpenApi)
71    } else if head.contains("\"tools\"") {
72        Some(SpecKind::McpTools)
73    } else if head.contains("type Query") || head.contains("type Mutation") {
74        Some(SpecKind::GraphQl)
75    } else {
76        None
77    }
78}