diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 000000000..d59f9b490 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,10 @@ +package docs + +import ( + _ "embed" +) + +// OpenAPIJSON holds the raw OpenAPI 3.0 JSON specification for the Permify API. +// +//go:embed api-reference/openapi.json +var OpenAPIJSON []byte diff --git a/internal/servers/openapi.go b/internal/servers/openapi.go new file mode 100644 index 000000000..60c79c827 --- /dev/null +++ b/internal/servers/openapi.go @@ -0,0 +1,19 @@ +package servers + +import ( + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + + "github.com/Permify/permify/docs" +) + +// RegisterOpenAPIHandlers registers the OpenAPI endpoint on the gRPC-Gateway ServeMux +// to expose the API specification over HTTP. +func RegisterOpenAPIHandlers(mux *runtime.ServeMux) error { + return mux.HandlePath(http.MethodGet, "/openapi.json", func(w http.ResponseWriter, r *http.Request, _ map[string]string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(docs.OpenAPIJSON) + }) +} diff --git a/internal/servers/openapi_test.go b/internal/servers/openapi_test.go new file mode 100644 index 000000000..6c8f6c341 --- /dev/null +++ b/internal/servers/openapi_test.go @@ -0,0 +1,44 @@ +package servers + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/stretchr/testify/require" +) + +func TestRegisterOpenAPIHandlers(t *testing.T) { + mux := runtime.NewServeMux() + err := RegisterOpenAPIHandlers(mux) + require.NoError(t, err) + + t.Run("GET /openapi.json returns 200 and valid OpenAPI JSON", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/openapi.json", nil) + w := httptest.NewRecorder() + + mux.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + require.Equal(t, "application/json", w.Header().Get("Content-Type")) + + var data map[string]any + err := json.Unmarshal(w.Body.Bytes(), &data) + require.NoError(t, err) + require.Equal(t, "3.0.0", data["openapi"]) + info, ok := data["info"].(map[string]any) + require.True(t, ok) + require.Equal(t, "Permify API", info["title"]) + }) + + t.Run("GET /unregistered returns 404", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/unregistered", nil) + w := httptest.NewRecorder() + + mux.ServeHTTP(w, req) + + require.Equal(t, http.StatusNotFound, w.Code) + }) +} diff --git a/internal/servers/server.go b/internal/servers/server.go index 6c7fa525f..1a4779f9a 100644 --- a/internal/servers/server.go +++ b/internal/servers/server.go @@ -329,6 +329,9 @@ func (s *Container) Run( if err = grpcV1.RegisterTenancyHandler(ctx, mux, conn); err != nil { return err } + if err = RegisterOpenAPIHandlers(mux); err != nil { + return err + } corsHandler := cors.New(cors.Options{ // CORS configuration AllowCredentials: true, // Allow credentials