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>
241 lines
6.5 KiB
Go
241 lines
6.5 KiB
Go
package api
|
|
|
|
// Schema represents a JSON Schema for OpenAPI.
|
|
type Schema map[string]any
|
|
|
|
// String creates a string schema.
|
|
func String() Schema {
|
|
return Schema{"type": "string"}
|
|
}
|
|
|
|
// StringWithFormat creates a string schema with a format.
|
|
// Common formats: email, uri, uuid, date, date-time, password
|
|
func StringWithFormat(format string) Schema {
|
|
return Schema{"type": "string", "format": format}
|
|
}
|
|
|
|
// StringEnum creates a string schema restricted to specific values.
|
|
func StringEnum(values ...string) Schema {
|
|
return Schema{"type": "string", "enum": values}
|
|
}
|
|
|
|
// StringWithMinMax creates a string schema with length constraints.
|
|
func StringWithMinMax(min, max int) Schema {
|
|
s := Schema{"type": "string"}
|
|
if min > 0 {
|
|
s["minLength"] = min
|
|
}
|
|
if max > 0 {
|
|
s["maxLength"] = max
|
|
}
|
|
return s
|
|
}
|
|
|
|
// Int creates an integer schema.
|
|
func Int() Schema {
|
|
return Schema{"type": "integer"}
|
|
}
|
|
|
|
// IntWithMinMax creates an integer schema with constraints.
|
|
func IntWithMinMax(min, max int) Schema {
|
|
s := Schema{"type": "integer"}
|
|
if min != 0 {
|
|
s["minimum"] = min
|
|
}
|
|
if max != 0 {
|
|
s["maximum"] = max
|
|
}
|
|
return s
|
|
}
|
|
|
|
// Int64 creates a 64-bit integer schema.
|
|
func Int64() Schema {
|
|
return Schema{"type": "integer", "format": "int64"}
|
|
}
|
|
|
|
// Number creates a number (float) schema.
|
|
func Number() Schema {
|
|
return Schema{"type": "number"}
|
|
}
|
|
|
|
// Bool creates a boolean schema.
|
|
func Bool() Schema {
|
|
return Schema{"type": "boolean"}
|
|
}
|
|
|
|
// Array creates an array schema with the given item type.
|
|
func Array(items Schema) Schema {
|
|
return Schema{
|
|
"type": "array",
|
|
"items": items,
|
|
}
|
|
}
|
|
|
|
// Object creates an object schema with the given properties.
|
|
// Required fields can be specified separately.
|
|
func Object(props map[string]Schema, required ...string) Schema {
|
|
properties := make(map[string]any, len(props))
|
|
for k, v := range props {
|
|
properties[k] = v
|
|
}
|
|
|
|
s := Schema{
|
|
"type": "object",
|
|
"properties": properties,
|
|
}
|
|
|
|
if len(required) > 0 {
|
|
s["required"] = required
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
// Ref creates a $ref to a schema in components/schemas.
|
|
func Ref(name string) Schema {
|
|
return Schema{"$ref": "#/components/schemas/" + name}
|
|
}
|
|
|
|
// RefArray creates an array of $ref items.
|
|
func RefArray(name string) Schema {
|
|
return Array(Ref(name))
|
|
}
|
|
|
|
// Nullable makes a schema nullable using oneOf pattern.
|
|
func Nullable(s Schema) Schema {
|
|
return Schema{
|
|
"oneOf": []Schema{
|
|
s,
|
|
{"type": "null"},
|
|
},
|
|
}
|
|
}
|
|
|
|
// WithDescription adds a description to a schema.
|
|
func (s Schema) WithDescription(desc string) Schema {
|
|
s["description"] = desc
|
|
return s
|
|
}
|
|
|
|
// WithExample adds an example to a schema.
|
|
func (s Schema) WithExample(example any) Schema {
|
|
s["example"] = example
|
|
return s
|
|
}
|
|
|
|
// WithDefault adds a default value to a schema.
|
|
func (s Schema) WithDefault(value any) Schema {
|
|
s["default"] = value
|
|
return s
|
|
}
|
|
|
|
// WithPattern adds a regex pattern to a string schema.
|
|
func (s Schema) WithPattern(pattern string) Schema {
|
|
s["pattern"] = pattern
|
|
return s
|
|
}
|
|
|
|
// Format sets the format for a schema.
|
|
// Common formats: email, uri, uuid, date, date-time, password, int32, int64
|
|
func (s Schema) Format(format string) Schema {
|
|
s["format"] = format
|
|
return s
|
|
}
|
|
|
|
// Description is an alias for WithDescription for cleaner chaining.
|
|
func (s Schema) Description(desc string) Schema {
|
|
return s.WithDescription(desc)
|
|
}
|
|
|
|
// Example is an alias for WithExample for cleaner chaining.
|
|
func (s Schema) Example(example any) Schema {
|
|
return s.WithExample(example)
|
|
}
|
|
|
|
// Default is an alias for WithDefault for cleaner chaining.
|
|
func (s Schema) Default(value any) Schema {
|
|
return s.WithDefault(value)
|
|
}
|
|
|
|
// Pattern is an alias for WithPattern for cleaner chaining.
|
|
func (s Schema) Pattern(pattern string) Schema {
|
|
return s.WithPattern(pattern)
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Common Schema Presets
|
|
// -----------------------------------------------------------------------------
|
|
|
|
// UUID creates a UUID string schema.
|
|
func UUID() Schema {
|
|
return StringWithFormat("uuid").WithExample("550e8400-e29b-41d4-a716-446655440000")
|
|
}
|
|
|
|
// Email creates an email string schema.
|
|
func Email() Schema {
|
|
return StringWithFormat("email").WithExample("user@example.com")
|
|
}
|
|
|
|
// URL creates a URL string schema.
|
|
func URL() Schema {
|
|
return StringWithFormat("uri").WithExample("https://example.com")
|
|
}
|
|
|
|
// DateTime creates a date-time string schema.
|
|
func DateTime() Schema {
|
|
return StringWithFormat("date-time").WithExample("2024-01-15T10:30:00Z")
|
|
}
|
|
|
|
// Password creates a password string schema (hidden in docs).
|
|
func Password() Schema {
|
|
return StringWithFormat("password")
|
|
}
|
|
|
|
// Pagination creates a common pagination object schema.
|
|
func Pagination() Schema {
|
|
return Object(map[string]Schema{
|
|
"page": Int().WithDescription("Current page number").WithExample(1),
|
|
"per_page": Int().WithDescription("Items per page").WithExample(20),
|
|
"total": Int().WithDescription("Total number of items").WithExample(100),
|
|
"total_pages": Int().WithDescription("Total number of pages").WithExample(5),
|
|
})
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Response Schema Helpers
|
|
// -----------------------------------------------------------------------------
|
|
|
|
// ResponseSchema creates the standard response envelope schema.
|
|
func ResponseSchema(dataSchema Schema) Schema {
|
|
return Object(map[string]Schema{
|
|
"data": dataSchema,
|
|
"meta": Object(map[string]Schema{
|
|
"request_id": String().WithDescription("Request correlation ID"),
|
|
"timestamp": DateTime().WithDescription("Response timestamp"),
|
|
}),
|
|
})
|
|
}
|
|
|
|
// ErrorResponseSchema creates the standard error response schema.
|
|
func ErrorResponseSchema() Schema {
|
|
return Object(map[string]Schema{
|
|
"error": Object(map[string]Schema{
|
|
"code": String().WithDescription("Machine-readable error code").WithExample("BAD_REQUEST"),
|
|
"message": String().WithDescription("Human-readable error message").WithExample("Invalid request"),
|
|
"details": Schema{"type": "object"}.WithDescription("Additional error details"),
|
|
}, "code", "message"),
|
|
"meta": Object(map[string]Schema{
|
|
"request_id": String().WithDescription("Request correlation ID"),
|
|
"timestamp": DateTime().WithDescription("Response timestamp"),
|
|
}),
|
|
})
|
|
}
|
|
|
|
// ValidationErrorSchema creates a validation error details schema.
|
|
func ValidationErrorSchema() Schema {
|
|
return Array(Object(map[string]Schema{
|
|
"field": String().WithDescription("Field that failed validation").WithExample("email"),
|
|
"message": String().WithDescription("Validation error message").WithExample("is required"),
|
|
}, "field", "message"))
|
|
}
|