plugin(3scale): merge api docs for the same service (#1249)
* fix(3scale): merge api docs for the same service * fix(3scale): merge API docs titles and descriptions * fix(3scale): fix title bug, improve tests * fix(3scale): add changeset * fix(3scale): remove console.log * fix(3scale): add license headers * fix(3scale): remove not working tsc command, use build instead of it * fix(3scale): dedupe 3scale yarn.lock file Signed-off-by: Oleksandr Andriienko <oandriie@redhat.com>
This commit is contained in:
parent
9328fa7ed0
commit
d8c823820b
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@backstage-community/plugin-3scale-backend': major
|
||||
---
|
||||
|
||||
Merge API docs for the same service.
|
||||
|
|
@ -33,22 +33,27 @@
|
|||
"postversion": "yarn run export-dynamic",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"start": "backstage-cli package start",
|
||||
"test": "backstage-cli package test --passWithNoTests --coverage",
|
||||
"tsc": "tsc"
|
||||
"test": "backstage-cli package test --passWithNoTests --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-plugin-api": "^1.0.0",
|
||||
"@backstage/catalog-model": "^1.7.0",
|
||||
"@backstage/errors": "^1.2.4",
|
||||
"@backstage/plugin-catalog-node": "^1.13.0"
|
||||
"@backstage/plugin-catalog-node": "^1.13.0",
|
||||
"atlassian-openapi": "^1.0.19",
|
||||
"openapi-merge": "^1.3.3",
|
||||
"swagger-converter": "2.1.0",
|
||||
"swagger2openapi": "^7.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-defaults": "^0.5.0",
|
||||
"@backstage/backend-test-utils": "0.4.4",
|
||||
"@backstage/cli": "^0.27.1",
|
||||
"@backstage/config": "^1.2.0",
|
||||
"@backstage/plugin-catalog-backend": "^1.26.0",
|
||||
"@janus-idp/cli": "1.13.1",
|
||||
"@types/supertest": "6.0.2",
|
||||
"@types/swagger2openapi": "^7.0.4",
|
||||
"msw": "1.3.3",
|
||||
"supertest": "7.0.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,199 @@
|
|||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"version": "1.0.0",
|
||||
"title": "Echo API.",
|
||||
"description": "A sample echo API."
|
||||
},
|
||||
"paths": {
|
||||
"/": {
|
||||
"get": {
|
||||
"description": "Echo API with no parameters",
|
||||
"operationId": "echo_no_params",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "user_key",
|
||||
"in": "query",
|
||||
"description": "Your API access key",
|
||||
"required": true,
|
||||
"x-data-threescale-name": "user_keys",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ResponseModel"
|
||||
}
|
||||
},
|
||||
"application/xml": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ResponseModel"
|
||||
}
|
||||
},
|
||||
"text/xml": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ResponseModel"
|
||||
}
|
||||
},
|
||||
"text/html": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ResponseModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "unexpected error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorModel"
|
||||
}
|
||||
},
|
||||
"application/xml": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorModel"
|
||||
}
|
||||
},
|
||||
"text/xml": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorModel"
|
||||
}
|
||||
},
|
||||
"text/html": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/{echo}": {
|
||||
"get": {
|
||||
"description": "Echo API with parameters",
|
||||
"operationId": "echo_with_params",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "echo",
|
||||
"in": "path",
|
||||
"description": "The string to be echoed",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "user_key",
|
||||
"in": "query",
|
||||
"description": "Your API access key",
|
||||
"required": true,
|
||||
"x-data-threescale-name": "user_keys",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ResponseModel"
|
||||
}
|
||||
},
|
||||
"application/xml": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ResponseModel"
|
||||
}
|
||||
},
|
||||
"text/xml": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ResponseModel"
|
||||
}
|
||||
},
|
||||
"text/html": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ResponseModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "unexpected error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorModel"
|
||||
}
|
||||
},
|
||||
"application/xml": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorModel"
|
||||
}
|
||||
},
|
||||
"text/xml": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorModel"
|
||||
}
|
||||
},
|
||||
"text/html": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "http://echo-api.3scale.net/"
|
||||
}
|
||||
],
|
||||
"components": {
|
||||
"schemas": {
|
||||
"ResponseModel": {
|
||||
"type": "object",
|
||||
"required": ["method", "path", "args", "headers"],
|
||||
"properties": {
|
||||
"method": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"args": {
|
||||
"type": "string"
|
||||
},
|
||||
"headers": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ErrorModel": {
|
||||
"type": "object",
|
||||
"required": ["code", "message"],
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "Ping Service.",
|
||||
"description": "A simple API that responds with the input message.",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://api.example.com/v1",
|
||||
"description": "Production Server"
|
||||
},
|
||||
{
|
||||
"url": "https://api.staging.example.com/v1",
|
||||
"description": "Staging Server"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/ping": {
|
||||
"post": {
|
||||
"summary": "Ping message",
|
||||
"description": "Returns the same message that was sent in the request body.",
|
||||
"operationId": "pingMessage",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Hello, world!"
|
||||
}
|
||||
},
|
||||
"required": ["message"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The echoed message",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Hello, world!"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid input, missing 'message' field"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"PingRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Hello, world!"
|
||||
}
|
||||
},
|
||||
"required": ["message"]
|
||||
},
|
||||
"PingResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Hello, world!"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
"apiVersion": "1.2",
|
||||
"swaggerVersion": "1.2",
|
||||
"basePath": "https://api.example.com/v1",
|
||||
"resourcePath": "/profile",
|
||||
"apis": [
|
||||
{
|
||||
"path": "/profile/{userId}",
|
||||
"description": "User profile operations",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get user profile by user ID",
|
||||
"notes": "Returns a user's profile details by their ID.",
|
||||
"type": "User",
|
||||
"nickname": "getUserProfile",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "ID of the user to fetch",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 200,
|
||||
"message": "Successful response",
|
||||
"responseModel": "User"
|
||||
},
|
||||
{
|
||||
"code": 404,
|
||||
"message": "User not found"
|
||||
},
|
||||
{
|
||||
"code": 500,
|
||||
"message": "Server error"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"models": {
|
||||
"User": {
|
||||
"id": "User",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "User ID",
|
||||
"example": "1001"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "User name",
|
||||
"example": "John Doe"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"description": "User email address",
|
||||
"example": "test@example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"version": "1.0.0",
|
||||
"title": "Simple API.",
|
||||
"description": "List users API."
|
||||
},
|
||||
"host": "api.example.com",
|
||||
"basePath": "/v1",
|
||||
"schemes": ["https"],
|
||||
"paths": {
|
||||
"/users": {
|
||||
"get": {
|
||||
"summary": "Get all users",
|
||||
"description": "Returns a list of users.",
|
||||
"produces": ["application/json"],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A list of users.",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/User"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{userId}": {
|
||||
"get": {
|
||||
"summary": "Get a user by ID",
|
||||
"description": "Returns a single user.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A single user.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/User"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "User not found."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"User": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
{
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "[Merged 2 API docs] Ping Service. Echo API.",
|
||||
"description": "[Merged 2 API docs] A simple API that responds with the input message. A sample echo API.",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://api.example.com/v1",
|
||||
"description": "Production Server"
|
||||
},
|
||||
{
|
||||
"url": "https://api.staging.example.com/v1",
|
||||
"description": "Staging Server"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/ping": {
|
||||
"post": {
|
||||
"summary": "Ping message",
|
||||
"description": "Returns the same message that was sent in the request body.",
|
||||
"operationId": "pingMessage",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Hello, world!"
|
||||
}
|
||||
},
|
||||
"required": ["message"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The echoed message",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Hello, world!"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid input, missing 'message' field"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/": {
|
||||
"get": {
|
||||
"description": "Echo API with no parameters",
|
||||
"operationId": "echo_no_params",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "user_key",
|
||||
"in": "query",
|
||||
"description": "Your API access key",
|
||||
"required": true,
|
||||
"x-data-threescale-name": "user_keys",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ResponseModel"
|
||||
}
|
||||
},
|
||||
"application/xml": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ResponseModel"
|
||||
}
|
||||
},
|
||||
"text/xml": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ResponseModel"
|
||||
}
|
||||
},
|
||||
"text/html": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ResponseModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "unexpected error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorModel"
|
||||
}
|
||||
},
|
||||
"application/xml": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorModel"
|
||||
}
|
||||
},
|
||||
"text/xml": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorModel"
|
||||
}
|
||||
},
|
||||
"text/html": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/{echo}": {
|
||||
"get": {
|
||||
"description": "Echo API with parameters",
|
||||
"operationId": "echo_with_params",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "echo",
|
||||
"in": "path",
|
||||
"description": "The string to be echoed",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "user_key",
|
||||
"in": "query",
|
||||
"description": "Your API access key",
|
||||
"required": true,
|
||||
"x-data-threescale-name": "user_keys",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ResponseModel"
|
||||
}
|
||||
},
|
||||
"application/xml": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ResponseModel"
|
||||
}
|
||||
},
|
||||
"text/xml": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ResponseModel"
|
||||
}
|
||||
},
|
||||
"text/html": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ResponseModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "unexpected error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorModel"
|
||||
}
|
||||
},
|
||||
"application/xml": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorModel"
|
||||
}
|
||||
},
|
||||
"text/xml": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorModel"
|
||||
}
|
||||
},
|
||||
"text/html": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"PingRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Hello, world!"
|
||||
}
|
||||
},
|
||||
"required": ["message"]
|
||||
},
|
||||
"PingResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Hello, world!"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ResponseModel": {
|
||||
"type": "object",
|
||||
"required": ["method", "path", "args", "headers"],
|
||||
"properties": {
|
||||
"method": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"args": {
|
||||
"type": "string"
|
||||
},
|
||||
"headers": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ErrorModel": {
|
||||
"type": "object",
|
||||
"required": ["code", "message"],
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
{
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "[Merged 3 API docs] Ping Service. Simple API. Title was not specified",
|
||||
"description": "[Merged 3 API docs] A simple API that responds with the input message. List users API.",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://api.example.com/v1",
|
||||
"description": "Production Server"
|
||||
},
|
||||
{
|
||||
"url": "https://api.staging.example.com/v1",
|
||||
"description": "Staging Server"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/ping": {
|
||||
"post": {
|
||||
"summary": "Ping message",
|
||||
"description": "Returns the same message that was sent in the request body.",
|
||||
"operationId": "pingMessage",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Hello, world!"
|
||||
}
|
||||
},
|
||||
"required": ["message"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The echoed message",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Hello, world!"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid input, missing 'message' field"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users": {
|
||||
"get": {
|
||||
"summary": "Get all users",
|
||||
"description": "Returns a list of users.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A list of users.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/User"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{userId}": {
|
||||
"get": {
|
||||
"summary": "Get a user by ID",
|
||||
"description": "Returns a single user.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A single user.",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/User"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "User not found."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/profile/{userId}": {
|
||||
"get": {
|
||||
"operationId": "getUserProfile",
|
||||
"summary": "Get user profile by user ID",
|
||||
"description": "Returns a user's profile details by their ID.",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"description": "ID of the user to fetch",
|
||||
"name": "userId",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/User1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "User not found"
|
||||
},
|
||||
"500": {
|
||||
"description": "Server error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"PingRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Hello, world!"
|
||||
}
|
||||
},
|
||||
"required": ["message"]
|
||||
},
|
||||
"PingResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Hello, world!"
|
||||
}
|
||||
}
|
||||
},
|
||||
"User": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"User1": {
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"description": "User email address",
|
||||
"example": "test@example.com"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "User ID",
|
||||
"example": "1001"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "User name",
|
||||
"example": "John Doe"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"host": "api.example.com",
|
||||
"basePath": "/v1",
|
||||
"schemes": ["https"],
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "Title was not specified",
|
||||
"version": "1.2"
|
||||
},
|
||||
"paths": {
|
||||
"/profile/{userId}": {
|
||||
"get": {
|
||||
"operationId": "getUserProfile",
|
||||
"summary": "Get user profile by user ID",
|
||||
"description": "Returns a user's profile details by their ID.",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"description": "ID of the user to fetch",
|
||||
"name": "userId",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/User"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "User not found"
|
||||
},
|
||||
"500": {
|
||||
"description": "Server error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"User": {
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"description": "User email address",
|
||||
"example": "test@example.com"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "User ID",
|
||||
"example": "1001"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "User name",
|
||||
"example": "John Doe"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"proxy": {
|
||||
"service_id": 2,
|
||||
"endpoint": "https://api-3scale-apicast-production.apps.rosa.ceazq-ocd3j-vy9.8aja.p3.openshiftapps.com:443",
|
||||
"api_backend": "https://echo-api.3scale.net:443",
|
||||
"credentials_location": "query",
|
||||
"auth_app_key": "app_key",
|
||||
"auth_app_id": "app_id",
|
||||
"auth_user_key": "user_key",
|
||||
"error_auth_failed": "Authentication failed",
|
||||
"error_auth_missing": "Authentication parameters missing",
|
||||
"error_status_auth_failed": 403,
|
||||
"error_headers_auth_failed": "text/plain; charset=us-ascii",
|
||||
"error_status_auth_missing": 403,
|
||||
"error_headers_auth_missing": "text/plain; charset=us-ascii",
|
||||
"error_no_match": "No Mapping Rule matched",
|
||||
"error_status_no_match": 404,
|
||||
"error_headers_no_match": "text/plain; charset=us-ascii",
|
||||
"error_limits_exceeded": "Usage limit exceeded",
|
||||
"error_status_limits_exceeded": 429,
|
||||
"error_headers_limits_exceeded": "text/plain; charset=us-ascii",
|
||||
"secret_token": "Shared_secret_sent_from_proxy_to_API_backend_60e69c4e8b2d7666",
|
||||
"sandbox_endpoint": "https://api-3scale-apicast-staging.apps.rosa.ceazq-ocd3j-vy9.8aja.p3.openshiftapps.com:443",
|
||||
"api_test_path": "/",
|
||||
"api_test_success": true,
|
||||
"policies_config": [
|
||||
{
|
||||
"name": "apicast",
|
||||
"version": "builtin",
|
||||
"configuration": {},
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"created_at": "2024-09-17T09:57:53Z",
|
||||
"updated_at": "2024-09-17T10:06:42Z",
|
||||
"deployment_option": "hosted",
|
||||
"lock_version": 1,
|
||||
"links": [
|
||||
{
|
||||
"rel": "mapping_rules",
|
||||
"href": "/admin/api/services/2/proxy/mapping_rules"
|
||||
},
|
||||
{
|
||||
"rel": "self",
|
||||
"href": "/admin/api/services/2/proxy"
|
||||
},
|
||||
{
|
||||
"rel": "service",
|
||||
"href": "/admin/api/services/2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"services": [
|
||||
{
|
||||
"service": {
|
||||
"id": 2,
|
||||
"name": "API",
|
||||
"description": "Nice API",
|
||||
"state": "incomplete",
|
||||
"system_name": "api",
|
||||
"backend_version": "1",
|
||||
"deployment_option": "hosted",
|
||||
"support_email": "admin@3scale.apps.rosa.ceazq-ocd3j-vy9.8aja.p3.openshiftapps.com",
|
||||
"intentions_required": false,
|
||||
"buyers_manage_apps": true,
|
||||
"buyers_manage_keys": true,
|
||||
"referrer_filters_required": false,
|
||||
"custom_keys_enabled": true,
|
||||
"buyer_key_regenerate_enabled": true,
|
||||
"mandatory_app_key": true,
|
||||
"buyer_can_select_plan": false,
|
||||
"buyer_plan_change_permission": "request",
|
||||
"created_at": "2024-09-17T09:57:53Z",
|
||||
"updated_at": "2024-09-17T10:06:42Z",
|
||||
"links": [
|
||||
{
|
||||
"rel": "metrics",
|
||||
"href": "https://3scale-admin.apps.rosa.ceazq-ocd3j-vy9.8aja.p3.openshiftapps.com/admin/api/services/2/metrics"
|
||||
},
|
||||
{
|
||||
"rel": "self",
|
||||
"href": "https://3scale-admin.apps.rosa.ceazq-ocd3j-vy9.8aja.p3.openshiftapps.com/admin/api/services/2"
|
||||
},
|
||||
{
|
||||
"rel": "service_plans",
|
||||
"href": "https://3scale-admin.apps.rosa.ceazq-ocd3j-vy9.8aja.p3.openshiftapps.com/admin/api/services/2/service_plans"
|
||||
},
|
||||
{
|
||||
"rel": "application_plans",
|
||||
"href": "https://3scale-admin.apps.rosa.ceazq-ocd3j-vy9.8aja.p3.openshiftapps.com/admin/api/services/2/application_plans"
|
||||
},
|
||||
{
|
||||
"rel": "features",
|
||||
"href": "https://3scale-admin.apps.rosa.ceazq-ocd3j-vy9.8aja.p3.openshiftapps.com/admin/api/services/2/features"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,356 @@
|
|||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { mockServices } from '@backstage/backend-test-utils';
|
||||
import { ThreeScaleApiEntityProvider } from './ThreeScaleApiEntityProvider';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import {
|
||||
SchedulerService,
|
||||
SchedulerServiceTaskRunner,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { resolve } from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
const requestJsonDataMock = jest.fn().mockResolvedValue([]);
|
||||
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () => Promise.resolve(requestJsonDataMock()),
|
||||
headers: new Headers(),
|
||||
text: () => Promise.resolve('mocked text data'),
|
||||
} as Response),
|
||||
);
|
||||
|
||||
const loggerMock = mockServices.logger.mock();
|
||||
|
||||
const schedulerTaskRunnerMock = {
|
||||
run: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
const shedulerServiceMock = {
|
||||
triggerTask: jest.fn().mockImplementation(),
|
||||
scheduleTask: jest.fn().mockImplementation(),
|
||||
createScheduledTaskRunner: jest.fn().mockImplementation(),
|
||||
getScheduledTasks: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
const entityProviderConnection = {
|
||||
applyMutation: jest.fn().mockImplementation(),
|
||||
refresh: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('ThreeScaleApiEntityProvider', () => {
|
||||
let conf: ConfigReader;
|
||||
|
||||
beforeEach(() => {
|
||||
conf = new ConfigReader({
|
||||
catalog: {
|
||||
providers: {
|
||||
threeScaleApiEntity: {
|
||||
test: {
|
||||
baseUrl: 'test',
|
||||
accessToken: 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
function createApiEntityProvider(
|
||||
schedule: SchedulerServiceTaskRunner,
|
||||
scheduler: SchedulerService,
|
||||
): ThreeScaleApiEntityProvider[] {
|
||||
return ThreeScaleApiEntityProvider.fromConfig(
|
||||
{ config: conf, logger: loggerMock },
|
||||
{ schedule, scheduler },
|
||||
);
|
||||
}
|
||||
|
||||
it('should be defined', () => {
|
||||
const threeScaleApiEntityProvider = createApiEntityProvider(
|
||||
schedulerTaskRunnerMock,
|
||||
shedulerServiceMock,
|
||||
);
|
||||
expect(threeScaleApiEntityProvider).toBeDefined();
|
||||
expect(threeScaleApiEntityProvider).toBeInstanceOf(Array);
|
||||
expect(threeScaleApiEntityProvider.length).toBe(1);
|
||||
});
|
||||
|
||||
describe('run', () => {
|
||||
let threeScaleApiEntityProvider: ThreeScaleApiEntityProvider;
|
||||
beforeEach(async () => {
|
||||
entityProviderConnection.applyMutation.mockClear();
|
||||
threeScaleApiEntityProvider = createApiEntityProvider(
|
||||
schedulerTaskRunnerMock,
|
||||
shedulerServiceMock,
|
||||
)[0];
|
||||
await threeScaleApiEntityProvider.connect(entityProviderConnection);
|
||||
});
|
||||
|
||||
it('should be created catalog entity with single open API 3.0 doc', async () => {
|
||||
const services = readTestJSONFile('services');
|
||||
requestJsonDataMock.mockResolvedValueOnce(services);
|
||||
|
||||
const openAPI3_0Spec = readTestJSONFile('input/open-api-3.0-doc');
|
||||
const apiDoc = createAPIDoc(
|
||||
'ping',
|
||||
'ping',
|
||||
'A simple API that responds with the input message.',
|
||||
openAPI3_0Spec,
|
||||
);
|
||||
const apiDocs = { api_docs: [apiDoc] };
|
||||
requestJsonDataMock.mockResolvedValueOnce(apiDocs);
|
||||
|
||||
const proxy = readTestJSONFile('proxy');
|
||||
requestJsonDataMock.mockResolvedValueOnce(proxy);
|
||||
|
||||
await threeScaleApiEntityProvider.run();
|
||||
|
||||
const entities = [
|
||||
createExpectedEntity(
|
||||
'input/open-api-3.0-doc',
|
||||
'A simple API that responds with the input message.',
|
||||
),
|
||||
];
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
|
||||
type: 'full',
|
||||
entities,
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created catalog entity with single api doc but swagger 2.0 should not be converted to API 3.0', async () => {
|
||||
const services = readTestJSONFile('services');
|
||||
requestJsonDataMock.mockResolvedValueOnce(services);
|
||||
|
||||
const swagger2_0Spec = readTestJSONFile('input/swagger-2.0-doc');
|
||||
const apiDoc = createAPIDoc(
|
||||
'list-users',
|
||||
'List users API',
|
||||
'List users API.',
|
||||
swagger2_0Spec,
|
||||
);
|
||||
const apiDocs = { api_docs: [apiDoc] };
|
||||
requestJsonDataMock.mockResolvedValueOnce(apiDocs);
|
||||
|
||||
const proxy = readTestJSONFile('proxy');
|
||||
requestJsonDataMock.mockResolvedValueOnce(proxy);
|
||||
|
||||
await threeScaleApiEntityProvider.run();
|
||||
|
||||
const entities = [
|
||||
createExpectedEntity('input/swagger-2.0-doc', 'List users API.'),
|
||||
];
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
|
||||
type: 'full',
|
||||
entities,
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created catalog entity with single api doc but converted from swagger 1.2 to swagger 2.0', async () => {
|
||||
const services = readTestJSONFile('services');
|
||||
requestJsonDataMock.mockResolvedValueOnce(services);
|
||||
const swagger1_2Spec = readTestJSONFile('input/swagger-1.2-doc');
|
||||
const apiDoc = createAPIDoc(
|
||||
'get-user-profile-by-id',
|
||||
'Get User Profile By ID',
|
||||
'User profile API.',
|
||||
swagger1_2Spec,
|
||||
);
|
||||
const apiDocs = { api_docs: [apiDoc] };
|
||||
requestJsonDataMock.mockResolvedValueOnce(apiDocs);
|
||||
const proxy = readTestJSONFile('proxy');
|
||||
requestJsonDataMock.mockResolvedValueOnce(proxy);
|
||||
|
||||
await threeScaleApiEntityProvider.run();
|
||||
|
||||
const entities = [
|
||||
createExpectedEntity(
|
||||
'output/swagger-1.2-converted-to-swagger-2.0',
|
||||
'Nice API',
|
||||
),
|
||||
];
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
|
||||
type: 'full',
|
||||
entities,
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created catalog entity with merged 2 open API 3.0 docs', async () => {
|
||||
const services = readTestJSONFile('services');
|
||||
requestJsonDataMock.mockResolvedValueOnce(services);
|
||||
|
||||
const openAPI3_0Spec1 = readTestJSONFile('input/open-api-3.0-doc');
|
||||
const apiDoc1 = createAPIDoc(
|
||||
'ping',
|
||||
'Ping',
|
||||
'A simple API that responds with the input message.',
|
||||
openAPI3_0Spec1,
|
||||
);
|
||||
const openAPI3_0Spec2 = readTestJSONFile('input/open-api-3.0-doc-2');
|
||||
const apiDoc2 = createAPIDoc(
|
||||
'echo',
|
||||
'Echo',
|
||||
'A sample echo API.',
|
||||
openAPI3_0Spec2,
|
||||
);
|
||||
|
||||
const apiDocs = { api_docs: [apiDoc1, apiDoc2] };
|
||||
|
||||
requestJsonDataMock.mockResolvedValueOnce(apiDocs);
|
||||
|
||||
const proxy = readTestJSONFile('proxy');
|
||||
requestJsonDataMock.mockResolvedValueOnce(proxy);
|
||||
|
||||
await threeScaleApiEntityProvider.run();
|
||||
|
||||
const entities = [
|
||||
createExpectedEntity(
|
||||
'output/merged-2-open-api-3.0-docs',
|
||||
'[Merged 2 API docs] A simple API that responds with the input message. A sample echo API.',
|
||||
),
|
||||
];
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
|
||||
type: 'full',
|
||||
entities,
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created catalog entity with merged 3 api docs in different formats', async () => {
|
||||
const services = readTestJSONFile('services');
|
||||
requestJsonDataMock.mockResolvedValueOnce(services);
|
||||
|
||||
const openAPI3_0Spec1 = readTestJSONFile('input/open-api-3.0-doc');
|
||||
const apiDoc1 = createAPIDoc(
|
||||
'ping',
|
||||
'ping',
|
||||
'A simple API that responds with the input message.',
|
||||
openAPI3_0Spec1,
|
||||
);
|
||||
const swagger2_0Spec = readTestJSONFile('input/swagger-2.0-doc');
|
||||
const apiDoc2 = createAPIDoc(
|
||||
'list-users',
|
||||
'List users API',
|
||||
'List users API.',
|
||||
swagger2_0Spec,
|
||||
);
|
||||
const swagger1_2Spec = readTestJSONFile('input/swagger-1.2-doc');
|
||||
const apiDoc3 = createAPIDoc(
|
||||
'get-user-profile-by-id',
|
||||
'Get User Profile By ID',
|
||||
'User profile API.',
|
||||
swagger1_2Spec,
|
||||
);
|
||||
|
||||
const apiDocs = { api_docs: [apiDoc1, apiDoc2, apiDoc3] };
|
||||
|
||||
requestJsonDataMock.mockResolvedValueOnce(apiDocs);
|
||||
|
||||
const proxy = readTestJSONFile('proxy');
|
||||
requestJsonDataMock.mockResolvedValueOnce(proxy);
|
||||
|
||||
await threeScaleApiEntityProvider.run();
|
||||
|
||||
const entities = [
|
||||
createExpectedEntity(
|
||||
'output/merged-3-different-api-docs',
|
||||
'[Merged 3 API docs] A simple API that responds with the input message. List users API.',
|
||||
),
|
||||
];
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
|
||||
type: 'full',
|
||||
entities,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function readTestJSONFile(fileName: string): any {
|
||||
const file = resolve(__dirname, `./../__fixtures__/data/${fileName}.json`);
|
||||
const fileContent = fs.readFileSync(file, 'utf8');
|
||||
return JSON.parse(fileContent);
|
||||
}
|
||||
|
||||
function createExpectedEntity(
|
||||
fileWithExpectedOpenAPISpec: string,
|
||||
description: string,
|
||||
): any {
|
||||
return {
|
||||
entity: {
|
||||
kind: 'API',
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location': 'url:test/apiconfig/services/2',
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:test/apiconfig/services/2',
|
||||
},
|
||||
name: 'api',
|
||||
description,
|
||||
links: [
|
||||
{
|
||||
url: 'test/apiconfig/services/2',
|
||||
title: '3scale Overview',
|
||||
},
|
||||
{
|
||||
url: 'https://api-3scale-apicast-staging.apps.rosa.ceazq-ocd3j-vy9.8aja.p3.openshiftapps.com:443',
|
||||
title: 'Staging Apicast Endpoint',
|
||||
},
|
||||
{
|
||||
url: 'https://api-3scale-apicast-production.apps.rosa.ceazq-ocd3j-vy9.8aja.p3.openshiftapps.com:443',
|
||||
title: 'Production Apicast Endpoint',
|
||||
},
|
||||
],
|
||||
},
|
||||
spec: {
|
||||
type: 'openapi',
|
||||
lifecycle: 'test',
|
||||
system: '3scale',
|
||||
owner: '3scale',
|
||||
definition: JSON.stringify(
|
||||
readTestJSONFile(fileWithExpectedOpenAPISpec),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
},
|
||||
},
|
||||
locationKey: 'ThreeScaleApiEntityProvider:test',
|
||||
};
|
||||
}
|
||||
|
||||
function createAPIDoc(
|
||||
systemName: string,
|
||||
name: string,
|
||||
description: string,
|
||||
apiDocBody: any,
|
||||
) {
|
||||
return {
|
||||
api_doc: {
|
||||
id: 1,
|
||||
system_name: systemName,
|
||||
name: name,
|
||||
description: description,
|
||||
published: true,
|
||||
skip_swagger_validations: false,
|
||||
body: JSON.stringify(apiDocBody),
|
||||
service_id: 2,
|
||||
created_at: '2024-09-17T10:09:04Z',
|
||||
updated_at: '2024-09-17T10:09:04Z',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -19,7 +19,6 @@ import type {
|
|||
SchedulerService,
|
||||
LoggerService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
import {
|
||||
ANNOTATION_LOCATION,
|
||||
ANNOTATION_ORIGIN_LOCATION,
|
||||
|
|
@ -48,7 +47,14 @@ import type {
|
|||
Services,
|
||||
} from '../clients/types';
|
||||
import { readThreeScaleApiEntityConfigs } from './config';
|
||||
import type { ThreeScaleConfig } from './types';
|
||||
import { isNonEmptyArray, NonEmptyArray, ThreeScaleConfig } from './types';
|
||||
import {
|
||||
isOpenAPI3_0,
|
||||
isSwagger1_2,
|
||||
isSwagger2_0,
|
||||
OpenAPIMergerAndConverter,
|
||||
} from './open-api-merger-converter';
|
||||
import { Swagger } from 'atlassian-openapi';
|
||||
|
||||
export class ThreeScaleApiEntityProvider implements EntityProvider {
|
||||
private static SERVICES_FETCH_SIZE: number = 500;
|
||||
|
|
@ -57,6 +63,7 @@ export class ThreeScaleApiEntityProvider implements EntityProvider {
|
|||
private readonly accessToken: string;
|
||||
private readonly logger: LoggerService;
|
||||
private readonly scheduleFn: () => Promise<void>;
|
||||
private readonly openApiMerger: OpenAPIMergerAndConverter;
|
||||
private connection?: EntityProviderConnection;
|
||||
|
||||
static fromConfig(
|
||||
|
|
@ -119,6 +126,7 @@ export class ThreeScaleApiEntityProvider implements EntityProvider {
|
|||
});
|
||||
|
||||
this.scheduleFn = this.createScheduleFn(taskRunner);
|
||||
this.openApiMerger = new OpenAPIMergerAndConverter();
|
||||
}
|
||||
|
||||
private createScheduleFn(
|
||||
|
|
@ -186,24 +194,19 @@ export class ThreeScaleApiEntityProvider implements EntityProvider {
|
|||
const service = element;
|
||||
this.logger.debug(`Find service ${service.service.name}`);
|
||||
|
||||
// Trying to find the API Doc for the service and validate if api doc was assigned to an API.
|
||||
const apiDoc = apiDocs.api_docs.find(obj => {
|
||||
if (obj.api_doc.service_id !== undefined) {
|
||||
return obj.api_doc.service_id === service.service.id;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const docs = apiDocs.api_docs.filter(
|
||||
obj => obj.api_doc.service_id === service.service.id,
|
||||
);
|
||||
const proxy = await getProxyConfig(
|
||||
this.baseUrl,
|
||||
this.accessToken,
|
||||
service.service.id,
|
||||
);
|
||||
if (apiDoc !== undefined) {
|
||||
this.logger.info(JSON.stringify(apiDoc));
|
||||
const apiEntity: ApiEntity = this.buildApiEntityFromService(
|
||||
if (isNonEmptyArray(docs)) {
|
||||
this.logger.info(JSON.stringify(docs));
|
||||
const apiEntity: ApiEntity = await this.buildApiEntityFromService(
|
||||
service,
|
||||
apiDoc,
|
||||
docs,
|
||||
proxy,
|
||||
);
|
||||
entities.push(apiEntity);
|
||||
|
|
@ -231,14 +234,55 @@ export class ThreeScaleApiEntityProvider implements EntityProvider {
|
|||
});
|
||||
}
|
||||
|
||||
private buildApiEntityFromService(
|
||||
private async buildApiEntityFromService(
|
||||
service: ServiceElement,
|
||||
apiDoc: APIDocElement,
|
||||
apiDocs: NonEmptyArray<APIDocElement>,
|
||||
proxy: Proxy,
|
||||
): ApiEntity {
|
||||
): Promise<ApiEntity> {
|
||||
const location = `url:${this.baseUrl}/apiconfig/services/${service.service.id}`;
|
||||
const serviceDescription = service.service.description || '';
|
||||
let entityDescription: string | undefined;
|
||||
|
||||
const spec = JSON.parse(apiDoc.api_doc.body);
|
||||
const docs = apiDocs.map(doc => JSON.parse(doc.api_doc.body));
|
||||
|
||||
let swaggerDocJSON;
|
||||
if (docs.length > 1) {
|
||||
// convert all docs to openapi 3.0 and merge them
|
||||
let mergedDescription = `[Merged ${docs.length} API docs]`;
|
||||
let mergedTitle = mergedDescription;
|
||||
const convertedDocs: Swagger.SwaggerV3[] = [];
|
||||
for (const doc of docs) {
|
||||
const convertedDoc = await this.openApiMerger.convertAPIDocToOpenAPI3(
|
||||
doc,
|
||||
);
|
||||
convertedDocs.push(convertedDoc);
|
||||
mergedDescription = getDocInfo(convertedDoc)?.description
|
||||
? `${mergedDescription} ${getDocInfo(convertedDoc)?.description}`
|
||||
: mergedDescription;
|
||||
mergedTitle = getDocInfo(convertedDoc)?.title
|
||||
? `${mergedTitle} ${getDocInfo(convertedDoc)?.title}`
|
||||
: mergedTitle;
|
||||
}
|
||||
if (isNonEmptyArray(convertedDocs)) {
|
||||
swaggerDocJSON = await this.openApiMerger.mergeOpenAPI3Docs(
|
||||
convertedDocs,
|
||||
);
|
||||
swaggerDocJSON.info.description = mergedDescription;
|
||||
swaggerDocJSON.info.title = mergedTitle;
|
||||
entityDescription = mergedDescription;
|
||||
}
|
||||
}
|
||||
|
||||
if (docs.length === 1) {
|
||||
swaggerDocJSON = docs[0];
|
||||
|
||||
const spec = JSON.parse(apiDocs[0].api_doc.body);
|
||||
if (isSwagger1_2(spec)) {
|
||||
// Backstage UI can render only openapi 3.0 or swagger 2.0. That's why we need to convert swagger 1.2 to swagger 2.0.
|
||||
swaggerDocJSON = await this.openApiMerger.convertSwagger1_2To2_0(spec);
|
||||
}
|
||||
entityDescription = getDocInfo(spec)?.description;
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'API',
|
||||
|
|
@ -250,8 +294,7 @@ export class ThreeScaleApiEntityProvider implements EntityProvider {
|
|||
},
|
||||
// TODO: add tenant name
|
||||
name: `${service.service.system_name}`,
|
||||
description:
|
||||
spec.info.description || `Version: ${service.service.description}`,
|
||||
description: entityDescription || serviceDescription,
|
||||
// TODO: add labels
|
||||
// labels: this.getApiEntityLabels(service),
|
||||
links: [
|
||||
|
|
@ -274,8 +317,19 @@ export class ThreeScaleApiEntityProvider implements EntityProvider {
|
|||
lifecycle: this.env,
|
||||
system: '3scale',
|
||||
owner: '3scale',
|
||||
definition: apiDoc.api_doc.body,
|
||||
definition: JSON.stringify(swaggerDocJSON, null, 2),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getDocInfo(
|
||||
spec: any,
|
||||
): { description: string; title: string } | undefined {
|
||||
if (isSwagger2_0(spec) || isOpenAPI3_0(spec)) {
|
||||
return spec.info;
|
||||
}
|
||||
|
||||
// swagger 1.2 spec doc defined by single file doesn't have description field
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { merge, isErrorResult, MergeInput } from 'openapi-merge';
|
||||
import { Swagger } from 'atlassian-openapi';
|
||||
import Swagger2OpenAPI from 'swagger2openapi';
|
||||
// @ts-ignore
|
||||
import SwaggerConverter from 'swagger-converter';
|
||||
import { NonEmptyArray } from './types';
|
||||
|
||||
export function isSwagger1_2(apiDoc: any): boolean {
|
||||
return apiDoc.swaggerVersion && apiDoc.swaggerVersion === '1.2';
|
||||
}
|
||||
|
||||
export function isSwagger2_0(apiDoc: any): boolean {
|
||||
return apiDoc.swagger && apiDoc.swagger === '2.0';
|
||||
}
|
||||
|
||||
export function isOpenAPI3_0(apiDoc: any): boolean {
|
||||
return apiDoc.openapi;
|
||||
}
|
||||
|
||||
export class OpenAPIMergerAndConverter {
|
||||
async mergeOpenAPI3Docs(
|
||||
docs: NonEmptyArray<Swagger.SwaggerV3>,
|
||||
): Promise<Swagger.SwaggerV3> {
|
||||
const mergeInput: MergeInput = docs.map(doc => {
|
||||
return { oas: doc };
|
||||
});
|
||||
|
||||
const result = await merge(mergeInput);
|
||||
if (isErrorResult(result)) {
|
||||
throw new Error(result.message);
|
||||
}
|
||||
return result.output;
|
||||
}
|
||||
|
||||
// Convert api doc to format openAPI 3. Do nothing with doc if it has format openAPI 3.0.
|
||||
// 3scale supports API docs in formats:
|
||||
// - swagger 1.2
|
||||
// - swagger 2.0
|
||||
// - openAPI 3.0
|
||||
async convertAPIDocToOpenAPI3(apiDoc: any): Promise<Swagger.SwaggerV3> {
|
||||
if (isOpenAPI3_0(apiDoc)) {
|
||||
return apiDoc;
|
||||
}
|
||||
if (isSwagger1_2(apiDoc)) {
|
||||
// Unfortunately there is no library in the JavaScript world, which can convert both swagger 1.2 and 2.0 to openAPI 3.0.
|
||||
// That's why, for swagger 1.2 we are using convertation to swagger 2.0. And then swagger 2.0 will be converted to openAPI 3.0.
|
||||
const swagger2_0Doc = await this.convertSwagger1_2To2_0(apiDoc);
|
||||
return await this.convertSwagger2_0ToOpenAPI3_0(swagger2_0Doc);
|
||||
}
|
||||
if (isSwagger2_0(apiDoc)) {
|
||||
return await this.convertSwagger2_0ToOpenAPI3_0(apiDoc);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unsupported API document. Plugin supports Swagger 1.2, 2.0, 3.0(Open API 3.0)`,
|
||||
);
|
||||
}
|
||||
|
||||
async convertSwagger1_2To2_0(swaggerDoc: any): Promise<any> {
|
||||
try {
|
||||
const result = SwaggerConverter.convert(swaggerDoc, {});
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error converting Swagger 1.2 to Swagger 2.0:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async convertSwagger2_0ToOpenAPI3_0(swaggerDoc: any): Promise<any> {
|
||||
try {
|
||||
const result = await Swagger2OpenAPI.convertObj(swaggerDoc, {
|
||||
patch: true, // patch: true helps to fix minor issues
|
||||
warnOnly: true, // Do not throw on non-patchable errors
|
||||
});
|
||||
return result.openapi;
|
||||
} catch (error) {
|
||||
console.error('Error converting Swagger 2.0 to OpenAPI 3.0:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,3 +25,9 @@ export type ThreeScaleConfig = {
|
|||
addLabels?: boolean;
|
||||
schedule?: SchedulerServiceTaskScheduleDefinition;
|
||||
};
|
||||
|
||||
export type NonEmptyArray<T> = [T, ...T[]];
|
||||
|
||||
export function isNonEmptyArray<T>(arr: T[]): arr is NonEmptyArray<T> {
|
||||
return arr.length > 0;
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue