This commit is contained in:
Sandipsinh Rathod 2024-12-19 19:42:36 -05:00
commit fe3ec74e35
17 changed files with 7309 additions and 0 deletions

1
.gitignore vendored Normal file

@ -0,0 +1 @@
/target

1460
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

22
Cargo.toml Normal file

@ -0,0 +1,22 @@
[package]
name = "foo"
version = "0.1.0"
edition = "2021"
[dependencies]
serde_json = "1.0.133"
serde_yaml = "0.9.34+deprecated"
schemars = "1.0.0-alpha.17"
anyhow = "1.0.94"
convert_case = "0.6.0"
protox = "0.7.1"
protox-parse = "0.7.0"
oapi = "0.1.2"
sppparse = "0.1.4"
oas3 = {version = "0.12.1", features = []}
openapi3-parser = { version = "0.1.0", features = [] }
serde = { version = "1.0.216", features = ["derive"] }
tailcall-valid = "0.1.3"
wit-parser = "0.222.0"
lazy_static = "1.5.0"

37
foo.proto Normal file

@ -0,0 +1,37 @@
syntax = "proto3";
package core.todo.v1;
message TodoListRequest {string user_id = 1;}
message TodoAddRequest {
string user_id = 1;
string task = 2;
}
message TodoDeleteRequest {
string user_id = 1;
string id = 2;
}
message TodoObject {
string user_id = 1;
string id = 2;
string task = 3;
}
message TodoChangeEvent {
string idemponcy_id = 1;
TodoObject current = 3;
TodoObject original = 4;
}
message TodoAddResponse {TodoObject todo = 1;}
message TodoListResponse {repeated TodoObject todos = 1;}
message TodoDeleteResponse {string message = 1;}
service TodoService {
rpc TodoAdd (TodoAddRequest) returns (TodoAddResponse);
rpc TodoDelete (TodoDeleteRequest) returns (TodoDeleteResponse);
rpc TodoList (TodoListRequest) returns (TodoListResponse);
}

89
foo.wit Normal file

@ -0,0 +1,89 @@
package api:todos@1.0.0;
// Common types used across the API
interface types {
// Authentication types derived from OpenAPI security schemes
record bearer-token {
token: string,
}
// Core resource type
record todo {
id: string,
title: string,
description: option<string>,
completed: bool,
due-date: option<string>,
user-id: string,
created-at: string,
updated-at: string,
}
variant error {
unauthorized,
not-found,
validation-error(list<string>),
rate-limited { retry-after: u32 },
server-error,
}
}
interface todos-collection {
use types.{todo, bearer-token, error};
// Request/response types with domain-appropriate fields
record list-request {
auth: bearer-token,
user-id: string,
status: option<string>,
limit: option<u32>,
%offset: option<u32>,
}
record list-response {
items: list<todo>,
total: u32,
limit: u32,
%offset: u32,
version: string, // From ETag
last-updated: option<string>, // From Last-Modified
}
record create-request {
auth: bearer-token,
title: string,
description: option<string>,
due-date: option<string>,
user-id: string,
}
// Note: Response includes versioning info directly in return type
list: func(request: list-request) -> result<list-response, error>;
create: func(request: create-request) -> result<todo, error>;
}
interface todos-resource {
use types.{todo, bearer-token, error};
record update-request {
auth: bearer-token,
title: option<string>,
description: option<string>,
completed: option<bool>,
due-date: option<string>,
expected-version: option<string>, // If-Match header
}
get: func(id: string, auth: bearer-token) -> result<todo, error>;
update: func(id: string, request: update-request) -> result<todo, error>;
delete: func(id: string, auth: bearer-token) -> result<unit, error>;
}
world todos-api {
export todos-collection;
export todos-resource;
}

3190
foo.yml Normal file

File diff suppressed because it is too large Load Diff

552
fooschema.yml Normal file

@ -0,0 +1,552 @@
$defs:
Components:
properties:
examples:
additionalProperties:
$ref: '#/$defs/Example'
type:
- object
- 'null'
headers:
additionalProperties:
$ref: '#/$defs/Header'
type:
- object
- 'null'
parameters:
additionalProperties:
$ref: '#/$defs/Parameter'
type:
- object
- 'null'
requestBodies:
additionalProperties:
$ref: '#/$defs/RequestBody'
type:
- object
- 'null'
responses:
additionalProperties:
$ref: '#/$defs/Response'
type:
- object
- 'null'
schemas:
additionalProperties:
$ref: '#/$defs/Schema'
type:
- object
- 'null'
securitySchemes:
additionalProperties:
$ref: '#/$defs/SecurityScheme'
type:
- object
- 'null'
type: object
Discriminator:
properties:
mapping:
additionalProperties:
type: string
type:
- object
- 'null'
propertyName:
type:
- string
- 'null'
type: object
Example:
properties:
description:
type:
- string
- 'null'
externalValue:
type:
- string
- 'null'
summary:
type:
- string
- 'null'
value: true
type: object
Header:
properties:
description:
type:
- string
- 'null'
example: true
schema:
anyOf:
- $ref: '#/$defs/Schema'
- type: 'null'
type: object
Info:
properties:
description:
type:
- string
- 'null'
title:
type:
- string
- 'null'
version:
type:
- string
- 'null'
type: object
MediaType:
properties:
example: true
examples:
additionalProperties:
$ref: '#/$defs/Example'
type:
- object
- 'null'
schema:
anyOf:
- $ref: '#/$defs/Schema'
- type: 'null'
type: object
OAuthFlow:
properties:
authorizationUrl:
type:
- string
- 'null'
refreshUrl:
type:
- string
- 'null'
scopes:
additionalProperties:
type: string
type:
- object
- 'null'
tokenUrl:
type:
- string
- 'null'
type: object
OAuthFlows:
properties:
authorizationCode:
anyOf:
- $ref: '#/$defs/OAuthFlow'
- type: 'null'
clientCredentials:
anyOf:
- $ref: '#/$defs/OAuthFlow'
- type: 'null'
implicit:
anyOf:
- $ref: '#/$defs/OAuthFlow'
- type: 'null'
password:
anyOf:
- $ref: '#/$defs/OAuthFlow'
- type: 'null'
type: object
Operation:
properties:
description:
type:
- string
- 'null'
operationId:
type:
- string
- 'null'
parameters:
items:
$ref: '#/$defs/Parameter'
type:
- array
- 'null'
requestBody:
anyOf:
- $ref: '#/$defs/RequestBody'
- type: 'null'
responses:
anyOf:
- $ref: '#/$defs/Responses'
- type: 'null'
summary:
type:
- string
- 'null'
tags:
items:
type: string
type:
- array
- 'null'
type: object
Parameter:
properties:
description:
type:
- string
- 'null'
example: true
examples:
additionalProperties:
$ref: '#/$defs/Example'
type:
- object
- 'null'
in:
type:
- string
- 'null'
name:
type:
- string
- 'null'
required:
type:
- boolean
- 'null'
schema:
anyOf:
- $ref: '#/$defs/Schema'
- type: 'null'
type: object
PathItem:
properties:
delete:
anyOf:
- $ref: '#/$defs/Operation'
- type: 'null'
get:
anyOf:
- $ref: '#/$defs/Operation'
- type: 'null'
head:
anyOf:
- $ref: '#/$defs/Operation'
- type: 'null'
options:
anyOf:
- $ref: '#/$defs/Operation'
- type: 'null'
patch:
anyOf:
- $ref: '#/$defs/Operation'
- type: 'null'
post:
anyOf:
- $ref: '#/$defs/Operation'
- type: 'null'
put:
anyOf:
- $ref: '#/$defs/Operation'
- type: 'null'
trace:
anyOf:
- $ref: '#/$defs/Operation'
- type: 'null'
type: object
RequestBody:
properties:
content:
additionalProperties:
$ref: '#/$defs/MediaType'
type:
- object
- 'null'
description:
type:
- string
- 'null'
required:
type:
- boolean
- 'null'
type: object
Response:
properties:
content:
additionalProperties:
$ref: '#/$defs/MediaType'
type:
- object
- 'null'
description:
type:
- string
- 'null'
headers:
additionalProperties:
$ref: '#/$defs/Header'
type:
- object
- 'null'
type: object
Responses:
additionalProperties:
$ref: '#/$defs/Response'
properties:
default:
anyOf:
- $ref: '#/$defs/Response'
- type: 'null'
type: object
Schema:
properties:
additionalProperties:
anyOf:
- $ref: '#/$defs/Schema'
- type: 'null'
allOf:
items:
$ref: '#/$defs/Schema'
type:
- array
- 'null'
anyOf:
items:
$ref: '#/$defs/Schema'
type:
- array
- 'null'
default: true
deprecated:
type:
- boolean
- 'null'
description:
type:
- string
- 'null'
discriminator:
anyOf:
- $ref: '#/$defs/Discriminator'
- type: 'null'
example: true
exclusiveMaximum:
type:
- boolean
- 'null'
exclusiveMinimum:
type:
- boolean
- 'null'
format:
type:
- string
- 'null'
items:
anyOf:
- $ref: '#/$defs/Schema'
- type: 'null'
maxItems:
format: uint32
minimum: 0
type:
- integer
- 'null'
maxLength:
format: uint32
minimum: 0
type:
- integer
- 'null'
maxProperties:
format: uint32
minimum: 0
type:
- integer
- 'null'
maximum:
format: double
type:
- number
- 'null'
minItems:
format: uint32
minimum: 0
type:
- integer
- 'null'
minLength:
format: uint32
minimum: 0
type:
- integer
- 'null'
minProperties:
format: uint32
minimum: 0
type:
- integer
- 'null'
minimum:
format: double
type:
- number
- 'null'
multipleOf:
format: double
type:
- number
- 'null'
not:
anyOf:
- $ref: '#/$defs/Schema'
- type: 'null'
nullable:
type:
- boolean
- 'null'
oneOf:
items:
$ref: '#/$defs/Schema'
type:
- array
- 'null'
pattern:
type:
- string
- 'null'
properties:
additionalProperties:
$ref: '#/$defs/Schema'
type:
- object
- 'null'
readOnly:
type:
- boolean
- 'null'
required:
items:
type: string
type:
- array
- 'null'
title:
type:
- string
- 'null'
type:
type:
- string
- 'null'
uniqueItems:
type:
- boolean
- 'null'
writeOnly:
type:
- boolean
- 'null'
xml:
anyOf:
- $ref: '#/$defs/XML'
- type: 'null'
type: object
SecurityScheme:
properties:
bearerFormat:
type:
- string
- 'null'
description:
type:
- string
- 'null'
flows:
anyOf:
- $ref: '#/$defs/OAuthFlows'
- type: 'null'
in:
type:
- string
- 'null'
name:
type:
- string
- 'null'
openIdConnectUrl:
type:
- string
- 'null'
scheme:
type:
- string
- 'null'
type:
type:
- string
- 'null'
type: object
Server:
properties:
description:
type:
- string
- 'null'
url:
type:
- string
- 'null'
type: object
XML:
properties:
attribute:
type:
- boolean
- 'null'
name:
type:
- string
- 'null'
namespace:
type:
- string
- 'null'
prefix:
type:
- string
- 'null'
wrapped:
type:
- boolean
- 'null'
type: object
$schema: https://json-schema.org/draft/2020-12/schema
properties:
components:
anyOf:
- $ref: '#/$defs/Components'
- type: 'null'
info:
anyOf:
- $ref: '#/$defs/Info'
- type: 'null'
openapi:
type:
- string
- 'null'
paths:
additionalProperties:
$ref: '#/$defs/PathItem'
type:
- object
- 'null'
servers:
items:
$ref: '#/$defs/Server'
type:
- array
- 'null'
title: OpenApiSpec
type: object

441
openapi.yaml Normal file

@ -0,0 +1,441 @@
openapi: '3.0.3'
info:
title: Todo REST API
version: '1.0.0'
description: A RESTful API for managing todo items
servers:
- url: /api/v1
description: Base API path
components:
schemas:
Todo:
type: object
properties:
id:
type: string
format: uuid
readOnly: true
title:
type: string
minLength: 1
maxLength: 200
description:
type: string
maxLength: 2000
completed:
type: boolean
default: false
dueDate:
type: string
format: date-time
userId:
type: string
createdAt:
type: string
format: date-time
readOnly: true
updatedAt:
type: string
format: date-time
readOnly: true
required:
- id
- title
- completed
- userId
- createdAt
- updatedAt
TodoCreate:
type: object
properties:
title:
type: string
minLength: 1
maxLength: 200
description:
type: string
maxLength: 2000
dueDate:
type: string
format: date-time
userId:
type: string
required:
- title
- userId
TodoUpdate:
type: object
properties:
title:
type: string
minLength: 1
maxLength: 200
description:
type: string
maxLength: 2000
completed:
type: boolean
dueDate:
type: string
format: date-time
minProperties: 1
TodoList:
type: object
properties:
data:
type: string
# items:
# $ref: '#/components/schemas/Todo'
metadata:
type: string
# properties:
# total:
# type: integer
# minimum: 0
# limit:
# type: integer
# minimum: 1
# offset:
# type: integer
# minimum: 0
# required:
# - total
# - limit
# - offset
required:
- data
- metadata
TodoResponse:
type: object
properties:
data:
type: string
# $ref: '#/components/schemas/Todo'
required:
- data
Error:
type: object
properties:
error:
type: string
# properties:
# code:
# type: string
# enum:
# - VALIDATION_ERROR
# - UNAUTHORIZED
# - FORBIDDEN
# - NOT_FOUND
# - RATE_LIMIT_EXCEEDED
# - INTERNAL_ERROR
# message:
# type: string
# details:
# type: array
# items:
# type: object
# properties:
# field:
# type: string
# message:
# type: string
# required:
# - field
# - message
# required:
# - code
# - message
parameters:
TodoId:
name: todoId
in: path
required: true
schema:
type: string
format: uuid
UserId:
name: userId
in: query
required: true
schema:
type: string
Status:
name: status
in: query
schema:
type: string
enum: [active, completed]
Limit:
name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 50
Offset:
name: offset
in: query
schema:
type: integer
minimum: 0
default: 0
SortBy:
name: sortBy
in: query
schema:
type: string
enum: [createdAt, dueDate]
default: createdAt
SortOrder:
name: sortOrder
in: query
schema:
type: string
enum: [asc, desc]
default: desc
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- bearerAuth: []
paths:
/todos:
get:
summary: List todos
parameters:
- $ref: '#/components/parameters/UserId'
- $ref: '#/components/parameters/Status'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Offset'
- $ref: '#/components/parameters/SortBy'
- $ref: '#/components/parameters/SortOrder'
responses:
'200':
description: Successfully retrieved todos
headers:
ETag:
schema:
type: string
Last-Modified:
schema:
type: string
X-Request-ID:
schema:
type: string
X-RateLimit-Limit:
schema:
type: integer
X-RateLimit-Remaining:
schema:
type: integer
X-RateLimit-Reset:
schema:
type: integer
content:
application/json:
schema:
$ref: '#/components/schemas/TodoList'
'400':
description: Invalid parameters
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: Too many requests
headers:
Retry-After:
schema:
type: integer
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
post:
summary: Create a new todo
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TodoCreate'
responses:
'201':
description: Todo created successfully
headers:
Location:
schema:
type: string
format: uri
ETag:
schema:
type: string
X-Request-ID:
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/TodoResponse'
'400':
description: Invalid input
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/todos/{todoId}:
parameters:
- $ref: '#/components/parameters/TodoId'
get:
summary: Get a specific todo
responses:
'200':
description: Successfully retrieved todo
headers:
ETag:
schema:
type: string
Last-Modified:
schema:
type: string
X-Request-ID:
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/TodoResponse'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: Todo not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
patch:
summary: Update a todo
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TodoUpdate'
responses:
'200':
description: Todo updated successfully
headers:
ETag:
schema:
type: string
X-Request-ID:
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/TodoResponse'
'400':
description: Invalid input
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: Todo not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
delete:
summary: Delete a todo
responses:
'204':
description: Todo deleted successfully
headers:
X-Request-ID:
schema:
type: string
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: Todo not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'

60
src/config/config.rs Normal file

@ -0,0 +1,60 @@
use crate::config::wit_types::WitType;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Config {
pub package: String,
pub interfaces: Vec<Interface>,
pub world: World,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct World {
pub name: String,
pub uses: Vec<UseStatement>,
pub imports: Vec<Interface>,
pub exports: Vec<Interface>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Interface {
pub name: String,
pub records: Vec<Record>,
pub uses: Vec<UseStatement>,
pub functions: Vec<Function>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Record {
pub name: String,
pub fields: Vec<Field>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct UseStatement {
pub name: String,
pub items: Vec<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Function {
pub name: String,
pub parameters: Vec<Parameter>,
pub return_type: ReturnTy,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ReturnTy {
pub return_type: String,
pub error_type: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Parameter {
pub name: String,
pub parameter_type: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Field {
pub name: String,
pub field_type: WitType,
}

3
src/config/mod.rs Normal file

@ -0,0 +1,3 @@
pub mod config;
pub mod wit_types;
pub mod to_wit;

280
src/config/to_wit.rs Normal file

@ -0,0 +1,280 @@
use crate::config::config::{Config, Field, Function, Interface, Parameter, Record, ReturnTy, UseStatement, World};
use crate::config::wit_types::WitType;
use convert_case::{Case, Casing};
use std::collections::HashSet;
pub trait ToWit {
fn to_wit(&self) -> String;
}
lazy_static::lazy_static! {
static ref RESERVED_WORDS: HashSet<&'static str> = {
let mut set = HashSet::new();
set.insert("type");
set.insert("record");
set.insert("interface");
set.insert("world");
set.insert("use");
set.insert("import");
set.insert("export");
// Add more reserved words if necessary
set
};
}
/// Generate a WIT-compatible name by applying kebab-case, truncation, and reserved word handling.
fn generate_wit_name(name: &str) -> String {
let kebab_case = name.to_case(Case::Kebab);
// Prefix reserved words with %
let mut final_name = if RESERVED_WORDS.contains(kebab_case.as_str()) {
format!("%{}", kebab_case)
} else {
kebab_case
};
// Truncate to 64 characters
if final_name.len() > 64 {
final_name.truncate(64);
}
final_name
}
impl ToWit for Config {
fn to_wit(&self) -> String {
let package = format!("package {};\n", generate_wit_name(&self.package));
let world = self.world.to_wit();
let interfaces = self
.interfaces
.iter()
.map(|interface| interface.to_wit())
.collect::<Vec<String>>()
.join("\n\n");
format!("{}\n{}\n{}", package, interfaces, world)
}
}
impl ToWit for World {
fn to_wit(&self) -> String {
if self == &World::default() {
return String::new();
}
let uses = self
.uses
.iter()
.map(|use_statement| use_statement.to_wit())
.collect::<Vec<String>>()
.join("\n");
let imports = self
.imports
.iter()
.map(|interface| format!("import {};", generate_wit_name(&interface.name)))
.collect::<Vec<String>>()
.join("\n");
let exports = self
.exports
.iter()
.map(|interface| format!("export {};", generate_wit_name(&interface.name)))
.collect::<Vec<String>>()
.join("\n");
format!(
"world {} {{\n{}\n{}\n{}\n}}",
generate_wit_name(&self.name),
uses,
imports,
exports
)
}
}
impl ToWit for Interface {
fn to_wit(&self) -> String {
let uses = self
.uses
.iter()
.map(|use_statement| use_statement.to_wit())
.collect::<Vec<String>>()
.join("\n ");
let records = self
.records
.iter()
.map(|record| record.to_wit())
.collect::<Vec<String>>()
.join("\n ");
let functions = self
.functions
.iter()
.map(|function| function.to_wit())
.collect::<Vec<String>>()
.join("\n ");
format!(
"interface {} {{\n {}\n {}\n {}\n}}",
generate_wit_name(&self.name),
uses,
records,
functions
)
}
}
impl ToWit for Record {
fn to_wit(&self) -> String {
let fields = self
.fields
.iter()
.map(|field| field.to_wit())
.collect::<Vec<String>>()
.join(", ");
format!("record {} {{ {} }}", generate_wit_name(&self.name), fields)
}
}
impl ToWit for Field {
fn to_wit(&self) -> String {
format!(
"{}: {}",
generate_wit_name(&self.name),
self.field_type.to_wit()
)
}
}
impl ToWit for Function {
fn to_wit(&self) -> String {
let params = self
.parameters
.iter()
.map(|param| param.to_wit())
.collect::<Vec<String>>()
.join(", ");
let return_type = if self.return_type.return_type.is_empty() {
String::new()
} else {
format!(" -> {}", self.return_type.to_wit())
};
format!(
"func {}({}){}",
generate_wit_name(&self.name),
params,
return_type
)
}
}
impl ToWit for ReturnTy {
fn to_wit(&self) -> String {
if self.error_type.is_empty() {
self.return_type.clone()
} else {
format!(
"result<{}, {}>",
self.return_type,
self.error_type
)
}
}
}
impl ToWit for Parameter {
fn to_wit(&self) -> String {
format!(
"{}: {}",
generate_wit_name(&self.name),
generate_wit_name(&self.parameter_type)
)
}
}
impl ToWit for UseStatement {
fn to_wit(&self) -> String {
format!(
"use {}.{{{}}};",
generate_wit_name(&self.name),
self.items
.iter()
.map(|item| generate_wit_name(item))
.collect::<Vec<String>>()
.join(", ")
)
}
}
impl ToWit for WitType {
fn to_wit(&self) -> String {
match self {
WitType::Bool => "bool".to_string(),
WitType::U8 => "u8".to_string(),
WitType::U16 => "u16".to_string(),
WitType::U32 => "u32".to_string(),
WitType::U64 => "u64".to_string(),
WitType::S8 => "s8".to_string(),
WitType::S16 => "s16".to_string(),
WitType::S32 => "s32".to_string(),
WitType::S64 => "s64".to_string(),
WitType::Float32 => "float32".to_string(),
WitType::Float64 => "float64".to_string(),
WitType::Char => "char".to_string(),
WitType::String => "string".to_string(),
WitType::Option(inner) => format!("option<{}>", inner.to_wit()),
WitType::Result(ok, err) => format!("result<{}, {}>", ok.to_wit(), err.to_wit()),
WitType::List(inner) => format!("list<{}>", inner.to_wit()),
WitType::Tuple(elements) => format!(
"tuple<{}>",
elements.iter().map(|e| e.to_wit()).collect::<Vec<_>>().join(", ")
),
WitType::Record(fields) => format!(
"{{ {} }}",
fields
.iter()
.map(|(name, ty)| format!("{}: {}", generate_wit_name(name), ty))
.collect::<Vec<_>>()
.join(", ")
),
WitType::Variant(variants) => format!(
"variant {{ {} }}",
variants
.iter()
.map(|(name, ty)| {
if let Some(ty) = ty {
format!("{}: {}", generate_wit_name(name), ty.to_wit())
} else {
generate_wit_name(name)
}
})
.collect::<Vec<_>>()
.join(", ")
),
WitType::Enum(variants) => format!(
"enum {{ {} }}",
variants
.iter()
.map(|variant| generate_wit_name(variant))
.collect::<Vec<_>>()
.join(", ")
),
WitType::Flags(flags) => format!(
"flags {{ {} }}",
flags
.iter()
.map(|flag| generate_wit_name(flag))
.collect::<Vec<_>>()
.join(", ")
),
WitType::Handle(name) => format!("handle<{}>", generate_wit_name(name)),
WitType::TypeAlias(name, inner) => format!("type {} = {}", generate_wit_name(name), inner.to_wit()),
}
}
}

183
src/config/wit_types.rs Normal file

@ -0,0 +1,183 @@
use tailcall_valid::{Valid, Validator};
use crate::config::to_wit::ToWit;
use crate::ser::{OpenApiSpec, Schema};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum WitType {
// Primitive Types
Bool,
U8,
U16,
U32,
U64,
S8,
S16,
S32,
S64,
Float32,
Float64,
Char,
#[default] // TODO: maybe drop the default
String,
// Compound Types
Option(Box<WitType>), // Option<T>
Result(Box<WitType>, Box<WitType>), // Result<T, E>
List(Box<WitType>), // List<T>
Tuple(Vec<WitType>), // (T1, T2, ...)
// Custom Types
Record(Vec<(String, String)>), // Record { field_name: Type }
Variant(Vec<(String, Option<WitType>)>), // Variant { name: Option<Type> }
Enum(Vec<String>), // Enum { name1, name2, ... }
Flags(Vec<String>), // Flags { flag1, flag2, ... }
// Special Types
Handle(String), // Handle<Resource>
TypeAlias(String, Box<WitType>), // TypeAlias { alias_name, type }
}
impl WitType {
pub fn from_schema(
schema: &Schema,
openapi: &OpenApiSpec,
) -> Valid<WitType, anyhow::Error, anyhow::Error> {
if let Some(reference) = &schema.ref_ {
return Valid::from_option(
openapi.resolve_ref(reference),
anyhow::anyhow!("Failed to resolve reference: {}", reference),
)
.and_then(|resolved_schema| WitType::from_schema(resolved_schema, openapi));
}
Valid::from_option(schema.type_.as_ref(), anyhow::anyhow!("SchemaType is required")).and_then(|ty| {
match ty.as_str() {
"bool" | "boolean" => Valid::succeed(WitType::Bool),
"integer" => Valid::succeed(WitType::S32),
"number" => Valid::succeed(WitType::Float64),
"string" => Valid::succeed(WitType::String),
"array" => {
Valid::from_option(schema.items.as_ref(), anyhow::anyhow!("Items are required"))
.and_then(|items| WitType::from_schema(items, openapi))
.map(|items_ty| WitType::List(Box::new(items_ty)))
}
"object" => {
Valid::from_option(schema.properties.as_ref(), anyhow::anyhow!("Properties are required"))
.and_then(|properties| {
Valid::from_iter(properties.iter(), |(name, schema)| {
Valid::from(WitType::from_schema(schema, openapi)).map(|ty| (name.clone(), ty.to_wit()))
})
}
)
.map(|fields| WitType::Record(fields))
}
_ => Valid::fail(anyhow::anyhow!("Unknown type: {}", ty)),
}
})
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use crate::ser::Components;
use super::*;
#[test]
fn test_ref_resolution() {
let mut schemas = HashMap::new();
schemas.insert(
"ReferencedSchema".to_string(),
Schema {
type_: Some("string".to_string()),
..Default::default()
},
);
let openapi = OpenApiSpec {
components: Some(Components {
schemas: Some(schemas),
..Default::default()
}),
..Default::default()
};
let schema = Schema {
ref_: Some("#/components/schemas/ReferencedSchema".to_string()),
..Default::default()
};
let result = WitType::from_schema(&schema, &openapi).to_result().unwrap();
assert_eq!(result, WitType::String);
}
#[test]
fn test_array_with_ref() {
let mut schemas = HashMap::new();
schemas.insert(
"ReferencedSchema".to_string(),
Schema {
type_: Some("string".to_string()),
..Default::default()
},
);
let openapi = OpenApiSpec {
components: Some(Components {
schemas: Some(schemas),
..Default::default()
}),
..Default::default()
};
let schema = Schema {
type_: Some("array".to_string()),
items: Some(Box::new(Schema {
ref_: Some("#/components/schemas/ReferencedSchema".to_string()),
..Default::default()
})),
..Default::default()
};
let result = WitType::from_schema(&schema, &openapi).to_result().unwrap();
assert_eq!(result, WitType::List(Box::new(WitType::String)));
}
#[test]
fn test_object_with_properties() {
let properties = vec![
(
"id".to_string(),
Schema {
type_: Some("integer".to_string()),
..Default::default()
},
),
(
"name".to_string(),
Schema {
type_: Some("string".to_string()),
..Default::default()
},
),
];
let schema = Schema {
type_: Some("object".to_string()),
properties: Some(properties.into_iter().collect()),
..Default::default()
};
let openapi = OpenApiSpec::default();
let result = WitType::from_schema(&schema, &openapi).to_result().unwrap();
assert_eq!(
result,
WitType::Record(vec![
("id".to_string(), WitType::S32.to_wit()),
("name".to_string(), WitType::String.to_wit()),
])
);
}
}

134
src/main.rs Normal file

@ -0,0 +1,134 @@
mod value;
mod proto;
mod ser;
mod config;
use serde_json::{Map, Value};
use anyhow::{Result, bail};
use convert_case::{Casing, Case};
use crate::value::value;
#[derive(Debug, Clone)]
pub struct WIPValue(Value);
impl WIPValue {
pub fn to_wip(&self) -> Result<String> {
let value = &self.0;
match value {
Value::Object(map) => {
let mut wip = String::new();
wip.push_str("package api:todos@1.0.0;\n\n");
if let Some(components) = map.get("components") {
if let Some(schemas) = components.get("schemas") {
if let Value::Object(schemas_map) = schemas {
wip.push_str("interface types {\n");
for (name, schema) in schemas_map {
let record = Self::process_schema(name, schema, schemas_map)?;
wip.push_str(&record);
}
wip.push_str("}\n\n");
}
}
}
if let Some(paths) = map.get("paths") {
if let Value::Object(paths_map) = paths {
for (path, methods) in paths_map {
let interfaces = Self::process_paths(path, methods)?;
wip.push_str(&interfaces);
}
}
}
wip.push_str("world todos-api {\n");
wip.push_str(" export todos-collection;\n");
wip.push_str(" export todos-resource;\n");
wip.push_str("}\n");
Ok(wip)
}
_ => bail!("Root JSON value must be an object."),
}
}
fn process_schema(name: &str, schema: &Value, schemas_map: &Map<String, Value>) -> Result<String> {
if let Value::Object(schema_map) = schema {
let mut record = format!(" record {} {{\n", name.from_case(Case::Camel).to_case(Case::Kebab));
if let Some(properties) = schema_map.get("properties") {
if let Value::Object(properties_map) = properties {
for (field, field_schema) in properties_map {
let field_type = match field_schema.get("$ref") {
Some(Value::String(ref_path)) => {
let referenced_name = ref_path.split('/').last().unwrap();
referenced_name.from_case(Case::Camel).to_case(Case::Kebab)
}
_ => Self::json_to_wip_type(field_schema)?.to_string(),
};
record.push_str(&format!(" {}: {},\n", field.replace('-', "_"), field_type));
}
}
}
record.push_str(" }\n");
return Ok(record);
}
bail!("Invalid schema format.")
}
fn process_paths(path: &str, methods: &Value) -> Result<String> {
let mut interfaces = String::new();
if let Value::Object(methods_map) = methods {
for (method, details) in methods_map {
let interface_name = format!("{}-{}", path.split('/').filter(|v| v.is_empty()).collect::<Vec<_>>().join("-"), method).from_case(Case::Camel).to_case(Case::Kebab);
interfaces.push_str(&format!("interface {} {{\n", interface_name));
if let Value::Object(details_map) = details {
if let Some(parameters) = details_map.get("parameters") {
if let Value::Array(params_array) = parameters {
for param in params_array {
if let Value::Object(param_map) = param {
let param_name = param_map.get("name").and_then(Value::as_str).unwrap_or("unknown");
let param_type = param_map.get("schema").map(|schema| Self::json_to_wip_type(schema).unwrap_or("unknown")).unwrap_or("unknown");
interfaces.push_str(&format!(" {}: {},\n", param_name.replace('-', "_"), param_type));
}
}
}
}
}
interfaces.push_str("}\n\n");
}
}
Ok(interfaces)
}
fn json_to_wip_type(value: &Value) -> Result<&'static str> {
match value {
Value::String(_) => Ok("string"),
Value::Number(num) => {
if num.is_i64() {
Ok("s64")
} else if num.is_u64() {
Ok("u64")
} else {
Ok("float64")
}
}
Value::Bool(_) => Ok("bool"),
Value::Array(_) => Ok("list<any>"),
Value::Object(_) => Ok("record"),
Value::Null => Ok("option<any>"),
}
}
}
// Example usage
fn main() -> Result<()> {
let json_schema = value();
let wip_value = WIPValue(json_schema);
let wip_string = wip_value.to_wip()?;
println!("{}", wip_string);
Ok(())
}

84
src/proto.rs Normal file

@ -0,0 +1,84 @@
use anyhow::{anyhow, Error};
use tailcall_valid::{Valid, Validator};
use crate::config::config::{Config, Field, Interface, Record};
use crate::config::wit_types::WitType;
use crate::ser::OpenApiSpec;
fn handle_types(config: Config, spec: &OpenApiSpec) -> Valid<Config, Error, Error> {
Valid::succeed(config).and_then(|mut config: Config| {
Valid::from_option(spec.components.as_ref(), anyhow!("Components are required"))
.and_then(|v| Valid::from_option(v.schemas.as_ref(), anyhow!("Schemas are required")))
.and_then(|schemas| {
Valid::from_iter(schemas.iter(), |(record_name, v)| {
Valid::from_option(v.type_.as_ref(), anyhow!("Type is required"))
.and_then(|type_| {
Valid::from_option(v.properties.as_ref(), anyhow!("Properties are required"))
.and_then(|a| {
Valid::from_iter(a, |(field_name, v)| {
Valid::from(WitType::from_schema(v, spec))
.and_then(|wit_type| {
let field = Field {
name: field_name.clone(),
field_type: wit_type,
};
Valid::succeed(field)
})
})
})
.and_then(|fields| {
let record = Record {
name: record_name.clone(),
fields,
};
Valid::succeed(record)
})
})
}).and_then(|records| {
let interface = Interface {
name: "types".to_string(),
records,
..Default::default()
};
config.interfaces.push(interface);
Valid::succeed(())
})
}).and_then(|_| Valid::succeed(config))
})
}
fn to_config(spec: OpenApiSpec) -> Valid<Config, Error, Error> {
Valid::succeed(Config::default())
.and_then(|config| handle_types(config, &spec))
}
#[cfg(test)]
mod t {
use tailcall_valid::Validator;
use wit_parser::Resolve;
use crate::proto::to_config;
use crate::ser::OpenApiSpec;
use crate::config::to_wit::ToWit;
#[test]
fn tp() {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/openapi.yaml");
let content = std::fs::read_to_string(path).unwrap();
let y: OpenApiSpec = serde_yaml::from_str(&content).unwrap();
let mut res = to_config(y).to_result();
match res.as_mut() {
Ok(v) => {
v.package = "api:todos@1.0.0".to_string();
println!("{}", v.to_wit());
let mut resolve = Resolve::new();
resolve.push_str("foox.wit", &v.to_wit()).expect("TODO: panic message`");
println!("{:#?}", resolve);
}
Err(e) => {
println!("err: {:?}", e);
}
}
}
}

231
src/ser.rs Normal file

@ -0,0 +1,231 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use schemars::JsonSchema;
#[derive(Serialize, Clone, Deserialize, Debug, JsonSchema, Default)]
#[serde(rename_all = "camelCase")]
pub struct OpenApiSpec {
pub openapi: Option<String>,
pub info: Option<Info>,
pub paths: Option<HashMap<String, PathItem>>,
pub components: Option<Components>,
pub servers: Option<Vec<Server>>,
}
impl OpenApiSpec {
pub fn resolve_ref(&self, reference: &str) -> Option<&Schema> {
if let Some(components) = &self.components {
if reference.starts_with("#/components/schemas/") {
let name = reference.trim_start_matches("#/components/schemas/");
return components.schemas.as_ref()?.get(name);
}
}
None
}
}
#[derive(Serialize, Clone, Deserialize, Debug, JsonSchema, Default)]
#[serde(rename_all = "camelCase")]
pub struct Info {
pub title: Option<String>,
pub description: Option<String>,
pub version: Option<String>,
}
#[derive(Serialize, Clone, Deserialize, Debug, JsonSchema, Default)]
#[serde(rename_all = "camelCase")]
pub struct Server {
pub url: Option<String>,
pub description: Option<String>,
}
#[derive(Serialize, Clone, Deserialize, Debug, JsonSchema, Default)]
#[serde(rename_all = "camelCase")]
pub struct PathItem {
pub get: Option<Operation>,
pub post: Option<Operation>,
pub put: Option<Operation>,
pub delete: Option<Operation>,
pub patch: Option<Operation>,
pub options: Option<Operation>,
pub head: Option<Operation>,
pub trace: Option<Operation>,
}
#[derive(Serialize, Clone, Deserialize, Debug, JsonSchema, Default)]
#[serde(rename_all = "camelCase")]
pub struct Operation {
pub tags: Option<Vec<String>>,
pub summary: Option<String>,
pub description: Option<String>,
pub operation_id: Option<String>,
pub parameters: Option<Vec<Parameter>>,
pub request_body: Option<RequestBody>,
pub responses: Option<Responses>,
}
#[derive(Serialize, Clone, Deserialize, Debug, JsonSchema, Default)]
#[serde(rename_all = "camelCase")]
pub struct Parameter {
pub name: Option<String>,
#[serde(rename = "in")] // in is a reserved keyword in Rust
pub in_: Option<String>,
pub required: Option<bool>,
pub schema: Option<Schema>,
pub description: Option<String>,
pub example: Option<serde_json::Value>,
pub examples: Option<HashMap<String, Example>>,
}
#[derive(Serialize, Clone, Deserialize, Debug, JsonSchema, Default)]
#[serde(rename_all = "camelCase")]
pub struct RequestBody {
pub content: Option<HashMap<String, MediaType>>,
pub description: Option<String>,
pub required: Option<bool>,
}
#[derive(Serialize, Clone, Deserialize, Debug, JsonSchema, Default)]
#[serde(rename_all = "camelCase")]
pub struct MediaType {
pub schema: Option<Schema>,
pub example: Option<serde_json::Value>,
pub examples: Option<HashMap<String, Example>>,
}
#[derive(Serialize, Clone, Deserialize, Debug, JsonSchema, Default)]
#[serde(rename_all = "camelCase")]
pub struct Responses {
#[serde(flatten)]
pub responses: Option<HashMap<String, Response>>,
pub default: Option<Response>,
}
#[derive(Serialize, Clone, Deserialize, Debug, JsonSchema, Default)]
#[serde(rename_all = "camelCase")]
pub struct Response {
pub description: Option<String>,
pub content: Option<HashMap<String, MediaType>>,
pub headers: Option<HashMap<String, Header>>,
}
#[derive(Serialize, Clone, Deserialize, Debug, JsonSchema, Default)]
#[serde(rename_all = "camelCase")]
pub struct Header {
pub description: Option<String>,
pub schema: Option<Schema>,
pub example: Option<serde_json::Value>,
}
#[derive(Serialize, Clone, Deserialize, Debug, JsonSchema, Default)]
#[serde(rename_all = "camelCase")]
pub struct Components {
pub schemas: Option<HashMap<String, Schema>>,
pub responses: Option<HashMap<String, Response>>,
pub parameters: Option<HashMap<String, Parameter>>,
pub examples: Option<HashMap<String, Example>>,
pub request_bodies: Option<HashMap<String, RequestBody>>,
pub headers: Option<HashMap<String, Header>>,
pub security_schemes: Option<HashMap<String, SecurityScheme>>,
}
#[derive(Serialize, Clone, Deserialize, Debug, JsonSchema, Default)]
#[serde(rename_all = "camelCase")]
pub struct Example {
pub summary: Option<String>,
pub description: Option<String>,
pub value: Option<serde_json::Value>,
pub external_value: Option<String>,
}
#[derive(Serialize, Clone, Deserialize, Debug, JsonSchema, Default)]
#[serde(rename_all = "camelCase")]
pub struct Schema {
#[serde(rename = "$ref")]
pub ref_: Option<String>,
pub title: Option<String>,
pub multiple_of: Option<f64>,
pub maximum: Option<f64>,
pub exclusive_maximum: Option<bool>,
pub minimum: Option<f64>,
pub exclusive_minimum: Option<bool>,
pub max_length: Option<u32>,
pub min_length: Option<u32>,
pub pattern: Option<String>,
pub max_items: Option<u32>,
pub min_items: Option<u32>,
pub unique_items: Option<bool>,
pub max_properties: Option<u32>,
pub min_properties: Option<u32>,
pub required: Option<Vec<String>>,
#[serde(rename = "type")]
pub type_: Option<String>,
pub not: Option<Box<Schema>>,
pub all_of: Option<Vec<Schema>>,
pub one_of: Option<Vec<Schema>>,
pub any_of: Option<Vec<Schema>>,
pub items: Option<Box<Schema>>,
pub properties: Option<HashMap<String, Schema>>,
pub additional_properties: Option<Box<Schema>>,
pub description: Option<String>,
pub format: Option<String>,
pub default: Option<serde_json::Value>,
pub nullable: Option<bool>,
pub discriminator: Option<Discriminator>,
pub read_only: Option<bool>,
pub write_only: Option<bool>,
pub xml: Option<XML>,
pub example: Option<serde_json::Value>,
pub deprecated: Option<bool>,
}
#[derive(Serialize, Clone, Deserialize, Debug, JsonSchema, Default)]
#[serde(rename_all = "camelCase")]
pub struct Discriminator {
pub property_name: Option<String>,
pub mapping: Option<HashMap<String, String>>,
}
#[derive(Serialize, Clone, Deserialize, Debug, JsonSchema, Default)]
#[serde(rename_all = "camelCase")]
pub struct SecurityScheme {
#[serde(rename = "type")]
pub type_: Option<String>,
pub description: Option<String>,
pub name: Option<String>,
pub in_: Option<String>, // `in` is renamed to `in_` in Rust
pub scheme: Option<String>,
pub bearer_format: Option<String>,
pub flows: Option<OAuthFlows>,
pub open_id_connect_url: Option<String>,
}
#[derive(Serialize, Clone, Deserialize, Debug, JsonSchema, Default)]
#[serde(rename_all = "camelCase")]
pub struct OAuthFlows {
pub implicit: Option<OAuthFlow>,
pub password: Option<OAuthFlow>,
pub client_credentials: Option<OAuthFlow>,
pub authorization_code: Option<OAuthFlow>,
}
#[derive(Serialize, Clone, Deserialize, Debug, JsonSchema, Default)]
#[serde(rename_all = "camelCase")]
pub struct OAuthFlow {
pub authorization_url: Option<String>,
pub token_url: Option<String>,
pub refresh_url: Option<String>,
pub scopes: Option<HashMap<String, String>>,
}
#[derive(Serialize, Clone, Deserialize, Debug, JsonSchema, Default)]
#[serde(rename_all = "camelCase")]
pub struct XML {
pub name: Option<String>,
pub namespace: Option<String>,
pub prefix: Option<String>,
pub attribute: Option<bool>,
pub wrapped: Option<bool>,
}

6
src/value.rs Normal file

@ -0,0 +1,6 @@
use serde_json::Value;
pub fn value() -> Value {
let schema = std::fs::read_to_string("openapi.yaml").unwrap();
serde_yaml::from_str::<serde_json::Value>(&schema).unwrap()
}

536
test.md Normal file

@ -0,0 +1,536 @@
## Example for OpenAPI
```yaml
openapi: '3.0.3'
info:
title: Todo REST API
version: '1.0.0'
description: A RESTful API for managing todo items
servers:
- url: /api/v1
description: Base API path
components:
schemas:
Todo:
type: object
properties:
id:
type: string
format: uuid
readOnly: true
title:
type: string
minLength: 1
maxLength: 200
description:
type: string
maxLength: 2000
completed:
type: boolean
default: false
dueDate:
type: string
format: date-time
userId:
type: string
createdAt:
type: string
format: date-time
readOnly: true
updatedAt:
type: string
format: date-time
readOnly: true
required:
- id
- title
- completed
- userId
- createdAt
- updatedAt
TodoCreate:
type: object
properties:
title:
type: string
minLength: 1
maxLength: 200
description:
type: string
maxLength: 2000
dueDate:
type: string
format: date-time
userId:
type: string
required:
- title
- userId
TodoUpdate:
type: object
properties:
title:
type: string
minLength: 1
maxLength: 200
description:
type: string
maxLength: 2000
completed:
type: boolean
dueDate:
type: string
format: date-time
minProperties: 1
TodoList:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Todo'
metadata:
type: object
properties:
total:
type: integer
minimum: 0
limit:
type: integer
minimum: 1
offset:
type: integer
minimum: 0
required:
- total
- limit
- offset
required:
- data
- metadata
TodoResponse:
type: object
properties:
data:
$ref: '#/components/schemas/Todo'
required:
- data
Error:
type: object
properties:
error:
type: object
properties:
code:
type: string
enum:
- VALIDATION_ERROR
- UNAUTHORIZED
- FORBIDDEN
- NOT_FOUND
- RATE_LIMIT_EXCEEDED
- INTERNAL_ERROR
message:
type: string
details:
type: array
items:
type: object
properties:
field:
type: string
message:
type: string
required:
- field
- message
required:
- code
- message
parameters:
TodoId:
name: todoId
in: path
required: true
schema:
type: string
format: uuid
UserId:
name: userId
in: query
required: true
schema:
type: string
Status:
name: status
in: query
schema:
type: string
enum: [active, completed]
Limit:
name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 50
Offset:
name: offset
in: query
schema:
type: integer
minimum: 0
default: 0
SortBy:
name: sortBy
in: query
schema:
type: string
enum: [createdAt, dueDate]
default: createdAt
SortOrder:
name: sortOrder
in: query
schema:
type: string
enum: [asc, desc]
default: desc
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- bearerAuth: []
paths:
/todos:
get:
summary: List todos
parameters:
- $ref: '#/components/parameters/UserId'
- $ref: '#/components/parameters/Status'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Offset'
- $ref: '#/components/parameters/SortBy'
- $ref: '#/components/parameters/SortOrder'
responses:
'200':
description: Successfully retrieved todos
headers:
ETag:
schema:
type: string
Last-Modified:
schema:
type: string
X-Request-ID:
schema:
type: string
X-RateLimit-Limit:
schema:
type: integer
X-RateLimit-Remaining:
schema:
type: integer
X-RateLimit-Reset:
schema:
type: integer
content:
application/json:
schema:
$ref: '#/components/schemas/TodoList'
'400':
description: Invalid parameters
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: Too many requests
headers:
Retry-After:
schema:
type: integer
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
post:
summary: Create a new todo
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TodoCreate'
responses:
'201':
description: Todo created successfully
headers:
Location:
schema:
type: string
format: uri
ETag:
schema:
type: string
X-Request-ID:
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/TodoResponse'
'400':
description: Invalid input
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/todos/{todoId}:
parameters:
- $ref: '#/components/parameters/TodoId'
get:
summary: Get a specific todo
responses:
'200':
description: Successfully retrieved todo
headers:
ETag:
schema:
type: string
Last-Modified:
schema:
type: string
X-Request-ID:
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/TodoResponse'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: Todo not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
patch:
summary: Update a todo
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TodoUpdate'
responses:
'200':
description: Todo updated successfully
headers:
ETag:
schema:
type: string
X-Request-ID:
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/TodoResponse'
'400':
description: Invalid input
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: Todo not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
delete:
summary: Delete a todo
responses:
'204':
description: Todo deleted successfully
headers:
X-Request-ID:
schema:
type: string
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: Todo not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
```
```wit
package api:todos@1.0.0;
// Common types used across the API
interface types {
// Authentication types derived from OpenAPI security schemes
record bearer-token {
token: string,
}
// Core resource type
record todo {
id: string,
title: string,
description: option<string>,
completed: bool,
due-date: option<string>,
user-id: string,
created-at: string,
updated-at: string,
}
variant error {
unauthorized,
not-found,
validation-error(list<string>),
rate-limited { retry-after: u32 },
server-error,
}
}
interface todos-collection {
use types.{todo, bearer-token, error};
// Request/response types with domain-appropriate fields
record list-request {
auth: bearer-token,
user-id: string,
status: option<string>,
limit: option<u32>,
%offset: option<u32>,
}
record list-response {
items: list<todo>,
total: u32,
limit: u32,
%offset: u32,
version: string, // From ETag
last-updated: option<string>, // From Last-Modified
}
record create-request {
auth: bearer-token,
title: string,
description: option<string>,
due-date: option<string>,
user-id: string,
}
// Note: Response includes versioning info directly in return type
list: func(request: list-request) -> result<list-response, error>;
create: func(request: create-request) -> result<todo, error>;
}
interface todos-resource {
use types.{todo, bearer-token, error};
record update-request {
auth: bearer-token,
title: option<string>,
description: option<string>,
completed: option<bool>,
due-date: option<string>,
expected-version: option<string>, // If-Match header
}
get: func(id: string, auth: bearer-token) -> result<todo, error>;
update: func(id: string, request: update-request) -> result<todo, error>;
delete: func(id: string, auth: bearer-token) -> result<unit, error>;
}
world todos-api {
export todos-collection;
export todos-resource;
}
```