diff --git a/data/specs/bookstore.openapi.json b/data/specs/bookstore.openapi.json index 5f4535e..c93ba8e 100644 --- a/data/specs/bookstore.openapi.json +++ b/data/specs/bookstore.openapi.json @@ -8,7 +8,7 @@ }, "servers": [ { - "url": "https://alzasloneuap05.azure-api.net/book-store-api" + "url": "https://contoso.com" } ], "paths": { diff --git a/data/specs/petstore.openapi.json b/data/specs/petstore.openapi.json index 5b0d1da..8cc0722 100644 --- a/data/specs/petstore.openapi.json +++ b/data/specs/petstore.openapi.json @@ -1,177 +1 @@ -{ - "openapi": "3.0.0", - "info": { - "version": "1.0.0", - "title": "Swagger Petstore", - "description": "Example API thats show cases OpenAPI spec", - "x:thumbnail": "https://i.pinimg.com/originals/6a/97/3a/6a973acc6f9e9fb337ba5509bb77e58e.jpg", - "license": { - "name": "MIT" - } - }, - "servers": [ - { - "url": "http://petstore.swagger.io/v1" - } - ], - "paths": { - "/pets": { - "get": { - "summary": "List all pets", - "operationId": "listPets", - "tags": [ - "pets" - ], - "parameters": [ - { - "name": "limit", - "in": "query", - "description": "How many items to return at one time (max 100)", - "required": false, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "A paged array of pets", - "headers": { - "x-next": { - "description": "A link to the next page of responses", - "schema": { - "type": "string" - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pets" - } - } - } - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - } - }, - "post": { - "summary": "Create a pet", - "operationId": "createPets", - "tags": [ - "pets" - ], - "responses": { - "201": { - "description": "Null response" - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - } - } - }, - "/pets/{petId}": { - "get": { - "summary": "Info for a specific pet", - "operationId": "showPetById", - "tags": [ - "pets" - ], - "parameters": [ - { - "name": "petId", - "in": "path", - "required": true, - "description": "The id of the pet to retrieve", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Expected response to a valid request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pets" - } - } - } - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "Pet": { - "required": [ - "id", - "name" - ], - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string" - }, - "tag": { - "type": "string" - } - } - }, - "Pets": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - }, - "Error": { - "required": [ - "code", - "message" - ], - "properties": { - "code": { - "type": "integer", - "format": "int32" - }, - "message": { - "type": "string" - } - } - } - } - } -} \ No newline at end of file +{"openapi":"3.0.2","info":{"title":"Swagger Petstore","description":"This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\n\nSome useful links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)","termsOfService":"http://swagger.io/terms/","contact":{"email":"apiteam@swagger.io"},"license":{"name":"Apache 2.0","url":"http://www.apache.org/licenses/LICENSE-2.0.html"},"version":"1.0.5"},"externalDocs":{"description":"Find out more about Swagger","url":"http://swagger.io"},"servers":[{"url":"/api/v3"}],"tags":[{"name":"pet","description":"Everything about your Pets","externalDocs":{"description":"Find out more","url":"http://swagger.io"}},{"name":"store","description":"Operations about user"},{"name":"user","description":"Access to Petstore orders","externalDocs":{"description":"Find out more about our store","url":"http://swagger.io"}}],"paths":{"/pet":{"put":{"tags":["pet"],"summary":"Update an existing pet","description":"Update an existing pet by Id","operationId":"updatePet","requestBody":{"description":"Update an existent pet in the store","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pet"}},"application/xml":{"schema":{"$ref":"#/components/schemas/Pet"}},"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Pet"}}},"required":true},"responses":{"200":{"description":"Successful operation","content":{"application/xml":{"schema":{"$ref":"#/components/schemas/Pet"}},"application/json":{"schema":{"$ref":"#/components/schemas/Pet"}}}},"400":{"description":"Invalid ID supplied"},"404":{"description":"Pet not found"},"405":{"description":"Validation exception"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]},"post":{"tags":["pet"],"summary":"Add a new pet to the store","description":"Add a new pet to the store","operationId":"addPet","requestBody":{"description":"Create a new pet in the store","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pet"}},"application/xml":{"schema":{"$ref":"#/components/schemas/Pet"}},"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Pet"}}},"required":true},"responses":{"200":{"description":"Successful operation","content":{"application/xml":{"schema":{"$ref":"#/components/schemas/Pet"}},"application/json":{"schema":{"$ref":"#/components/schemas/Pet"}}}},"405":{"description":"Invalid input"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/findByStatus":{"get":{"tags":["pet"],"summary":"Finds Pets by status","description":"Multiple status values can be provided with comma separated strings","operationId":"findPetsByStatus","parameters":[{"name":"status","in":"query","description":"Status values that need to be considered for filter","required":false,"explode":true,"schema":{"type":"string","default":"available","enum":["available","pending","sold"]}}],"responses":{"200":{"description":"successful operation","content":{"application/xml":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Pet"}}},"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Pet"}}}}},"400":{"description":"Invalid status value"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/findByTags":{"get":{"tags":["pet"],"summary":"Finds Pets by tags","description":"Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.","operationId":"findPetsByTags","parameters":[{"name":"tags","in":"query","description":"Tags to filter by","required":false,"explode":true,"schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"successful operation","content":{"application/xml":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Pet"}}},"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Pet"}}}}},"400":{"description":"Invalid tag value"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/{petId}":{"get":{"tags":["pet"],"summary":"Find pet by ID","description":"Returns a single pet","operationId":"getPetById","parameters":[{"name":"petId","in":"path","description":"ID of pet to return","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"successful operation","content":{"application/xml":{"schema":{"$ref":"#/components/schemas/Pet"}},"application/json":{"schema":{"$ref":"#/components/schemas/Pet"}}}},"400":{"description":"Invalid ID supplied"},"404":{"description":"Pet not found"}},"security":[{"api_key":[]},{"petstore_auth":["write:pets","read:pets"]}]},"post":{"tags":["pet"],"summary":"Updates a pet in the store with form data","description":"","operationId":"updatePetWithForm","parameters":[{"name":"petId","in":"path","description":"ID of pet that needs to be updated","required":true,"schema":{"type":"integer","format":"int64"}},{"name":"name","in":"query","description":"Name of pet that needs to be updated","schema":{"type":"string"}},{"name":"status","in":"query","description":"Status of pet that needs to be updated","schema":{"type":"string"}}],"responses":{"405":{"description":"Invalid input"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]},"delete":{"tags":["pet"],"summary":"Deletes a pet","description":"","operationId":"deletePet","parameters":[{"name":"api_key","in":"header","description":"","required":false,"schema":{"type":"string"}},{"name":"petId","in":"path","description":"Pet id to delete","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"400":{"description":"Invalid pet value"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/{petId}/uploadImage":{"post":{"tags":["pet"],"summary":"uploads an image","description":"","operationId":"uploadFile","parameters":[{"name":"petId","in":"path","description":"ID of pet to update","required":true,"schema":{"type":"integer","format":"int64"}},{"name":"additionalMetadata","in":"query","description":"Additional Metadata","required":false,"schema":{"type":"string"}}],"requestBody":{"content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}}},"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/store/inventory":{"get":{"tags":["store"],"summary":"Returns pet inventories by status","description":"Returns a map of status codes to quantities","operationId":"getInventory","responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"integer","format":"int32"}}}}}},"security":[{"api_key":[]}]}},"/store/order":{"post":{"tags":["store"],"summary":"Place an order for a pet","description":"Place a new order in the store","operationId":"placeOrder","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Order"}},"application/xml":{"schema":{"$ref":"#/components/schemas/Order"}},"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Order"}}}},"responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Order"}}}},"405":{"description":"Invalid input"}}}},"/store/order/{orderId}":{"get":{"tags":["store"],"summary":"Find purchase order by ID","description":"For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions","operationId":"getOrderById","parameters":[{"name":"orderId","in":"path","description":"ID of order that needs to be fetched","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"successful operation","content":{"application/xml":{"schema":{"$ref":"#/components/schemas/Order"}},"application/json":{"schema":{"$ref":"#/components/schemas/Order"}}}},"400":{"description":"Invalid ID supplied"},"404":{"description":"Order not found"}}},"delete":{"tags":["store"],"summary":"Delete purchase order by ID","description":"For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors","operationId":"deleteOrder","parameters":[{"name":"orderId","in":"path","description":"ID of the order that needs to be deleted","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"400":{"description":"Invalid ID supplied"},"404":{"description":"Order not found"}}}},"/user":{"post":{"tags":["user"],"summary":"Create user","description":"This can only be done by the logged in user.","operationId":"createUser","requestBody":{"description":"Created user object","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}},"application/xml":{"schema":{"$ref":"#/components/schemas/User"}},"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/User"}}}},"responses":{"default":{"description":"successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}},"application/xml":{"schema":{"$ref":"#/components/schemas/User"}}}}}}},"/user/createWithList":{"post":{"tags":["user"],"summary":"Creates list of users with given input array","description":"Creates list of users with given input array","operationId":"createUsersWithListInput","requestBody":{"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/User"}}}}},"responses":{"200":{"description":"Successful operation","content":{"application/xml":{"schema":{"$ref":"#/components/schemas/User"}},"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"default":{"description":"successful operation"}}}},"/user/login":{"get":{"tags":["user"],"summary":"Logs user into the system","description":"","operationId":"loginUser","parameters":[{"name":"username","in":"query","description":"The user name for login","required":false,"schema":{"type":"string"}},{"name":"password","in":"query","description":"The password for login in clear text","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"successful operation","headers":{"X-Rate-Limit":{"description":"calls per hour allowed by the user","schema":{"type":"integer","format":"int32"}},"X-Expires-After":{"description":"date in UTC when toekn expires","schema":{"type":"string","format":"date-time"}}},"content":{"application/xml":{"schema":{"type":"string"}},"application/json":{"schema":{"type":"string"}}}},"400":{"description":"Invalid username/password supplied"}}}},"/user/logout":{"get":{"tags":["user"],"summary":"Logs out current logged in user session","description":"","operationId":"logoutUser","parameters":[],"responses":{"default":{"description":"successful operation"}}}},"/user/{username}":{"get":{"tags":["user"],"summary":"Get user by user name","description":"","operationId":"getUserByName","parameters":[{"name":"username","in":"path","description":"The name that needs to be fetched. Use user1 for testing. ","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"successful operation","content":{"application/xml":{"schema":{"$ref":"#/components/schemas/User"}},"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"400":{"description":"Invalid username supplied"},"404":{"description":"User not found"}}},"put":{"tags":["user"],"summary":"Update user","description":"This can only be done by the logged in user.","operationId":"updateUser","parameters":[{"name":"username","in":"path","description":"name that need to be deleted","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Update an existent user in the store","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}},"application/xml":{"schema":{"$ref":"#/components/schemas/User"}},"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/User"}}}},"responses":{"default":{"description":"successful operation"}}},"delete":{"tags":["user"],"summary":"Delete user","description":"This can only be done by the logged in user.","operationId":"deleteUser","parameters":[{"name":"username","in":"path","description":"The name that needs to be deleted","required":true,"schema":{"type":"string"}}],"responses":{"400":{"description":"Invalid username supplied"},"404":{"description":"User not found"}}}}},"components":{"schemas":{"Order":{"type":"object","properties":{"id":{"type":"integer","format":"int64","example":10},"petId":{"type":"integer","format":"int64","example":198772},"quantity":{"type":"integer","format":"int32","example":7},"shipDate":{"type":"string","format":"date-time"},"status":{"type":"string","description":"Order Status","example":"approved","enum":["placed","approved","delivered"]},"complete":{"type":"boolean"}},"xml":{"name":"order"}},"Customer":{"type":"object","properties":{"id":{"type":"integer","format":"int64","example":100000},"username":{"type":"string","example":"fehguy"},"address":{"type":"array","xml":{"name":"addresses","wrapped":true},"items":{"$ref":"#/components/schemas/Address"}}},"xml":{"name":"customer"}},"Address":{"type":"object","properties":{"street":{"type":"string","example":"437 Lytton"},"city":{"type":"string","example":"Palo Alto"},"state":{"type":"string","example":"CA"},"zip":{"type":"string","example":"94301"}},"xml":{"name":"address"}},"Category":{"type":"object","properties":{"id":{"type":"integer","format":"int64","example":1},"name":{"type":"string","example":"Dogs"}},"xml":{"name":"category"}},"User":{"type":"object","properties":{"id":{"type":"integer","format":"int64","example":10},"username":{"type":"string","example":"theUser"},"firstName":{"type":"string","example":"John"},"lastName":{"type":"string","example":"James"},"email":{"type":"string","example":"john@email.com"},"password":{"type":"string","example":"12345"},"phone":{"type":"string","example":"12345"},"userStatus":{"type":"integer","description":"User Status","format":"int32","example":1}},"xml":{"name":"user"}},"Tag":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"}},"xml":{"name":"tag"}},"Pet":{"required":["name","photoUrls"],"type":"object","properties":{"id":{"type":"integer","format":"int64","example":10},"name":{"type":"string","example":"doggie"},"category":{"$ref":"#/components/schemas/Category"},"photoUrls":{"type":"array","xml":{"wrapped":true},"items":{"type":"string","xml":{"name":"photoUrl"}}},"tags":{"type":"array","xml":{"wrapped":true},"items":{"$ref":"#/components/schemas/Tag"}},"status":{"type":"string","description":"pet status in the store","enum":["available","pending","sold"]}},"xml":{"name":"pet"}},"ApiResponse":{"type":"object","properties":{"code":{"type":"integer","format":"int32"},"type":{"type":"string"},"message":{"type":"string"}},"xml":{"name":"##default"}}},"requestBodies":{"Pet":{"description":"Pet object that needs to be added to the store","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pet"}},"application/xml":{"schema":{"$ref":"#/components/schemas/Pet"}}}},"UserArray":{"description":"List of user object","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/User"}}}}}},"securitySchemes":{"petstore_auth":{"type":"oauth2","flows":{"implicit":{"authorizationUrl":"https://petstore3.swagger.io/oauth/authorize","scopes":{"write:pets":"modify pets in your account","read:pets":"read your pets"}}}},"api_key":{"type":"apiKey","name":"api_key","in":"header"}}}} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0bafb61..ebe71db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "remark": "^13.0.0", "remark-html": "^13.0.1", "routing-controllers": "^0.9.0-alpha.6", + "saxen": "^8.1.2", "slick": "^1.12.2", "topojson-client": "^3.1.0", "truncate-html": "^1.0.3", @@ -12804,6 +12805,11 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, + "node_modules/saxen": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/saxen/-/saxen-8.1.2.tgz", + "integrity": "sha512-xUOiiFbc3Ow7p8KMxwsGICPx46ZQvy3+qfNVhrkwfz3Vvq45eGt98Ft5IQaA1R/7Tb5B5MKh9fUR9x3c3nDTxw==" + }, "node_modules/scheduler": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", @@ -27221,6 +27227,11 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, + "saxen": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/saxen/-/saxen-8.1.2.tgz", + "integrity": "sha512-xUOiiFbc3Ow7p8KMxwsGICPx46ZQvy3+qfNVhrkwfz3Vvq45eGt98Ft5IQaA1R/7Tb5B5MKh9fUR9x3c3nDTxw==" + }, "scheduler": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", diff --git a/package.json b/package.json index 262a861..77672ff 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "remark": "^13.0.0", "remark-html": "^13.0.1", "routing-controllers": "^0.9.0-alpha.6", + "saxen": "^8.1.2", "slick": "^1.12.2", "topojson-client": "^3.1.0", "truncate-html": "^1.0.3", diff --git a/src/components/operations/operation-details/ko/runtime/operation-details.ts b/src/components/operations/operation-details/ko/runtime/operation-details.ts index 3923a90..dfe5c4e 100644 --- a/src/components/operations/operation-details/ko/runtime/operation-details.ts +++ b/src/components/operations/operation-details/ko/runtime/operation-details.ts @@ -211,7 +211,7 @@ export class OperationDetails { .map(p => p.typeName) .filter((item, pos, self) => self.indexOf(item) === pos); - const schemasPromises = schemaIds.map(schemaId => this.apiService.getApiSchema(`${apiId}/${schemaId}`)); + const schemasPromises = schemaIds.map(schemaId => this.apiService.getApiSchema(this.selectedApiName())); const schemas = await Promise.all(schemasPromises); const definitions = schemas.map(x => x.definitions).flat(); diff --git a/src/contracts/openapi/index.ts b/src/contracts/openapi/index.ts new file mode 100644 index 0000000..6db788f --- /dev/null +++ b/src/contracts/openapi/index.ts @@ -0,0 +1,16 @@ +export * from "./openApiComponents"; +export * from "./openApiExample"; +export * from "./openApiExternalDoc"; +export * from "./openApiMediaType"; +export * from "./openApiObjectInfo"; +export * from "./openApiOperation"; +export * from "./openApiParameter"; +export * from "./openApiPath"; +export * from "./openApiPaths"; +export * from "./openApiReference"; +export * from "./openApiRequestBody"; +export * from "./openApiResponse"; +export * from "./openApiResponses"; +export * from "./openApiSchema"; +export * from "./openApiServer"; +export * from "./openApiTag"; \ No newline at end of file diff --git a/src/contracts/openapi/openApi.ts b/src/contracts/openapi/openApi.ts new file mode 100644 index 0000000..0afa491 --- /dev/null +++ b/src/contracts/openapi/openApi.ts @@ -0,0 +1,50 @@ +import { + OpenApiComponents, + OpenApiExternalDoc, + OpenApiObjectInfo, + OpenApiPaths, + OpenApiServer, + OpenApiTag +} from "./"; + +/** + * This is the root document object of the + */ +export interface OpenApiSpec30 { + /** + * This string MUST be the semantic version number of the OpenAPI Specification version that the OpenAPI document uses. + */ + openapi: string; + + /** + * The available paths and operations for the API. + */ + paths: OpenApiPaths; + + /** + * Provides metadata about the API. The metadata MAY be used by tooling as required. + */ + info: OpenApiObjectInfo; + + /** + * Additional external documentation. + */ + externalDocs?: OpenApiExternalDoc; + + /** + * An array of Server Objects, which provide connectivity information to a target server. + * If the servers property is not provided, or is an empty array, the default value would + * be a Server Object with a URL value of /. + */ + servers?: OpenApiServer[]; + + /** + * A list of tags used by the specification with additional metadata. + */ + tags?: OpenApiTag[]; + + /** + * An element to hold various schemas for the specification. + */ + components: OpenApiComponents; +} \ No newline at end of file diff --git a/src/contracts/openapi/openApiComponents.ts b/src/contracts/openapi/openApiComponents.ts new file mode 100644 index 0000000..0e5e1f6 --- /dev/null +++ b/src/contracts/openapi/openApiComponents.ts @@ -0,0 +1,3 @@ +export interface OpenApiComponents { + schemas?: any; +} \ No newline at end of file diff --git a/src/contracts/openapi/openApiExample.ts b/src/contracts/openapi/openApiExample.ts new file mode 100644 index 0000000..992a555 --- /dev/null +++ b/src/contracts/openapi/openApiExample.ts @@ -0,0 +1,21 @@ +export interface OpenApiExample { + /** + * Short description for the example. + */ + summary: string; + + /** + * Long description for the example. + */ + description: string; + + /** + * Embedded literal example. The value field and externalValue field are mutually exclusive. To represent examples of media types that cannot naturally represented in JSON or YAML, use a string value to contain the example, escaping where necessary. + */ + value: any; + + /** + * A URL that points to the literal example. This provides the capability to reference examples that cannot easily be included in JSON or YAML documents. The value field and externalValue field are mutually exclusive. + */ + externalValue: string; +} diff --git a/src/contracts/openapi/openApiExternalDoc.ts b/src/contracts/openapi/openApiExternalDoc.ts new file mode 100644 index 0000000..7e1b563 --- /dev/null +++ b/src/contracts/openapi/openApiExternalDoc.ts @@ -0,0 +1,4 @@ +export interface OpenApiExternalDoc { + url: string; + description?: string; +} diff --git a/src/contracts/openapi/openApiMediaType.ts b/src/contracts/openapi/openApiMediaType.ts new file mode 100644 index 0000000..4886a92 --- /dev/null +++ b/src/contracts/openapi/openApiMediaType.ts @@ -0,0 +1,9 @@ +import { OpenApiExample } from "./openApiExample"; + + +export interface OpenApiMediaType { + schema: any; // OpenApiSchema | OpenApiReference; + example: OpenApiExample; + examples: any; + encoding: any; +} diff --git a/src/contracts/openapi/openApiObjectInfo.ts b/src/contracts/openapi/openApiObjectInfo.ts new file mode 100644 index 0000000..c582bfd --- /dev/null +++ b/src/contracts/openapi/openApiObjectInfo.ts @@ -0,0 +1,6 @@ +export interface OpenApiObjectInfo { + title: string; + description?: string; + version: string; + termsOfService?: string; +} diff --git a/src/contracts/openapi/openApiOperation.ts b/src/contracts/openapi/openApiOperation.ts new file mode 100644 index 0000000..9a09683 --- /dev/null +++ b/src/contracts/openapi/openApiOperation.ts @@ -0,0 +1,15 @@ +import { OpenApiParameter } from "./openApiParameter"; +import { OpenApiResponses } from "./openApiResponses"; +import { OpenApiRequestBody } from "./openApiRequestBody"; + + +export interface OpenApiOperation { + operationId: string; + description: string; + parameters: OpenApiParameter[]; + responses: OpenApiResponses; + summary: string; + consumes?: string[]; + produces?: string[]; + requestBody?: OpenApiRequestBody; +} diff --git a/src/contracts/openapi/openApiParameter.ts b/src/contracts/openapi/openApiParameter.ts new file mode 100644 index 0000000..909bd12 --- /dev/null +++ b/src/contracts/openapi/openApiParameter.ts @@ -0,0 +1,10 @@ +export interface OpenApiParameter { + name: string; + in: string; + required: boolean; + description: string; + type?: string; + schema?: any; + default?: string; + enum: string[]; +} diff --git a/src/contracts/openapi/openApiPath.ts b/src/contracts/openapi/openApiPath.ts new file mode 100644 index 0000000..2e5b29f --- /dev/null +++ b/src/contracts/openapi/openApiPath.ts @@ -0,0 +1,6 @@ +import { OpenApiOperation } from "./openApiOperation"; + + +export interface OpenApiPath { + [key: string]: OpenApiOperation; +} diff --git a/src/contracts/openapi/openApiPaths.ts b/src/contracts/openapi/openApiPaths.ts new file mode 100644 index 0000000..e1c00c4 --- /dev/null +++ b/src/contracts/openapi/openApiPaths.ts @@ -0,0 +1,5 @@ +import { OpenApiPath } from "./openApiPath"; + +export interface OpenApiPaths { + [key: string]: OpenApiPath; +} diff --git a/src/contracts/openapi/openApiReference.ts b/src/contracts/openapi/openApiReference.ts new file mode 100644 index 0000000..b6e1d77 --- /dev/null +++ b/src/contracts/openapi/openApiReference.ts @@ -0,0 +1,3 @@ +export interface OpenApiReference { + $ref: string; +} diff --git a/src/contracts/openapi/openApiRequestBody.ts b/src/contracts/openapi/openApiRequestBody.ts new file mode 100644 index 0000000..9ea5249 --- /dev/null +++ b/src/contracts/openapi/openApiRequestBody.ts @@ -0,0 +1,24 @@ +import { Bag } from "@paperbits/common"; +import { OpenApiMediaType } from "./openApiMediaType"; + +/** + * Describes a single request body. + */ +export interface OpenApiRequestBody { + /** + * A brief description of the request body. This could contain examples of use. + */ + description: string; + + /** + * Determines if the request body is required in the request. Defaults to false. + */ + required: boolean; + + /** + * The content of the request body. The key is a media type or media type range and the value describes it. + * For requests that match multiple keys, only the most specific key is applicable, + * e.g. text/plain overrides text/* + */ + content: Bag; +} diff --git a/src/contracts/openapi/openApiResponse.ts b/src/contracts/openapi/openApiResponse.ts new file mode 100644 index 0000000..3a9ae2f --- /dev/null +++ b/src/contracts/openapi/openApiResponse.ts @@ -0,0 +1,10 @@ +import { Bag } from "@paperbits/common"; +import { OpenApiMediaType } from "./openApiMediaType"; +import { OpenApiParameter } from "./openApiParameter"; + + +export interface OpenApiResponse { + description: string; + headers: Bag; + content: Bag; +} diff --git a/src/contracts/openapi/openApiResponses.ts b/src/contracts/openapi/openApiResponses.ts new file mode 100644 index 0000000..c5c1319 --- /dev/null +++ b/src/contracts/openapi/openApiResponses.ts @@ -0,0 +1,5 @@ +import { OpenApiResponse } from "./openApiResponse"; + +export interface OpenApiResponses { + [key: string]: OpenApiResponse; +} diff --git a/src/contracts/openapi/openApiSchema.ts b/src/contracts/openapi/openApiSchema.ts new file mode 100644 index 0000000..31a977f --- /dev/null +++ b/src/contracts/openapi/openApiSchema.ts @@ -0,0 +1 @@ +export interface OpenApiSchema { } \ No newline at end of file diff --git a/src/contracts/openapi/openApiServer.ts b/src/contracts/openapi/openApiServer.ts new file mode 100644 index 0000000..5bab62e --- /dev/null +++ b/src/contracts/openapi/openApiServer.ts @@ -0,0 +1,5 @@ + +export interface OpenApiServer { + url: string; + description?: string; +} diff --git a/src/contracts/openapi/openApiTag.ts b/src/contracts/openapi/openApiTag.ts new file mode 100644 index 0000000..35f52e0 --- /dev/null +++ b/src/contracts/openapi/openApiTag.ts @@ -0,0 +1,8 @@ +import { OpenApiExternalDoc } from "./openApiExternalDoc"; + + +export interface OpenApiTag { + name: string; + description?: string; + externalDocs?: OpenApiExternalDoc; +} diff --git a/src/contracts/request.ts b/src/contracts/request.ts index 9cad079..019796d 100644 --- a/src/contracts/request.ts +++ b/src/contracts/request.ts @@ -1,12 +1,12 @@ import { ParameterContract } from "./parameter"; import { RepresentationContract } from "./representation"; -/* -Model of API operation request -*/ +/** + * Model of API operation request + */ export interface RequestContract { description?: string; - queryParameters: ParameterContract[]; - headers: ParameterContract[]; - representations: RepresentationContract[]; + queryParameters?: ParameterContract[]; + headers?: ParameterContract[]; + representations?: RepresentationContract[]; } diff --git a/src/contracts/schema.ts b/src/contracts/schema.ts index 7860ac1..3bc61dd 100644 --- a/src/contracts/schema.ts +++ b/src/contracts/schema.ts @@ -30,6 +30,8 @@ export interface SchemaObjectContract extends ReferenceObjectContract { */ required?: string[]; + readOnly?: boolean; + properties?: Bag; items?: SchemaObjectContract; @@ -72,7 +74,25 @@ export interface SchemaObjectContract extends ReferenceObjectContract { minProperties?: number; + /** + * Example of the payload represented by this schema object. + */ example?: string; + + /** + * Format of payload example represented by this schema object. It is used for syntax highlighting. + */ + exampleFormat?: string; + + /** + * Raw schema representation. + */ + rawSchema?: string; + + /** + * Raw schema format. It is used for syntax highlighting. + */ + rawSchemaFormat?: string; } /** @@ -122,17 +142,23 @@ export interface OpenApiSchemaContract { }; } +export interface XsdSchemaContract { + value: string; +} + + /** * */ -export interface SchemaContract extends ArmResource { +export interface SchemaContract extends ArmResource { properties: { contentType: string; - document?: SwaggerSchemaContract | OpenApiSchemaContract; + document?: SwaggerSchemaContract | OpenApiSchemaContract | XsdSchemaContract; }; } export enum SchemaType { swagger = "application/vnd.ms-azure-apim.swagger.definitions+json", - openapi = "application/vnd.oai.openapi.components+json" + openapi = "application/vnd.oai.openapi.components+json", + xsd = "application/vnd.ms-azure-apim.xsd+xml" } \ No newline at end of file diff --git a/src/contracts/swaggerObject.ts b/src/contracts/swaggerObject.ts deleted file mode 100644 index 0683d5e..0000000 --- a/src/contracts/swaggerObject.ts +++ /dev/null @@ -1,57 +0,0 @@ -export interface SwaggerObjectInfo { - title: string; - description: string; - version: string; -} - -export interface SwaggerParameter { - name: string; - in: string; - required: boolean; - description: string; - type?: string; - schema?: Object; - default?: string; - enum: string[]; -} - -export interface SwaggerOperation { - operationId: string; - description: string; - parameters: SwaggerParameter[]; - responses: Object; - security: SecurityType[]; - summary: string; -} - -export interface PathItem { - [key: string]: SwaggerOperation; -} - -export interface SwaggerPath { - [key: string]: PathItem; -} - -export interface SwaggerObject { - swagger: string; - info: SwaggerObjectInfo; - host: string; - basePath: string; - schemes: string[]; - consumes: string[]; - produces: string[]; - paths: SwaggerPath; - definitions?: Object; - securityDefinitions?: SecurityDefinitions; -} - -export interface SecurityDefinitions { - apikeyQuery: SecurityType; - apikeyHeader: SecurityType; -} - -export interface SecurityType { - type: string; - name: string; - in: string; -} \ No newline at end of file diff --git a/src/middlewares/staticContentMiddleware.ts b/src/middlewares/staticContentMiddleware.ts index 7761ddc..b87e2fa 100644 --- a/src/middlewares/staticContentMiddleware.ts +++ b/src/middlewares/staticContentMiddleware.ts @@ -76,6 +76,8 @@ export class StaticContentMiddleware implements ExpressMiddlewareInterface { return; } + // if no published website yet, we can serve specs from /data/spec folder directly + // if (!websiteVersion) { // no website yet, non-admins receive error message. // response.statusCode = 200; // return "Developer portal has not been published yet."; diff --git a/src/models/jObject.ts b/src/models/jObject.ts new file mode 100644 index 0000000..8439dc5 --- /dev/null +++ b/src/models/jObject.ts @@ -0,0 +1,314 @@ +import { Parser } from "saxen"; + +type JElementType = "element" | "comment" | "cdata" | "text" | "document" | "template" | "question"; + +export class JAttribute { + public ns: string; + public name: string; + public value: string; + + constructor(name: string, value?: string, ns?: string) { + this.name = name; + this.value = value; + this.ns = ns; + } +} + +export class JObject { + public ns: string; + public children: JObject[]; + public attributes: JAttribute[]; + public name: string; + public value: string; + public type: JElementType; + + constructor(name?: string, ns?: string) { + this.type = "element"; + this.name = name; + this.children = []; + this.attributes = []; + this.ns = ns; + } + + public toString(): string { + return this.name; + } + + public join(values: string[], separator: string): string { + return values.filter(x => x && x !== "").join(separator); + } + + public static fromXml(xml: string, parseCallbacks?: { + attribute?: (value: string) => string, + text?: (value: string) => string, + cdata?: (value: string) => string, + comment?: (value: string) => string, + }): JObject { + + const root = new JObject("document"); + root.type = "document"; + const elementStack = [root]; + const parser = new Parser({ proxy: true }); + + const pushChild = (element: JObject) => { + const currentElement = elementStack[elementStack.length - 1]; + currentElement.children.push(element); + + elementStack.push(element); + }; + + const popChild = () => { + elementStack.pop(); + }; + + const pushSibling = (element: JObject) => { + const currentElement = elementStack[elementStack.length - 1]; + currentElement.children.push(element); + }; + + parser.on("question", (str, decodeEntities, contextGetter) => { + const element = new JObject("", ""); + element.type = "question"; + element.value = str; + + pushSibling(element); + }); + + parser.on("openTag", (el, decodeEntities, selfClosing, getContext) => { + const elementNameParts = el.name.split(":"); + + let elementNamespace: string; + let elementName: string; + + if (elementNameParts.length > 1) { + elementNamespace = elementNameParts[0]; + elementName = elementNameParts[1]; + } else { + elementName = el.name; + } + + const element = new JObject(elementName, elementNamespace); + + Object.keys(el.attrs).forEach(key => { + const attributeNameParts = key.split(":"); + + let attributeNamespace: string; + let attributeName: string; + + if (attributeNameParts.length > 1) { + attributeNamespace = attributeNameParts[0]; + attributeName = attributeNameParts[1]; + } else { + attributeName = key; + } + + const tempValue = XmlUtil.decode(el.attrs[key]); + const attributeValue = parseCallbacks && parseCallbacks.attribute ? parseCallbacks.attribute(tempValue) : tempValue; + element.attributes.push(new JAttribute(attributeName, attributeValue, attributeNamespace)); + }); + + if (el.attrs["template"] && el.attrs["template"].toUpperCase() === "LIQUID" || el.name === "xsl-transform") { + element.type = "template"; + } + + pushChild(element); + }); + + parser.on("closeTag", (el, decodeEntities, selfClosing, getContext) => { + popChild(); + }); + + parser.on("error", (err, contextGetter) => { + throw new Error("Unable to parse XML."); + }); + + parser.on("text", (text: string, decodeEntities, contextGetter) => { + text = text.trim(); + + if (!text) { + return; + } + + const currentElement = elementStack[elementStack.length - 1]; + + if (!currentElement.value) { + currentElement.value = ""; + } + + currentElement.value += parseCallbacks && parseCallbacks.text ? parseCallbacks.text(text) : text; + }); + + parser.on("cdata", (value: string) => { + const element = new JObject("", ""); + element.value = parseCallbacks && parseCallbacks.cdata ? parseCallbacks.cdata(value) : value; + element.type = "cdata"; + + pushSibling(element); + }); + + parser.on("comment", (value: string,) => { + pushSibling(new JComment(parseCallbacks && parseCallbacks.comment ? parseCallbacks.comment(value) : value)); + }); + + parser.parse(xml); + + return root; + } + + private toFormattedXml(identation: number = 0, escapeCallbacks?: { + attribute?: (value: string) => boolean + }): string { + let result = ""; + const content = this.value; + let lineBreak = "\n"; + + for (let i = 0; i < identation; i++) { + lineBreak += " "; + } + + switch (this.type) { + case "document": + this.children.forEach(child => { + result += child.toFormattedXml(0, escapeCallbacks) + "\n"; + }); + break; + + case "element": + case "template": + const tag = this.join([this.ns, this.name], ":"); + + result += `${lineBreak}<${tag}`; + + this.attributes.forEach(attribute => { + let value = attribute.value.toString(); + value = escapeCallbacks && escapeCallbacks.attribute && !escapeCallbacks.attribute(value) ? value : XmlUtil.encode(value); + result += ` ${this.join([attribute.ns, attribute.name], ":")}="${value}"`; + }); + + if (this.children.length > 0) { + result += `>`; + + this.children.forEach(child => { + result += child.toFormattedXml(identation + 4, escapeCallbacks); + }); + + result += `${lineBreak}`; + } else if (content) { + result += `>${content}`; + } else { + result += ` />`; + } + break; + + case "question": + result += this.value; + break; + + case "comment": + result += `${lineBreak}`; + break; + + case "cdata": + result += ``; + break; + + case "text": + if (content) { + result += content; + } + break; + + default: + throw new Error(`Unknown element type ${this.type}.`); + } + + return result; + } + + public toXml(escapeCallbacks?: { attribute?: (value: string) => boolean }): string { + return this.toFormattedXml(0, escapeCallbacks); + } + + public innerXml(): string { + return this.children.map(x => x.toFormattedXml()).join(); + } + + public getAttribute(attributeName: string): string { + const attribute = this.attributes.find(x => x.name === attributeName); + + if (attribute && attribute.value) { + return attribute.value; + } + + return undefined; + } + + public getAttributeAsNumber(attributeName: string): number { + const value = this.getAttribute(attributeName); + const result = +value; + return isNaN(+value) ? undefined : result; + } + + public setAttribute(attributeName: string, attributeValue: string): void { + if (attributeValue) { + this.attributes.push(new JAttribute(attributeName, attributeValue)); + } + } +} + +export class JComment extends JObject { + constructor(comment: string) { + super("", ""); + + this.value = comment; + this.type = "comment"; + } +} + +export class JText extends JObject { + constructor(text: string) { + super("", ""); + + this.value = text; + this.type = "text"; + } +} + +class XmlUtil { + private static readonly chars: string[][] = [ + ["\"", """], + ["&", "&"], + ["'", "'"], + ["<", "<"], + [">", ">"], + ["\t", " "], + ["\n", " "], + ["\r", " "], + ]; + + private static encodeRegex(): RegExp { + return new RegExp(XmlUtil.chars.map((e) => e[0]).join("|"), "g"); + } + + private static decodeRegex(): RegExp { + return new RegExp(XmlUtil.chars.map((e) => e[1]).join("|"), "g"); + } + + private static encodeMap = XmlUtil.chars.reduce((i, v) => { + i[v[0]] = v[1]; + return i; + }, {}); + + private static decodeMap = XmlUtil.chars.reduce((i, v) => { + i[v[1]] = v[0]; + return i; + }, {}); + + public static encode(str: string): string { + return str.replace(XmlUtil.encodeRegex(), (s) => XmlUtil.encodeMap[s]); + } + + public static decode(str: string): string { + return str.replace(XmlUtil.decodeRegex(), (s) => XmlUtil.decodeMap[s]); + } +} \ No newline at end of file diff --git a/src/models/schema.ts b/src/models/schema.ts index a2ac01b..fc035a8 100644 --- a/src/models/schema.ts +++ b/src/models/schema.ts @@ -1,4 +1,5 @@ -import { SchemaContract, SchemaObjectContract, SchemaType, OpenApiSchemaContract, SwaggerSchemaContract } from "../contracts/schema"; +import { XsdSchemaConverter } from "./xsdSchemaConverter"; +import { SchemaContract, SchemaType, OpenApiSchemaContract, SwaggerSchemaContract, XsdSchemaContract } from "../contracts/schema"; import { TypeDefinition } from "./typeDefinition"; export class Schema { @@ -6,26 +7,43 @@ export class Schema { constructor(contract?: SchemaContract) { this.definitions = []; - if (contract) { - const definitionType = contract.properties?.contentType; - let definitions = {}; - - if (definitionType === SchemaType.swagger) { + + if (!contract) { + return; + } + + const definitionType = contract.properties?.contentType; + let definitions = {}; + + switch (definitionType) { + case SchemaType.swagger: const swaggerDoc = contract.properties?.document; definitions = swaggerDoc?.definitions || {}; - } else { - if (definitionType === SchemaType.openapi) { - const openApiDoc = contract.properties?.document; - definitions = openApiDoc?.components?.schemas || {}; + break; + + case SchemaType.openapi: + const openApiDoc = contract.properties?.document; + definitions = openApiDoc?.components?.schemas || {}; + break; + + case SchemaType.xsd: + const xsdDoc = contract.properties?.document; + + try { + definitions = new XsdSchemaConverter().convertXsdSchema(xsdDoc.value); } - } - - this.definitions = Object.keys(definitions) - .map(definitionName => { - return new TypeDefinition(definitionName, definitions[definitionName]); - }); + catch (error) { + console.warn(`Unable to parse XSD schema document. Skipping type definition setup.`); + } + break; + default: + console.warn(`Unsupported schema type: ${definitionType}`); } - + + this.definitions = Object.keys(definitions) + .map(definitionName => { + return new TypeDefinition(definitionName, definitions[definitionName]); + }); } } \ No newline at end of file diff --git a/src/models/xsdSchemaConverter.ts b/src/models/xsdSchemaConverter.ts new file mode 100644 index 0000000..4a564a8 --- /dev/null +++ b/src/models/xsdSchemaConverter.ts @@ -0,0 +1,238 @@ +import { JObject } from "./jObject"; +import { SchemaObjectContract } from "../contracts/schema"; +import { Bag } from "@paperbits/common"; + +interface SchemaNode { + name: string; + definition?: any; +} + +/** + * Basic XSD to internal schema representation converter. + */ +export class XsdSchemaConverter { + /** + * Determines if specified type is built-in primitive type. + * @param type {string} Type name. + */ + private isPrimitiveType(type: string): boolean { + return [ + "anySimpleType", + "anyType", + "string", + "normalizedString", + "token", + "language", + "Name", + "NCName", + "ID", + "IDREF", + "IDREFS", + "ENTITY", + "ENTITIES", + "NMTOKEN", + "NMTOKENS", + "boolean", + "base64Binary", + "hexBinary", + "float", + "decimal", + "integer", + "nonPositiveInteger", + "negativeInteger", + "long", + "int", + "short", + "byte", + "nonNegativeInteger", + "unsignedLong", + "unsignedInt", + "unsignedShort", + "unsignedByte", + "positiveInteger", + "double", + "anyURI", + "QName", + "duration", + "dateTime", + "date", + "time", + "anySimpleType", + "anyType", + "string", + "normalizedString", + "token", + "language", + "Name", + "NCName", + "ID", + "IDREF", + "IDREFS", + "ENTITY", + "ENTITIES", + "NMTOKEN", + "NMTOKENS", + "boolean", + "base64Binary", + "hexBinary", + "float", + "decimal", + "integer", + "nonPositiveInteger", + "negativeInteger", + "long", + "int", + "short", + "byte", + "nonNegativeInteger", + "unsignedLong", + "unsignedInt", + "unsignedShort", + "unsignedByte", + "positiveInteger", + "double", + "anyURI", + "QName", + "duration", + "dateTime", + "date", + "time", + ].includes(type); + } + + /** + * Converts XSD element into schema node. + * @param jObject {JObject} JObject representing XSD element. + */ + private convertElement(jObject: JObject): SchemaNode { + const name = jObject.getAttribute("name"); + const originalType = jObject.getAttribute("type"); + const isPrimitive = this.isPrimitiveType(originalType); + + let type: string; + let $ref: string; + + if (isPrimitive) { + type = originalType; + $ref = undefined; + } + else { + type = "object"; + $ref = originalType?.split(":").pop(); + } + + const definition: SchemaObjectContract = { + type: type, + properties: undefined, + $ref: $ref, + rawSchema: jObject.toXml().trim(), + rawSchemaFormat: "xml" + }; + + jObject.children.forEach(child => { + switch (child.name) { + case "simpleType": + definition.properties = definition.properties || {}; + const simpleTypeNode = this.convertSimpleType(child); + definition.properties[simpleTypeNode.name] = simpleTypeNode.definition; + break; + + case "complexType": + const complexTypeNode = this.convertComplexType(child); + if (complexTypeNode.name) { + definition.properties = definition.properties || {}; + definition.properties[complexTypeNode.name] = complexTypeNode.definition; + } + else { + Object.assign(definition, complexTypeNode.definition); + } + + break; + + case "element": + const elementNode = this.convertElement(child); + definition.properties = definition.properties || {}; + definition.properties[elementNode.name] = elementNode.definition; + break; + + default: + console.warn(`Element "${child.name}" by XSD schema converter.`); + break; + } + }); + + const resultNode: SchemaNode = { + name: name, + definition: definition + }; + + return resultNode; + } + + /** + * Converts XSD simple type into schema node. + * @param jObject {JObject} JObject representing XSD simple type. + */ + private convertSimpleType(jObject: JObject): SchemaNode { + const restriction = jObject.children[0]; + const type = restriction.getAttribute("base").split(":").pop(); + + const definition: SchemaObjectContract = { + type: type, + rawSchema: jObject.toXml().trim(), + rawSchemaFormat: "xml" + }; + + const resultNode: SchemaNode = { + name: jObject.getAttribute("name"), + definition: definition + }; + + return resultNode; + } + + /** + * Converts XSD simple type into schema node + * @param jObject {JObject} JObject representing XSD complex type. + */ + private convertComplexType(jObject: JObject): SchemaNode { + const name = jObject.getAttribute("name"); + + const definition: SchemaObjectContract = { + type: "object" + }; + + const collection = jObject.children.find(x => x.name === "sequence" || x.name === "all"); + + collection?.children.forEach(x => { + const elementNode = this.convertElement(x); + definition.properties = definition.properties || {}; + definition.properties[elementNode.name] = elementNode.definition; + }); + + const resultNode: SchemaNode = { + name: name, + definition: definition + }; + + return resultNode; + } + + /** + * Converts XSD schema into internal schema representation. + * @param xsdDocument {string} String containing XSD document. + */ + public convertXsdSchema(xsdDocument: string): Bag { + const documentJObject = JObject.fromXml(xsdDocument); + + const schemaJObject = documentJObject.children.find(x => x.name === "schema"); + + if (!schemaJObject) { + throw new Error(`Element "schema" not found in the document.`); + } + + const schemaNode = this.convertElement(schemaJObject); + + return schemaNode.definition.properties; + } +} diff --git a/src/services/apiService.ts b/src/services/apiService.ts index 5152c85..1a35d28 100644 --- a/src/services/apiService.ts +++ b/src/services/apiService.ts @@ -1,6 +1,5 @@ import * as lunr from "lunr"; import { HttpClient, HttpResponse } from "@paperbits/common/http"; -import { AzureBlobStorage } from "@paperbits/azure"; import { SearchQuery } from "../contracts/searchQuery"; import { Api } from "../models/api"; import { VersionSet } from "../models/versionSet"; @@ -266,13 +265,13 @@ export class ApiService { * Returns API schema with sepcified identifier. * @param schemaId {string} ARM-formatted schema identifier. */ - public async getApiSchema(schemaId: string): Promise { - // const contract = await this.mapiClient.get(`${schemaId}`); - // const model = new Schema(contract); + public async getApiSchema(apiName: string): Promise { + const specs = await this.fetchSpecs(); + const converter = new OpenApiConverter(); + const spec = specs.find(spec => spec.info.title === apiName); + const schema = converter.getSchema(spec); - // return model; - - return null; + return schema; } public async getApiHostnames(apiName: string): Promise { diff --git a/src/services/openApiConverter.ts b/src/services/openApiConverter.ts index 9bc7d9a..76ac29e 100644 --- a/src/services/openApiConverter.ts +++ b/src/services/openApiConverter.ts @@ -1,104 +1,123 @@ +import { OpenApiOperation } from "./../contracts/openapi/openApiOperation"; +import { OpenApiMediaType } from "../contracts/openapi/openApiMediaType"; +import { OpenApiParameter } from "../contracts/openapi/openApiParameter"; import { RepresentationContract } from "./../contracts/representation"; import { ResponseContract } from "./../contracts/response"; import { ParameterContract } from "../contracts/parameter"; import { RequestContract } from "../contracts/request"; import { OperationContract } from "./../contracts/operation"; import { ApiContract } from "../contracts/api"; +import { Schema } from "../models/schema"; +import { TypeDefinition } from "../models/typeDefinition"; +import { OpenApiSpec30 } from "../contracts/openapi/openApi"; +import { OpenApiResponse } from "../contracts/openapi/openApiResponse"; +import { Bag } from "@paperbits/common"; export class OpenApiConverter { - public convertParameter(parameterObject: object): ParameterContract { + public convertParameter(openApiParameter: OpenApiParameter): ParameterContract { const parameter: ParameterContract = { - name: parameterObject["name"], - description: parameterObject["description"], - in: parameterObject["in"], - type: parameterObject["schema"] - ? parameterObject["schema"]["type"] - : null, + name: openApiParameter.name, + description: openApiParameter.description, + in: openApiParameter.in, + type: openApiParameter.schema?.type, values: [], - required: parameterObject["required"] + required: openApiParameter.required }; return parameter; } - // public convertRequest(requestObject: object): RequestContract { - - // const request: RequestContract = { - // description: "", - // queryParameters: ParameterContract[]; - // headers: ParameterContract[]; - // representations: RepresentationContract[]; - // } - // } - - public convertResponse(statusCode: number, responseObject: object): ResponseContract { - const response: ResponseContract = { - statusCode: statusCode, - // representations?: RepresentationContract[]; - description: responseObject["description"] + public convertRequest(spec: OpenApiSpec30, openApiOperation: OpenApiOperation): RequestContract { + const request: RequestContract = { + description: openApiOperation.description, }; - const headersObject = responseObject["headers"]; + if (openApiOperation.parameters) { + request.queryParameters = openApiOperation.parameters + ? openApiOperation.parameters + .filter(parameter => parameter.in === "query") + .map(parameter => this.convertParameter(parameter)) + : []; - if (headersObject) { - const headers: ParameterContract[] = []; - - for (const headerKey of Object.keys(headersObject)) { - const headerObject = headersObject[headerKey]; - - const header: ParameterContract = { - name: headerKey, - description: headerObject["description"], - in: headerObject["in"], - type: headerObject["schema"] - ? headerObject["schema"]["type"] - : null - }; - - headers.push(header); - } - - response.headers = headers; + request.headers = openApiOperation.parameters + ? openApiOperation.parameters + .filter(parameter => parameter.in === "header") + .map(parameter => this.convertParameter(parameter)) + : []; } - const contentObject = responseObject["content"]; + if (openApiOperation.requestBody) { + request.representations = this.convertRepresentations(spec, openApiOperation.requestBody.content); + } + + return request; + } + + public getTypeNameFromRef($ref: string): string { + return $ref && $ref.split("/").pop(); + } + + public convertRepresentation(spec: OpenApiSpec30, contentType: string, mediaType: OpenApiMediaType): RepresentationContract { + const representation: RepresentationContract = { + contentType: contentType, + typeName: this.getTypeNameFromRef(mediaType.schema?.$ref), + schemaId: `${spec.info.title}` + }; + + return representation; + } + + public convertRepresentations(spec: OpenApiSpec30, representationObjects: Bag): RepresentationContract[] { + const mediaTypes = Object.keys(representationObjects); + + const representations = mediaTypes.map(mediaType => + this.convertRepresentation(spec, mediaType, representationObjects[mediaType])); + + return representations; + } + + private convertHeaders(headersObject: Bag): ParameterContract[] { + const parameters: ParameterContract[] = []; + + for (const headerKey of Object.keys(headersObject)) { + const headerObject = headersObject[headerKey]; + + const header: ParameterContract = { + name: headerKey, + description: headerObject.description, + in: headerObject.in, + type: headerObject.schema?.type + }; + + parameters.push(header); + } + + return parameters; + } + + public convertResponse(spec: OpenApiSpec30, statusCode: number, responseObject: OpenApiResponse): ResponseContract { + const response: ResponseContract = { + statusCode: statusCode, + description: responseObject.description + }; + + const headersObject = responseObject.headers; + + if (headersObject) { + response.headers = this.convertHeaders(headersObject); + } + + const contentObject = responseObject.content; if (contentObject) { - const representations: RepresentationContract[] = []; - - for (const representationKey of Object.keys(contentObject)) { - const representationObject = contentObject[representationKey]; - - const representation: RepresentationContract = { - contentType: representationKey, - sample: representationObject.examples?.["response"] - // generatedSample?: string; - // schemaId?: string; - // typeName?: string; - // formParameters?: ParameterContract[]; - }; - - const representationExamplesObject = representationObject.examples; - - if (representationExamplesObject) { - const exampleKeys = Object.keys(representationExamplesObject); - - if (exampleKeys.length > 0) { - representation.sample = JSON.stringify(representationExamplesObject[exampleKeys[0]]); - } - } - - - representations.push(representation); - } - - response.representations = representations; + response.representations = this.convertRepresentations(spec, contentObject); } return response; } - public convertPaths(pathsObject: object): OperationContract[] { + public convertPaths(spec: OpenApiSpec30): OperationContract[] { + const pathsObject = spec.paths; const operations: OperationContract[] = []; for (const pathKey of Object.keys(pathsObject)) { @@ -117,19 +136,19 @@ export class OpenApiConverter { urlTemplate: pathKey, templateParameters: methodObject.parameters ? methodObject.parameters - .filter(x => x["in"] === "template") - .map(x => this.convertParameter(x)) + .filter(parameter => parameter.in === "template") + .map(parameter => this.convertParameter(parameter)) : [], method: methodKey.toUpperCase(), version: "", - request: null, // RequestContract; + request: this.convertRequest(spec, methodObject) } }; - const responsesObject = methodObject["responses"]; + const responsesObject = methodObject.responses; if (responsesObject) { - const responses: ResponseContract[] = []; + const responseContracts: ResponseContract[] = []; for (const responseKey of Object.keys(responsesObject)) { const statusCode = parseInt(responseKey); @@ -138,10 +157,10 @@ export class OpenApiConverter { continue; } - const response = this.convertResponse(statusCode, responsesObject[responseKey]); - responses.push(response); + const responseContract = this.convertResponse(spec, statusCode, responsesObject[responseKey]); + responseContracts.push(responseContract); } - operation.properties.responses = responses; + operation.properties.responses = responseContracts; } operations.push(operation); @@ -151,27 +170,53 @@ export class OpenApiConverter { return operations; } - public getApi(spec: any): ApiContract { - const api: ApiContract = { + public getApi(spec: OpenApiSpec30): ApiContract { + const apiContract: ApiContract = { name: spec.info.title, properties: { displayName: spec.info.title, description: spec.info.description, subscriptionRequired: false, protocols: ["http", "https"], - thumbnail: spec.info["x:thumbnail"] || "https://repository-images.githubusercontent.com/168243877/bc582a00-838e-11e9-82cd-708afc2d2a11" + thumbnail: spec.info["x:thumbnail"] } }; - return api; + return apiContract; } - public getOperations(spec: any): OperationContract[] { - const operations = this.convertPaths(spec.paths); + public getOperations(spec: OpenApiSpec30): OperationContract[] { + const operations = this.convertPaths(spec); return operations; } - public getHostnames(spec: any): string[] { - return spec.servers?.map(x => new URL(x.url).hostname) || []; + public getHostnames(spec: OpenApiSpec30): string[] { + if (!spec.servers) { + return []; + } + + return spec.servers?.map(server => + server.url.startsWith("http://") || server.url.startsWith("https://") + ? new URL(server.url).hostname + : "https://contoso.com"); + } + + public getSchema(spec: OpenApiSpec30): Schema { + const schemasObject = spec.components?.schemas; + + if (!schemasObject) { + return null; + } + + const definitions = Object + .keys(schemasObject) + .map(definitionName => { + return new TypeDefinition(definitionName, schemasObject[definitionName]); + }); + + const schema = new Schema(); + schema.definitions = definitions; + + return schema; } } \ No newline at end of file