diff --git a/cmd/api/cmd_spec.go b/cmd/api/cmd_spec.go index c91122f..57d6af6 100644 --- a/cmd/api/cmd_spec.go +++ b/cmd/api/cmd_spec.go @@ -77,6 +77,7 @@ func registerSpecBuilderFlags(cmd *cli.Command, cfg *specBuilderConfig) { cli.StringFlag(cmd, &cfg.swaggerPath, "swagger-path", "", "", "Swagger UI path in generated spec") cli.StringFlag(cmd, &cfg.graphqlPath, "graphql-path", "", "", "GraphQL endpoint path in generated spec") cli.BoolFlag(cmd, &cfg.graphqlPlayground, "graphql-playground", "", false, "Include the GraphQL playground endpoint in generated spec") + cli.StringFlag(cmd, &cfg.graphqlPlaygroundPath, "graphql-playground-path", "", "", "GraphQL playground path in generated spec") cli.StringFlag(cmd, &cfg.ssePath, "sse-path", "", "", "SSE endpoint path in generated spec") cli.StringFlag(cmd, &cfg.wsPath, "ws-path", "", "", "WebSocket endpoint path in generated spec") cli.BoolFlag(cmd, &cfg.pprofEnabled, "pprof", "", false, "Include pprof endpoints in generated spec") diff --git a/cmd/api/cmd_test.go b/cmd/api/cmd_test.go index 1f3b812..5b50fdc 100644 --- a/cmd/api/cmd_test.go +++ b/cmd/api/cmd_test.go @@ -103,6 +103,9 @@ func TestAPISpecCmd_Good_JSON(t *testing.T) { if specCmd.Flag("graphql-playground") == nil { t.Fatal("expected --graphql-playground flag on spec command") } + if specCmd.Flag("graphql-playground-path") == nil { + t.Fatal("expected --graphql-playground-path flag on spec command") + } if specCmd.Flag("sse-path") == nil { t.Fatal("expected --sse-path flag on spec command") } @@ -456,6 +459,50 @@ func TestAPISpecCmd_Good_GraphQLPlaygroundFlagPopulatesSpecPaths(t *testing.T) { } } +func TestAPISpecCmd_Good_GraphQLPlaygroundPathFlagOverridesGeneratedPath(t *testing.T) { + root := &cli.Command{Use: "root"} + AddAPICommands(root) + + outputFile := t.TempDir() + "/spec.json" + root.SetArgs([]string{ + "api", "spec", + "--graphql-path", "/graphql", + "--graphql-playground", + "--graphql-playground-path", "/graphql-ui", + "--output", outputFile, + }) + root.SetErr(new(bytes.Buffer)) + + if err := root.Execute(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + data, err := os.ReadFile(outputFile) + if err != nil { + t.Fatalf("expected spec file to be written: %v", err) + } + + var spec map[string]any + if err := json.Unmarshal(data, &spec); err != nil { + t.Fatalf("expected valid JSON spec, got error: %v", err) + } + + paths, ok := spec["paths"].(map[string]any) + if !ok { + t.Fatal("expected paths object in generated spec") + } + if _, ok := paths["/graphql-ui"]; !ok { + t.Fatal("expected custom GraphQL playground path in generated spec") + } + if _, ok := paths["/graphql/playground"]; ok { + t.Fatal("expected default GraphQL playground path to be overridden") + } + + if got := spec["x-graphql-playground-path"]; got != "/graphql-ui" { + t.Fatalf("expected x-graphql-playground-path=/graphql-ui, got %v", got) + } +} + func TestAPISpecCmd_Good_EnabledExtensionsFollowProvidedPaths(t *testing.T) { root := &cli.Command{Use: "root"} AddAPICommands(root) diff --git a/cmd/api/spec_builder.go b/cmd/api/spec_builder.go index f271504..bdd32b3 100644 --- a/cmd/api/spec_builder.go +++ b/cmd/api/spec_builder.go @@ -17,6 +17,7 @@ type specBuilderConfig struct { swaggerPath string graphqlPath string graphqlPlayground bool + graphqlPlaygroundPath string ssePath string wsPath string pprofEnabled bool @@ -61,6 +62,7 @@ func newSpecBuilder(cfg specBuilderConfig) (*goapi.SpecBuilder, error) { GraphQLEnabled: graphqlPath != "" || cfg.graphqlPlayground, GraphQLPath: graphqlPath, GraphQLPlayground: cfg.graphqlPlayground, + GraphQLPlaygroundPath: strings.TrimSpace(cfg.graphqlPlaygroundPath), SSEEnabled: ssePath != "", SSEPath: ssePath, WSEnabled: wsPath != "", diff --git a/openapi.go b/openapi.go index d032aaa..66260b7 100644 --- a/openapi.go +++ b/openapi.go @@ -299,7 +299,11 @@ func (sb *SpecBuilder) buildPaths(groups []preparedRouteGroup) map[string]any { } paths[graphqlPath] = item if sb.GraphQLPlayground { - playgroundPath := normaliseOpenAPIPath(graphqlPath + "/playground") + playgroundPath := sb.effectiveGraphQLPlaygroundPath() + if playgroundPath == "" { + playgroundPath = graphqlPath + "/playground" + } + playgroundPath = normaliseOpenAPIPath(playgroundPath) item := graphqlPlaygroundPathItem(playgroundPath, operationIDs) if isPublicPathForList(playgroundPath, publicPaths) { makePathItemPublic(item)