Weeks 1-7 of the template upgrade plan: - pkg/api: typed HTTPError with sentinels, Wrap/WrapMiddleware, Bind, health probes, OpenAPI schema/param builders - skeleton/packages: ui (design tokens, components), layout (DashboardShell), auth (AuthProvider, ProtectedRoute), api-client - skeleton/pkg: httperror, app/handler, app/bind, app/health, auth (JWT/API key middleware) - components/app-nextjs: Next.js 14 App Router template with dashboard, server actions, auth - cookbooks/feature-development.md with test and validation scripts - Handler tests for components, project management, and woodpecker webhook - 3 rounds of code review fixes applied Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
210 lines
6.1 KiB
Go
210 lines
6.1 KiB
Go
package api
|
|
|
|
import "strings"
|
|
|
|
// Parameter represents an OpenAPI parameter.
|
|
type Parameter map[string]any
|
|
|
|
// PathParam creates a required path parameter.
|
|
//
|
|
// Example:
|
|
//
|
|
// PathParam("id", "User identifier")
|
|
func PathParam(name, description string) Parameter {
|
|
return Parameter{
|
|
"name": name,
|
|
"in": "path",
|
|
"required": true,
|
|
"description": description,
|
|
"schema": String(),
|
|
}
|
|
}
|
|
|
|
// PathParamWithSchema creates a required path parameter with a custom schema.
|
|
func PathParamWithSchema(name, description string, schema Schema) Parameter {
|
|
return Parameter{
|
|
"name": name,
|
|
"in": "path",
|
|
"required": true,
|
|
"description": description,
|
|
"schema": schema,
|
|
}
|
|
}
|
|
|
|
// QueryParam creates an optional query parameter.
|
|
//
|
|
// Example:
|
|
//
|
|
// QueryParam("page", "Page number", false)
|
|
func QueryParam(name, description string, required bool) Parameter {
|
|
return Parameter{
|
|
"name": name,
|
|
"in": "query",
|
|
"required": required,
|
|
"description": description,
|
|
"schema": String(),
|
|
}
|
|
}
|
|
|
|
// QueryParamWithSchema creates a query parameter with a custom schema.
|
|
func QueryParamWithSchema(name, description string, required bool, schema Schema) Parameter {
|
|
return Parameter{
|
|
"name": name,
|
|
"in": "query",
|
|
"required": required,
|
|
"description": description,
|
|
"schema": schema,
|
|
}
|
|
}
|
|
|
|
// HeaderParam creates a header parameter.
|
|
func HeaderParam(name, description string, required bool) Parameter {
|
|
return Parameter{
|
|
"name": name,
|
|
"in": "header",
|
|
"required": required,
|
|
"description": description,
|
|
"schema": String(),
|
|
}
|
|
}
|
|
|
|
// CookieParam creates a cookie parameter.
|
|
func CookieParam(name, description string, required bool) Parameter {
|
|
return Parameter{
|
|
"name": name,
|
|
"in": "cookie",
|
|
"required": required,
|
|
"description": description,
|
|
"schema": String(),
|
|
}
|
|
}
|
|
|
|
// WithExample adds an example to a parameter.
|
|
func (p Parameter) WithExample(example any) Parameter {
|
|
p["example"] = example
|
|
return p
|
|
}
|
|
|
|
// WithDefault adds a default value to a parameter.
|
|
func (p Parameter) WithDefault(value any) Parameter {
|
|
if schema, ok := p["schema"].(Schema); ok {
|
|
schema["default"] = value
|
|
p["schema"] = schema
|
|
}
|
|
return p
|
|
}
|
|
|
|
// WithDeprecated marks a parameter as deprecated.
|
|
func (p Parameter) WithDeprecated(deprecated bool) Parameter {
|
|
p["deprecated"] = deprecated
|
|
return p
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Common Parameter Presets
|
|
// -----------------------------------------------------------------------------
|
|
|
|
// IDParam creates a standard ID path parameter.
|
|
func IDParam() Parameter {
|
|
return PathParamWithSchema("id", "Resource identifier", UUID())
|
|
}
|
|
|
|
// PageParam creates a standard pagination page parameter.
|
|
func PageParam() Parameter {
|
|
return QueryParamWithSchema("page", "Page number (1-indexed)", false, Int().WithDefault(1))
|
|
}
|
|
|
|
// PerPageParam creates a standard items-per-page parameter.
|
|
func PerPageParam() Parameter {
|
|
return QueryParamWithSchema("per_page", "Items per page (max 100)", false, IntWithMinMax(1, 100).WithDefault(20))
|
|
}
|
|
|
|
// SortParam creates a sort parameter.
|
|
// If allowedFields are provided, they're listed in the description.
|
|
func SortParam(allowedFields ...string) Parameter {
|
|
desc := "Sort field and direction (e.g., name:asc, created_at:desc)"
|
|
if len(allowedFields) > 0 {
|
|
desc = "Sort by: " + strings.Join(allowedFields, ", ") + " (append :asc or :desc)"
|
|
}
|
|
return QueryParamWithSchema("sort", desc, false,
|
|
String().WithExample("created_at:desc"))
|
|
}
|
|
|
|
// SearchParam creates a search query parameter.
|
|
func SearchParam() Parameter {
|
|
return QueryParam("q", "Search query", false).WithExample("keyword")
|
|
}
|
|
|
|
// APIKeyHeader creates the X-API-Key header parameter.
|
|
func APIKeyHeader() Parameter {
|
|
return HeaderParam("X-API-Key", "API key for authentication", true)
|
|
}
|
|
|
|
// AuthorizationHeader creates the Authorization header parameter.
|
|
func AuthorizationHeader() Parameter {
|
|
return HeaderParam("Authorization", "Bearer token for authentication", true).
|
|
WithExample("Bearer eyJhbGciOiJIUzI1NiIs...")
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Request Body Helpers
|
|
// -----------------------------------------------------------------------------
|
|
|
|
// RequestBody creates a JSON request body.
|
|
func RequestBody(schema Schema, required bool) map[string]any {
|
|
return map[string]any{
|
|
"required": required,
|
|
"content": map[string]any{
|
|
"application/json": map[string]any{
|
|
"schema": schema,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Response Helpers
|
|
// -----------------------------------------------------------------------------
|
|
|
|
// OpResponse creates a response definition for an OpenAPI operation.
|
|
func OpResponse(description string, schema Schema) map[string]any {
|
|
return map[string]any{
|
|
"description": description,
|
|
"content": map[string]any{
|
|
"application/json": map[string]any{
|
|
"schema": schema,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// OpResponseNoContent creates a 204 No Content response.
|
|
func OpResponseNoContent() map[string]any {
|
|
return map[string]any{
|
|
"description": "No content",
|
|
}
|
|
}
|
|
|
|
// OpResponses creates a responses map for an operation.
|
|
func OpResponses(responses map[string]map[string]any) map[string]any {
|
|
result := make(map[string]any, len(responses))
|
|
for code, resp := range responses {
|
|
result[code] = resp
|
|
}
|
|
return result
|
|
}
|
|
|
|
// OpStandardResponses returns common error responses to include in operations.
|
|
func OpStandardResponses() map[string]map[string]any {
|
|
return map[string]map[string]any{
|
|
"400": OpResponse("Bad request", ErrorResponseSchema()),
|
|
"401": OpResponse("Unauthorized", ErrorResponseSchema()),
|
|
"403": OpResponse("Forbidden", ErrorResponseSchema()),
|
|
"404": OpResponse("Not found", ErrorResponseSchema()),
|
|
"422": OpResponse("Unprocessable entity", ErrorResponseSchema()),
|
|
"429": OpResponse("Too many requests", ErrorResponseSchema()),
|
|
"500": OpResponse("Internal server error", ErrorResponseSchema()),
|
|
"503": OpResponse("Service unavailable", ErrorResponseSchema()),
|
|
}
|
|
}
|