feat(openapi): document GraphQL playground endpoint
Adds GraphQL Playground coverage to the generated OpenAPI spec when the GraphQL playground option is enabled, and wires the engine metadata through so runtime docs match the mounted route. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
41615bbe47
commit
273bc3d70a
4 changed files with 143 additions and 1 deletions
101
openapi.go
101
openapi.go
|
|
@ -25,6 +25,7 @@ type SpecBuilder struct {
|
|||
Description string
|
||||
Version string
|
||||
GraphQLPath string
|
||||
GraphQLPlayground bool
|
||||
SSEPath string
|
||||
TermsOfService string
|
||||
ContactName string
|
||||
|
|
@ -192,6 +193,10 @@ func (sb *SpecBuilder) buildPaths(groups []preparedRouteGroup) map[string]any {
|
|||
if graphqlPath := strings.TrimSpace(sb.GraphQLPath); graphqlPath != "" {
|
||||
graphqlPath = normaliseOpenAPIPath(graphqlPath)
|
||||
paths[graphqlPath] = graphqlPathItem(graphqlPath, operationIDs)
|
||||
if sb.GraphQLPlayground {
|
||||
playgroundPath := normaliseOpenAPIPath(graphqlPath + "/playground")
|
||||
paths[playgroundPath] = graphqlPlaygroundPathItem(playgroundPath, operationIDs)
|
||||
}
|
||||
}
|
||||
|
||||
if ssePath := strings.TrimSpace(sb.SSEPath); ssePath != "" {
|
||||
|
|
@ -693,6 +698,23 @@ func graphqlPathItem(path string, operationIDs map[string]int) map[string]any {
|
|||
}
|
||||
}
|
||||
|
||||
func graphqlPlaygroundPathItem(path string, operationIDs map[string]int) map[string]any {
|
||||
return map[string]any{
|
||||
"get": map[string]any{
|
||||
"summary": "GraphQL playground",
|
||||
"description": "Interactive GraphQL IDE for the configured schema",
|
||||
"tags": []string{"graphql"},
|
||||
"operationId": operationID("get", path, operationIDs),
|
||||
"security": []any{
|
||||
map[string]any{
|
||||
"bearerAuth": []any{},
|
||||
},
|
||||
},
|
||||
"responses": graphqlPlaygroundResponses(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ssePathItem(path string, operationIDs map[string]int) map[string]any {
|
||||
return map[string]any{
|
||||
"get": map[string]any{
|
||||
|
|
@ -939,6 +961,85 @@ func graphqlResponses() map[string]any {
|
|||
}
|
||||
}
|
||||
|
||||
func graphqlPlaygroundResponses() map[string]any {
|
||||
successHeaders := mergeHeaders(standardResponseHeaders(), rateLimitSuccessHeaders())
|
||||
errorHeaders := mergeHeaders(standardResponseHeaders(), rateLimitSuccessHeaders())
|
||||
|
||||
return map[string]any{
|
||||
"200": map[string]any{
|
||||
"description": "GraphQL playground",
|
||||
"content": map[string]any{
|
||||
"text/html": map[string]any{
|
||||
"schema": map[string]any{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"headers": successHeaders,
|
||||
},
|
||||
"401": map[string]any{
|
||||
"description": "Unauthorised",
|
||||
"content": map[string]any{
|
||||
"application/json": map[string]any{
|
||||
"schema": map[string]any{
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"headers": errorHeaders,
|
||||
},
|
||||
"403": map[string]any{
|
||||
"description": "Forbidden",
|
||||
"content": map[string]any{
|
||||
"application/json": map[string]any{
|
||||
"schema": map[string]any{
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"headers": errorHeaders,
|
||||
},
|
||||
"429": map[string]any{
|
||||
"description": "Too many requests",
|
||||
"content": map[string]any{
|
||||
"application/json": map[string]any{
|
||||
"schema": map[string]any{
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"headers": mergeHeaders(standardResponseHeaders(), rateLimitHeaders()),
|
||||
},
|
||||
"500": map[string]any{
|
||||
"description": "Internal server error",
|
||||
"content": map[string]any{
|
||||
"application/json": map[string]any{
|
||||
"schema": map[string]any{
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"headers": errorHeaders,
|
||||
},
|
||||
"504": map[string]any{
|
||||
"description": "Gateway timeout",
|
||||
"content": map[string]any{
|
||||
"application/json": map[string]any{
|
||||
"schema": map[string]any{
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"headers": errorHeaders,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func sseResponses() map[string]any {
|
||||
successHeaders := mergeHeaders(
|
||||
standardResponseHeaders(),
|
||||
|
|
|
|||
|
|
@ -267,6 +267,43 @@ func TestSpecBuilder_Good_GraphQLEndpoint(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSpecBuilder_Good_GraphQLPlaygroundEndpoint(t *testing.T) {
|
||||
sb := &api.SpecBuilder{
|
||||
Title: "Test",
|
||||
Version: "1.0.0",
|
||||
GraphQLPath: "/graphql",
|
||||
GraphQLPlayground: true,
|
||||
}
|
||||
|
||||
data, err := sb.Build(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var spec map[string]any
|
||||
if err := json.Unmarshal(data, &spec); err != nil {
|
||||
t.Fatalf("invalid JSON: %v", err)
|
||||
}
|
||||
|
||||
paths := spec["paths"].(map[string]any)
|
||||
pathItem, ok := paths["/graphql/playground"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("expected /graphql/playground path in spec")
|
||||
}
|
||||
|
||||
getOp := pathItem["get"].(map[string]any)
|
||||
if getOp["operationId"] != "get_graphql_playground" {
|
||||
t.Fatalf("expected playground operationId to be get_graphql_playground, got %v", getOp["operationId"])
|
||||
}
|
||||
|
||||
responses := getOp["responses"].(map[string]any)
|
||||
success := responses["200"].(map[string]any)
|
||||
content := success["content"].(map[string]any)
|
||||
if _, ok := content["text/html"]; !ok {
|
||||
t.Fatal("expected text/html content type for GraphQL playground response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecBuilder_Good_ServerSentEventsEndpoint(t *testing.T) {
|
||||
sb := &api.SpecBuilder{
|
||||
Title: "Test",
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ func (e *Engine) OpenAPISpecBuilder() *SpecBuilder {
|
|||
|
||||
if e.graphql != nil {
|
||||
builder.GraphQLPath = e.graphql.path
|
||||
builder.GraphQLPlayground = e.graphql.playground
|
||||
}
|
||||
if e.sseBroker != nil {
|
||||
builder.SSEPath = resolveSSEPath(e.ssePath)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ func TestEngine_Good_OpenAPISpecBuilderCarriesEngineMetadata(t *testing.T) {
|
|||
api.WithSwaggerServers("https://api.example.com", "/", "https://api.example.com"),
|
||||
api.WithSwaggerLicense("EUPL-1.2", "https://eupl.eu/1.2/en/"),
|
||||
api.WithSwaggerExternalDocs("Developer guide", "https://example.com/docs"),
|
||||
api.WithGraphQL(newTestSchema(), api.WithGraphQLPath("/gql")),
|
||||
api.WithGraphQL(newTestSchema(), api.WithPlayground(), api.WithGraphQLPath("/gql")),
|
||||
api.WithSSE(broker),
|
||||
api.WithSSEPath("/events"),
|
||||
api.WithPprof(),
|
||||
|
|
@ -106,6 +106,9 @@ func TestEngine_Good_OpenAPISpecBuilderCarriesEngineMetadata(t *testing.T) {
|
|||
if _, ok := paths["/gql"]; !ok {
|
||||
t.Fatal("expected GraphQL path from engine metadata in generated spec")
|
||||
}
|
||||
if _, ok := paths["/gql/playground"]; !ok {
|
||||
t.Fatal("expected GraphQL playground path from engine metadata in generated spec")
|
||||
}
|
||||
if _, ok := paths["/events"]; !ok {
|
||||
t.Fatal("expected SSE path from engine metadata in generated spec")
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue