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