From d04f973b57024fa88f44f12af923183b7e7c06ea Mon Sep 17 00:00:00 2001 From: "jeremiah.zink" Date: Wed, 16 Jul 2025 12:27:57 -0600 Subject: [PATCH 1/9] added security options and some helper functions --- openapi.go | 47 ++++++++++++++++++------ security.go | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 12 deletions(-) create mode 100644 security.go diff --git a/openapi.go b/openapi.go index c01b910..d22387a 100644 --- a/openapi.go +++ b/openapi.go @@ -6,13 +6,14 @@ import ( // OpenAPI represents the definition of the openapi specification 3.0.3 type OpenAPI struct { - Version string `json:"openapi"` // the semantic version number of the OpenAPI Specification version - Servers []Server `json:"servers,omitempty"` // Array of Server Objects, which provide connectivity information to a target server. - Info Info `json:"info"` // REQUIRED. Provides metadata about the API. The metadata MAY be used by tooling as required. - Tags []Tag `json:"tags,omitempty"` // A list of tags used by the specification with additional metadata - Paths Router `json:"paths"` // key= path|method - Components Components `json:"components,omitempty"` // reuseable components - ExternalDocs *ExternalDocs `json:"externalDocs,omitempty"` //Additional external documentation. + Version string `json:"openapi"` // the semantic version number of the OpenAPI Specification version + Servers []Server `json:"servers,omitempty"` // Array of Server Objects, which provide connectivity information to a target server. + Info Info `json:"info"` // REQUIRED. Provides metadata about the API. The metadata MAY be used by tooling as required. + Tags []Tag `json:"tags,omitempty"` // A list of tags used by the specification with additional metadata + Paths Router `json:"paths"` // key= path|method + Components Components `json:"components,omitempty"` // reuseable components + ExternalDocs *ExternalDocs `json:"externalDocs,omitempty"` //Additional external documentation. + Security []map[string][]string `json:"security,omitempty"` // Lists the required security schemes to execute this operation. The name used for each property MUST correspond to a security scheme declared in the Security Schemes under the Components Object. } type Server struct { @@ -89,19 +90,16 @@ type Media struct { Examples map[string]Example `json:"examples,omitempty"` // NOT Supported: - //Example of the media type. The example object SHOULD be in the correct format as specified by the media type. The example field is mutually exclusive of the examples field. Furthermore, if referencing a schema which contains an example, the example value SHALL override the example provided by the schema. - //Example any `json:"example,omitempty"` -> uses examples even for one example - //A map between a property name and its encoding information. The key, being the property name, MUST exist in the schema as a property. //Encoding map[string]Encoding `json:"encoding,omitempty"` } type Components struct { - Schemas map[string]Schema `json:"schemas,omitempty"` + Schemas map[string]Schema `json:"schemas,omitempty"` + SecuritySchemes map[string]SecurityScheme `json:"securitySchemes,omitempty"` //NOT implemented /* Parameters []Params - SecuritySchemes struct{} RequestBodies []RequestBody Responses Responses Headers []Params @@ -110,6 +108,31 @@ type Components struct { Callbacks struct{} */ } +type SecurityScheme struct { + Type string `json:"type"` // REQUIRED. The type of the security scheme. Valid values are "apiKey", "http", "oauth2", "openIdConnect". + In string `json:"in"` // REQUIRED. The location of the API key. Valid values are "query", "header", or "cookie". + Name string `json:"name"` // REQUIRED. The name of the header, query or cookie parameter to be used. + Description string `json:"description,omitempty"` // A description for security scheme. CommonMark syntax MAY be used for rich text representation. + Scheme string `json:"scheme,omitempty"` // REQUIRED (maybe not) The name of the HTTP Authentication scheme to be used + BearerFormat string `json:"bearerFormat,omitempty"` // A hint to the client to identify how the bearer token is formatted. Bearer tokens are usually generated by an authorization server, so this information is primarily for documentation purposes. + Flows *Flows `json:"flows,omitempty"` // REQUIRED (maybe not) The type of flow used by the OAuth2 security scheme. + OpenIDConnectURL string `json:"openIdConnectUrl,omitempty"` // REQUIRED (maybe not) OpenId Connect URL to discover OAuth2 configuration values. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS. +} + +type Flows struct { + Implicit *Flow `json:"implicit,omitempty"` // Configuration for the OAuth Implicit flow + Password *Flow `json:"password,omitempty"` // Configuration for the OAuth Resource Owner Password flow + ClientCredentials *Flow `json:"clientCredentials,omitempty"` // Configuration for the OAuth Client Credentials flow. Previously called application in OpenAPI 2.0. + AuthorizationCode *Flow `json:"authorizationCode,omitempty"` // Configuration for the OAuth Authorization Code flow. Previously called accessCode in OpenAPI 2.0. +} + +type Flow struct { + AuthorizationURL string `json:"authorizationUrl,omitempty"` // REQUIRED. The authorization URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS. + TokenURL string `json:"tokenUrl,omitempty"` // REQUIRED. The token URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS. + RefreshURL string `json:"refreshUrl,omitempty"` // The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS. + Scopes map[string]string `json:"scopes,omitempty"` // REQUIRED. The available scopes for the OAuth2 security scheme. A map between the scope name and a short description for it. The map MAY be empty. +} + type Encoding struct { ContentType string `json:"contentType,omitempty"` // The Content-Type for encoding a specific property. // headers map[string]headerObject : not implemented needed if media is multipart diff --git a/security.go b/security.go new file mode 100644 index 0000000..f1716ad --- /dev/null +++ b/security.go @@ -0,0 +1,103 @@ +package openapi + +func (o *OpenAPI) AddSecurity(security string) { + if o.Security == nil { + o.Security = make([]map[string][]string, 0) + } + + // Create a security requirement map with empty scopes for the given security scheme + requirement := map[string][]string{ + security: {}, + } + + o.Security = append(o.Security, requirement) +} + +// AddSecurityScheme adds a security scheme to the OpenAPI specification +func (o *OpenAPI) AddSecurityScheme(name string, scheme SecurityScheme) { + // if the global security schemes are not defined, create them + o.AddSecurity(name) + + // if the security schemes are not defined, create them + if o.Components.SecuritySchemes == nil { + o.Components.SecuritySchemes = make(map[string]SecurityScheme) + } + o.Components.SecuritySchemes[name] = scheme +} + +// AddAPIKeyAuth adds an API key authentication scheme +func (o *OpenAPI) AddAPIKeyAuth(name string, keyName string, location string, description string) { + scheme := SecurityScheme{ + Type: "apiKey", + Name: keyName, + In: location, // "query", "header", or "cookie" + Description: description, + } + o.AddSecurityScheme(name, scheme) +} + +// AddBearerAuth adds a bearer token authentication scheme +func (o *OpenAPI) AddBearerAuth(name string, bearerFormat string, description string) { + scheme := SecurityScheme{ + Type: "http", + Scheme: "Bearer", + BearerFormat: bearerFormat, + Description: description, + } + o.AddSecurityScheme(name, scheme) +} + +// AddBasicAuth adds a basic authentication scheme +func (o *OpenAPI) AddBasicAuth(name string, description string) { + scheme := SecurityScheme{ + Type: "http", + Scheme: "basic", + Description: description, + } + o.AddSecurityScheme(name, scheme) +} + +// AddOAuth2Auth adds an OAuth2 authentication scheme +func (o *OpenAPI) AddOAuth2Auth(name string, flows *Flows, description string) { + scheme := SecurityScheme{ + Type: "oauth2", + Flows: flows, + Description: description, + } + o.AddSecurityScheme(name, scheme) +} + +// AddOpenIDConnectAuth adds an OpenID Connect authentication scheme +func (o *OpenAPI) AddOpenIDConnectAuth(name string, openIDConnectURL string, description string) { + scheme := SecurityScheme{ + Type: "openIdConnect", + OpenIDConnectURL: openIDConnectURL, + Description: description, + } + o.AddSecurityScheme(name, scheme) +} + +// AddSecurityRequirement adds a security requirement to the OpenAPI specification +// For non-OAuth2 schemes, pass an empty slice for scopes +// For OAuth2 schemes, pass the required scopes +func (o *OpenAPI) AddSecurityRequirement(schemeName string, scopes []string) { + if o.Security == nil { + o.Security = make([]map[string][]string, 0) + } + + requirement := map[string][]string{ + schemeName: scopes, + } + + o.Security = append(o.Security, requirement) +} + +// AddMultipleSecurityRequirement adds a security requirement with multiple schemes (AND logic) +// All schemes in the map must be satisfied +func (o *OpenAPI) AddMultipleSecurityRequirement(schemes map[string][]string) { + if o.Security == nil { + o.Security = make([]map[string][]string, 0) + } + + o.Security = append(o.Security, schemes) +} From bd2ff01f2f61457151dc7033b5962d5665a01963 Mon Sep 17 00:00:00 2001 From: "jeremiah.zink" Date: Wed, 16 Jul 2025 12:31:30 -0600 Subject: [PATCH 2/9] update comments --- openapi.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openapi.go b/openapi.go index d22387a..97534d7 100644 --- a/openapi.go +++ b/openapi.go @@ -113,10 +113,10 @@ type SecurityScheme struct { In string `json:"in"` // REQUIRED. The location of the API key. Valid values are "query", "header", or "cookie". Name string `json:"name"` // REQUIRED. The name of the header, query or cookie parameter to be used. Description string `json:"description,omitempty"` // A description for security scheme. CommonMark syntax MAY be used for rich text representation. - Scheme string `json:"scheme,omitempty"` // REQUIRED (maybe not) The name of the HTTP Authentication scheme to be used + Scheme string `json:"scheme,omitempty"` // REQUIRED. The name of the HTTP Authentication scheme to be used BearerFormat string `json:"bearerFormat,omitempty"` // A hint to the client to identify how the bearer token is formatted. Bearer tokens are usually generated by an authorization server, so this information is primarily for documentation purposes. - Flows *Flows `json:"flows,omitempty"` // REQUIRED (maybe not) The type of flow used by the OAuth2 security scheme. - OpenIDConnectURL string `json:"openIdConnectUrl,omitempty"` // REQUIRED (maybe not) OpenId Connect URL to discover OAuth2 configuration values. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS. + Flows *Flows `json:"flows,omitempty"` // REQUIRED. The type of flow used by the OAuth2 security scheme. + OpenIDConnectURL string `json:"openIdConnectUrl,omitempty"` // REQUIRED. OpenId Connect URL to discover OAuth2 configuration values. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS. } type Flows struct { From 4da793aced286aa2f99fec151b76fe93e2f0fabd Mon Sep 17 00:00:00 2001 From: Jeremiah Date: Wed, 16 Jul 2025 12:38:18 -0600 Subject: [PATCH 3/9] Update security.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- security.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/security.go b/security.go index f1716ad..6575a4f 100644 --- a/security.go +++ b/security.go @@ -40,7 +40,7 @@ func (o *OpenAPI) AddAPIKeyAuth(name string, keyName string, location string, de func (o *OpenAPI) AddBearerAuth(name string, bearerFormat string, description string) { scheme := SecurityScheme{ Type: "http", - Scheme: "Bearer", + Scheme: "bearer", BearerFormat: bearerFormat, Description: description, } From ff5afd349bbb6a6925e4a94a8a44d3ae68db4e93 Mon Sep 17 00:00:00 2001 From: Jeremiah Date: Wed, 16 Jul 2025 12:38:43 -0600 Subject: [PATCH 4/9] Update security.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- security.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/security.go b/security.go index 6575a4f..044abbc 100644 --- a/security.go +++ b/security.go @@ -81,15 +81,9 @@ func (o *OpenAPI) AddOpenIDConnectAuth(name string, openIDConnectURL string, des // For non-OAuth2 schemes, pass an empty slice for scopes // For OAuth2 schemes, pass the required scopes func (o *OpenAPI) AddSecurityRequirement(schemeName string, scopes []string) { - if o.Security == nil { - o.Security = make([]map[string][]string, 0) - } - - requirement := map[string][]string{ + o.addSecurityRequirementHelper(map[string][]string{ schemeName: scopes, - } - - o.Security = append(o.Security, requirement) + }) } // AddMultipleSecurityRequirement adds a security requirement with multiple schemes (AND logic) From 1a767401f50f0decf8b342014f99175e1406eb03 Mon Sep 17 00:00:00 2001 From: "jeremiah.zink" Date: Wed, 16 Jul 2025 12:42:49 -0600 Subject: [PATCH 5/9] reverted copilot change that broke things --- security.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/security.go b/security.go index 044abbc..6575a4f 100644 --- a/security.go +++ b/security.go @@ -81,9 +81,15 @@ func (o *OpenAPI) AddOpenIDConnectAuth(name string, openIDConnectURL string, des // For non-OAuth2 schemes, pass an empty slice for scopes // For OAuth2 schemes, pass the required scopes func (o *OpenAPI) AddSecurityRequirement(schemeName string, scopes []string) { - o.addSecurityRequirementHelper(map[string][]string{ + if o.Security == nil { + o.Security = make([]map[string][]string, 0) + } + + requirement := map[string][]string{ schemeName: scopes, - }) + } + + o.Security = append(o.Security, requirement) } // AddMultipleSecurityRequirement adds a security requirement with multiple schemes (AND logic) From 66b6c108af835195b5548816f865bc575b940583 Mon Sep 17 00:00:00 2001 From: "jeremiah.zink" Date: Thu, 17 Jul 2025 10:15:18 -0600 Subject: [PATCH 6/9] updated readme to better explain code usage and features --- README.md | 582 ++++++++++++++++++++++++++++++++++++++++++---------- security.go | 9 +- 2 files changed, 475 insertions(+), 116 deletions(-) diff --git a/README.md b/README.md index 7920da8..ad989cc 100644 --- a/README.md +++ b/README.md @@ -1,153 +1,515 @@ # GoOpenAPI -A Go Lang SDK to help create OpenApi 3.0.3 Spec -[OpenAPI spec](https://swagger.io/specification/) +A Go SDK for building OpenAPI 3.0.3 specifications programmatically. Generate complete OpenAPI documentation from your Go code with type-safe schema generation and comprehensive security support. [![codecov](https://codecov.io/gh/hydronica/go-openapi/graph/badge.svg?token=E3I51BL34W)](https://codecov.io/gh/hydronica/go-openapi) +## Features -## OpenAPI Document Breakdown - +- **Complete OpenAPI 3.0.3 Support**: Generate valid OpenAPI specifications +- **Type-Safe Schema Generation**: Automatic schema creation from Go structs, maps, and JSON +- **Comprehensive Security**: Support for API Key, Bearer, Basic, OAuth2, and OpenID Connect +- **Parameter Management**: Path, query, header, and cookie parameters with examples +- **Request/Response Bodies**: Multiple content types and examples +- **Route Management**: Fluent API for building routes and operations +- **Schema Compilation**: Automatic schema consolidation and reference generation +## Installation -## Getting Started +```bash +go get github.com/hydronica/go-openapi +``` + +## Quick Start + +### Basic Usage + +```go +package main -``` go import ( - _ "embed" - + "fmt" "github.com/hydronica/go-openapi" ) -// go:embed base.json -var base string func main() { + // Create a new OpenAPI document + doc := openapi.New("My API", "1.0.0", "A sample API") - // create doc from base template - doc, err := openapi.NewFromJson(base) - if err != nil { - log.Fatal(err) - } - - // create doc from scratch - doc = openapi.New("title", "v1.0.0", "all about this API") - - doc.AddRoute( - openapi.NewRoute("/path/v1", "get"). - AddResponse( - openapi.Resp{Code: 200, Desc:"valid response"}.WithJSONString('{"status":"ok"}' - ). - AddRequest( - openapi.Req{MType: "application/json", Desc:"pull data"}. - WithParams(myStruct) - ) - ) - - // print generated json document - fmt.Println(string(doc.JSON())) + // Add a route + route := doc.GetRoute("/users/{id}", "GET") + route.AddResponse(openapi.Response{ + Status: 200, + Desc: "User retrieved successfully", + }.WithExample(map[string]any{ + "id": 123, + "name": "John Doe", + "email": "john@example.com", + })) + + // Generate JSON + fmt.Println(doc.JSON()) } ``` -### Adding Schema +### From Existing JSON -#### JSONExample - - Easiest to implement - - go-openapi converts to object - - hard to read unique hash16 name of object based on keys - - No way to add descriptions to fields +```go +//go:embed openapi.json +var baseSpec string -``` go - openapi.NewRoute("path/v1/test","post"). - AddRequest(openapi.RequestBody{}.WithJSONString(`{"name":"bob","age":99,"country":"United States"}`) +doc, err := openapi.NewFromJson(baseSpec) +if err != nil { + log.Fatal(err) +} ``` -``` json -{ - "title": "fd4b3d4f5cce2e6d", - "type": "object", - "properties": { - "age": { - "type": "number" - }, - "country": { - "type": "string" +## Schema Generation + +### 1. Go Structs (Recommended) + +Use Go structs with `json` and `desc` tags for the cleanest schema generation: + +```go +type User struct { + ID int `json:"id" desc:"Unique user identifier"` + Name string `json:"name" desc:"User's full name"` + Email string `json:"email" desc:"User's email address"` + Active bool `json:"active" desc:"Whether the user is active"` + Created time.Time `json:"created" desc:"Account creation timestamp"` +} + +route.AddRequest(openapi.RequestBody{}.WithExample(User{})) +``` + +**Benefits:** +- Clear, readable schema names (`User` instead of hash) +- Field descriptions from `desc` tags +- Type-safe and refactor-friendly + +### 2. JSON Strings + +Convert JSON strings directly to schemas: + +```go +route.AddRequest(openapi.RequestBody{}.WithJSONString(`{ + "name": "John Doe", + "age": 30, + "email": "john@example.com" +}`)) +``` + +**Benefits:** +- Quick prototyping +- Easy for simple schemas + +**Limitations:** +- Auto-generated hash names +- No field descriptions + +### 3. Example Maps + +Use `map[string]Example` for detailed field descriptions: + +```go +route.AddRequest(openapi.RequestBody{}.WithExample(map[string]openapi.Example{ + "name": {Value: "John Doe", Desc: "User's full name"}, + "age": {Value: 30, Desc: "User's age in years"}, + "email": {Value: "john@example.com", Desc: "User's email address"}, +})) +``` + +**Benefits:** +- Field-level descriptions +- Flexible value types + +**Limitations:** +- Auto-generated hash names +- More verbose syntax + +## Route Management + +### Creating Routes + +```go +// Get or create a route +route := doc.GetRoute("/users/{id}", "GET") + +// Add tags for grouping +route.Tags("users", "management") + +// Add summary +route.Summary = "Get user by ID" +``` + +### Path Parameters + +Path parameters are automatically detected from the path: + +```go +route := doc.GetRoute("/users/{id}/posts/{postId}", "GET") +// Automatically creates path parameters for 'id' and 'postId' + +// Add examples for path parameters +route.PathParam("id", 123, "User ID") +route.PathParam("postId", "abc-123", "Post ID") +``` + +### Query Parameters + +```go +// Single parameter +route.QueryParam("limit", 10, "Maximum number of results") +route.QueryParam("offset", 0, "Number of results to skip") + +// Multiple parameters from struct +type QueryParams struct { + Limit int `json:"limit" desc:"Maximum results"` + Offset int `json:"offset" desc:"Results to skip"` + Search string `json:"search" desc:"Search term"` +} +route.QueryParams(QueryParams{Limit: 10, Offset: 0, Search: "example"}) + +// Multiple parameters from map +route.QueryParams(map[string]any{ + "limit": 10, + "offset": 0, + "search": "example", +}) +``` + +### Header Parameters + +```go +route.HeaderParam("X-API-Version", "v1", "API version") +route.HeaderParam("X-Request-ID", "abc-123", "Request identifier") +``` + +### Cookie Parameters + +```go +route.CookieParam("session", "abc123", "Session identifier") +``` + +### Multiple Parameter Examples + +```go +// Multiple examples for a single parameter +route.QueryParam("status", []string{"active", "inactive", "pending"}, "User status") + +// Using Example struct for custom names +route.QueryParam("priority", []openapi.Example{ + {Summary: "low", Value: 1}, + {Summary: "medium", Value: 5}, + {Summary: "high", Value: 10}, +}, "Priority level") +``` + +## Request and Response Bodies + +### Request Bodies + +```go +// From struct +type CreateUserRequest struct { + Name string `json:"name" desc:"User's name"` + Email string `json:"email" desc:"User's email"` +} + +route.AddRequest(openapi.RequestBody{ + Desc: "User creation data", + Required: true, +}.WithExample(CreateUserRequest{})) + +// Multiple examples +route.AddRequest(openapi.RequestBody{}. + WithNamedExample("admin", CreateUserRequest{Name: "Admin", Email: "admin@example.com"}). + WithNamedExample("user", CreateUserRequest{Name: "User", Email: "user@example.com"})) +``` + +### Response Bodies + +```go +// Success response +route.AddResponse(openapi.Response{ + Status: 200, + Desc: "User created successfully", +}.WithExample(User{})) + +// Error response +route.AddResponse(openapi.Response{ + Status: 400, + Desc: "Invalid request", +}.WithJSONString(`{"error": "validation failed", "details": "name is required"}`)) + +// Multiple status codes +route.AddResponse(openapi.Response{Status: 200, Desc: "Success"}.WithExample(User{})) +route.AddResponse(openapi.Response{Status: 404, Desc: "Not found"}.WithJSONString(`{"error": "user not found"}`)) +route.AddResponse(openapi.Response{Status: 500, Desc: "Server error"}.WithJSONString(`{"error": "internal server error"}`)) +``` + +## Security + +### API Key Authentication + +```go +// Header-based API key +doc.AddAPIKeyAuth("ApiKeyAuth", "X-API-Key", "header", "API key for authentication") + +// Query parameter API key +doc.AddAPIKeyAuth("ApiKeyQuery", "api_key", "query", "API key as query parameter") + +// Cookie-based API key +doc.AddAPIKeyAuth("ApiKeyCookie", "auth_token", "cookie", "API key in cookie") +``` + +### Bearer Token Authentication + +```go +doc.AddBearerAuth("BearerAuth", "JWT", "Bearer token authentication") +``` + +### Basic Authentication + +```go +doc.AddBasicAuth("BasicAuth", "HTTP Basic authentication") +``` + +### OAuth2 Authentication + +```go +flows := &openapi.Flows{ + AuthorizationCode: &openapi.Flow{ + AuthorizationURL: "https://example.com/oauth/authorize", + TokenURL: "https://example.com/oauth/token", + Scopes: map[string]string{ + "read": "Read access", + "write": "Write access", + "admin": "Admin access", + }, }, - "name": { - "type": "string" - } - } } +doc.AddOAuth2Auth("OAuth2", flows, "OAuth2 authentication") ``` -#### map[string]openapi.Example - - hard to read unique hash16 name of object based on keys - - Ability to add Description to fields +### OpenID Connect + +```go +doc.AddOpenIDConnectAuth("OpenIDConnect", "https://example.com/.well-known/openid_configuration", "OpenID Connect authentication") +``` +### Security Requirements -``` go -openapi.NewRoute("path/v1/test","post"). - AddRequest(RequestBody{}.WithExample(map[string]Example{ - "age": {Value: 12, Desc: "age in earth years"}, - "country": {Value: "USA", Desc: "3 character ISO Code"}, - "name": {Value: "bob", Desc: "individual name"}, - }) +```go +// Single security requirement +doc.AddSecurityRequirement("ApiKeyAuth", []string{}) + +// OAuth2 with scopes +doc.AddSecurityRequirement("OAuth2", []string{"read", "write"}) + +// Multiple security schemes (AND logic) +doc.AddMultipleSecurityRequirement(map[string][]string{ + "ApiKeyAuth": {}, + "OAuth2": {"read"}, +}) ``` -``` json -{ - "title": "fd4b3d4f5cce2e6d", - "type": "object", - "properties": { - "age": { - "type": "integer", - "description": "age in earth years" +## Advanced Features + +### Tags + +```go +doc.AddTags( + openapi.Tag{ + Name: "users", + Desc: "User management operations", }, - "country": { - "type": "string", - "description": "3 character ISO Code" + openapi.Tag{ + Name: "posts", + Desc: "Post management operations", + }, +) +``` + +### Servers + +```go +doc.Servers = []openapi.Server{ + { + URL: "https://api.example.com/v1", + Desc: "Production server", + }, + { + URL: "https://staging-api.example.com/v1", + Desc: "Staging server", }, - "name": { - "type": "string", - "description": "individual name" - } - } } ``` -#### struct - - description pulled from the `desc` struct tag - - clear title based on the struct name - - +### Path Conversion -``` go -type myStruct struct { - Name string `json:"name" desc:"individual name"` - Age int `json:"age" desc:"age in earth years"` - Country string `json:"country" desc:"3 character ISO Code"` +Convert Go-style paths to OpenAPI format: + +```go +// Convert ":id" to "{id}" +path := openapi.CleanPath("/users/:id/posts/:postId") +// Result: "/users/{id}/posts/{postId}" +``` + +### Custom Time Formatting + +```go +type Event struct { + Name string `json:"name"` + Date openapi.Time `json:"date"` +} + +event := Event{ + Name: "Meeting", + Date: openapi.Time{Time: time.Now(), Format: "2006-01-02"}, } +``` + +### Schema Compilation +Compile the document to consolidate schemas and validate: -openapi.NewRoute("path/v1/test","post"). - AddRequest(RequestBody{}.WithExample(myStruct{}) +```go +if err := doc.Compile(); err != nil { + log.Printf("Validation errors: %v", err) +} ``` -``` json -{ - "title": "openapi.myStruct", - "type": "object", - "properties": { - "age": { - "type": "integer", - "description": "age in earth years" - }, - "country": { - "type": "string", - "description": "3 character ISO Code" - }, - "name": { - "type": "string", - "description": "individual name" +### Custom Schema Names + +```go +// Set custom name for JSON schema +jsonData := openapi.JSONString(`{"name": "value"}`).SetName("CustomSchema") +route.AddRequest(openapi.RequestBody{}.WithExample(jsonData)) +``` + +## Complete Example + +```go +package main + +import ( + "fmt" + "time" + "github.com/hydronica/go-openapi" +) + +type User struct { + ID int `json:"id" desc:"Unique user identifier"` + Name string `json:"name" desc:"User's full name"` + Email string `json:"email" desc:"User's email address"` + Active bool `json:"active" desc:"Whether the user is active"` + Created time.Time `json:"created" desc:"Account creation timestamp"` +} + +type CreateUserRequest struct { + Name string `json:"name" desc:"User's name"` + Email string `json:"email" desc:"User's email"` +} + +type ErrorResponse struct { + Error string `json:"error" desc:"Error message"` + Details string `json:"details" desc:"Error details"` +} + +func main() { + // Create document + doc := openapi.New("User API", "1.0.0", "A simple user management API") + + // Add security + doc.AddBearerAuth("BearerAuth", "JWT", "Bearer token authentication") + + // Add tags + doc.AddTags(openapi.Tag{ + Name: "users", + Desc: "User management operations", + }) + + // GET /users + listRoute := doc.GetRoute("/users", "GET") + listRoute.Tags("users") + listRoute.Summary = "List users" + listRoute.QueryParam("limit", 10, "Maximum number of users to return") + listRoute.QueryParam("offset", 0, "Number of users to skip") + listRoute.AddResponse(openapi.Response{ + Status: 200, + Desc: "List of users", + }.WithExample([]User{{ID: 1, Name: "John Doe", Email: "john@example.com", Active: true}})) + + // POST /users + createRoute := doc.GetRoute("/users", "POST") + createRoute.Tags("users") + createRoute.Summary = "Create user" + createRoute.AddRequest(openapi.RequestBody{ + Desc: "User creation data", + Required: true, + }.WithExample(CreateUserRequest{Name: "Jane Doe", Email: "jane@example.com"})) + createRoute.AddResponse(openapi.Response{ + Status: 201, + Desc: "User created successfully", + }.WithExample(User{ID: 2, Name: "Jane Doe", Email: "jane@example.com", Active: true})) + createRoute.AddResponse(openapi.Response{ + Status: 400, + Desc: "Invalid request", + }.WithExample(ErrorResponse{Error: "validation failed", Details: "name is required"})) + + // GET /users/{id} + getRoute := doc.GetRoute("/users/{id}", "GET") + getRoute.Tags("users") + getRoute.Summary = "Get user by ID" + getRoute.PathParam("id", 123, "User ID") + getRoute.AddResponse(openapi.Response{ + Status: 200, + Desc: "User details", + }.WithExample(User{ID: 1, Name: "John Doe", Email: "john@example.com", Active: true})) + getRoute.AddResponse(openapi.Response{ + Status: 404, + Desc: "User not found", + }.WithExample(ErrorResponse{Error: "not found", Details: "user with id 123 not found"})) + + // Compile and validate + if err := doc.Compile(); err != nil { + fmt.Printf("Validation errors: %v\n", err) } - } + + // Output JSON + fmt.Println(doc.JSON()) } -``` \ No newline at end of file +``` + +## API Reference + +### Core Types + +- `OpenAPI`: Main document structure +- `Route`: Individual API endpoint +- `RequestBody`: Request body definition +- `Response`: Response definition +- `Param`: Parameter definition +- `Schema`: Data type definition +- `Example`: Example value with description + +### Methods + +- `New(title, version, description)`: Create new document +- `NewFromJson(spec)`: Create from existing JSON +- `GetRoute(path, method)`: Get or create route +- `AddResponse(response)`: Add response to route +- `AddRequest(request)`: Add request body to route +- `WithExample(value)`: Add example to request/response +- `WithJSONString(json)`: Add JSON string example +- `JSON()`: Generate JSON output +- `Compile()`: Validate and consolidate schemas + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. \ No newline at end of file diff --git a/security.go b/security.go index 6575a4f..31aec95 100644 --- a/security.go +++ b/security.go @@ -15,9 +15,6 @@ func (o *OpenAPI) AddSecurity(security string) { // AddSecurityScheme adds a security scheme to the OpenAPI specification func (o *OpenAPI) AddSecurityScheme(name string, scheme SecurityScheme) { - // if the global security schemes are not defined, create them - o.AddSecurity(name) - // if the security schemes are not defined, create them if o.Components.SecuritySchemes == nil { o.Components.SecuritySchemes = make(map[string]SecurityScheme) @@ -26,7 +23,7 @@ func (o *OpenAPI) AddSecurityScheme(name string, scheme SecurityScheme) { } // AddAPIKeyAuth adds an API key authentication scheme -func (o *OpenAPI) AddAPIKeyAuth(name string, keyName string, location string, description string) { +func (o *OpenAPI) AddAPIKeyAuth(name, keyName, location, description string) { scheme := SecurityScheme{ Type: "apiKey", Name: keyName, @@ -37,7 +34,7 @@ func (o *OpenAPI) AddAPIKeyAuth(name string, keyName string, location string, de } // AddBearerAuth adds a bearer token authentication scheme -func (o *OpenAPI) AddBearerAuth(name string, bearerFormat string, description string) { +func (o *OpenAPI) AddBearerAuth(name, bearerFormat, description string) { scheme := SecurityScheme{ Type: "http", Scheme: "bearer", @@ -68,7 +65,7 @@ func (o *OpenAPI) AddOAuth2Auth(name string, flows *Flows, description string) { } // AddOpenIDConnectAuth adds an OpenID Connect authentication scheme -func (o *OpenAPI) AddOpenIDConnectAuth(name string, openIDConnectURL string, description string) { +func (o *OpenAPI) AddOpenIDConnectAuth(name, openIDConnectURL, description string) { scheme := SecurityScheme{ Type: "openIdConnect", OpenIDConnectURL: openIDConnectURL, From f6208a89c847e4a2515a6ae45a56a29e11bb4d0c Mon Sep 17 00:00:00 2001 From: "jeremiah.zink" Date: Fri, 18 Jul 2025 08:15:56 -0600 Subject: [PATCH 7/9] update security code, updated readme examples --- README.md | 34 ++++++++++++++--- build.go | 18 +++++---- cmd/gherkin/main.go | 2 +- docs/chart.drawio.svg | 88 ++++++++++++++++++++++--------------------- openapi.go | 13 ++++--- paths.go | 12 +++--- security.go | 62 +++++++++++++++++++++++------- 7 files changed, 148 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index ad989cc..556cf62 100644 --- a/README.md +++ b/README.md @@ -254,19 +254,19 @@ route.AddResponse(openapi.Response{Status: 500, Desc: "Server error"}.WithJSONSt ```go // Header-based API key -doc.AddAPIKeyAuth("ApiKeyAuth", "X-API-Key", "header", "API key for authentication") +doc.AddAPIKeyAuth("YourUniqueNameApiKeyAuth", "X-API-Key", openapi.APIKeyInHeader, "API key for authentication") // Query parameter API key -doc.AddAPIKeyAuth("ApiKeyQuery", "api_key", "query", "API key as query parameter") +doc.AddAPIKeyAuth("ApiKeyQuery", "api_key", openapi.APIKeyInQuery, "API key as query parameter") // Cookie-based API key -doc.AddAPIKeyAuth("ApiKeyCookie", "auth_token", "cookie", "API key in cookie") +doc.AddAPIKeyAuth("ApiKeyCookie", "auth_token", openapi.APIKeyInCookie, "API key in cookie") ``` ### Bearer Token Authentication ```go -doc.AddBearerAuth("BearerAuth", "JWT", "Bearer token authentication") +doc.AddBearerAuth("BearerAuth", openapi.BearerFormatJWT, "Bearer token authentication") ``` ### Basic Authentication @@ -314,6 +314,30 @@ doc.AddMultipleSecurityRequirement(map[string][]string{ }) ``` +### Available Constants + +The library provides constants for common security values: + +```go +// Security scheme types +openapi.SecurityTypeAPIKey // "apiKey" +openapi.SecurityTypeHTTP // "http" +openapi.SecurityTypeOAuth2 // "oauth2" +openapi.SecurityTypeOpenID // "openIdConnect" + +// HTTP authentication schemes +openapi.HTTPSchemeBearer // "bearer" +openapi.HTTPSchemeBasic // "basic" + +// API key locations +openapi.APIKeyInQuery // "query" +openapi.APIKeyInHeader // "header" +openapi.APIKeyInCookie // "cookie" + +// Common bearer token formats +openapi.BearerFormatJWT // "JWT" +``` + ## Advanced Features ### Tags @@ -422,7 +446,7 @@ func main() { doc := openapi.New("User API", "1.0.0", "A simple user management API") // Add security - doc.AddBearerAuth("BearerAuth", "JWT", "Bearer token authentication") + doc.AddBearerAuth("BearerAuth", openapi.BearerFormatJWT, "Bearer token authentication") // Add tags doc.AddTags(openapi.Tag{ diff --git a/build.go b/build.go index 75360ec..8ea30c3 100644 --- a/build.go +++ b/build.go @@ -14,6 +14,8 @@ import ( "time" ) +// NewFromJson creates a new OpenAPI object from an existing json string +// This allows the user to create a spec with a base structure and then add to it. func NewFromJson(spec string) (api *OpenAPI, err error) { api = &OpenAPI{ Paths: make(Router), @@ -238,11 +240,11 @@ func (o *OpenAPI) Compile() error { o.Components.Schemas = make(map[string]Schema) } var errs error - for _, r := range o.Paths { - if r.Requests != nil { - for k, c := range r.Requests.Content { + for _, path := range o.Paths { + if path.Requests != nil { + for k, c := range path.Requests.Content { if k == "invalid/json" { - errs = errors.Join(errs, fmt.Errorf("invalid json %v request at %v: %q", r.method, r.path, c.Examples["invalid"].Value)) + errs = errors.Join(errs, fmt.Errorf("invalid json %v request at %v: %q", path.method, path.path, c.Examples["invalid"].Value)) continue } if c.Schema.Type != Object { @@ -252,13 +254,13 @@ func (o *OpenAPI) Compile() error { o.Components.Schemas[c.Schema.Title] = c.Schema } c.Schema = Schema{Ref: "#/components/schemas/" + c.Schema.Title} - r.Requests.Content[k] = c + path.Requests.Content[k] = c } } - for _, resp := range r.Responses { + for _, resp := range path.Responses { for k, c := range resp.Content { if k == "invalid/json" { - errs = errors.Join(errs, fmt.Errorf("invalid json %v response at %v: %q", r.method, r.path, c.Examples["invalid"].Value)) + errs = errors.Join(errs, fmt.Errorf("invalid json %v response at %v: %q", path.method, path.path, c.Examples["invalid"].Value)) continue } if c.Schema.Type != Object { @@ -272,7 +274,7 @@ func (o *OpenAPI) Compile() error { } } - for _, p := range r.Params { + for _, p := range path.Params { if strings.Contains(p.Desc, "err:") { errs = errors.Join(errs, fmt.Errorf("%v param %v| %v", p.In, p.Name, p.Desc)) } diff --git a/cmd/gherkin/main.go b/cmd/gherkin/main.go index 512eb64..498a362 100644 --- a/cmd/gherkin/main.go +++ b/cmd/gherkin/main.go @@ -147,7 +147,7 @@ func main() { // generate the output swagger doc f, err := os.Create(c.Out) if err != nil { - log.Fatalf("issue with writing %q: %w", c.Out, err) + log.Fatalf("issue with writing %q: %+v", c.Out, err) } f.Write([]byte(doc.JSON())) } diff --git a/docs/chart.drawio.svg b/docs/chart.drawio.svg index 080ed69..4cfa07b 100644 --- a/docs/chart.drawio.svg +++ b/docs/chart.drawio.svg @@ -1,4 +1,4 @@ - + @@ -24,10 +24,10 @@ - - - - + + + + @@ -94,10 +94,10 @@ - - - - + + + + @@ -158,8 +158,8 @@ - - + + @@ -242,6 +242,8 @@ + + @@ -259,8 +261,8 @@ - - + + @@ -278,8 +280,8 @@ - - + + @@ -334,8 +336,8 @@ - - + + @@ -370,8 +372,8 @@ - - + + @@ -469,8 +471,8 @@ - - + + @@ -488,8 +490,8 @@ - - + + @@ -537,8 +539,8 @@ - - + + @@ -556,8 +558,8 @@ - - + + @@ -575,8 +577,8 @@ - - + + @@ -645,8 +647,8 @@ - - + + @@ -664,8 +666,8 @@ - - + + @@ -841,38 +843,38 @@ - +
-
-
+
+
ExternalDocs:
- + ExternalDocs: - - - + + +
-
-
+
+
Components
- + Components diff --git a/openapi.go b/openapi.go index 97534d7..252b128 100644 --- a/openapi.go +++ b/openapi.go @@ -4,16 +4,19 @@ import ( "strconv" ) +type SecurityRequirement map[string][]string + // OpenAPI represents the definition of the openapi specification 3.0.3 +// https://swagger.io/specification/v3/#openapi-object type OpenAPI struct { Version string `json:"openapi"` // the semantic version number of the OpenAPI Specification version Servers []Server `json:"servers,omitempty"` // Array of Server Objects, which provide connectivity information to a target server. - Info Info `json:"info"` // REQUIRED. Provides metadata about the API. The metadata MAY be used by tooling as required. + Info Info `json:"info"` // https://swagger.io/specification/v3/#info-object - REQUIRED. Provides metadata about the API. The metadata MAY be used by tooling as required. Tags []Tag `json:"tags,omitempty"` // A list of tags used by the specification with additional metadata - Paths Router `json:"paths"` // key= path|method - Components Components `json:"components,omitempty"` // reuseable components - ExternalDocs *ExternalDocs `json:"externalDocs,omitempty"` //Additional external documentation. - Security []map[string][]string `json:"security,omitempty"` // Lists the required security schemes to execute this operation. The name used for each property MUST correspond to a security scheme declared in the Security Schemes under the Components Object. + Paths Router `json:"paths"` // https://swagger.io/specification/v3/#paths-object - key= path|method + Components Components `json:"components,omitempty"` // https://swagger.io/specification/v3/#components-object - reuseable components + ExternalDocs *ExternalDocs `json:"externalDocs,omitempty"` // https://swagger.io/specification/v3/#external-documentation-object - Additional external documentation. + Security []SecurityRequirement `json:"security,omitempty"` // https://swagger.io/specification/v3/#security-requirement-object - Lists the required security schemes to execute this operation. The name used for each property MUST correspond to a security scheme declared in the Security Schemes under the Components Object. } type Server struct { diff --git a/paths.go b/paths.go index c291c8a..d03926e 100644 --- a/paths.go +++ b/paths.go @@ -14,16 +14,18 @@ import ( type Router map[string]*Route // Route is a simplified definition for managing routes in code +// this object represents the operation object in the OpenAPI specification type Route struct { // internal reference path string method string - Tag []string `json:"tags,omitempty"` - Summary string `json:"summary,omitempty"` - Responses map[Code]Response `json:"responses,omitempty"` // [status_code]Response - Params Params `json:"parameters,omitempty"` // key reference for params. key is name of Param - Requests *RequestBody `json:"requestBody,omitempty"` // key reference for requests + Tag []string `json:"tags,omitempty"` + Summary string `json:"summary,omitempty"` + Responses map[Code]Response `json:"responses,omitempty"` // [status_code]Response + Params Params `json:"parameters,omitempty"` // key reference for params. key is name of Param + Security []SecurityRequirement `json:"security,omitempty"` // https://swagger.io/specification/v3/#security-requirement-object - Lists the required security schemes to execute this operation. The name used for each property MUST correspond to a security scheme declared in the Security Schemes under the Components Object. + Requests *RequestBody `json:"requestBody,omitempty"` // key reference for requests /* NOT CURRENTLY SUPPORT VALUES // operationId is an optional unique string used to identify an operation diff --git a/security.go b/security.go index 31aec95..5900597 100644 --- a/security.go +++ b/security.go @@ -1,12 +1,38 @@ package openapi +// Security scheme types +const ( + SecurityTypeAPIKey = "apiKey" + SecurityTypeHTTP = "http" + SecurityTypeOAuth2 = "oauth2" + SecurityTypeOpenID = "openIdConnect" +) + +// HTTP authentication schemes +const ( + HTTPSchemeBearer = "bearer" + HTTPSchemeBasic = "basic" +) + +// API key locations +const ( + APIKeyInQuery = "query" + APIKeyInHeader = "header" + APIKeyInCookie = "cookie" +) + +// Common bearer token formats +const ( + BearerFormatJWT = "JWT" +) + func (o *OpenAPI) AddSecurity(security string) { if o.Security == nil { - o.Security = make([]map[string][]string, 0) + o.Security = make([]SecurityRequirement, 0) } // Create a security requirement map with empty scopes for the given security scheme - requirement := map[string][]string{ + requirement := SecurityRequirement{ security: {}, } @@ -23,22 +49,30 @@ func (o *OpenAPI) AddSecurityScheme(name string, scheme SecurityScheme) { } // AddAPIKeyAuth adds an API key authentication scheme +// name is your unique identifier for the security scheme +// keyName is the name of the API key +// location is the location of the API key i.e., APIKeyInQuery, APIKeyInHeader, or APIKeyInCookie +// description is the description of the security scheme func (o *OpenAPI) AddAPIKeyAuth(name, keyName, location, description string) { scheme := SecurityScheme{ - Type: "apiKey", + Type: SecurityTypeAPIKey, Name: keyName, - In: location, // "query", "header", or "cookie" + In: location, // APIKeyInQuery, APIKeyInHeader, or APIKeyInCookie Description: description, } o.AddSecurityScheme(name, scheme) } // AddBearerAuth adds a bearer token authentication scheme +// name is your unique identifier for the security scheme +// bearerFormat is the format of the bearer token i.e., BearerFormatJWT +// description is the description of the security scheme func (o *OpenAPI) AddBearerAuth(name, bearerFormat, description string) { scheme := SecurityScheme{ - Type: "http", - Scheme: "bearer", + Type: SecurityTypeHTTP, + Scheme: HTTPSchemeBearer, BearerFormat: bearerFormat, + In: APIKeyInHeader, Description: description, } o.AddSecurityScheme(name, scheme) @@ -47,8 +81,8 @@ func (o *OpenAPI) AddBearerAuth(name, bearerFormat, description string) { // AddBasicAuth adds a basic authentication scheme func (o *OpenAPI) AddBasicAuth(name string, description string) { scheme := SecurityScheme{ - Type: "http", - Scheme: "basic", + Type: SecurityTypeHTTP, + Scheme: HTTPSchemeBasic, Description: description, } o.AddSecurityScheme(name, scheme) @@ -57,7 +91,7 @@ func (o *OpenAPI) AddBasicAuth(name string, description string) { // AddOAuth2Auth adds an OAuth2 authentication scheme func (o *OpenAPI) AddOAuth2Auth(name string, flows *Flows, description string) { scheme := SecurityScheme{ - Type: "oauth2", + Type: SecurityTypeOAuth2, Flows: flows, Description: description, } @@ -67,7 +101,7 @@ func (o *OpenAPI) AddOAuth2Auth(name string, flows *Flows, description string) { // AddOpenIDConnectAuth adds an OpenID Connect authentication scheme func (o *OpenAPI) AddOpenIDConnectAuth(name, openIDConnectURL, description string) { scheme := SecurityScheme{ - Type: "openIdConnect", + Type: SecurityTypeOpenID, OpenIDConnectURL: openIDConnectURL, Description: description, } @@ -79,10 +113,10 @@ func (o *OpenAPI) AddOpenIDConnectAuth(name, openIDConnectURL, description strin // For OAuth2 schemes, pass the required scopes func (o *OpenAPI) AddSecurityRequirement(schemeName string, scopes []string) { if o.Security == nil { - o.Security = make([]map[string][]string, 0) + o.Security = make([]SecurityRequirement, 0) } - requirement := map[string][]string{ + requirement := SecurityRequirement{ schemeName: scopes, } @@ -93,8 +127,8 @@ func (o *OpenAPI) AddSecurityRequirement(schemeName string, scopes []string) { // All schemes in the map must be satisfied func (o *OpenAPI) AddMultipleSecurityRequirement(schemes map[string][]string) { if o.Security == nil { - o.Security = make([]map[string][]string, 0) + o.Security = make([]SecurityRequirement, 0) } - o.Security = append(o.Security, schemes) + o.Security = append(o.Security, SecurityRequirement(schemes)) } From 7aa69c44b2a45650efc93557c9cf356986d963a4 Mon Sep 17 00:00:00 2001 From: "jeremiah.zink" Date: Mon, 28 Jul 2025 09:02:32 -0600 Subject: [PATCH 8/9] added unit tests, and updated readme for new security object on routes, and on the openapi object --- .cursor/rules/tests.mdc | 38 ++++ README.md | 108 +++++++++++- paths.go | 33 ++++ paths_test.go | 339 +++++++++++++++++++++++++++++++++++ security.go | 6 +- security_test.go | 379 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 893 insertions(+), 10 deletions(-) create mode 100644 .cursor/rules/tests.mdc create mode 100644 security_test.go diff --git a/.cursor/rules/tests.mdc b/.cursor/rules/tests.mdc new file mode 100644 index 0000000..95c3383 --- /dev/null +++ b/.cursor/rules/tests.mdc @@ -0,0 +1,38 @@ +--- +description: +globs: +alwaysApply: false +--- +Write tests using TDD with the hydronica.trial package following this pattern. +The most important thing is to keep things as simple and understandable as possible. + +``` +func TestFunction(t *testing.T) { + // basic function that takes the table's input and returns the output value and an error. + // All errors cases should be handled as an error response + // if no errors return nil in the second value + // Keep the input and output values as simple as possible, preferable primitive types or the exact values of the function being tested + // do not create inline structs, define the struct in the test function first, if a simple struct is needed + fn := func(i string) (RawJSON, error) { + var v RawJSON + err := json.Unmarshal([]byte(i), &v) + return v, err + } + // Tables (test cases) should be clear and as simple as possible to provide logical coverage. + cases := trial.Cases[string, RawJSON]{ + "test_case": { + Input:{}, + Expected:{}, + } + } + + trial.New(fn, cases).SubTest(t) +} +``` + +When dealing time using the following helper functions +- TimeHour(s string) - uses format "2006-01-02T15" +- TimeDay(s string) - uses format "2006-01-02" +- Time(layout string, value) +- Times(layout string, values ...string) +- TimeP(layout string, s string) returns a *time.Time diff --git a/README.md b/README.md index 556cf62..1f65c85 100644 --- a/README.md +++ b/README.md @@ -300,20 +300,51 @@ doc.AddOpenIDConnectAuth("OpenIDConnect", "https://example.com/.well-known/openi ### Security Requirements +#### Global Security Requirements + ```go -// Single security requirement +// Single security requirement for all endpoints doc.AddSecurityRequirement("ApiKeyAuth", []string{}) -// OAuth2 with scopes +// OAuth2 with scopes for all endpoints doc.AddSecurityRequirement("OAuth2", []string{"read", "write"}) -// Multiple security schemes (AND logic) +// Multiple security schemes (AND logic) for all endpoints doc.AddMultipleSecurityRequirement(map[string][]string{ "ApiKeyAuth": {}, "OAuth2": {"read"}, }) ``` +#### Route-Specific Security Requirements + +```go +// Add security to individual routes +route := doc.GetRoute("/admin/users", "POST") + +// Single security requirement for this route only +route.AddSecurity("OAuth2Auth", "admin", "write") + +// Multiple security schemes for this route (AND logic) +route.AddMultipleSecurity(map[string][]string{ + "BearerAuth": {}, + "ApiKeyAuth": {}, +}) + +// Different routes can have different security requirements +publicRoute := doc.GetRoute("/public", "GET") +// No security requirements needed for public route + +protectedRoute := doc.GetRoute("/protected", "GET") +protectedRoute.AddSecurity("BearerAuth") + +adminRoute := doc.GetRoute("/admin", "DELETE") +adminRoute.AddSecurity("OAuth2Auth", "admin") + +// Clear security from a route if needed +route.ClearSecurity() +``` + ### Available Constants The library provides constants for common security values: @@ -445,9 +476,23 @@ func main() { // Create document doc := openapi.New("User API", "1.0.0", "A simple user management API") - // Add security + // Add security schemes doc.AddBearerAuth("BearerAuth", openapi.BearerFormatJWT, "Bearer token authentication") + // Add OAuth2 for admin endpoints + flows := &openapi.Flows{ + AuthorizationCode: &openapi.Flow{ + AuthorizationURL: "https://example.com/oauth/authorize", + TokenURL: "https://example.com/oauth/token", + Scopes: map[string]string{ + "read": "Read access to user data", + "write": "Write access to user data", + "admin": "Administrative access", + }, + }, + } + doc.AddOAuth2Auth("OAuth2Auth", flows, "OAuth2 authentication for admin operations") + // Add tags doc.AddTags(openapi.Tag{ Name: "users", @@ -482,11 +527,12 @@ func main() { Desc: "Invalid request", }.WithExample(ErrorResponse{Error: "validation failed", Details: "name is required"})) - // GET /users/{id} + // GET /users/{id} - requires bearer auth getRoute := doc.GetRoute("/users/{id}", "GET") getRoute.Tags("users") getRoute.Summary = "Get user by ID" getRoute.PathParam("id", 123, "User ID") + getRoute.AddSecurity("BearerAuth") // Route-specific security getRoute.AddResponse(openapi.Response{ Status: 200, Desc: "User details", @@ -496,6 +542,31 @@ func main() { Desc: "User not found", }.WithExample(ErrorResponse{Error: "not found", Details: "user with id 123 not found"})) + // DELETE /users/{id} - requires admin OAuth2 scope + deleteRoute := doc.GetRoute("/users/{id}", "DELETE") + deleteRoute.Tags("users") + deleteRoute.Summary = "Delete user" + deleteRoute.PathParam("id", 123, "User ID") + deleteRoute.AddSecurity("OAuth2Auth", "admin") // Admin-only endpoint + deleteRoute.AddResponse(openapi.Response{ + Status: 204, + Desc: "User deleted successfully", + }) + deleteRoute.AddResponse(openapi.Response{ + Status: 403, + Desc: "Insufficient permissions", + }.WithExample(ErrorResponse{Error: "forbidden", Details: "admin scope required"})) + + // GET /public/info - no security required + publicRoute := doc.GetRoute("/public/info", "GET") + publicRoute.Tags("public") + publicRoute.Summary = "Get public information" + publicRoute.AddResponse(openapi.Response{ + Status: 200, + Desc: "Public information", + }.WithJSONString(`{"version": "1.0.0", "status": "operational"}`)) + // No security requirements for public endpoints + // Compile and validate if err := doc.Compile(); err != nil { fmt.Printf("Validation errors: %v\n", err) @@ -520,15 +591,38 @@ func main() { ### Methods +#### Document Methods - `New(title, version, description)`: Create new document - `NewFromJson(spec)`: Create from existing JSON - `GetRoute(path, method)`: Get or create route +- `JSON()`: Generate JSON output +- `Compile()`: Validate and consolidate schemas + +#### Global Security Methods +- `AddAPIKeyAuth(name, keyName, location, description)`: Add API key authentication +- `AddBearerAuth(name, bearerFormat, description)`: Add bearer token authentication +- `AddBasicAuth(name, description)`: Add basic authentication +- `AddOAuth2Auth(name, flows, description)`: Add OAuth2 authentication +- `AddOpenIDConnectAuth(name, url, description)`: Add OpenID Connect authentication +- `AddSecurityRequirement(schemeName, scopes...)`: Add global security requirement +- `AddMultipleSecurityRequirement(schemes)`: Add multiple global security schemes + +#### Route Methods - `AddResponse(response)`: Add response to route - `AddRequest(request)`: Add request body to route +- `AddSecurity(schemeName, scopes...)`: Add security requirement to specific route +- `AddMultipleSecurity(schemes)`: Add multiple security schemes to specific route +- `ClearSecurity()`: Remove all security requirements from route +- `PathParam(name, value, desc)`: Add path parameter +- `QueryParam(name, value, desc)`: Add query parameter +- `HeaderParam(name, value, desc)`: Add header parameter +- `CookieParam(name, value, desc)`: Add cookie parameter +- `Tags(tags...)`: Add tags to route + +#### Request/Response Methods - `WithExample(value)`: Add example to request/response - `WithJSONString(json)`: Add JSON string example -- `JSON()`: Generate JSON output -- `Compile()`: Validate and consolidate schemas +- `WithNamedExample(name, value)`: Add named example ## Contributing diff --git a/paths.go b/paths.go index d03926e..6b2f6a1 100644 --- a/paths.go +++ b/paths.go @@ -537,3 +537,36 @@ func (p *Params) UnmarshalJSON(b []byte) error { } return nil } + +// AddSecurity adds a security requirement to this specific route +// For non-OAuth2 schemes, pass no scopes or empty slice +// For OAuth2 schemes, pass the required scopes +func (r *Route) AddSecurity(schemeName string, scopes ...string) *Route { + if r.Security == nil { + r.Security = make([]SecurityRequirement, 0) + } + + requirement := SecurityRequirement{ + schemeName: scopes, + } + + r.Security = append(r.Security, requirement) + return r +} + +// AddMultipleSecurity adds a security requirement with multiple schemes to this specific route (AND logic) +// All schemes in the map must be satisfied for this route +func (r *Route) AddMultipleSecurity(schemes map[string][]string) *Route { + if r.Security == nil { + r.Security = make([]SecurityRequirement, 0) + } + + r.Security = append(r.Security, SecurityRequirement(schemes)) + return r +} + +// ClearSecurity removes all security requirements from this specific route +func (r *Route) ClearSecurity() *Route { + r.Security = nil + return r +} diff --git a/paths_test.go b/paths_test.go index 1f9a069..b9f9d63 100644 --- a/paths_test.go +++ b/paths_test.go @@ -335,3 +335,342 @@ func TestMarshalRoute(t *testing.T) { trial.New(fn, cases).SubTest(t) } + +func TestRouteAddSecurityRequirement(t *testing.T) { + type input struct { + schemeName string + scopes []string + } + + fn := func(i input) ([]SecurityRequirement, error) { + doc := New("Test API", "1.0.0", "Test") + route := doc.GetRoute("/users", "GET") + route.AddSecurity(i.schemeName, i.scopes...) + return route.Security, nil + } + + cases := trial.Cases[input, []SecurityRequirement]{ + "api_key_no_scopes": { + Input: input{ + schemeName: "ApiKeyAuth", + scopes: []string{}, + }, + Expected: []SecurityRequirement{ + { + "ApiKeyAuth": {}, + }, + }, + }, + "bearer_no_scopes": { + Input: input{ + schemeName: "BearerAuth", + scopes: nil, + }, + Expected: []SecurityRequirement{ + { + "BearerAuth": {}, + }, + }, + }, + "oauth2_with_scopes": { + Input: input{ + schemeName: "OAuth2Auth", + scopes: []string{"read", "write"}, + }, + Expected: []SecurityRequirement{ + { + "OAuth2Auth": {"read", "write"}, + }, + }, + }, + "single_scope": { + Input: input{ + schemeName: "OAuth2Auth", + scopes: []string{"admin"}, + }, + Expected: []SecurityRequirement{ + { + "OAuth2Auth": {"admin"}, + }, + }, + }, + } + + trial.New(fn, cases).SubTest(t) +} + +func TestRouteAddMultipleSecurityRequirement(t *testing.T) { + type input struct { + schemes map[string][]string + } + + fn := func(i input) ([]SecurityRequirement, error) { + doc := New("Test API", "1.0.0", "Test") + route := doc.GetRoute("/admin", "POST") + route.AddMultipleSecurity(i.schemes) + return route.Security, nil + } + + cases := trial.Cases[input, []SecurityRequirement]{ + "api_key_and_oauth2": { + Input: input{ + schemes: map[string][]string{ + "ApiKeyAuth": {}, + "OAuth2Auth": {"admin"}, + }, + }, + Expected: []SecurityRequirement{ + { + "ApiKeyAuth": {}, + "OAuth2Auth": {"admin"}, + }, + }, + }, + "multiple_schemes_various_scopes": { + Input: input{ + schemes: map[string][]string{ + "BearerAuth": {}, + "OAuth2Auth": {"read", "write"}, + "ApiKeyAuth": {}, + }, + }, + Expected: []SecurityRequirement{ + { + "BearerAuth": {}, + "OAuth2Auth": {"read", "write"}, + "ApiKeyAuth": {}, + }, + }, + }, + } + + trial.New(fn, cases).SubTest(t) +} + +func TestRouteMultipleSecurityRequirements(t *testing.T) { + type input struct { + requirements []map[string][]string + } + + fn := func(i input) ([]SecurityRequirement, error) { + doc := New("Test API", "1.0.0", "Test") + route := doc.GetRoute("/flexible", "GET") + + // Add multiple different security requirement options (OR logic) + for _, req := range i.requirements { + route.AddMultipleSecurity(req) + } + + return route.Security, nil + } + + cases := trial.Cases[input, []SecurityRequirement]{ + "multiple_alternatives": { + Input: input{ + requirements: []map[string][]string{ + {"ApiKeyAuth": {}}, // Option 1: API Key only + {"OAuth2Auth": {"read"}}, // Option 2: OAuth2 with read scope + {"BearerAuth": {}, "ApiKeyAuth": {}}, // Option 3: Both Bearer and API Key + }, + }, + Expected: []SecurityRequirement{ + {"ApiKeyAuth": {}}, + {"OAuth2Auth": {"read"}}, + {"BearerAuth": {}, "ApiKeyAuth": {}}, + }, + }, + } + + trial.New(fn, cases).SubTest(t) +} + +func TestRouteClearSecurity(t *testing.T) { + fn := func(hasInitialSecurity bool) ([]SecurityRequirement, error) { + doc := New("Test API", "1.0.0", "Test") + route := doc.GetRoute("/test", "DELETE") + + if hasInitialSecurity { + route.AddSecurity("ApiKeyAuth") + route.AddSecurity("OAuth2Auth", "admin") + } + + route.ClearSecurity() + return route.Security, nil + } + + cases := trial.Cases[bool, []SecurityRequirement]{ + "clear_existing_security": { + Input: true, + Expected: nil, + }, + "clear_no_security": { + Input: false, + Expected: nil, + }, + } + + trial.New(fn, cases).SubTest(t) +} + +func TestRouteSecurityWithFullIntegration(t *testing.T) { + type routeConfig struct { + path string + method string + securityScheme string + scopes []string + addGlobalAuth bool + } + + fn := func(config routeConfig) (map[string]any, error) { + doc := New("Test API", "1.0.0", "Test") + + // Add security schemes to the document + if config.addGlobalAuth { + doc.AddBearerAuth("BearerAuth", BearerFormatJWT, "JWT Bearer token") + doc.AddAPIKeyAuth("ApiKeyAuth", "X-API-Key", APIKeyInHeader, "API key") + doc.AddOAuth2Auth("OAuth2Auth", &Flows{ + AuthorizationCode: &Flow{ + AuthorizationURL: "https://example.com/auth", + TokenURL: "https://example.com/token", + Scopes: map[string]string{ + "read": "Read access", + "write": "Write access", + "admin": "Admin access", + }, + }, + }, "OAuth2 authentication") + } + + // Create route with security + route := doc.GetRoute(config.path, config.method) + route.AddSecurity(config.securityScheme, config.scopes...) + route.AddResponse(Response{ + Status: 200, + Desc: "Success", + }) + + // Return relevant data for validation + return map[string]any{ + "hasSecuritySchemes": len(doc.Components.SecuritySchemes) > 0, + "routeSecurity": route.Security, + "routeExists": route != nil, + }, nil + } + + cases := trial.Cases[routeConfig, map[string]any]{ + "bearer_auth_integration": { + Input: routeConfig{ + path: "/protected", + method: "GET", + securityScheme: "BearerAuth", + scopes: []string{}, + addGlobalAuth: true, + }, + Expected: map[string]any{ + "hasSecuritySchemes": true, + "routeSecurity": []SecurityRequirement{ + {"BearerAuth": {}}, + }, + "routeExists": true, + }, + }, + "oauth2_with_scopes": { + Input: routeConfig{ + path: "/admin/users", + method: "POST", + securityScheme: "OAuth2Auth", + scopes: []string{"admin", "write"}, + addGlobalAuth: true, + }, + Expected: map[string]any{ + "hasSecuritySchemes": true, + "routeSecurity": []SecurityRequirement{ + {"OAuth2Auth": {"admin", "write"}}, + }, + "routeExists": true, + }, + }, + } + + trial.New(fn, cases).SubTest(t) +} + +func TestRouteSecurityInheritance(t *testing.T) { + type testData struct { + addGlobalSecurity bool + addRouteSecurity bool + } + + fn := func(data testData) (map[string]any, error) { + doc := New("Test API", "1.0.0", "Test") + + // Add security schemes + doc.AddAPIKeyAuth("ApiKeyAuth", "X-API-Key", APIKeyInHeader, "API key") + doc.AddBearerAuth("BearerAuth", BearerFormatJWT, "Bearer token") + + // Add global security if requested + if data.addGlobalSecurity { + doc.AddSecurityRequirement("ApiKeyAuth") + } + + // Create route + route := doc.GetRoute("/test", "GET") + + // Add route-specific security if requested + if data.addRouteSecurity { + route.AddSecurity("BearerAuth") + } + + return map[string]any{ + "globalSecurity": doc.Security, + "routeSecurity": route.Security, + "bothExist": doc.Security != nil && route.Security != nil, + }, nil + } + + cases := trial.Cases[testData, map[string]any]{ + "global_only": { + Input: testData{ + addGlobalSecurity: true, + addRouteSecurity: false, + }, + Expected: map[string]any{ + "globalSecurity": []SecurityRequirement{ + {"ApiKeyAuth": {}}, + }, + "routeSecurity": []SecurityRequirement(nil), + "bothExist": false, + }, + }, + "route_only": { + Input: testData{ + addGlobalSecurity: false, + addRouteSecurity: true, + }, + Expected: map[string]any{ + "globalSecurity": []SecurityRequirement(nil), + "routeSecurity": []SecurityRequirement{ + {"BearerAuth": {}}, + }, + "bothExist": false, + }, + }, + "both_global_and_route": { + Input: testData{ + addGlobalSecurity: true, + addRouteSecurity: true, + }, + Expected: map[string]any{ + "globalSecurity": []SecurityRequirement{ + {"ApiKeyAuth": {}}, + }, + "routeSecurity": []SecurityRequirement{ + {"BearerAuth": {}}, + }, + "bothExist": true, + }, + }, + } + + trial.New(fn, cases).SubTest(t) +} diff --git a/security.go b/security.go index 5900597..e183a26 100644 --- a/security.go +++ b/security.go @@ -109,9 +109,9 @@ func (o *OpenAPI) AddOpenIDConnectAuth(name, openIDConnectURL, description strin } // AddSecurityRequirement adds a security requirement to the OpenAPI specification -// For non-OAuth2 schemes, pass an empty slice for scopes -// For OAuth2 schemes, pass the required scopes -func (o *OpenAPI) AddSecurityRequirement(schemeName string, scopes []string) { +// For non-OAuth2 schemes, pass an empty string for scopes +// For OAuth2 schemes, pass each scope as a separate argument +func (o *OpenAPI) AddSecurityRequirement(schemeName string, scopes ...string) { if o.Security == nil { o.Security = make([]SecurityRequirement, 0) } diff --git a/security_test.go b/security_test.go new file mode 100644 index 0000000..e83bdbb --- /dev/null +++ b/security_test.go @@ -0,0 +1,379 @@ +package openapi + +import ( + "testing" + + "github.com/hydronica/trial" +) + +func TestAddAPIKeyAuth(t *testing.T) { + type input struct { + name string + keyName string + location string + description string + } + + fn := func(i input) (SecurityScheme, error) { + doc := New("Test API", "1.0.0", "Test") + doc.AddAPIKeyAuth(i.name, i.keyName, i.location, i.description) + + scheme, exists := doc.Components.SecuritySchemes[i.name] + if !exists { + return SecurityScheme{}, nil + } + return scheme, nil + } + + cases := trial.Cases[input, SecurityScheme]{ + "header_auth": { + Input: input{ + name: "ApiKeyAuth", + keyName: "X-API-Key", + location: APIKeyInHeader, + description: "API key authentication", + }, + Expected: SecurityScheme{ + Type: SecurityTypeAPIKey, + Name: "X-API-Key", + In: APIKeyInHeader, + Description: "API key authentication", + }, + }, + "query_auth": { + Input: input{ + name: "QueryAuth", + keyName: "api_key", + location: APIKeyInQuery, + description: "Query parameter API key", + }, + Expected: SecurityScheme{ + Type: SecurityTypeAPIKey, + Name: "api_key", + In: APIKeyInQuery, + Description: "Query parameter API key", + }, + }, + "cookie_auth": { + Input: input{ + name: "CookieAuth", + keyName: "auth_token", + location: APIKeyInCookie, + description: "Cookie-based authentication", + }, + Expected: SecurityScheme{ + Type: SecurityTypeAPIKey, + Name: "auth_token", + In: APIKeyInCookie, + Description: "Cookie-based authentication", + }, + }, + } + + trial.New(fn, cases).SubTest(t) +} + +func TestAddBearerAuth(t *testing.T) { + type input struct { + name string + bearerFormat string + description string + } + + fn := func(i input) (SecurityScheme, error) { + doc := New("Test API", "1.0.0", "Test") + doc.AddBearerAuth(i.name, i.bearerFormat, i.description) + + scheme, exists := doc.Components.SecuritySchemes[i.name] + if !exists { + return SecurityScheme{}, nil + } + return scheme, nil + } + + cases := trial.Cases[input, SecurityScheme]{ + "jwt_bearer": { + Input: input{ + name: "BearerAuth", + bearerFormat: BearerFormatJWT, + description: "JWT Bearer token", + }, + Expected: SecurityScheme{ + Type: SecurityTypeHTTP, + Scheme: HTTPSchemeBearer, + BearerFormat: BearerFormatJWT, + In: APIKeyInHeader, + Description: "JWT Bearer token", + }, + }, + "custom_bearer": { + Input: input{ + name: "CustomBearer", + bearerFormat: "Custom", + description: "Custom bearer token", + }, + Expected: SecurityScheme{ + Type: SecurityTypeHTTP, + Scheme: HTTPSchemeBearer, + BearerFormat: "Custom", + In: APIKeyInHeader, + Description: "Custom bearer token", + }, + }, + } + + trial.New(fn, cases).SubTest(t) +} + +func TestAddBasicAuth(t *testing.T) { + type input struct { + name string + description string + } + + fn := func(i input) (SecurityScheme, error) { + doc := New("Test API", "1.0.0", "Test") + doc.AddBasicAuth(i.name, i.description) + + scheme, exists := doc.Components.SecuritySchemes[i.name] + if !exists { + return SecurityScheme{}, nil + } + return scheme, nil + } + + cases := trial.Cases[input, SecurityScheme]{ + "basic_auth": { + Input: input{ + name: "BasicAuth", + description: "HTTP Basic authentication", + }, + Expected: SecurityScheme{ + Type: SecurityTypeHTTP, + Scheme: HTTPSchemeBasic, + Description: "HTTP Basic authentication", + }, + }, + } + + trial.New(fn, cases).SubTest(t) +} + +func TestAddOAuth2Auth(t *testing.T) { + type input struct { + name string + flows *Flows + description string + } + + fn := func(i input) (SecurityScheme, error) { + doc := New("Test API", "1.0.0", "Test") + doc.AddOAuth2Auth(i.name, i.flows, i.description) + + scheme, exists := doc.Components.SecuritySchemes[i.name] + if !exists { + return SecurityScheme{}, nil + } + return scheme, nil + } + + testFlows := &Flows{ + AuthorizationCode: &Flow{ + AuthorizationURL: "https://example.com/auth", + TokenURL: "https://example.com/token", + Scopes: map[string]string{ + "read": "Read access", + "write": "Write access", + }, + }, + } + + cases := trial.Cases[input, SecurityScheme]{ + "oauth2_auth": { + Input: input{ + name: "OAuth2Auth", + flows: testFlows, + description: "OAuth2 authentication", + }, + Expected: SecurityScheme{ + Type: SecurityTypeOAuth2, + Flows: testFlows, + Description: "OAuth2 authentication", + }, + }, + } + + trial.New(fn, cases).SubTest(t) +} + +func TestAddOpenIDConnectAuth(t *testing.T) { + type input struct { + name string + openIDConnectURL string + description string + } + + fn := func(i input) (SecurityScheme, error) { + doc := New("Test API", "1.0.0", "Test") + doc.AddOpenIDConnectAuth(i.name, i.openIDConnectURL, i.description) + + scheme, exists := doc.Components.SecuritySchemes[i.name] + if !exists { + return SecurityScheme{}, nil + } + return scheme, nil + } + + cases := trial.Cases[input, SecurityScheme]{ + "openid_connect": { + Input: input{ + name: "OpenIDConnect", + openIDConnectURL: "https://example.com/.well-known/openid_configuration", + description: "OpenID Connect authentication", + }, + Expected: SecurityScheme{ + Type: SecurityTypeOpenID, + OpenIDConnectURL: "https://example.com/.well-known/openid_configuration", + Description: "OpenID Connect authentication", + }, + }, + } + + trial.New(fn, cases).SubTest(t) +} + +func TestAddSecurityRequirement(t *testing.T) { + type input struct { + schemeName string + scopes []string + } + + fn := func(i input) ([]SecurityRequirement, error) { + doc := New("Test API", "1.0.0", "Test") + doc.AddSecurityRequirement(i.schemeName, i.scopes...) + return doc.Security, nil + } + + cases := trial.Cases[input, []SecurityRequirement]{ + "api_key_requirement": { + Input: input{ + schemeName: "ApiKeyAuth", + scopes: []string{}, + }, + Expected: []SecurityRequirement{ + { + "ApiKeyAuth": {}, + }, + }, + }, + "oauth2_with_scopes": { + Input: input{ + schemeName: "OAuth2Auth", + scopes: []string{"read", "write"}, + }, + Expected: []SecurityRequirement{ + { + "OAuth2Auth": {"read", "write"}, + }, + }, + }, + } + + trial.New(fn, cases).SubTest(t) +} + +func TestAddMultipleSecurityRequirement(t *testing.T) { + type input struct { + schemes map[string][]string + } + + fn := func(i input) ([]SecurityRequirement, error) { + doc := New("Test API", "1.0.0", "Test") + doc.AddMultipleSecurityRequirement(i.schemes) + return doc.Security, nil + } + + cases := trial.Cases[input, []SecurityRequirement]{ + "multiple_schemes": { + Input: input{ + schemes: map[string][]string{ + "ApiKeyAuth": {}, + "OAuth2Auth": {"read"}, + }, + }, + Expected: []SecurityRequirement{ + { + "ApiKeyAuth": {}, + "OAuth2Auth": {"read"}, + }, + }, + }, + "single_scheme_in_map": { + Input: input{ + schemes: map[string][]string{ + "BearerAuth": {}, + }, + }, + Expected: []SecurityRequirement{ + { + "BearerAuth": {}, + }, + }, + }, + } + + trial.New(fn, cases).SubTest(t) +} + +func TestSecurityConstants(t *testing.T) { + // Test that constants have the correct values + fn := func(constant string) (string, error) { + return constant, nil + } + + cases := trial.Cases[string, string]{ + "SecurityTypeAPIKey": {Input: SecurityTypeAPIKey, Expected: "apiKey"}, + "SecurityTypeHTTP": {Input: SecurityTypeHTTP, Expected: "http"}, + "SecurityTypeOAuth2": {Input: SecurityTypeOAuth2, Expected: "oauth2"}, + "SecurityTypeOpenID": {Input: SecurityTypeOpenID, Expected: "openIdConnect"}, + "HTTPSchemeBearer": {Input: HTTPSchemeBearer, Expected: "bearer"}, + "HTTPSchemeBasic": {Input: HTTPSchemeBasic, Expected: "basic"}, + "APIKeyInQuery": {Input: APIKeyInQuery, Expected: "query"}, + "APIKeyInHeader": {Input: APIKeyInHeader, Expected: "header"}, + "APIKeyInCookie": {Input: APIKeyInCookie, Expected: "cookie"}, + "BearerFormatJWT": {Input: BearerFormatJWT, Expected: "JWT"}, + } + + trial.New(fn, cases).SubTest(t) +} + +func TestSecuritySchemeInitialization(t *testing.T) { + fn := func(authType string) (bool, error) { + doc := New("Test API", "1.0.0", "Test") + + // Initially, Components.SecuritySchemes should be nil + if doc.Components.SecuritySchemes != nil { + return false, nil + } + + // After adding any auth method, it should be initialized + switch authType { + case "apikey": + doc.AddAPIKeyAuth("test", "key", APIKeyInHeader, "desc") + case "bearer": + doc.AddBearerAuth("test", BearerFormatJWT, "desc") + case "basic": + doc.AddBasicAuth("test", "desc") + } + + return doc.Components.SecuritySchemes != nil, nil + } + + cases := trial.Cases[string, bool]{ + "apikey_initializes": {Input: "apikey", Expected: true}, + "bearer_initializes": {Input: "bearer", Expected: true}, + "basic_initializes": {Input: "basic", Expected: true}, + } + + trial.New(fn, cases).SubTest(t) +} From dd473d6489f2a08d2ba33aed7a5bcd47e8a1e193 Mon Sep 17 00:00:00 2001 From: "jeremiah.zink" Date: Mon, 28 Jul 2025 12:26:40 -0600 Subject: [PATCH 9/9] updated svg docs with new security objects --- docs/chart.drawio.svg | 788 +++++++++++++++++++++++------------------- 1 file changed, 430 insertions(+), 358 deletions(-) diff --git a/docs/chart.drawio.svg b/docs/chart.drawio.svg index 4cfa07b..4dc4b43 100644 --- a/docs/chart.drawio.svg +++ b/docs/chart.drawio.svg @@ -1,11 +1,11 @@ - + - + -
+
OpenAPI @@ -19,20 +19,20 @@
- + OpenAPI... - - - - - + + + + + -
+
Route @@ -40,16 +40,16 @@
- + Route - + -
+

@@ -72,16 +72,16 @@

- + Tags: []string... - + -
+
path|method @@ -89,63 +89,20 @@
- + path|method - - - - - + + + + + -
-
-
- Media -
-
-
-
- - Media - -
-
- - - - -
-
-
- Response - -
- Desc: string -
- Status Code -
-
-
-
-
-
- - Response... - -
-
- - - - - - -
+
Code @@ -153,35 +110,16 @@
- + Code - - - - - - -
-
-
- MIMEType -
-
-
-
- - MIMEType - -
-
- + -
+
Info @@ -189,7 +127,7 @@
- + Info @@ -197,7 +135,7 @@ -
+

@@ -235,20 +173,20 @@

- + Title: string... - - - - - + + + + + -
+
Paths: Router @@ -256,18 +194,18 @@
- + Paths: Router - - - + + + -
+
Tags: []Tag @@ -275,18 +213,18 @@
- + Tags: []Tag - - - + + + -
+
Servers:[]Server @@ -294,16 +232,16 @@
- + Servers:[]Server - + -
+
Params: @@ -314,16 +252,16 @@
- + Params: []Param - + -
+
RequestBody @@ -331,18 +269,18 @@
- + RequestBody - - - + + + -
+
Responses @@ -350,16 +288,16 @@
- + Responses - + -
+
ExternalDocs: @@ -367,117 +305,185 @@
- + ExternalDocs: - - - + -
+
- Content + Example
- - Content + + Example - + -
+
- Schema -
- -
-
+ Tag
- - Schema + + Tag - -
+
+
+
+ + + Name: string +
+ Desc: string +
+
+
+
+
+
+ + + Name: string... + + + + + + + +
+
+
+ ExternalDocs: +
+
+
+
+ + ExternalDocs: + +
+
+ + + + +
- Request Body + Server
- - Request Body + + Server
-
-
+
+
-

- + + + URL: string +
Desc: string +
-

-

- - Required: bool - -

+
- - Desc: string... + + URL: string... - + -
+
+
+
+ ExternalDocs: +
+
+
+ + + ExternalDocs: + + + + + + + + + +
+
+
+ Components +
+
+
+
+ + Components + +
+
+ + + + +
- Content + Component
- - Content + + Component
- - - + + + -
+
Schema @@ -485,236 +491,280 @@
- + Schema - - - + + + -
+
-
- Examples +
+ id
- - Examples + + id -
-
+
+
- - - - Title: string -
-
-
- - Desc: string -
- Type: string -
-
-
-
+ @schema
- - Title: string... + + @schema - - - + -
+
+
+
+ Security +
+
+
+ + + Security + + + + + + + + + +
- Items + Media
- - Items + + Media
- - - + -
+
- Properties + Response + +
+ Desc: string +
+ Status Code +
+
- - Properties + + Response... - - - + + + -
+
- Name + MIMEType
- - Name + + MIMEType - + + + -
+
- Param + Content
- - Param + + Content + -
-
+
+
+
+ Schema +
+ +
+
+
+
+
+ + + Schema + + + + + + + +
+
+
+ Request Body +
+
+
+
+ + Request Body + +
+
+ + + +
+

- - Name: string - -

-

- + Desc: string

- In: string + Required: bool -
-

-

-

- - Name: string... + + Desc: string...
- - - + -
+
- Schema + Content
- - Schema + + Content - - - + + + -
+
- Examples + Schema
- - Examples + + Schema - + + + -
+
- Example + Examples
- - Example + + Examples -
+
+ + + Title: string +
+
+
- Summary: string -
- Desc: String -
- Value: any + Desc: string +
+ Type: string +

@@ -722,231 +772,253 @@
- - Summary: string... + + Title: string... - - - + + + -
+
-
- Name +
+ Items
- - Name + + Items - + + + -
+
- Tag + Properties
- - Tag + + Properties + + + -
-
-
- - - Name: string -
- Desc: string -
-
-
+
+
+
+ Name
- - Name: string... + + Name - + -
-
-
- ExternalDocs: +
+
+
+ Param
- - ExternalDocs: + + Param - -
-
-
- Server +
+
+
+

+ + Name: string + +

+

+ + Desc: string + +

+

+ + In: string + +
+

+

+
+

- - Server + + Name: string... + + + -
-
-
- - - URL: string -
- Desc: string -
-
-
+
+
+
+ Schema
- - URL: string... + + Schema - + + + -
-
-
- ExternalDocs: +
+
+
+ Examples
- - ExternalDocs: + + Examples - - - -
-
-
- Components +
+
+
+ + + Summary: string +
+ Desc: String +
+ Value: any +
+
+
- - Components + + Summary: string... - + + + -
+
-
- Component +
+ Name
- - Component + + Name - - - + -
-
-
- Schema +
+
+
+ Global +
+ Security
- - Schema + + Global... - - - + + + -
-
-
- id +
+
+
+ JWT
- - id + + JWT + -
+
-
- @schema +
+ API Key
- - @schema + + API Key