init
This commit is contained in:
commit
fe3ec74e35
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
1460
Cargo.lock
generated
Normal file
1460
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
Cargo.toml
Normal file
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
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
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;
|
||||
}
|
||||
|
552
fooschema.yml
Normal file
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
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
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
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
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
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
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
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
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
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
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;
|
||||
}
|
||||
```
|
Loading…
Reference in New Issue
Block a user