diff --git a/platform-api/src/internal/dto/openapi.go b/platform-api/src/internal/dto/openapi.go index fdbf0438..34064f43 100644 --- a/platform-api/src/internal/dto/openapi.go +++ b/platform-api/src/internal/dto/openapi.go @@ -35,3 +35,10 @@ type OpenAPIValidationResponse struct { Errors []string `json:"errors,omitempty"` API *API `json:"api,omitempty"` } + +// ImportOpenAPIRequest represents the request for importing an OpenAPI definition +type ImportOpenAPIRequest struct { + URL string `form:"url"` // Optional: URL to fetch OpenAPI definition + Definition *multipart.FileHeader `form:"definition"` // Optional: Uploaded OpenAPI file (JSON/YAML) + API API `form:"api"` // API details for the imported definition +} diff --git a/platform-api/src/internal/handler/api.go b/platform-api/src/internal/handler/api.go index 24155b6a..3aedaa20 100644 --- a/platform-api/src/internal/handler/api.go +++ b/platform-api/src/internal/handler/api.go @@ -18,6 +18,7 @@ package handler import ( + "encoding/json" "errors" "log" "net/http" @@ -26,6 +27,7 @@ import ( "platform-api/src/internal/middleware" "platform-api/src/internal/service" "platform-api/src/internal/utils" + "strings" "github.com/gin-gonic/gin" ) @@ -810,6 +812,153 @@ func (h *APIHandler) ValidateOpenAPI(c *gin.Context) { c.JSON(http.StatusOK, response) } +// ImportOpenAPI handles POST /import/open-api and imports an API from OpenAPI definition +func (h *APIHandler) ImportOpenAPI(c *gin.Context) { + orgId, exists := middleware.GetOrganizationFromContext(c) + if !exists { + c.JSON(http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", + "Organization claim not found in token")) + return + } + + // Parse multipart form + err := c.Request.ParseMultipartForm(10 << 20) // 10 MB max + if err != nil { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Failed to parse multipart form")) + return + } + + var req dto.ImportOpenAPIRequest + + // Get URL from form if provided + if url := c.PostForm("url"); url != "" { + req.URL = url + } + + // Get definition file from form if provided + if file, header, err := c.Request.FormFile("definition"); err == nil { + req.Definition = header + defer file.Close() + } + + // Validate that at least one input is provided + if req.URL == "" && req.Definition == nil { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Either URL or definition file must be provided")) + return + } + + // Get API details from form data (JSON string in 'api' field) + apiJSON := c.PostForm("api") + if apiJSON == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "API details are required")) + return + } + + // Parse API details from JSON string + if err := json.Unmarshal([]byte(apiJSON), &req.API); err != nil { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Invalid API details: "+err.Error())) + return + } + + if req.API.Name == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "API name is required")) + return + } + if req.API.Context == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "API context is required")) + return + } + if req.API.Version == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "API version is required")) + return + } + if req.API.ProjectID == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Project ID is required")) + return + } + + // Import API from OpenAPI definition + api, err := h.apiService.ImportFromOpenAPI(&req, orgId) + if err != nil { + if errors.Is(err, constants.ErrAPIAlreadyExists) { + c.JSON(http.StatusConflict, utils.NewErrorResponse(409, "Conflict", + "API already exists in the project")) + return + } + if errors.Is(err, constants.ErrProjectNotFound) { + c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "Project not found")) + return + } + if errors.Is(err, constants.ErrInvalidAPIName) { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Invalid API name format")) + return + } + if errors.Is(err, constants.ErrInvalidAPIContext) { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Invalid API context format")) + return + } + if errors.Is(err, constants.ErrInvalidAPIVersion) { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Invalid API version format")) + return + } + if errors.Is(err, constants.ErrInvalidLifecycleState) { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Invalid lifecycle status")) + return + } + if errors.Is(err, constants.ErrInvalidAPIType) { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Invalid API type")) + return + } + if errors.Is(err, constants.ErrInvalidTransport) { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Invalid transport protocol")) + return + } + // Handle OpenAPI-specific errors + if strings.Contains(err.Error(), "failed to fetch OpenAPI from URL") { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Failed to fetch OpenAPI definition from URL")) + return + } + if strings.Contains(err.Error(), "failed to open OpenAPI definition file") || + strings.Contains(err.Error(), "failed to read OpenAPI definition file") { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Failed to fetch OpenAPI definition from file")) + return + } + if strings.Contains(err.Error(), "failed to validate and parse OpenAPI definition") { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Invalid OpenAPI definition")) + return + } + if strings.Contains(err.Error(), "failed to merge API details") { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Failed to create API from OpenAPI definition: incompatible details")) + return + } + + c.JSON(http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Failed to import API from OpenAPI definition")) + return + } + + c.JSON(http.StatusCreated, api) +} + // RegisterRoutes registers all API routes func (h *APIHandler) RegisterRoutes(r *gin.Engine) { // API routes @@ -830,6 +979,7 @@ func (h *APIHandler) RegisterRoutes(r *gin.Engine) { importGroup := r.Group("/api/v1/import") { importGroup.POST("/api-project", h.ImportAPIProject) + importGroup.POST("/open-api", h.ImportOpenAPI) } validateGroup := r.Group("/api/v1/validate") { diff --git a/platform-api/src/internal/service/api.go b/platform-api/src/internal/service/api.go index 46f76e48..fda62896 100644 --- a/platform-api/src/internal/service/api.go +++ b/platform-api/src/internal/service/api.go @@ -1375,3 +1375,76 @@ func (s *APIService) ValidateOpenAPIDefinition(req *dto.ValidateOpenAPIRequest) return response, nil } + +// ImportFromOpenAPI imports an API from an OpenAPI definition +func (s *APIService) ImportFromOpenAPI(req *dto.ImportOpenAPIRequest, orgId string) (*dto.API, error) { + var content []byte + var err error + var errorList []string + + // If URL is provided, fetch content from URL + if req.URL != "" { + content, err = s.apiUtil.FetchOpenAPIFromURL(req.URL) + if err != nil { + content = make([]byte, 0) + errorList = append(errorList, fmt.Sprintf("failed to fetch OpenAPI from URL: %s", err.Error())) + } + } + + // If definition file is provided, read from file + if req.Definition != nil { + file, err := req.Definition.Open() + if err != nil { + errorList = append(errorList, fmt.Sprintf("failed to open OpenAPI definition file: %s", err.Error())) + return nil, fmt.Errorf(strings.Join(errorList, "; ")) + } + defer file.Close() + + content, err = io.ReadAll(file) + if err != nil { + errorList = append(errorList, fmt.Sprintf("failed to read OpenAPI definition file: %s", err.Error())) + return nil, fmt.Errorf(strings.Join(errorList, "; ")) + } + } + + // If neither URL nor file is provided + if len(content) == 0 { + errorList = append(errorList, "either URL or definition file must be provided") + return nil, fmt.Errorf(strings.Join(errorList, "; ")) + } + + // Validate and parse the OpenAPI definition + apiDetails, err := s.apiUtil.ValidateAndParseOpenAPI(content) + if err != nil { + return nil, fmt.Errorf("failed to validate and parse OpenAPI definition: %w", err) + } + + // Merge provided API details with extracted details from OpenAPI + mergedAPI := s.apiUtil.MergeAPIDetails(&req.API, apiDetails) + if mergedAPI == nil { + return nil, errors.New("failed to merge API details") + } + + // Create API using existing CreateAPI logic + createReq := &CreateAPIRequest{ + Name: mergedAPI.Name, + DisplayName: mergedAPI.DisplayName, + Description: mergedAPI.Description, + Context: mergedAPI.Context, + Version: mergedAPI.Version, + Provider: mergedAPI.Provider, + ProjectID: mergedAPI.ProjectID, + LifeCycleStatus: mergedAPI.LifeCycleStatus, + Type: mergedAPI.Type, + Transport: mergedAPI.Transport, + MTLS: mergedAPI.MTLS, + Security: mergedAPI.Security, + CORS: mergedAPI.CORS, + BackendServices: mergedAPI.BackendServices, + APIRateLimiting: mergedAPI.APIRateLimiting, + Operations: mergedAPI.Operations, + } + + // Create the API + return s.CreateAPI(createReq, orgId) +} diff --git a/platform-api/src/internal/service/gateway_test.go b/platform-api/src/internal/service/gateway_test.go index d3f6a3b4..f300fc48 100644 --- a/platform-api/src/internal/service/gateway_test.go +++ b/platform-api/src/internal/service/gateway_test.go @@ -18,6 +18,7 @@ package service import ( + "platform-api/src/internal/constants" "testing" ) @@ -26,143 +27,206 @@ func TestValidateGatewayInput(t *testing.T) { service := &GatewayService{} tests := []struct { - name string - orgID string - gatewayName string - displayName string - vhost string - wantErr bool - errContains string + name string + orgID string + gatewayName string + displayName string + vhost string + functionalityType string + wantErr bool + errContains string }{ { - name: "valid input", - orgID: "123e4567-e89b-12d3-a456-426614174000", - gatewayName: "prod-gateway-01", - displayName: "Production Gateway 01", - vhost: "api.example.com", - wantErr: false, + name: "valid input", + orgID: "123e4567-e89b-12d3-a456-426614174000", + gatewayName: "prod-gateway-01", + displayName: "Production Gateway 01", + vhost: "api.example.com", + functionalityType: constants.GatewayFunctionalityTypeRegular, + wantErr: false, }, { - name: "empty organization ID", - orgID: "", - gatewayName: "prod-gateway-01", - displayName: "Production Gateway 01", - vhost: "api.example.com", - wantErr: true, - errContains: "organization ID is required", + name: "empty organization ID", + orgID: "", + gatewayName: "prod-gateway-01", + displayName: "Production Gateway 01", + vhost: "api.example.com", + functionalityType: constants.GatewayFunctionalityTypeRegular, + wantErr: true, + errContains: "organization ID is required", }, { - name: "invalid organization ID format", - orgID: "not-a-uuid", - gatewayName: "prod-gateway-01", - displayName: "Production Gateway 01", - vhost: "api.example.com", - wantErr: true, - errContains: "invalid organization ID format", + name: "invalid organization ID format", + orgID: "not-a-uuid", + gatewayName: "prod-gateway-01", + displayName: "Production Gateway 01", + vhost: "api.example.com", + functionalityType: constants.GatewayFunctionalityTypeRegular, + wantErr: true, + errContains: "invalid organization ID format", }, { - name: "empty gateway name", - orgID: "123e4567-e89b-12d3-a456-426614174000", - gatewayName: "", - displayName: "Production Gateway 01", - vhost: "api.example.com", - wantErr: true, - errContains: "gateway name is required", + name: "empty gateway name", + orgID: "123e4567-e89b-12d3-a456-426614174000", + gatewayName: "", + displayName: "Production Gateway 01", + vhost: "api.example.com", + functionalityType: constants.GatewayFunctionalityTypeRegular, + wantErr: true, + errContains: "gateway name is required", }, { - name: "gateway name too short", - orgID: "123e4567-e89b-12d3-a456-426614174000", - gatewayName: "ab", - displayName: "Production Gateway 01", - vhost: "api.example.com", - wantErr: true, - errContains: "at least 3 characters", + name: "gateway name too short", + orgID: "123e4567-e89b-12d3-a456-426614174000", + gatewayName: "ab", + displayName: "Production Gateway 01", + vhost: "api.example.com", + functionalityType: constants.GatewayFunctionalityTypeRegular, + wantErr: true, + errContains: "at least 3 characters", }, { - name: "gateway name too long", - orgID: "123e4567-e89b-12d3-a456-426614174000", - gatewayName: "this-is-a-very-long-gateway-name-that-exceeds-the-maximum-length-of-64-characters", - displayName: "Production Gateway 01", - vhost: "api.example.com", - wantErr: true, - errContains: "must not exceed 64 characters", + name: "gateway name too long", + orgID: "123e4567-e89b-12d3-a456-426614174000", + gatewayName: "this-is-a-very-long-gateway-name-that-exceeds-the-maximum-length-of-64-characters", + displayName: "Production Gateway 01", + vhost: "api.example.com", + functionalityType: constants.GatewayFunctionalityTypeRegular, + wantErr: true, + errContains: "must not exceed 64 characters", }, { - name: "gateway name with uppercase", - orgID: "123e4567-e89b-12d3-a456-426614174000", - gatewayName: "Prod-Gateway-01", - displayName: "Production Gateway 01", - vhost: "api.example.com", - wantErr: true, - errContains: "lowercase letters, numbers, and hyphens", + name: "gateway name with uppercase", + orgID: "123e4567-e89b-12d3-a456-426614174000", + gatewayName: "Prod-Gateway-01", + displayName: "Production Gateway 01", + vhost: "api.example.com", + functionalityType: constants.GatewayFunctionalityTypeRegular, + wantErr: true, + errContains: "lowercase letters, numbers, and hyphens", }, { - name: "gateway name with special characters", - orgID: "123e4567-e89b-12d3-a456-426614174000", - gatewayName: "prod_gateway_01", - displayName: "Production Gateway 01", - vhost: "api.example.com", - wantErr: true, - errContains: "lowercase letters, numbers, and hyphens", + name: "gateway name with special characters", + orgID: "123e4567-e89b-12d3-a456-426614174000", + gatewayName: "prod_gateway_01", + displayName: "Production Gateway 01", + vhost: "api.example.com", + functionalityType: constants.GatewayFunctionalityTypeRegular, + wantErr: true, + errContains: "lowercase letters, numbers, and hyphens", }, { - name: "gateway name with leading hyphen", - orgID: "123e4567-e89b-12d3-a456-426614174000", - gatewayName: "-prod-gateway-01", - displayName: "Production Gateway 01", - vhost: "api.example.com", - wantErr: true, - errContains: "cannot start or end with a hyphen", + name: "gateway name with leading hyphen", + orgID: "123e4567-e89b-12d3-a456-426614174000", + gatewayName: "-prod-gateway-01", + displayName: "Production Gateway 01", + vhost: "api.example.com", + functionalityType: constants.GatewayFunctionalityTypeRegular, + wantErr: true, + errContains: "cannot start or end with a hyphen", }, { - name: "gateway name with trailing hyphen", - orgID: "123e4567-e89b-12d3-a456-426614174000", - gatewayName: "prod-gateway-01-", - displayName: "Production Gateway 01", - vhost: "api.example.com", - wantErr: true, - errContains: "cannot start or end with a hyphen", + name: "gateway name with trailing hyphen", + orgID: "123e4567-e89b-12d3-a456-426614174000", + gatewayName: "prod-gateway-01-", + displayName: "Production Gateway 01", + vhost: "api.example.com", + functionalityType: constants.GatewayFunctionalityTypeRegular, + wantErr: true, + errContains: "cannot start or end with a hyphen", }, { - name: "empty display name", - orgID: "123e4567-e89b-12d3-a456-426614174000", - gatewayName: "prod-gateway-01", - displayName: "", - vhost: "api.example.com", - wantErr: true, - errContains: "display name is required", + name: "empty display name", + orgID: "123e4567-e89b-12d3-a456-426614174000", + gatewayName: "prod-gateway-01", + displayName: "", + vhost: "api.example.com", + functionalityType: constants.GatewayFunctionalityTypeRegular, + wantErr: true, + errContains: "display name is required", }, { - name: "display name too long", - orgID: "123e4567-e89b-12d3-a456-426614174000", - gatewayName: "prod-gateway-01", - displayName: "This is a very long display name that exceeds the maximum allowed length of 128 characters which should trigger a validation error in the system", - vhost: "api.example.com", - wantErr: true, - errContains: "must not exceed 128 characters", + name: "display name too long", + orgID: "123e4567-e89b-12d3-a456-426614174000", + gatewayName: "prod-gateway-01", + displayName: "This is a very long display name that exceeds the maximum allowed length of 128 characters which should trigger a validation error in the system", + vhost: "api.example.com", + functionalityType: constants.GatewayFunctionalityTypeRegular, + wantErr: true, + errContains: "must not exceed 128 characters", }, { - name: "empty display name", - orgID: "123e4567-e89b-12d3-a456-426614174000", - gatewayName: "prod-gateway-01", - displayName: "Production Gateway 01", - vhost: "", - wantErr: true, - errContains: "vhost is required", + name: "empty vhost", + orgID: "123e4567-e89b-12d3-a456-426614174000", + gatewayName: "prod-gateway-01", + displayName: "Production Gateway 01", + vhost: "", + functionalityType: constants.GatewayFunctionalityTypeRegular, + wantErr: true, + errContains: "vhost is required", }, { - name: "display name with spaces (valid)", - orgID: "123e4567-e89b-12d3-a456-426614174000", - gatewayName: "prod-gateway-01", - displayName: "Production Gateway 01 - Main", - vhost: "api.example.com", - wantErr: false, + name: "display name with spaces (valid)", + orgID: "123e4567-e89b-12d3-a456-426614174000", + gatewayName: "prod-gateway-01", + displayName: "Production Gateway 01 - Main", + vhost: "api.example.com", + functionalityType: constants.GatewayFunctionalityTypeRegular, + wantErr: false, + }, + { + name: "empty functionality type", + orgID: "123e4567-e89b-12d3-a456-426614174000", + gatewayName: "prod-gateway-01", + displayName: "Production Gateway 01", + vhost: "api.example.com", + functionalityType: "", + wantErr: true, + errContains: "functionality type is required", + }, + { + name: "whitespace-only functionality type", + orgID: "123e4567-e89b-12d3-a456-426614174000", + gatewayName: "prod-gateway-01", + displayName: "Production Gateway 01", + vhost: "api.example.com", + functionalityType: " ", + wantErr: true, + errContains: "functionality type is required", + }, + { + name: "invalid functionality type", + orgID: "123e4567-e89b-12d3-a456-426614174000", + gatewayName: "prod-gateway-01", + displayName: "Production Gateway 01", + vhost: "api.example.com", + functionalityType: "invalid-type", + wantErr: true, + errContains: "invalid functionality type", + }, + { + name: "valid functionality type - ai", + orgID: "123e4567-e89b-12d3-a456-426614174000", + gatewayName: "ai-gateway-01", + displayName: "AI Gateway 01", + vhost: "ai.example.com", + functionalityType: "ai", + wantErr: false, + }, + { + name: "valid functionality type - event", + orgID: "123e4567-e89b-12d3-a456-426614174000", + gatewayName: "event-gateway-01", + displayName: "Event Gateway 01", + vhost: "events.example.com", + functionalityType: "event", + wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := service.validateGatewayInput(tt.orgID, tt.gatewayName, tt.displayName, tt.vhost) + err := service.validateGatewayInput(tt.orgID, tt.gatewayName, tt.displayName, tt.vhost, tt.functionalityType) if (err != nil) != tt.wantErr { t.Errorf("validateGatewayInput() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/platform-api/src/internal/utils/api.go b/platform-api/src/internal/utils/api.go index 5d039762..d05bff56 100644 --- a/platform-api/src/internal/utils/api.go +++ b/platform-api/src/internal/utils/api.go @@ -1432,3 +1432,104 @@ func (u *APIUtil) convertSwagger2ToBackendServices(host, basePath string, scheme return backendServices } + +// ValidateAndParseOpenAPI validates and parses OpenAPI definition content +func (u *APIUtil) ValidateAndParseOpenAPI(content []byte) (*dto.API, error) { + // Validate the OpenAPI definition + if err := u.ValidateOpenAPIDefinition(content); err != nil { + return nil, fmt.Errorf("invalid OpenAPI definition: %w", err) + } + + // Parse and extract API details + api, err := u.ParseAPIDefinition(content) + if err != nil { + return nil, fmt.Errorf("failed to parse OpenAPI definition: %w", err) + } + + return api, nil +} + +// MergeAPIDetails merges user-provided API details with extracted OpenAPI details +// User-provided details take precedence over extracted details +func (u *APIUtil) MergeAPIDetails(userAPI *dto.API, extractedAPI *dto.API) *dto.API { + if userAPI == nil || extractedAPI == nil { + return nil + } + + merged := &dto.API{} + + // Required fields from user input (these must be provided) + merged.Name = userAPI.Name + merged.Context = userAPI.Context + merged.Version = userAPI.Version + merged.ProjectID = userAPI.ProjectID + + // Optional fields - use user input if provided, otherwise use extracted values + if userAPI.DisplayName != "" { + merged.DisplayName = userAPI.DisplayName + } else { + merged.DisplayName = extractedAPI.DisplayName + } + + if userAPI.Description != "" { + merged.Description = userAPI.Description + } else { + merged.Description = extractedAPI.Description + } + + if userAPI.Provider != "" { + merged.Provider = userAPI.Provider + } else { + merged.Provider = extractedAPI.Provider + } + + if userAPI.Type != "" { + merged.Type = userAPI.Type + } else { + merged.Type = extractedAPI.Type + } + + if len(userAPI.Transport) > 0 { + merged.Transport = userAPI.Transport + } else { + merged.Transport = extractedAPI.Transport + } + + if userAPI.LifeCycleStatus != "" { + merged.LifeCycleStatus = userAPI.LifeCycleStatus + } else { + merged.LifeCycleStatus = extractedAPI.LifeCycleStatus + } + + if len(userAPI.BackendServices) > 0 { + merged.BackendServices = userAPI.BackendServices + } else { + merged.BackendServices = extractedAPI.BackendServices + } + + // Use extracted operations from OpenAPI + merged.Operations = extractedAPI.Operations + + // Use user-provided configuration if available + if userAPI.MTLS != nil { + merged.MTLS = userAPI.MTLS + } + if userAPI.Security != nil { + merged.Security = userAPI.Security + } + if userAPI.CORS != nil { + merged.CORS = userAPI.CORS + } + if userAPI.APIRateLimiting != nil { + merged.APIRateLimiting = userAPI.APIRateLimiting + } + + // Copy boolean fields from user input + merged.HasThumbnail = userAPI.HasThumbnail + merged.IsDefaultVersion = userAPI.IsDefaultVersion + merged.IsRevision = userAPI.IsRevision + merged.RevisionedAPIID = userAPI.RevisionedAPIID + merged.RevisionID = userAPI.RevisionID + + return merged +} diff --git a/platform-api/src/resources/openapi.yaml b/platform-api/src/resources/openapi.yaml index e47350a2..e62f6f9a 100644 --- a/platform-api/src/resources/openapi.yaml +++ b/platform-api/src/resources/openapi.yaml @@ -296,6 +296,51 @@ paths: '500': $ref: '#/components/responses/InternalServerError' + /import/open-api: + post: + summary: Import and create API from OpenAPI definition + description: | + Imports an OpenAPI definition into the platform from a provided URL or file upload. + An API is created from the imported OpenAPI definition and is associated with a specified project within the + organization in the JWT token. + operationId: ImportOpenAPI + tags: + - APIs + requestBody: + description: OpenAPI import details + required: true + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/ImportOpenAPIRequest' + encoding: + url: + contentType: text/plain + style: form + definition: + contentType: application/octet-stream, application/json, application/yaml, text/yaml + style: form + api: + contentType: application/json + style: form + responses: + '201': + description: OpenAPI imported successfully + content: + application/json: + schema: + $ref: '#/components/schemas/API' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + '500': + $ref: '#/components/responses/InternalServerError' + /import/api-project: post: summary: Import API project @@ -2568,6 +2613,50 @@ components: - required: [url] - required: [definition] + ImportOpenAPIRequest: + type: object + required: + - api + description: | + Multipart form data request for importing OpenAPI definition. All fields are provided as form data. + Either 'url' or 'definition' must be provided to specify the OpenAPI source, and 'api' is required + to provide the API details for creation. If the OpenAPI definition is valid and successfully imported, + and the api details are valid an API will be created. + properties: + url: + type: string + format: uri + description: | + Form field containing URL to fetch the OpenAPI definition from. + example: "https://petstore3.swagger.io/api/v3/openapi.json" + definition: + type: string + format: binary + description: | + Form field for OpenAPI definition file upload (YAML or JSON). + This should be provided as a file upload in the multipart form. + api: + type: string + description: | + Form field containing JSON-encoded API details for the imported OpenAPI. + This must be a JSON string containing the API configuration. + Required fields within the JSON: name, context, version, projectId. + example: | + { + "name": "PetStore API", + "displayName": "PetStore API v3", + "description": "This is a sample Pet Store Server", + "context": "/petstore", + "version": "1.0.0", + "provider": "PetStore Inc", + "projectId": "550e8400-e29b-41d4-a716-446655440000", + "type": "HTTP", + "transport": ["http", "https"] + } + anyOf: + - required: [ url ] + - required: [ definition ] + APIProjectValidationResponse: type: object required: