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}