add openapi interface and interface tests generation
This commit is contained in:
220
lib/schemas/openapi/controller_http.v
Normal file
220
lib/schemas/openapi/controller_http.v
Normal file
@@ -0,0 +1,220 @@
|
||||
module openapi
|
||||
|
||||
import veb
|
||||
import freeflowuniverse.herolib.schemas.jsonschema {Schema}
|
||||
import x.json2 {Any}
|
||||
import net.http
|
||||
|
||||
pub struct HTTPController {
|
||||
Handler // Handles OpenAPI requests
|
||||
pub:
|
||||
specification OpenAPI
|
||||
}
|
||||
|
||||
pub struct Context {
|
||||
veb.Context
|
||||
}
|
||||
|
||||
// Creates a new HTTPController instance
|
||||
pub fn new_http_controller(c HTTPController) &HTTPController {
|
||||
return &HTTPController{
|
||||
...c,
|
||||
Handler: c.Handler
|
||||
}
|
||||
}
|
||||
|
||||
@['/:path...'; get; post; put; delete; patch]
|
||||
pub fn (mut c HTTPController) index(mut ctx Context, path string) veb.Result {
|
||||
println('Requested path: $path')
|
||||
|
||||
// Extract the HTTP method
|
||||
method := ctx.req.method.str().to_lower()
|
||||
|
||||
// Matches the request path against the OpenAPI specification and retrieves the corresponding PathItem
|
||||
path_item := match_path(path, c.specification) or {
|
||||
// Return a 404 error if no matching path is found
|
||||
return ctx.not_found()
|
||||
}
|
||||
|
||||
|
||||
// // Check if the path exists in the OpenAPI specification
|
||||
// path_item := c.specification.paths[path] or {
|
||||
// // Return a 404 error if the path is not defined
|
||||
// return ctx.not_found()
|
||||
// }
|
||||
|
||||
// Match the HTTP method with the OpenAPI specification
|
||||
operation := match method {
|
||||
'get' { path_item.get }
|
||||
'post' { path_item.post }
|
||||
'put' { path_item.put }
|
||||
'delete' { path_item.delete }
|
||||
'patch' { path_item.patch }
|
||||
else {
|
||||
// Return 405 Method Not Allowed if the method is not supported
|
||||
return ctx.method_not_allowed()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
mut arg_map := map[string]Any
|
||||
path_arg := path.all_after_last('/')
|
||||
// the OpenAPI Parameter specification belonging to the path argument
|
||||
arg_params := operation.parameters.filter(it.in_ == 'path')
|
||||
if arg_params.len > 1 {
|
||||
// TODO: use path template to support multiple arguments (right now just last arg supported)
|
||||
panic('implement')
|
||||
} else if arg_params.len == 1 {
|
||||
arg_map[arg_params[0].name] = arg_params[0].typed(path_arg)
|
||||
}
|
||||
|
||||
mut parameters := ctx.query.clone()
|
||||
// Build the Request object
|
||||
request := Request{
|
||||
path: path
|
||||
operation: operation
|
||||
method: method
|
||||
arguments: arg_map
|
||||
parameters: parameters
|
||||
body: ctx.req.data
|
||||
header: ctx.req.header
|
||||
}
|
||||
|
||||
// Use the handler to process the request
|
||||
response := c.handler.handle(request) or {
|
||||
// Use OpenAPI spec to determine the response status for the error
|
||||
return ctx.handle_error(operation.responses, err)
|
||||
}
|
||||
|
||||
// Return the response to the client
|
||||
ctx.res.set_status(response.status)
|
||||
|
||||
// ctx.res.header = response.header
|
||||
// ctx.set_content_type('application/json')
|
||||
|
||||
// return ctx.ok('[]')
|
||||
return ctx.send_response_to_client('application/json', response.body)
|
||||
}
|
||||
|
||||
// Handles errors and maps them to OpenAPI-defined response statuses
|
||||
fn (mut ctx Context) handle_error(possible_responses map[string]ResponseSpec, err IError) veb.Result {
|
||||
// Match the error with the defined responses
|
||||
for code, _ in possible_responses {
|
||||
if matches_error_to_status(err, code.int()) {
|
||||
ctx.res.set_status(http.status_from_int(code.int()))
|
||||
ctx.set_content_type('application/json')
|
||||
return ctx.send_response_to_client(
|
||||
'application/json',
|
||||
'{"error": "$err.msg()", "status": $code}'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Default to 500 Internal HTTPController Error if no match is found
|
||||
return ctx.server_error(
|
||||
'{"error": "Internal HTTPController Error", "status": 500}'
|
||||
)
|
||||
}
|
||||
|
||||
// Helper for 405 Method Not Allowed response
|
||||
fn (mut ctx Context) method_not_allowed() veb.Result {
|
||||
ctx.res.set_status(.method_not_allowed)
|
||||
ctx.set_content_type('application/json')
|
||||
return ctx.send_response_to_client(
|
||||
'application/json',
|
||||
'{"error": "Method Not Allowed", "status": 405}'
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// Matches a request path against OpenAPI path templates in the parsed structs
|
||||
// Returns the matching path key and corresponding PathItem if found
|
||||
fn match_path(req_path string, spec OpenAPI) !PathItem {
|
||||
// Iterate through all paths in the OpenAPI specification
|
||||
for template, path_item in spec.paths {
|
||||
if is_path_match(req_path, template) {
|
||||
// Return the matching path template and its PathItem
|
||||
return path_item
|
||||
}
|
||||
}
|
||||
// If no match is found, return an error
|
||||
return error('Path not found')
|
||||
}
|
||||
|
||||
// Helper to match an error to a specific response status
|
||||
fn matches_error_to_status(err IError, status int) bool {
|
||||
// This can be customized to map specific errors to statuses
|
||||
// For simplicity, we'll use a direct comparison here.
|
||||
return err.code() == status
|
||||
}
|
||||
|
||||
// Checks if a request path matches a given OpenAPI path template
|
||||
// Allows for dynamic path segments like `{petId}` in templates
|
||||
fn is_path_match(req_path string, template string) bool {
|
||||
// Split the request path and template into segments
|
||||
req_segments := req_path.split('/')
|
||||
template_segments := template.split('/')
|
||||
|
||||
// If the number of segments doesn't match, the paths can't match
|
||||
if req_segments.len != template_segments.len {
|
||||
return false
|
||||
}
|
||||
|
||||
// Compare each segment in the template and request path
|
||||
for i, segment in template_segments {
|
||||
// If the segment is not dynamic (doesn't start with `{`), ensure it matches exactly
|
||||
if !segment.starts_with('{') && segment != req_segments[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// If all segments match or dynamic segments are valid, return true
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
pub fn (param Parameter) typed(value string) Any {
|
||||
param_schema := param.schema as Schema
|
||||
param_type := param_schema.typ
|
||||
param_format := param_schema.format
|
||||
|
||||
// Convert parameter value to corresponding type
|
||||
typ := match param_type {
|
||||
'integer' {
|
||||
param_format
|
||||
}
|
||||
'number' {
|
||||
param_format
|
||||
}
|
||||
else {
|
||||
param_type // Leave as param type for unknown types
|
||||
}
|
||||
}
|
||||
return typed(value, typ)
|
||||
}
|
||||
|
||||
// typed gets a value that is string and a desired type, and returns the typed string in Any Type.
|
||||
pub fn typed(value string, typ string) Any {
|
||||
match typ {
|
||||
'int32' {
|
||||
return value.int() // Convert to int
|
||||
}
|
||||
'int64' {
|
||||
return value.i64() // Convert to i64
|
||||
}
|
||||
'string' {
|
||||
return value // Already a string
|
||||
}
|
||||
'boolean' {
|
||||
return value.bool() // Convert to bool
|
||||
}
|
||||
'float' {
|
||||
return value.f32() // Convert to float
|
||||
}
|
||||
'double' {
|
||||
return value.f64() // Convert to double
|
||||
}
|
||||
else {
|
||||
return value.f64() // Leave as string for unknown types
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ module openapi
|
||||
|
||||
import net.http {CommonHeader}
|
||||
import x.json2 {Any}
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
|
||||
pub struct Request {
|
||||
pub:
|
||||
@@ -22,32 +23,44 @@ pub mut:
|
||||
header http.Header @[omitempty; str: skip; json:'-']// Response headers
|
||||
}
|
||||
|
||||
pub interface IHandler {
|
||||
mut:
|
||||
handle(Request) !Response
|
||||
}
|
||||
|
||||
pub struct Handler {
|
||||
pub:
|
||||
routes map[string]fn (Request) !Response // Map of route handlers
|
||||
specification OpenAPI @[required] // The OpenRPC specification
|
||||
pub mut:
|
||||
handler IHandler
|
||||
}
|
||||
|
||||
// Handle a request and return a response
|
||||
pub fn (handler Handler) handle(request Request) !Response {
|
||||
// Match the route based on the request path
|
||||
if route_handler := handler.routes[request.path] {
|
||||
// Call the corresponding route handler
|
||||
return route_handler(request)
|
||||
}
|
||||
|
||||
// Return 404 if no route matches
|
||||
return Response{
|
||||
status: .not_found
|
||||
body: 'Not Found'
|
||||
header: http.new_header(
|
||||
key: CommonHeader.content_type,
|
||||
value: 'text/plain'
|
||||
)
|
||||
}
|
||||
pub interface IHandler {
|
||||
mut:
|
||||
handle(Request) !Response // Custom handler for other methods
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct HandleParams {
|
||||
timeout int = 60 // Timeout in seconds
|
||||
retry int // Number of retries
|
||||
}
|
||||
|
||||
// Handle a JSON-RPC request and return a response
|
||||
pub fn (mut h Handler) handle(req Request, params HandleParams) !Response {
|
||||
// Validate the method exists in the specification
|
||||
// if req.method !in h.specification.methods.map(it.name) {
|
||||
// // Return 404 if no route matches
|
||||
// return Response{
|
||||
// status: .not_found
|
||||
// body: 'Not Found'
|
||||
// header: http.new_header(
|
||||
// key: CommonHeader.content_type,
|
||||
// value: 'text/plain'
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
// Enforce timeout and retries (dummy implementation)
|
||||
if params.timeout < 0 || params.retry < 0 {
|
||||
panic('implement')
|
||||
}
|
||||
|
||||
// Forward the request to the custom handler
|
||||
return h.handler.handle(req)
|
||||
}
|
||||
Reference in New Issue
Block a user