use pylon_kernel::AppManifest;
use serde_json::{json, Value};
pub fn generate_openapi(manifest: &AppManifest, base_url: &str) -> Value {
let mut paths = serde_json::Map::new();
let mut schemas = serde_json::Map::new();
paths.insert(
"/health".into(),
json!({
"get": {
"operationId": "healthCheck",
"summary": "Health check",
"tags": ["system"],
"responses": {
"200": {
"description": "Server is healthy",
"content": { "application/json": { "schema": {
"type": "object",
"properties": {
"status": { "type": "string" },
"version": { "type": "string" },
"uptime_secs": { "type": "integer" }
}
}}}
}
}
}
}),
);
paths.insert("/api/manifest".into(), json!({
"get": {
"operationId": "getManifest",
"summary": "Get application manifest",
"tags": ["system"],
"responses": {
"200": { "description": "Application manifest", "content": { "application/json": { "schema": { "type": "object" } } } }
}
}
}));
paths.insert("/api/openapi.json".into(), json!({
"get": {
"operationId": "getOpenApiSpec",
"summary": "Get OpenAPI specification",
"tags": ["system"],
"responses": {
"200": { "description": "OpenAPI 3.0.3 spec", "content": { "application/json": { "schema": { "type": "object" } } } }
}
}
}));
paths.insert("/api/query".into(), json!({
"post": {
"operationId": "graphQuery",
"summary": "Execute a graph query",
"tags": ["query"],
"security": [{ "BearerAuth": [] }],
"requestBody": {
"required": true,
"content": { "application/json": { "schema": { "type": "object" } } }
},
"responses": {
"200": { "description": "Query result", "content": { "application/json": { "schema": { "type": "object" } } } },
"400": { "description": "Invalid query", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
}
}
}));
paths.insert("/api/batch".into(), json!({
"post": {
"operationId": "batchOperations",
"summary": "Execute batch operations",
"tags": ["batch"],
"security": [{ "BearerAuth": [] }],
"requestBody": {
"required": true,
"content": { "application/json": { "schema": {
"type": "object",
"properties": {
"operations": {
"type": "array",
"items": { "type": "object" }
}
},
"required": ["operations"]
}}}
},
"responses": {
"200": { "description": "Batch results", "content": { "application/json": { "schema": { "type": "object" } } } }
}
}
}));
paths.insert("/api/transact".into(), json!({
"post": {
"operationId": "atomicTransaction",
"summary": "Execute an atomic transaction",
"tags": ["batch"],
"security": [{ "BearerAuth": [] }],
"requestBody": {
"required": true,
"content": { "application/json": { "schema": {
"type": "array",
"items": { "type": "object" }
}}}
},
"responses": {
"200": { "description": "Transaction committed", "content": { "application/json": { "schema": { "type": "object" } } } },
"400": { "description": "Transaction rolled back", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
}
}
}));
paths.insert("/api/export".into(), json!({
"get": {
"operationId": "exportAll",
"summary": "Export all data (admin only)",
"tags": ["admin"],
"security": [{ "BearerAuth": [] }],
"responses": {
"200": { "description": "Full data export", "content": { "application/json": { "schema": { "type": "object" } } } },
"403": { "description": "Forbidden", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
}
}
}));
paths.insert("/api/rooms".into(), json!({
"get": {
"operationId": "listRooms",
"summary": "List active rooms",
"tags": ["rooms"],
"responses": {
"200": { "description": "List of rooms", "content": { "application/json": { "schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"members": { "type": "integer" }
}
}
}}}}
}
}
}));
for (path, op_id, summary) in [
("/api/rooms/join", "joinRoom", "Join a room"),
("/api/rooms/leave", "leaveRoom", "Leave a room"),
(
"/api/rooms/presence",
"updatePresence",
"Update presence in a room",
),
(
"/api/rooms/broadcast",
"broadcastToRoom",
"Broadcast a message to a room",
),
] {
paths.insert(path.into(), json!({
"post": {
"operationId": op_id,
"summary": summary,
"tags": ["rooms"],
"security": [{ "BearerAuth": [] }],
"requestBody": {
"required": true,
"content": { "application/json": { "schema": { "type": "object" } } }
},
"responses": {
"200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object" } } } },
"401": { "description": "Auth required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
}
}
}));
}
paths.insert("/api/auth/session".into(), json!({
"post": {
"operationId": "createSession",
"summary": "Create a session",
"tags": ["auth"],
"requestBody": {
"required": true,
"content": { "application/json": { "schema": {
"type": "object",
"properties": { "user_id": { "type": "string" } },
"required": ["user_id"]
}}}
},
"responses": {
"201": { "description": "Session created", "content": { "application/json": { "schema": {
"type": "object",
"properties": {
"token": { "type": "string" },
"user_id": { "type": "string" }
}
}}}}
}
},
"delete": {
"operationId": "revokeSession",
"summary": "Revoke current session",
"tags": ["auth"],
"security": [{ "BearerAuth": [] }],
"responses": {
"200": { "description": "Session revoked", "content": { "application/json": { "schema": { "type": "object" } } } }
}
}
}));
paths.insert("/api/auth/guest".into(), json!({
"post": {
"operationId": "createGuestSession",
"summary": "Create a guest session",
"tags": ["auth"],
"responses": {
"201": { "description": "Guest session created", "content": { "application/json": { "schema": {
"type": "object",
"properties": {
"token": { "type": "string" },
"user_id": { "type": "string" },
"guest": { "type": "boolean" }
}
}}}}
}
}
}));
paths.insert("/api/auth/magic/send".into(), json!({
"post": {
"operationId": "sendMagicCode",
"summary": "Send a magic login code",
"tags": ["auth"],
"requestBody": {
"required": true,
"content": { "application/json": { "schema": {
"type": "object",
"properties": { "email": { "type": "string", "format": "email" } },
"required": ["email"]
}}}
},
"responses": {
"200": { "description": "Code sent", "content": { "application/json": { "schema": { "type": "object" } } } }
}
}
}));
paths.insert("/api/auth/magic/verify".into(), json!({
"post": {
"operationId": "verifyMagicCode",
"summary": "Verify a magic login code",
"tags": ["auth"],
"requestBody": {
"required": true,
"content": { "application/json": { "schema": {
"type": "object",
"properties": {
"email": { "type": "string", "format": "email" },
"code": { "type": "string" }
},
"required": ["email", "code"]
}}}
},
"responses": {
"200": { "description": "Verified and session created", "content": { "application/json": { "schema": {
"type": "object",
"properties": {
"token": { "type": "string" },
"user_id": { "type": "string" }
}
}}}},
"401": { "description": "Invalid code", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
}
}
}));
paths.insert("/api/auth/providers".into(), json!({
"get": {
"operationId": "listAuthProviders",
"summary": "List available OAuth providers",
"tags": ["auth"],
"responses": {
"200": { "description": "Provider list", "content": { "application/json": { "schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"provider": { "type": "string" },
"auth_url": { "type": "string" }
}
}
}}}}
}
}
}));
for entity in &manifest.entities {
let entity_lower = entity.name.to_lowercase();
let schema_ref = format!("#/components/schemas/{}", entity.name);
let tag = entity.name.clone();
let entity_schema = build_entity_schema(entity);
schemas.insert(entity.name.clone(), entity_schema);
let collection_path = format!("/api/entities/{entity_lower}");
paths.insert(collection_path, json!({
"get": {
"operationId": format!("list{}", entity.name),
"summary": format!("List all {} entities", entity.name),
"tags": [tag],
"security": [{ "BearerAuth": [] }],
"parameters": [
{ "name": "limit", "in": "query", "schema": { "type": "integer" }, "description": "Maximum number of results" },
{ "name": "offset", "in": "query", "schema": { "type": "integer", "default": 0 }, "description": "Number of results to skip" }
],
"responses": {
"200": { "description": format!("List of {}", entity.name), "content": { "application/json": { "schema": {
"type": "object",
"properties": {
"data": { "type": "array", "items": { "$ref": &schema_ref } },
"total": { "type": "integer" },
"offset": { "type": "integer" },
"limit": { "type": "integer", "nullable": true }
}
}}}}
}
},
"post": {
"operationId": format!("create{}", entity.name),
"summary": format!("Create a new {}", entity.name),
"tags": [tag],
"security": [{ "BearerAuth": [] }],
"requestBody": {
"required": true,
"content": { "application/json": { "schema": { "$ref": &schema_ref } } }
},
"responses": {
"201": { "description": "Created", "content": { "application/json": { "schema": {
"type": "object",
"properties": { "id": { "type": "string" } }
}}}},
"400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
}
}
}));
let item_path = format!("/api/entities/{entity_lower}/{{id}}");
paths.insert(item_path, json!({
"get": {
"operationId": format!("get{}ById", entity.name),
"summary": format!("Get a {} by ID", entity.name),
"tags": [tag],
"security": [{ "BearerAuth": [] }],
"parameters": [
{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
],
"responses": {
"200": { "description": format!("{} found", entity.name), "content": { "application/json": { "schema": { "$ref": &schema_ref } } } },
"404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
}
},
"patch": {
"operationId": format!("update{}", entity.name),
"summary": format!("Update a {}", entity.name),
"tags": [tag],
"security": [{ "BearerAuth": [] }],
"parameters": [
{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
],
"requestBody": {
"required": true,
"content": { "application/json": { "schema": { "$ref": &schema_ref } } }
},
"responses": {
"200": { "description": "Updated", "content": { "application/json": { "schema": { "type": "object", "properties": { "updated": { "type": "boolean" } } } } } },
"404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
}
},
"delete": {
"operationId": format!("delete{}", entity.name),
"summary": format!("Delete a {}", entity.name),
"tags": [tag],
"security": [{ "BearerAuth": [] }],
"parameters": [
{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
],
"responses": {
"200": { "description": "Deleted", "content": { "application/json": { "schema": { "type": "object", "properties": { "deleted": { "type": "boolean" } } } } } },
"404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
}
}
}));
let cursor_path = format!("/api/entities/{entity_lower}/cursor");
paths.insert(cursor_path, json!({
"get": {
"operationId": format!("list{}ByCursor", entity.name),
"summary": format!("Cursor-paginated list of {}", entity.name),
"tags": [tag],
"security": [{ "BearerAuth": [] }],
"parameters": [
{ "name": "after", "in": "query", "schema": { "type": "string" }, "description": "Cursor: ID of the last item from the previous page" },
{ "name": "limit", "in": "query", "schema": { "type": "integer", "default": 20, "maximum": 100 }, "description": "Maximum number of results" }
],
"responses": {
"200": { "description": format!("Paginated {} list", entity.name), "content": { "application/json": { "schema": {
"type": "object",
"properties": {
"data": { "type": "array", "items": { "$ref": &schema_ref } },
"next_cursor": { "type": "string", "nullable": true },
"has_more": { "type": "boolean" }
}
}}}}
}
}
}));
}
for action in &manifest.actions {
let action_lower = action.name.to_lowercase();
let input_schema_name = format!("{}Input", action.name);
let input_schema = build_fields_schema(&action.input);
schemas.insert(input_schema_name.clone(), input_schema);
let path = format!("/api/actions/{action_lower}");
paths.insert(path, json!({
"post": {
"operationId": format!("execute{}", action.name),
"summary": format!("Execute the {} action", action.name),
"tags": ["actions"],
"security": [{ "BearerAuth": [] }],
"requestBody": {
"required": true,
"content": { "application/json": { "schema": { "$ref": format!("#/components/schemas/{input_schema_name}") } } }
},
"responses": {
"200": { "description": "Action executed", "content": { "application/json": { "schema": {
"type": "object",
"properties": {
"action": { "type": "string" },
"input": { "type": "object" },
"executed": { "type": "boolean" }
}
}}}},
"400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
"404": { "description": "Action not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
}
}
}));
}
schemas.insert(
"Error".into(),
json!({
"type": "object",
"properties": {
"error": {
"type": "object",
"properties": {
"code": { "type": "string" },
"message": { "type": "string" },
"hint": { "type": "string" }
},
"required": ["code", "message"]
}
}
}),
);
json!({
"openapi": "3.0.3",
"info": {
"title": manifest.name,
"version": manifest.version,
"description": format!("Auto-generated API documentation for {}", manifest.name)
},
"servers": [{ "url": base_url }],
"paths": Value::Object(paths),
"components": {
"schemas": Value::Object(schemas),
"securitySchemes": {
"BearerAuth": {
"type": "http",
"scheme": "bearer"
}
}
}
})
}
fn map_field_type(field_type: &str) -> Value {
match field_type {
"string" => json!({ "type": "string" }),
"int" => json!({ "type": "integer" }),
"float" => json!({ "type": "number" }),
"bool" => json!({ "type": "boolean" }),
"datetime" => json!({ "type": "string", "format": "date-time" }),
"richtext" => json!({ "type": "string" }),
t if t.starts_with("id(") => json!({ "type": "string" }),
_ => json!({ "type": "string" }),
}
}
fn build_entity_schema(entity: &pylon_kernel::ManifestEntity) -> Value {
build_fields_schema_with_id(&entity.fields)
}
fn build_fields_schema_with_id(fields: &[pylon_kernel::ManifestField]) -> Value {
let mut properties = serde_json::Map::new();
let mut required = vec!["id".to_string()];
properties.insert("id".into(), json!({ "type": "string" }));
for field in fields {
properties.insert(field.name.clone(), map_field_type(&field.field_type));
if !field.optional {
required.push(field.name.clone());
}
}
json!({
"type": "object",
"properties": Value::Object(properties),
"required": required
})
}
fn build_fields_schema(fields: &[pylon_kernel::ManifestField]) -> Value {
let mut properties = serde_json::Map::new();
let mut required = Vec::new();
for field in fields {
properties.insert(field.name.clone(), map_field_type(&field.field_type));
if !field.optional {
required.push(field.name.clone());
}
}
let mut schema = json!({
"type": "object",
"properties": Value::Object(properties)
});
if !required.is_empty() {
schema["required"] = json!(required);
}
schema
}
#[cfg(test)]
mod tests {
use super::*;
use pylon_kernel::{ManifestAction, ManifestEntity, ManifestField, ManifestIndex};
fn sample_manifest() -> AppManifest {
AppManifest {
manifest_version: 1,
name: "TestApp".into(),
version: "0.1.0".into(),
entities: vec![
ManifestEntity {
name: "User".into(),
fields: vec![
ManifestField {
name: "email".into(),
field_type: "string".into(),
optional: false,
unique: true,
crdt: None,
},
ManifestField {
name: "age".into(),
field_type: "int".into(),
optional: true,
unique: false,
crdt: None,
},
ManifestField {
name: "score".into(),
field_type: "float".into(),
optional: true,
unique: false,
crdt: None,
},
ManifestField {
name: "active".into(),
field_type: "bool".into(),
optional: false,
unique: false,
crdt: None,
},
ManifestField {
name: "createdAt".into(),
field_type: "datetime".into(),
optional: true,
unique: false,
crdt: None,
},
ManifestField {
name: "bio".into(),
field_type: "richtext".into(),
optional: true,
unique: false,
crdt: None,
},
],
indexes: vec![ManifestIndex {
name: "email_idx".into(),
fields: vec!["email".into()],
unique: true,
}],
relations: vec![],
search: None,
crdt: true,
},
ManifestEntity {
name: "Post".into(),
fields: vec![
ManifestField {
name: "title".into(),
field_type: "string".into(),
optional: false,
unique: false,
crdt: None,
},
ManifestField {
name: "authorId".into(),
field_type: "id(User)".into(),
optional: false,
unique: false,
crdt: None,
},
],
indexes: vec![],
relations: vec![],
search: None,
crdt: true,
},
],
routes: vec![],
queries: vec![],
actions: vec![ManifestAction {
name: "PublishPost".into(),
input: vec![
ManifestField {
name: "postId".into(),
field_type: "id(Post)".into(),
optional: false,
unique: false,
crdt: None,
},
ManifestField {
name: "notify".into(),
field_type: "bool".into(),
optional: true,
unique: false,
crdt: None,
},
],
}],
policies: vec![],
auth: Default::default(),
}
}
#[test]
fn spec_has_correct_structure() {
let spec = generate_openapi(&sample_manifest(), "http://localhost:3000");
assert_eq!(spec["openapi"], "3.0.3");
assert_eq!(spec["info"]["title"], "TestApp");
assert_eq!(spec["info"]["version"], "0.1.0");
assert!(spec["info"]["description"]
.as_str()
.unwrap()
.contains("TestApp"));
assert_eq!(spec["servers"][0]["url"], "http://localhost:3000");
assert!(spec["paths"].is_object());
assert!(spec["components"]["schemas"].is_object());
assert!(spec["components"]["securitySchemes"]["BearerAuth"].is_object());
}
#[test]
fn spec_is_valid_json() {
let spec = generate_openapi(&sample_manifest(), "/");
let json_str = serde_json::to_string(&spec).unwrap();
let reparsed: Value = serde_json::from_str(&json_str).unwrap();
assert_eq!(spec, reparsed);
}
#[test]
fn entity_paths_generated_for_each_entity() {
let spec = generate_openapi(&sample_manifest(), "/");
let paths = spec["paths"].as_object().unwrap();
assert!(
paths.contains_key("/api/entities/user"),
"missing collection path for User"
);
assert!(
paths.contains_key("/api/entities/user/{id}"),
"missing item path for User"
);
assert!(
paths.contains_key("/api/entities/user/cursor"),
"missing cursor path for User"
);
assert!(
paths.contains_key("/api/entities/post"),
"missing collection path for Post"
);
assert!(
paths.contains_key("/api/entities/post/{id}"),
"missing item path for Post"
);
assert!(
paths.contains_key("/api/entities/post/cursor"),
"missing cursor path for Post"
);
let user_collection = &paths["/api/entities/user"];
assert!(user_collection.get("get").is_some());
assert!(user_collection.get("post").is_some());
let user_item = &paths["/api/entities/user/{id}"];
assert!(user_item.get("get").is_some());
assert!(user_item.get("patch").is_some());
assert!(user_item.get("delete").is_some());
}
#[test]
fn action_paths_generated() {
let spec = generate_openapi(&sample_manifest(), "/");
let paths = spec["paths"].as_object().unwrap();
assert!(
paths.contains_key("/api/actions/publishpost"),
"missing action path"
);
let action_path = &paths["/api/actions/publishpost"];
assert!(action_path.get("post").is_some());
assert_eq!(action_path["post"]["operationId"], "executePublishPost");
}
#[test]
fn action_input_schema_generated() {
let spec = generate_openapi(&sample_manifest(), "/");
let schemas = spec["components"]["schemas"].as_object().unwrap();
assert!(
schemas.contains_key("PublishPostInput"),
"missing action input schema"
);
let input = &schemas["PublishPostInput"];
assert!(input["properties"]["postId"].is_object());
assert!(input["properties"]["notify"].is_object());
let required = input["required"].as_array().unwrap();
assert!(required.contains(&json!("postId")));
assert!(!required.contains(&json!("notify")));
}
#[test]
fn entity_schemas_generated() {
let spec = generate_openapi(&sample_manifest(), "/");
let schemas = spec["components"]["schemas"].as_object().unwrap();
assert!(schemas.contains_key("User"));
assert!(schemas.contains_key("Post"));
let user = &schemas["User"];
assert!(user["properties"]["id"].is_object());
assert!(user["properties"]["email"].is_object());
assert!(user["properties"]["age"].is_object());
}
#[test]
fn field_types_mapped_correctly() {
let spec = generate_openapi(&sample_manifest(), "/");
let user = &spec["components"]["schemas"]["User"];
assert_eq!(user["properties"]["email"]["type"], "string");
assert_eq!(user["properties"]["age"]["type"], "integer");
assert_eq!(user["properties"]["score"]["type"], "number");
assert_eq!(user["properties"]["active"]["type"], "boolean");
assert_eq!(user["properties"]["createdAt"]["type"], "string");
assert_eq!(user["properties"]["createdAt"]["format"], "date-time");
assert_eq!(user["properties"]["bio"]["type"], "string");
let post = &spec["components"]["schemas"]["Post"];
assert_eq!(post["properties"]["authorId"]["type"], "string");
}
#[test]
fn required_fields_in_schema() {
let spec = generate_openapi(&sample_manifest(), "/");
let user = &spec["components"]["schemas"]["User"];
let required = user["required"].as_array().unwrap();
assert!(required.contains(&json!("id")));
assert!(required.contains(&json!("email")));
assert!(required.contains(&json!("active")));
assert!(!required.contains(&json!("age")));
assert!(!required.contains(&json!("score")));
assert!(!required.contains(&json!("createdAt")));
assert!(!required.contains(&json!("bio")));
}
#[test]
fn fixed_paths_present() {
let spec = generate_openapi(&sample_manifest(), "/");
let paths = spec["paths"].as_object().unwrap();
assert!(paths.contains_key("/health"));
assert!(paths.contains_key("/api/manifest"));
assert!(paths.contains_key("/api/query"));
assert!(paths.contains_key("/api/batch"));
assert!(paths.contains_key("/api/transact"));
assert!(paths.contains_key("/api/export"));
assert!(paths.contains_key("/api/rooms"));
assert!(paths.contains_key("/api/rooms/join"));
assert!(paths.contains_key("/api/rooms/leave"));
assert!(paths.contains_key("/api/rooms/presence"));
assert!(paths.contains_key("/api/rooms/broadcast"));
assert!(paths.contains_key("/api/auth/session"));
assert!(paths.contains_key("/api/auth/guest"));
assert!(paths.contains_key("/api/auth/magic/send"));
assert!(paths.contains_key("/api/auth/magic/verify"));
assert!(paths.contains_key("/api/auth/providers"));
}
#[test]
fn empty_manifest_produces_valid_spec() {
let manifest = AppManifest {
manifest_version: 1,
name: "Empty".into(),
version: "0.0.0".into(),
entities: vec![],
routes: vec![],
queries: vec![],
actions: vec![],
policies: vec![],
auth: Default::default(),
};
let spec = generate_openapi(&manifest, "");
assert_eq!(spec["openapi"], "3.0.3");
assert_eq!(spec["info"]["title"], "Empty");
let schemas = spec["components"]["schemas"].as_object().unwrap();
assert!(schemas.contains_key("Error"));
assert_eq!(schemas.len(), 1);
}
}