├── .eslintignore ├── test ├── fixtures │ ├── return-scalar.graphql │ ├── parameter-for-all-methods.graphql │ ├── simple.graphql │ ├── special-parameters.graphql │ ├── deep-nested.graphql │ ├── circular.graphql │ ├── return-scalar.json │ ├── parameter-for-all-methods.json │ ├── simple.json │ ├── special-parameters.json │ ├── deep-nested.json │ ├── circular.json │ ├── petstore.graphql │ ├── petstore.graphql.old │ ├── petstore.yaml │ ├── petstore-openapi3.yaml │ └── petstore.json ├── mocha.opts ├── .eslintrc.js ├── createTestOptions.ts ├── index-test.ts ├── fixture-test.ts ├── http-adapters-test.ts ├── typeMap-test.ts ├── getRequestOptions-test.ts ├── swagger-test.ts └── integration-test.ts ├── .prettierrc.js ├── .travis.yml ├── .babelrc ├── .gitignore ├── .eslintrc.js ├── example ├── request-promise.ts ├── app.ts └── node-fetch.ts ├── bin └── swagger-to-graphql ├── LICENSE ├── src ├── json-schema.ts ├── getRequestOptions.ts ├── index.ts ├── typeMap.ts └── swagger.ts ├── package.json ├── tsconfig.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | -------------------------------------------------------------------------------- /test/fixtures/return-scalar.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | get_mock_path: String! 3 | } 4 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ts-node/register 2 | --watch-extensions json,ts,graphql 3 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | mocha: true, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/parameter-for-all-methods.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | get_mock_path(id: String!): String! 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'all', 3 | singleQuote: true, 4 | proseWrap: 'always', 5 | }; 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | - 10 5 | - 12 6 | script: 7 | - npm run lint 8 | - npm run test 9 | -------------------------------------------------------------------------------- /test/fixtures/simple.graphql: -------------------------------------------------------------------------------- 1 | type get_mock_path_response { 2 | result: String! 3 | } 4 | 5 | type Query { 6 | get_mock_path: get_mock_path_response! 7 | } 8 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"], 3 | "plugins": ["add-module-exports", "syntax-flow", "transform-flow-strip-types", "transform-runtime"] 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/special-parameters.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | get_path_dashed_path_param(dashed_request_header: String, dashed_query_param: String, dashed_path_param: String!): Response! 3 | } 4 | 5 | type Response { 6 | result: String 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.sublime-project 3 | *.sublime-workspace 4 | .idea/ 5 | .vscode/ 6 | 7 | lib-cov 8 | *.seed 9 | *.log 10 | *.csv 11 | *.dat 12 | *.out 13 | *.pid 14 | *.gz 15 | *.map 16 | 17 | pids 18 | logs 19 | results 20 | 21 | node_modules 22 | npm-debug.log 23 | 24 | dump.rdb 25 | bundle.js 26 | 27 | coverage 28 | .nyc_output 29 | flow-coverage 30 | yarn.lock 31 | 32 | lib/ 33 | -------------------------------------------------------------------------------- /test/fixtures/deep-nested.graphql: -------------------------------------------------------------------------------- 1 | type DeepStructure { 2 | id: String! 3 | body: DeepStructure_body! 4 | } 5 | 6 | type DeepStructure_body { 7 | url: DeepStructure_body_url! 8 | images: [String!]! 9 | version: String! 10 | } 11 | 12 | type DeepStructure_body_url { 13 | protocol: String 14 | baseURL: String 15 | } 16 | 17 | type Query { 18 | get_test: [DeepStructure!]! 19 | } 20 | -------------------------------------------------------------------------------- /test/fixtures/circular.graphql: -------------------------------------------------------------------------------- 1 | type Circular { 2 | name: String 3 | reference: Circular 4 | } 5 | 6 | input CircularInput { 7 | name: String 8 | reference: CircularInput 9 | } 10 | 11 | type Mutation { 12 | """Updates a circular structure""" 13 | patch_swagger_graphql_circular(body: CircularInput): [Circular!]! 14 | } 15 | 16 | type Query { 17 | """A retrieves a circular structure""" 18 | get_swagger_graphql_circular: [Circular!]! 19 | } 20 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'airbnb-base', 4 | 'plugin:@typescript-eslint/recommended', 5 | 'plugin:prettier/recommended', 6 | 'prettier/@typescript-eslint', 7 | 'plugin:import/typescript', 8 | ], 9 | rules: { 10 | 'no-nested-ternary': 'off', 11 | 'import/no-extraneous-dependencies': [ 12 | 'error', 13 | { devDependencies: ['example/**/*.ts', 'test/**/*'] }, 14 | ], 15 | 'import/prefer-default-export': 'off', 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /test/fixtures/return-scalar.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "title": "Minimal swagger file", 5 | "version": "1.0.0" 6 | }, 7 | "host": "mock-host", 8 | "basePath": "/mock-basepath", 9 | "paths": { 10 | "/mock-path": { 11 | "get": { 12 | "responses": { 13 | "200": { 14 | "description": "gets the mock resource", 15 | "schema": { 16 | "type": "string" 17 | } 18 | } 19 | } 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/request-promise.ts: -------------------------------------------------------------------------------- 1 | import requestPromise from 'request-promise'; 2 | 3 | import { CallBackendArguments } from '../src'; 4 | 5 | export async function callBackend({ 6 | requestOptions: { method, body, baseUrl, path, query, headers, bodyType }, 7 | }: CallBackendArguments<{}>) { 8 | return requestPromise({ 9 | ...(bodyType === 'json' && { 10 | json: true, 11 | body, 12 | }), 13 | ...(bodyType === 'formData' && { 14 | form: body, 15 | }), 16 | qs: query, 17 | qsStringifyOptions: { 18 | indices: false, 19 | }, 20 | method, 21 | headers, 22 | baseUrl, 23 | uri: path, 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /test/fixtures/parameter-for-all-methods.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "title": "Parameter for all methods", 5 | "version": "1.0.0" 6 | }, 7 | "paths": { 8 | "/mock-path": { 9 | "parameters": [ 10 | { 11 | "name": "id", 12 | "in": "path", 13 | "type": "string", 14 | "required": true 15 | } 16 | ], 17 | "get": { 18 | "responses": { 19 | "200": { 20 | "description": "gets the mock resource", 21 | "schema": { 22 | "type": "string" 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/fixtures/simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "title": "Minimal swagger file", 5 | "version": "1.0.0" 6 | }, 7 | "host": "mock-host", 8 | "basePath": "/mock-basepath", 9 | "paths": { 10 | "/mock-path": { 11 | "get": { 12 | "responses": { 13 | "200": { 14 | "description": "gets the mock resource", 15 | "schema": { 16 | "type": "object", 17 | "properties": { 18 | "result": { 19 | "type": "string" 20 | } 21 | }, 22 | "required": ["result"] 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example/app.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import graphqlHTTP from 'express-graphql'; 3 | import { callBackend } from './node-fetch'; 4 | import { createSchema } from '../src'; 5 | 6 | const app = express(); 7 | 8 | const pathToSwaggerSchema = `${__dirname}/../test/fixtures/petstore.yaml`; 9 | 10 | createSchema({ 11 | swaggerSchema: pathToSwaggerSchema, 12 | callBackend, 13 | }) 14 | .then(schema => { 15 | app.use( 16 | '/graphql', 17 | graphqlHTTP(() => { 18 | return { 19 | schema, 20 | graphiql: true, 21 | }; 22 | }), 23 | ); 24 | 25 | app.listen(3009, 'localhost', () => { 26 | console.info('http://localhost:3009/graphql'); 27 | }); 28 | }) 29 | .catch(e => { 30 | console.log(e); 31 | }); 32 | -------------------------------------------------------------------------------- /test/createTestOptions.ts: -------------------------------------------------------------------------------- 1 | import requestPromise from 'request-promise'; 2 | import { JSONSchema } from 'json-schema-ref-parser'; 3 | import { Options } from '../src'; 4 | 5 | export function createTestOptions( 6 | swaggerSchema: string | JSONSchema, 7 | ): Options { 8 | return { 9 | swaggerSchema, 10 | async callBackend({ 11 | requestOptions: { method, body, baseUrl, path, query, headers, bodyType }, 12 | }) { 13 | return requestPromise({ 14 | ...(bodyType === 'json' && { 15 | json: true, 16 | body, 17 | }), 18 | ...(bodyType === 'formData' && { 19 | form: body, 20 | }), 21 | qs: query, 22 | method, 23 | headers, 24 | baseUrl, 25 | uri: path, 26 | }); 27 | }, 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /bin/swagger-to-graphql: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var swaggerToGraphql = require('../lib'); 3 | var graphql = require('graphql'); 4 | 5 | require('yargs') 6 | .scriptName('swagger-to-graphql') 7 | .command( 8 | '$0', 9 | 'Convert swagger schema to graphql schema', 10 | yargs => { 11 | yargs.options('swagger-schema', { 12 | describe: 'Path or url to a swagger schema, can be json or yaml', 13 | type: 'string', 14 | demandOption: true, 15 | }); 16 | }, 17 | async ({ swaggerSchema }) => { 18 | try { 19 | const schema = await swaggerToGraphql.createSchema({ 20 | swaggerSchema, 21 | }); 22 | console.log(graphql.printSchema(schema)); 23 | } catch (err) { 24 | console.error(err); 25 | process.exit(1); 26 | } 27 | }, 28 | ) 29 | .help().argv; 30 | -------------------------------------------------------------------------------- /test/index-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { 3 | GraphQLInt, 4 | GraphQLList, 5 | GraphQLObjectType, 6 | GraphQLString, 7 | } from 'graphql'; 8 | import { parseResponse } from '../src'; 9 | 10 | describe('parseResponse', () => { 11 | it('should convert non strings to string', () => { 12 | expect(parseResponse({ mock: 'object' }, GraphQLString)).to.equal( 13 | JSON.stringify({ mock: 'object' }), 14 | ); 15 | }); 16 | 17 | it('should ignore object', () => { 18 | expect( 19 | parseResponse( 20 | { a: 1 }, 21 | new GraphQLObjectType({ 22 | name: 'Test', 23 | fields: { a: { type: GraphQLInt } }, 24 | }), 25 | ), 26 | ).deep.equal({ a: 1 }); 27 | }); 28 | 29 | it('should ignore Lists', () => { 30 | expect(parseResponse([1, 2], new GraphQLList(GraphQLInt))).deep.equal([ 31 | 1, 32 | 2, 33 | ]); 34 | }); 35 | 36 | it('should ignore Ints', () => { 37 | expect(parseResponse(1, GraphQLInt)).to.equal(1); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Roman Krivtsov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/node-fetch.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import { URLSearchParams } from 'url'; 3 | import { CallBackendArguments } from '../src'; 4 | 5 | function getBodyAndHeaders( 6 | body: any, 7 | bodyType: 'json' | 'formData', 8 | headers: { [key: string]: string } | undefined, 9 | ) { 10 | if (!body) { 11 | return { headers }; 12 | } 13 | 14 | if (bodyType === 'json') { 15 | return { 16 | headers: { 17 | 'Content-Type': 'application/json', 18 | ...headers, 19 | }, 20 | body: JSON.stringify(body), 21 | }; 22 | } 23 | 24 | return { 25 | headers, 26 | body: new URLSearchParams(body), 27 | }; 28 | } 29 | 30 | export async function callBackend({ 31 | requestOptions: { method, body, baseUrl, path, query, headers, bodyType }, 32 | }: CallBackendArguments<{}>) { 33 | const searchPath = query ? `?${new URLSearchParams(query)}` : ''; 34 | const url = `${baseUrl}${path}${searchPath}`; 35 | const bodyAndHeaders = getBodyAndHeaders(body, bodyType, headers); 36 | const response = await fetch(url, { 37 | method, 38 | ...bodyAndHeaders, 39 | }); 40 | 41 | const text = await response.text(); 42 | if (response.ok) { 43 | try { 44 | return JSON.parse(text); 45 | } catch (e) { 46 | return text; 47 | } 48 | } 49 | throw new Error(`Response: ${response.status} - ${text}`); 50 | } 51 | -------------------------------------------------------------------------------- /test/fixtures/special-parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "version": "1.0.0", 4 | "title": "Api with headers" 5 | }, 6 | "paths": { 7 | "/path/{dashed-path-param}": { 8 | "get": { 9 | "responses": { 10 | "200": { 11 | "description": "successful operation", 12 | "schema": { 13 | "$ref": "#/definitions/Response" 14 | } 15 | } 16 | }, 17 | "parameters": [ 18 | { 19 | "in": "header", 20 | "type": "string", 21 | "description": "dashed-request-header", 22 | "name": "dashed-request-header" 23 | }, 24 | { 25 | "in": "query", 26 | "type": "string", 27 | "description": "dashed-query-param", 28 | "name": "dashed-query-param" 29 | }, 30 | { 31 | "required": true, 32 | "type": "string", 33 | "description": "dashed-query-param", 34 | "in": "path", 35 | "name": "dashed-path-param" 36 | } 37 | ] 38 | } 39 | } 40 | }, 41 | "host": "mock.api.com", 42 | "schemes": ["http"], 43 | "definitions": { 44 | "Response": { 45 | "type": "object", 46 | "properties": { 47 | "result": { 48 | "type": "string" 49 | } 50 | } 51 | } 52 | }, 53 | "basePath": "/v2", 54 | "swagger": "2.0" 55 | } 56 | -------------------------------------------------------------------------------- /test/fixture-test.ts: -------------------------------------------------------------------------------- 1 | import * as graphql from 'graphql'; 2 | import * as fs from 'fs'; 3 | import { expect } from 'chai'; 4 | 5 | import graphQLSchema from '../src'; 6 | 7 | describe('Fixture', () => { 8 | const directory = `${__dirname}/fixtures/`; 9 | fs.readdirSync(directory).forEach(file => { 10 | if (file.endsWith('.json')) { 11 | describe(file, () => { 12 | const graphqlFile = file.replace('.json', '.graphql'); 13 | it(`should convert to ${graphqlFile}`, () => 14 | graphQLSchema({ 15 | swaggerSchema: directory + file, 16 | callBackend() { 17 | return new Promise(() => {}); 18 | }, 19 | }).then(schema => { 20 | const graphqlfile = directory + graphqlFile; 21 | const graphschema = graphql.printSchema(schema); 22 | const expected = fs.readFileSync(graphqlfile, 'utf8'); 23 | expect(graphschema).to.equal(expected); 24 | })); 25 | }); 26 | } 27 | }); 28 | 29 | describe('petstore converted to openapi 3', () => { 30 | it('should have the same graphql schema as openapi 2', async () => { 31 | const swaggerSchema = `test/fixtures/petstore-openapi3.yaml`; 32 | const graphqlFile = `test/fixtures/petstore.graphql`; 33 | const schema = await graphQLSchema({ 34 | swaggerSchema, 35 | callBackend() { 36 | return new Promise(() => {}); 37 | }, 38 | }); 39 | const graphschema = graphql.printSchema(schema); 40 | const expected = fs.readFileSync(graphqlFile, 'utf8'); 41 | expect(graphschema).to.equal(expected); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/fixtures/deep-nested.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "title": "DeepNest", 5 | "description": "For testing required nested object types", 6 | "version": "1.0.0" 7 | }, 8 | "schemes": ["http"], 9 | "paths": { 10 | "/test": { 11 | "get": { 12 | "responses": { 13 | "200": { 14 | "description": "successful operation", 15 | "schema": { 16 | "type": "array", 17 | "items": { 18 | "$ref": "#/definitions/DeepStructure" 19 | } 20 | } 21 | } 22 | } 23 | } 24 | } 25 | }, 26 | "definitions": { 27 | "DeepStructure": { 28 | "type": "object", 29 | "properties": { 30 | "id": { 31 | "type": "integer", 32 | "format": "int64" 33 | }, 34 | "body": { 35 | "type": "object", 36 | "properties": { 37 | "url": { 38 | "type": "object", 39 | "properties": { 40 | "protocol": { 41 | "type": "string" 42 | }, 43 | "baseURL": { 44 | "type": "string" 45 | } 46 | } 47 | }, 48 | "images": { 49 | "type": "array", 50 | "items": { 51 | "type": "string" 52 | } 53 | }, 54 | "version": { 55 | "type": "string" 56 | } 57 | }, 58 | "required": ["url", "images", "version"] 59 | } 60 | }, 61 | "required": ["id", "body"] 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/fixtures/circular.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Fake API", 6 | "description": "For testing circular structures" 7 | }, 8 | "schemes": ["http"], 9 | "consumes": ["application/json"], 10 | "produces": ["application/json"], 11 | "paths": { 12 | "/swagger-graphql/circular": { 13 | "get": { 14 | "description": "A retrieves a circular structure", 15 | "parameters": [], 16 | "produces": ["application/json"], 17 | "responses": { 18 | "200": { 19 | "description": null, 20 | "schema": { 21 | "type": "array", 22 | "items": { 23 | "$ref": "#/definitions/Circular" 24 | } 25 | } 26 | } 27 | } 28 | }, 29 | "patch": { 30 | "description": "Updates a circular structure", 31 | "parameters": [ 32 | { 33 | "in": "body", 34 | "name": "body", 35 | "schema": { 36 | "$ref": "#/definitions/Circular" 37 | } 38 | } 39 | ], 40 | "produces": ["application/json"], 41 | "responses": { 42 | "200": { 43 | "description": null, 44 | "schema": { 45 | "type": "array", 46 | "items": { 47 | "$ref": "#/definitions/Circular" 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | }, 55 | "definitions": { 56 | "Circular": { 57 | "type": "object", 58 | "properties": { 59 | "name": { 60 | "type": "string" 61 | }, 62 | "reference": { 63 | "$ref": "#/definitions/Circular" 64 | } 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/json-schema.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType } from 'graphql'; 2 | 3 | export interface RootGraphQLSchema { 4 | query: GraphQLObjectType; 5 | mutation?: GraphQLObjectType; 6 | } 7 | 8 | interface CommonSchema { 9 | description?: string; 10 | title?: string; 11 | } 12 | 13 | export interface BodySchema extends CommonSchema { 14 | in: 'body'; 15 | schema: JSONSchemaType; 16 | required?: boolean; 17 | } 18 | 19 | export interface ObjectSchema extends CommonSchema { 20 | type: 'object'; 21 | properties: { 22 | [propertyName: string]: JSONSchemaType; 23 | }; 24 | required?: string[]; 25 | } 26 | 27 | export interface ArraySchema extends CommonSchema { 28 | type: 'array'; 29 | items: JSONSchemaNoBody | JSONSchemaNoBody[]; 30 | required?: boolean; 31 | } 32 | 33 | export type JSONSchemaTypes = 34 | | 'string' 35 | | 'date' 36 | | 'integer' 37 | | 'number' 38 | | 'boolean' 39 | | 'file'; 40 | 41 | export interface ScalarSchema extends CommonSchema { 42 | type: JSONSchemaTypes; 43 | format?: string; 44 | required?: boolean; 45 | } 46 | 47 | export type JSONSchemaNoBody = ObjectSchema | ArraySchema | ScalarSchema; 48 | 49 | export type JSONSchemaType = BodySchema | JSONSchemaNoBody; 50 | 51 | export const isBodyType = ( 52 | jsonSchema: JSONSchemaType, 53 | ): jsonSchema is BodySchema => 54 | Object.keys(jsonSchema).includes('in') && 55 | (jsonSchema as BodySchema).in === 'body'; 56 | 57 | export const isObjectType = ( 58 | jsonSchema: JSONSchemaType, 59 | ): jsonSchema is ObjectSchema => 60 | !isBodyType(jsonSchema) && 61 | (Object.keys(jsonSchema).includes('properties') || 62 | jsonSchema.type === 'object'); 63 | 64 | export const isArrayType = ( 65 | jsonSchema: JSONSchemaType, 66 | ): jsonSchema is ArraySchema => 67 | !isBodyType(jsonSchema) && 68 | (Object.keys(jsonSchema).includes('items') || jsonSchema.type === 'array'); 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swagger-to-graphql", 3 | "version": "4.0.2", 4 | "author": "Roman Krivtsov", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/yarax/swagger-to-graphql.git" 8 | }, 9 | "bin": "./bin/swagger-to-graphql", 10 | "dependencies": { 11 | "@types/json-schema": "^7.0.3", 12 | "json-schema-ref-parser": "^7.1.0", 13 | "yargs": "^14.0.0" 14 | }, 15 | "peerDependencies": { 16 | "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0" 17 | }, 18 | "devDependencies": { 19 | "@types/chai": "^4.2.0", 20 | "@types/express": "^4.17.1", 21 | "@types/express-graphql": "^0.8.0", 22 | "@types/graphql": "^14.2.3", 23 | "@types/lodash": "^4.14.137", 24 | "@types/mocha": "^5.2.7", 25 | "@types/nock": "^10.0.3", 26 | "@types/node-fetch": "^2.5.0", 27 | "@types/request": "^2.48.2", 28 | "@types/request-promise": "^4.1.44", 29 | "@types/supertest": "^2.0.8", 30 | "@typescript-eslint/eslint-plugin": "^2.0.0", 31 | "@typescript-eslint/parser": "^2.0.0", 32 | "chai": "^4.2.0", 33 | "eslint": "^6.2.1", 34 | "eslint-config-airbnb-base": "^14.0.0", 35 | "eslint-config-prettier": "^6.1.0", 36 | "eslint-plugin-import": "^2.18.2", 37 | "eslint-plugin-prettier": "^3.1.0", 38 | "express": "^4.17.1", 39 | "express-graphql": "^0.9.0", 40 | "graphql": "^14.4.2", 41 | "mocha": "^6.2.0", 42 | "nock": "^10.0.6", 43 | "node-fetch": "^2.6.0", 44 | "prettier": "^1.18.2", 45 | "request": "^2.88.0", 46 | "request-promise": "^4.2.4", 47 | "rimraf": "^3.0.0", 48 | "supertest": "^4.0.2", 49 | "ts-node": "^8.3.0", 50 | "ttypescript": "^1.5.7", 51 | "typescript": "^3.5.3", 52 | "typescript-is": "^0.12.2" 53 | }, 54 | "keywords": [ 55 | "graphql", 56 | "swagger" 57 | ], 58 | "files": [ 59 | "bin", 60 | "lib" 61 | ], 62 | "license": "MIT", 63 | "main": "lib/index.js", 64 | "types": "lib/index.d.ts", 65 | "scripts": { 66 | "prebuild": "rimraf lib", 67 | "build": "ttsc", 68 | "lint": "eslint --ext .js,.ts .", 69 | "lint:fix": "npm run lint -- --fix", 70 | "prepare": "npm run build", 71 | "start": "npm run example", 72 | "test": "TS_NODE_COMPILER=ttypescript mocha", 73 | "test:watch": "npm run test -- --watch", 74 | "example": "ts-node example/app" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/getRequestOptions.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchemaType } from './json-schema'; 2 | 3 | export interface EndpointParam { 4 | required: boolean; 5 | type: 'header' | 'query' | 'formData' | 'path' | 'body'; 6 | name: string; 7 | swaggerName: string; 8 | jsonSchema: JSONSchemaType; 9 | } 10 | 11 | export interface RequestOptionsInput { 12 | method: string; 13 | baseUrl: string | undefined; 14 | path: string; 15 | parameterDetails: EndpointParam[]; 16 | parameterValues: { 17 | [key: string]: any; 18 | }; 19 | formData?: boolean; 20 | } 21 | 22 | export interface RequestOptions { 23 | baseUrl?: string; 24 | path: string; 25 | method: string; 26 | headers?: { 27 | [key: string]: string; 28 | }; 29 | query?: { 30 | [key: string]: string | string[]; 31 | }; 32 | body?: any; 33 | bodyType: 'json' | 'formData'; 34 | } 35 | 36 | export function getRequestOptions({ 37 | method, 38 | baseUrl, 39 | path, 40 | parameterDetails, 41 | parameterValues, 42 | formData = false, 43 | }: RequestOptionsInput): RequestOptions { 44 | const result: RequestOptions = { 45 | method, 46 | baseUrl, 47 | path, 48 | bodyType: formData ? 'formData' : 'json', 49 | }; 50 | 51 | parameterDetails.forEach(({ name, swaggerName, type, required }) => { 52 | const value = parameterValues[name]; 53 | 54 | if (required && !value && value !== '') 55 | throw new Error( 56 | `No required request field ${name} for ${method.toUpperCase()} ${path}`, 57 | ); 58 | if (!value && value !== '') return; 59 | 60 | switch (type) { 61 | case 'body': 62 | result.body = value; 63 | break; 64 | case 'formData': 65 | result.body = result.body || {}; 66 | result.body[swaggerName] = value; 67 | break; 68 | case 'path': 69 | result.path = 70 | typeof result.path === 'string' 71 | ? result.path.replace(`{${swaggerName}}`, value) 72 | : result.path; 73 | break; 74 | case 'query': 75 | result.query = result.query || {}; 76 | result.query[swaggerName] = value; 77 | break; 78 | case 'header': 79 | result.headers = result.headers || {}; 80 | result.headers[swaggerName] = value; 81 | break; 82 | default: 83 | throw new Error( 84 | `Unsupported param type for param "${name}" and type "${type}"`, 85 | ); 86 | } 87 | }); 88 | 89 | return result; 90 | } 91 | -------------------------------------------------------------------------------- /test/http-adapters-test.ts: -------------------------------------------------------------------------------- 1 | import nock from 'nock'; 2 | import { expect } from 'chai'; 3 | import * as requestPromise from '../example/request-promise'; 4 | import * as nodeFetch from '../example/node-fetch'; 5 | 6 | type AdapterConfig = { 7 | name: string; 8 | callBackend: typeof requestPromise.callBackend; 9 | }[]; 10 | 11 | const adapterConfig: AdapterConfig = [ 12 | { 13 | name: 'request-promise', 14 | callBackend: requestPromise.callBackend, 15 | }, 16 | { 17 | name: 'node-fetch', 18 | callBackend: nodeFetch.callBackend, 19 | }, 20 | ]; 21 | 22 | adapterConfig.forEach(({ name, callBackend }) => { 23 | describe(name, () => { 24 | beforeEach(() => { 25 | nock.disableNetConnect(); 26 | nock.enableNetConnect('127.0.0.1'); 27 | }); 28 | 29 | afterEach(() => { 30 | nock.cleanAll(); 31 | nock.enableNetConnect(); 32 | }); 33 | 34 | it('should make json http calls', async () => { 35 | const nockScope = nock('http://mock-host') 36 | .post('/mock-uri', { 37 | mockBodyKey: 'mock body value', 38 | }) 39 | .query({ 40 | 'query-params': 'a,b', 41 | }) 42 | .matchHeader('mock-header', 'mock header value') 43 | .matchHeader('content-type', 'application/json') 44 | .reply(200, 'mock result'); 45 | 46 | const result = await callBackend({ 47 | requestOptions: { 48 | method: 'post', 49 | baseUrl: 'http://mock-host', 50 | path: '/mock-uri', 51 | headers: { 52 | 'mock-header': 'mock header value', 53 | }, 54 | query: { 55 | 'query-params': ['a', 'b'], 56 | }, 57 | body: { 58 | mockBodyKey: 'mock body value', 59 | }, 60 | bodyType: 'json', 61 | }, 62 | context: {}, 63 | }); 64 | 65 | expect(result).to.equal('mock result'); 66 | 67 | nockScope.done(); 68 | }); 69 | 70 | it('should post formData', async () => { 71 | const nockScope = nock('http://mock-host') 72 | .post('/mock-uri', body => { 73 | expect(body).to.deep.equal({ 74 | mockBodyKey: 'mock body value', 75 | }); 76 | return true; 77 | }) 78 | .matchHeader('content-type', /application\/x-www-form-urlencoded/) 79 | .reply(200, 'mock result'); 80 | 81 | const result = await callBackend({ 82 | requestOptions: { 83 | method: 'post', 84 | baseUrl: 'http://mock-host', 85 | path: '/mock-uri', 86 | body: { 87 | mockBodyKey: 'mock body value', 88 | }, 89 | bodyType: 'formData', 90 | }, 91 | context: {}, 92 | }); 93 | 94 | expect(result).to.equal('mock result'); 95 | 96 | nockScope.done(); 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /test/typeMap-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { 3 | GraphQLInputObjectType, 4 | GraphQLList, 5 | GraphQLNonNull, 6 | GraphQLScalarType, 7 | GraphQLString, 8 | } from 'graphql'; 9 | import { jsonSchemaTypeToGraphQL } from '../src/typeMap'; 10 | 11 | describe('typeMap', () => { 12 | describe('jsonSchemaTypeToGraphQL', () => { 13 | it('should give an unsupported type for files', () => { 14 | const graphqlFileType = jsonSchemaTypeToGraphQL( 15 | 'mocktitle', 16 | { 17 | type: 'file', 18 | }, 19 | 'mockpropertyname', 20 | true, 21 | {}, 22 | false, 23 | ) as GraphQLInputObjectType; 24 | 25 | expect(graphqlFileType).to.be.instanceOf(GraphQLInputObjectType); 26 | expect(graphqlFileType.name).to.equal('mocktitle_mockpropertynameInput'); 27 | 28 | expect(graphqlFileType.getFields()).to.deep.equal({ 29 | unsupported: { 30 | name: 'unsupported', 31 | description: undefined, 32 | type: GraphQLString, 33 | }, 34 | }); 35 | }); 36 | 37 | it('should give an unsupported type for list of files', () => { 38 | const graphqlList = jsonSchemaTypeToGraphQL( 39 | 'mocktitle', 40 | { 41 | type: 'array', 42 | items: { 43 | type: 'file', 44 | }, 45 | }, 46 | 'mockpropertyname', 47 | true, 48 | {}, 49 | false, 50 | ) as GraphQLList>; 51 | 52 | expect(graphqlList).to.be.instanceOf(GraphQLList); 53 | 54 | const nonNullable = graphqlList.ofType; 55 | expect(nonNullable).to.be.instanceOf(GraphQLNonNull); 56 | 57 | const itemType = nonNullable.ofType; 58 | expect(itemType.name).to.equal('mocktitle_mockpropertynameInput'); 59 | 60 | expect(itemType.getFields()).to.deep.equal({ 61 | unsupported: { 62 | name: 'unsupported', 63 | description: undefined, 64 | type: GraphQLString, 65 | }, 66 | }); 67 | }); 68 | 69 | // TODO: make this a union type? 70 | it('should take the first item type of an array with multiple item types', () => { 71 | const graphqlList = jsonSchemaTypeToGraphQL( 72 | 'mocktitle', 73 | { 74 | type: 'array', 75 | items: [ 76 | { 77 | type: 'string', 78 | }, 79 | { 80 | type: 'integer', 81 | }, 82 | ], 83 | }, 84 | 'mockpropertyname', 85 | true, 86 | {}, 87 | false, 88 | ) as GraphQLList>; 89 | 90 | expect(graphqlList).to.be.instanceOf(GraphQLList); 91 | 92 | const nonNullable = graphqlList.ofType; 93 | expect(nonNullable).to.be.instanceOf(GraphQLNonNull); 94 | const itemType = nonNullable.ofType; 95 | expect(itemType).to.equal(GraphQLString); 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /test/fixtures/petstore.graphql: -------------------------------------------------------------------------------- 1 | type Category { 2 | id: String 3 | name: String 4 | } 5 | 6 | input CategoryInput { 7 | id: String 8 | name: String 9 | } 10 | 11 | scalar JSON 12 | 13 | type Mutation { 14 | addPet(body: PetInput!): JSON! 15 | updatePet(body: PetInput!): JSON! 16 | updatePetWithForm(petId: String!, name: String, status: String): JSON! 17 | deletePet(api_key: String, petId: String!): JSON! 18 | placeOrder(body: OrderInput!): Order! 19 | 20 | """ 21 | For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors 22 | """ 23 | deleteOrder(orderId: String!): JSON! 24 | 25 | """This can only be done by the logged in user.""" 26 | createUser(body: UserInput!): JSON! 27 | createUsersWithArrayInput(body: [UserInput!]!): JSON! 28 | createUsersWithListInput(body: [UserInput!]!): JSON! 29 | 30 | """This can only be done by the logged in user.""" 31 | updateUser(username: String!, body: UserInput!): JSON! 32 | 33 | """This can only be done by the logged in user.""" 34 | deleteUser(username: String!): JSON! 35 | } 36 | 37 | type Order { 38 | id: String 39 | petId: String 40 | quantity: Int 41 | shipDate: String 42 | 43 | """Order Status""" 44 | status: String 45 | complete: Boolean 46 | } 47 | 48 | input OrderInput { 49 | id: String 50 | petId: String 51 | quantity: Int 52 | shipDate: String 53 | 54 | """Order Status""" 55 | status: String 56 | complete: Boolean 57 | } 58 | 59 | type Pet { 60 | id: String 61 | category: Category 62 | name: String! 63 | photoUrls: [String!]! 64 | tags: [Tag!] 65 | 66 | """pet status in the store""" 67 | status: String 68 | } 69 | 70 | input PetInput { 71 | id: String 72 | category: CategoryInput 73 | name: String! 74 | photoUrls: [String!]! 75 | tags: [TagInput!] 76 | 77 | """pet status in the store""" 78 | status: String 79 | } 80 | 81 | type Query { 82 | """Multiple status values can be provided with comma separated strings""" 83 | findPetsByStatus(status: [String!]!): [Pet!]! 84 | 85 | """ 86 | Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. 87 | """ 88 | findPetsByTags(tags: [String!]!): [Pet!]! 89 | 90 | """Returns a single pet""" 91 | getPetById(petId: String!): Pet! 92 | 93 | """Returns a map of status codes to quantities""" 94 | getInventory: JSON! 95 | 96 | """ 97 | For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions 98 | """ 99 | getOrderById(orderId: String!): Order! 100 | loginUser(username: String!, password: String!): String! 101 | logoutUser: JSON! 102 | getUserByName(username: String!): User! 103 | } 104 | 105 | type Tag { 106 | id: String 107 | name: String 108 | } 109 | 110 | input TagInput { 111 | id: String 112 | name: String 113 | } 114 | 115 | type User { 116 | id: String 117 | username: String 118 | firstName: String 119 | lastName: String 120 | email: String 121 | password: String 122 | phone: String 123 | 124 | """User Status""" 125 | userStatus: Int 126 | } 127 | 128 | input UserInput { 129 | id: String 130 | username: String 131 | firstName: String 132 | lastName: String 133 | email: String 134 | password: String 135 | phone: String 136 | 137 | """User Status""" 138 | userStatus: Int 139 | } 140 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLFieldConfig, 3 | GraphQLFieldConfigMap, 4 | GraphQLList, 5 | GraphQLNonNull, 6 | GraphQLObjectType, 7 | GraphQLOutputType, 8 | GraphQLResolveInfo, 9 | GraphQLSchema, 10 | } from 'graphql'; 11 | import refParser, { JSONSchema } from 'json-schema-ref-parser'; 12 | import { 13 | addTitlesToJsonSchemas, 14 | Endpoint, 15 | Endpoints, 16 | getAllEndPoints, 17 | GraphQLParameters, 18 | SwaggerSchema, 19 | } from './swagger'; 20 | import { 21 | GraphQLTypeMap, 22 | jsonSchemaTypeToGraphQL, 23 | mapParametersToFields, 24 | } from './typeMap'; 25 | import { RequestOptions } from './getRequestOptions'; 26 | import { RootGraphQLSchema } from './json-schema'; 27 | 28 | export function parseResponse(response: any, returnType: GraphQLOutputType) { 29 | const nullableType = 30 | returnType instanceof GraphQLNonNull ? returnType.ofType : returnType; 31 | if ( 32 | nullableType instanceof GraphQLObjectType || 33 | nullableType instanceof GraphQLList 34 | ) { 35 | return response; 36 | } 37 | 38 | if (nullableType.name === 'String' && typeof response !== 'string') { 39 | return JSON.stringify(response); 40 | } 41 | 42 | return response; 43 | } 44 | 45 | const getFields = ( 46 | endpoints: Endpoints, 47 | isMutation: boolean, 48 | gqlTypes: GraphQLTypeMap, 49 | { callBackend }: Options, 50 | ): GraphQLFieldConfigMap => { 51 | return Object.keys(endpoints) 52 | .filter((operationId: string) => { 53 | return !!endpoints[operationId].mutation === !!isMutation; 54 | }) 55 | .reduce((result, operationId) => { 56 | const endpoint: Endpoint = endpoints[operationId]; 57 | const type = jsonSchemaTypeToGraphQL( 58 | operationId, 59 | endpoint.response || { type: 'object', properties: {} }, 60 | 'response', 61 | false, 62 | gqlTypes, 63 | true, 64 | ); 65 | const gType: GraphQLFieldConfig = { 66 | type, 67 | description: endpoint.description, 68 | args: mapParametersToFields(endpoint.parameters, operationId, gqlTypes), 69 | resolve: async ( 70 | _source: any, 71 | args: GraphQLParameters, 72 | context: TContext, 73 | info: GraphQLResolveInfo, 74 | ): Promise => { 75 | return parseResponse( 76 | await callBackend({ 77 | context, 78 | requestOptions: endpoint.getRequestOptions(args), 79 | }), 80 | info.returnType, 81 | ); 82 | }, 83 | }; 84 | return { ...result, [operationId]: gType }; 85 | }, {}); 86 | }; 87 | 88 | const schemaFromEndpoints = ( 89 | endpoints: Endpoints, 90 | options: Options, 91 | ): GraphQLSchema => { 92 | const gqlTypes = {}; 93 | const queryFields = getFields(endpoints, false, gqlTypes, options); 94 | if (!Object.keys(queryFields).length) { 95 | throw new Error('Did not find any GET endpoints'); 96 | } 97 | const rootType = new GraphQLObjectType({ 98 | name: 'Query', 99 | fields: queryFields, 100 | }); 101 | 102 | const graphQLSchema: RootGraphQLSchema = { 103 | query: rootType, 104 | }; 105 | 106 | const mutationFields = getFields(endpoints, true, gqlTypes, options); 107 | if (Object.keys(mutationFields).length) { 108 | graphQLSchema.mutation = new GraphQLObjectType({ 109 | name: 'Mutation', 110 | fields: mutationFields, 111 | }); 112 | } 113 | 114 | return new GraphQLSchema(graphQLSchema); 115 | }; 116 | 117 | export { RequestOptions, JSONSchema }; 118 | 119 | export interface CallBackendArguments { 120 | context: TContext; 121 | requestOptions: RequestOptions; 122 | } 123 | 124 | export interface Options { 125 | swaggerSchema: string | JSONSchema; 126 | callBackend: (args: CallBackendArguments) => Promise; 127 | } 128 | 129 | export const createSchema = async ( 130 | options: Options, 131 | ): Promise => { 132 | const schemaWithoutReferences = (await refParser.dereference( 133 | options.swaggerSchema, 134 | )) as SwaggerSchema; 135 | const swaggerSchema = addTitlesToJsonSchemas(schemaWithoutReferences); 136 | const endpoints = getAllEndPoints(swaggerSchema); 137 | return schemaFromEndpoints(endpoints, options); 138 | }; 139 | 140 | export default createSchema; 141 | -------------------------------------------------------------------------------- /test/getRequestOptions-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import { 4 | EndpointParam, 5 | getRequestOptions, 6 | RequestOptionsInput, 7 | } from '../src/getRequestOptions'; 8 | 9 | const baseUrl = 'http://mock-baseurl'; 10 | 11 | function createParameterDetails( 12 | override: Partial, 13 | ): EndpointParam { 14 | return { 15 | type: 'query', 16 | name: 'mock name', 17 | required: true, 18 | swaggerName: 'mock swaggerName', 19 | jsonSchema: { 20 | type: 'string', 21 | }, 22 | ...override, 23 | }; 24 | } 25 | 26 | describe('getRequestOptions', () => { 27 | it('should add request body', () => { 28 | const options: RequestOptionsInput = { 29 | method: 'post', 30 | baseUrl, 31 | path: '/pet', 32 | parameterDetails: [ 33 | createParameterDetails({ 34 | type: 'body', 35 | name: 'body', 36 | }), 37 | ], 38 | parameterValues: { 39 | body: { name: 'test' }, 40 | }, 41 | }; 42 | const requestOptions = getRequestOptions(options); 43 | 44 | expect(requestOptions).to.deep.equal({ 45 | baseUrl, 46 | path: '/pet', 47 | method: 'post', 48 | bodyType: 'json', 49 | body: { name: 'test' }, 50 | }); 51 | }); 52 | 53 | it('should add request headers', () => { 54 | const options = { 55 | method: 'delete', 56 | baseUrl, 57 | path: '/pet', 58 | parameterDetails: [ 59 | createParameterDetails({ 60 | name: 'api_key', 61 | swaggerName: 'api_key', 62 | type: 'header', 63 | }), 64 | ], 65 | parameterValues: { 66 | // eslint-disable-next-line @typescript-eslint/camelcase 67 | api_key: 'mock api key', 68 | }, 69 | }; 70 | const requestOptions = getRequestOptions(options); 71 | expect(requestOptions).to.deep.equal({ 72 | baseUrl, 73 | path: '/pet', 74 | method: 'delete', 75 | bodyType: 'json', 76 | headers: { 77 | // eslint-disable-next-line @typescript-eslint/camelcase 78 | api_key: 'mock api key', 79 | }, 80 | }); 81 | }); 82 | 83 | it('should add query parameters', () => { 84 | const options = { 85 | method: 'delete', 86 | baseUrl, 87 | path: '/pet', 88 | parameterDetails: [ 89 | createParameterDetails({ 90 | name: 'id', 91 | swaggerName: 'swaggerId', 92 | type: 'query', 93 | }), 94 | ], 95 | parameterValues: { 96 | id: 'mock id', 97 | }, 98 | }; 99 | const requestOptions = getRequestOptions(options); 100 | expect(requestOptions).to.deep.equal({ 101 | baseUrl, 102 | path: '/pet', 103 | method: 'delete', 104 | bodyType: 'json', 105 | query: { swaggerId: 'mock id' }, 106 | }); 107 | }); 108 | 109 | it('should set path parameters', () => { 110 | const options: RequestOptionsInput = { 111 | method: 'get', 112 | baseUrl, 113 | path: '/{mock swaggerName}/', 114 | formData: false, 115 | parameterDetails: [ 116 | createParameterDetails({ 117 | type: 'path', 118 | }), 119 | ], 120 | parameterValues: { 121 | 'mock name': 'mock-path', 122 | }, 123 | }; 124 | const requestOptions = getRequestOptions(options); 125 | 126 | expect(requestOptions).to.deep.equal({ 127 | baseUrl, 128 | method: 'get', 129 | path: '/mock-path/', 130 | bodyType: 'json', 131 | }); 132 | }); 133 | 134 | it('should allow empty strings', () => { 135 | const path = '/pet/{petId}'; 136 | const options = { 137 | method: 'delete', 138 | baseUrl, 139 | path, 140 | parameterDetails: [ 141 | createParameterDetails({ 142 | name: 'petId', 143 | swaggerName: 'petId', 144 | type: 'path', 145 | }), 146 | ], 147 | parameterValues: { 148 | petId: '', 149 | }, 150 | }; 151 | const requestOptions = getRequestOptions(options); 152 | expect(requestOptions).to.deep.equal({ 153 | baseUrl, 154 | path: '/pet/', 155 | method: 'delete', 156 | bodyType: 'json', 157 | }); 158 | }); 159 | 160 | it('should send formdata', () => { 161 | const path = '/pet'; 162 | const options: RequestOptionsInput = { 163 | method: 'post', 164 | baseUrl, 165 | path, 166 | formData: true, 167 | parameterDetails: [ 168 | createParameterDetails({ 169 | type: 'formData', 170 | name: 'name', 171 | swaggerName: 'name', 172 | }), 173 | ], 174 | parameterValues: { 175 | name: 'mock name', 176 | }, 177 | }; 178 | const requestOptions = getRequestOptions(options); 179 | 180 | expect(requestOptions).to.deep.equal({ 181 | baseUrl, 182 | method: 'post', 183 | path: '/pet', 184 | body: { name: 'mock name' }, 185 | bodyType: 'formData', 186 | }); 187 | }); 188 | }); 189 | -------------------------------------------------------------------------------- /test/swagger-test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { expect } from 'chai'; 3 | import { assertType } from 'typescript-is'; 4 | import refParser from 'json-schema-ref-parser'; 5 | import { 6 | getServerPath, 7 | getParamDetails, 8 | getSuccessResponse, 9 | SwaggerSchema, 10 | Param, 11 | } from '../src/swagger'; 12 | import { EndpointParam } from '../src/getRequestOptions'; 13 | import { ArraySchema } from '../src/json-schema'; 14 | 15 | describe('swagger', () => { 16 | describe('getServerPath', () => { 17 | it('should support swagger 2 configuration', () => { 18 | expect( 19 | getServerPath({ 20 | host: 'mock-host', 21 | paths: {}, 22 | }), 23 | ).equal('http://mock-host'); 24 | }); 25 | 26 | it('should support swagger 2 with schemes and basePath', () => { 27 | expect( 28 | getServerPath({ 29 | schemes: ['https'], 30 | host: 'mock-host', 31 | basePath: '/mock-basepath', 32 | paths: {}, 33 | }), 34 | ).equal('https://mock-host/mock-basepath'); 35 | }); 36 | 37 | it('should support swagger 3 simple variables', () => { 38 | expect( 39 | getServerPath({ 40 | servers: [ 41 | { 42 | url: '{scheme}://mock-host{basePath}', 43 | variables: { 44 | scheme: 'https', 45 | basePath: '/mock-basepath', 46 | }, 47 | }, 48 | ], 49 | paths: {}, 50 | }), 51 | ).equal('https://mock-host/mock-basepath'); 52 | }); 53 | 54 | it('should support swagger 3 variables without default', () => { 55 | expect( 56 | getServerPath({ 57 | servers: [ 58 | { 59 | url: '{scheme}://mock-host', 60 | variables: { 61 | scheme: { 62 | enum: ['http'], 63 | }, 64 | }, 65 | }, 66 | ], 67 | paths: {}, 68 | }), 69 | ).equal('http://mock-host'); 70 | }); 71 | 72 | it('should support swagger 3 variables with default', () => { 73 | expect( 74 | getServerPath({ 75 | servers: [ 76 | { 77 | url: '{scheme}://mock-host', 78 | variables: { 79 | scheme: { 80 | enum: ['mock-scheme'], 81 | default: 'http', 82 | }, 83 | }, 84 | }, 85 | ], 86 | paths: {}, 87 | }), 88 | ).equal('http://mock-host'); 89 | }); 90 | }); 91 | 92 | describe('getParameterDetails', () => { 93 | it('should get details for openapi 2 and 3', async () => { 94 | function testParameter(parameter: Param): void { 95 | try { 96 | assertType(parameter); 97 | } catch (e) { 98 | console.log('Not a Param:', parameter); 99 | throw e; 100 | } 101 | let paramDetails; 102 | try { 103 | paramDetails = getParamDetails(parameter); 104 | assertType(paramDetails); 105 | } catch (e) { 106 | console.log('Not EndpointParam:', JSON.stringify(paramDetails)); 107 | console.log('parameter:', parameter); 108 | throw e; 109 | } 110 | } 111 | const openapi2Schema = (await refParser.dereference( 112 | `test/fixtures/petstore.yaml`, 113 | )) as SwaggerSchema; 114 | (openapi2Schema.paths['/pet'].post.parameters as Param[]).forEach( 115 | testParameter, 116 | ); 117 | const openapi3Schema = (await refParser.dereference( 118 | `test/fixtures/petstore-openapi3.yaml`, 119 | )) as SwaggerSchema; 120 | (openapi3Schema.paths['/pet/findByStatus'].get 121 | .parameters as Param[]).forEach(testParameter); 122 | }); 123 | }); 124 | }); 125 | 126 | describe('getSuccessResponse ', () => { 127 | it('should return responses for openapi 3', async () => { 128 | const openapi3Schema = (await refParser.dereference( 129 | `test/fixtures/petstore-openapi3.yaml`, 130 | )) as SwaggerSchema; 131 | const { 132 | get: { responses }, 133 | } = openapi3Schema.paths['/pet/findByStatus']; 134 | const successResponse = getSuccessResponse(responses); 135 | if (!successResponse) { 136 | throw new Error('successResponse not defined'); 137 | } 138 | expect((successResponse as ArraySchema).type).to.equal('array'); 139 | expect((successResponse as ArraySchema).items).to.be.an('object'); 140 | }); 141 | 142 | it('should return responses for openapi 2', async () => { 143 | const openapi3Schema = (await refParser.dereference( 144 | `test/fixtures/petstore.json`, 145 | )) as SwaggerSchema; 146 | const { 147 | get: { responses }, 148 | } = openapi3Schema.paths['/pet/findByStatus']; 149 | const successResponse = getSuccessResponse(responses); 150 | if (!successResponse) { 151 | throw new Error('successResponse not defined'); 152 | } 153 | expect((successResponse as ArraySchema).type).to.equal('array'); 154 | expect((successResponse as ArraySchema).items).to.be.an('object'); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 5 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 6 | "lib": [] /* Specify library files to be included in the compilation. */, 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | "declaration": true /* Generates corresponding '.d.ts' file. */, 11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | "outDir": "./lib" /* Redirect output structure to the directory. */, 15 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "composite": true, /* Enable project compilation */ 17 | // "incremental": true, /* Enable incremental compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true /* Enable all strict type-checking options. */, 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | "noUnusedLocals": true /* Report errors on unused locals. */, 37 | "noUnusedParameters": true /* Report errors on unused parameters. */, 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | 52 | /* Source Map Options */ 53 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 54 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 55 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 56 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 57 | 58 | /* Experimental Options */ 59 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 60 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 61 | "resolveJsonModule": true, 62 | "plugins": [ 63 | { "transform": "typescript-is/lib/transform-inline/transformer" } 64 | ] 65 | }, 66 | "include": ["src/**/*.ts"] 67 | } 68 | -------------------------------------------------------------------------------- /test/fixtures/petstore.graphql.old: -------------------------------------------------------------------------------- 1 | type addPet { 2 | """default field""" 3 | empty: String 4 | } 5 | 6 | type createUser { 7 | """default field""" 8 | empty: String 9 | } 10 | 11 | type createUsersWithArrayInput { 12 | """default field""" 13 | empty: String 14 | } 15 | 16 | type createUsersWithListInput { 17 | """default field""" 18 | empty: String 19 | } 20 | 21 | type deleteOrder { 22 | """default field""" 23 | empty: String 24 | } 25 | 26 | type deletePet { 27 | """default field""" 28 | empty: String 29 | } 30 | 31 | type deleteUser { 32 | """default field""" 33 | empty: String 34 | } 35 | 36 | type findPetsByStatus_items { 37 | id: String 38 | category: findPetsByStatus_items_category 39 | name: String 40 | photoUrls: [String] 41 | tags: [findPetsByStatus_items_tags_items] 42 | 43 | """pet status in the store""" 44 | status: String 45 | } 46 | 47 | type findPetsByStatus_items_category { 48 | id: String 49 | name: String 50 | } 51 | 52 | type findPetsByStatus_items_tags_items { 53 | id: String 54 | name: String 55 | } 56 | 57 | type findPetsByTags_items { 58 | id: String 59 | category: findPetsByTags_items_category 60 | name: String 61 | photoUrls: [String] 62 | tags: [findPetsByTags_items_tags_items] 63 | 64 | """pet status in the store""" 65 | status: String 66 | } 67 | 68 | type findPetsByTags_items_category { 69 | id: String 70 | name: String 71 | } 72 | 73 | type findPetsByTags_items_tags_items { 74 | id: String 75 | name: String 76 | } 77 | 78 | type getInventory { 79 | """default field""" 80 | empty: String 81 | } 82 | 83 | type getOrderById { 84 | id: String 85 | petId: String 86 | quantity: Int 87 | shipDate: String 88 | 89 | """Order Status""" 90 | status: String 91 | complete: Boolean 92 | } 93 | 94 | type getPetById { 95 | id: String 96 | category: getPetById_category 97 | name: String 98 | photoUrls: [String] 99 | tags: [getPetById_tags_items] 100 | 101 | """pet status in the store""" 102 | status: String 103 | } 104 | 105 | type getPetById_category { 106 | id: String 107 | name: String 108 | } 109 | 110 | type getPetById_tags_items { 111 | id: String 112 | name: String 113 | } 114 | 115 | type getUserByName { 116 | id: String 117 | username: String 118 | firstName: String 119 | lastName: String 120 | email: String 121 | password: String 122 | phone: String 123 | 124 | """User Status""" 125 | userStatus: Int 126 | } 127 | 128 | type loginUser { 129 | """default field""" 130 | empty: String 131 | } 132 | 133 | type logoutUser { 134 | """default field""" 135 | empty: String 136 | } 137 | 138 | type Mutation { 139 | addPet(body: param_addPet_body): addPet 140 | updatePet(body: param_updatePet_body): updatePet 141 | updatePetWithForm(petId: String, name: String, status: String): updatePetWithForm 142 | deletePet(api_key: String, petId: String): deletePet 143 | placeOrder(body: param_placeOrder_body): placeOrder 144 | 145 | """ 146 | For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors 147 | """ 148 | deleteOrder(orderId: String): deleteOrder 149 | 150 | """This can only be done by the logged in user.""" 151 | createUser(body: param_createUser_body): createUser 152 | createUsersWithArrayInput(body: param_createUsersWithArrayInput_body): createUsersWithArrayInput 153 | createUsersWithListInput(body: param_createUsersWithListInput_body): createUsersWithListInput 154 | 155 | """This can only be done by the logged in user.""" 156 | updateUser(username: String, body: param_updateUser_body): updateUser 157 | 158 | """This can only be done by the logged in user.""" 159 | deleteUser(username: String): deleteUser 160 | } 161 | 162 | """Pet object that needs to be added to the store""" 163 | input param_addPet_body { 164 | """default field""" 165 | empty: String 166 | } 167 | 168 | """Created user object""" 169 | input param_createUser_body { 170 | """default field""" 171 | empty: String 172 | } 173 | 174 | """List of user object""" 175 | input param_createUsersWithArrayInput_body { 176 | """default field""" 177 | empty: String 178 | } 179 | 180 | """List of user object""" 181 | input param_createUsersWithListInput_body { 182 | """default field""" 183 | empty: String 184 | } 185 | 186 | """order placed for purchasing the pet""" 187 | input param_placeOrder_body { 188 | """default field""" 189 | empty: String 190 | } 191 | 192 | """Pet object that needs to be added to the store""" 193 | input param_updatePet_body { 194 | """default field""" 195 | empty: String 196 | } 197 | 198 | """Updated user object""" 199 | input param_updateUser_body { 200 | """default field""" 201 | empty: String 202 | } 203 | 204 | type placeOrder { 205 | id: String 206 | petId: String 207 | quantity: Int 208 | shipDate: String 209 | 210 | """Order Status""" 211 | status: String 212 | complete: Boolean 213 | } 214 | 215 | type Query { 216 | viewer: viewer 217 | } 218 | 219 | type updatePet { 220 | """default field""" 221 | empty: String 222 | } 223 | 224 | type updatePetWithForm { 225 | """default field""" 226 | empty: String 227 | } 228 | 229 | type updateUser { 230 | """default field""" 231 | empty: String 232 | } 233 | 234 | type viewer { 235 | """Multiple status values can be provided with comma separated strings""" 236 | findPetsByStatus(status: [String]): [findPetsByStatus_items] 237 | 238 | """ 239 | Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. 240 | """ 241 | findPetsByTags(tags: [String]): [findPetsByTags_items] 242 | 243 | """Returns a single pet""" 244 | getPetById(petId: String): getPetById 245 | 246 | """Returns a map of status codes to quantities""" 247 | getInventory: getInventory 248 | 249 | """ 250 | For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions 251 | """ 252 | getOrderById(orderId: String): getOrderById 253 | loginUser(username: String, password: String): loginUser 254 | logoutUser: logoutUser 255 | getUserByName(username: String): getUserByName 256 | } 257 | 258 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Build Status](https://travis-ci.org/yarax/swagger-to-graphql.svg?branch=master) 2 | 3 | # Swagger-to-GraphQL 4 | 5 | Swagger-to-GraphQL converts your existing Swagger schema to an executable 6 | GraphQL schema where resolvers perform HTTP calls to certain real endpoints. It 7 | allows you to move your API to GraphQL with nearly zero effort and maintain both 8 | REST and GraphQL APIs. Our CLI tool also allows you get the GraphQL schema in 9 | Schema Definition Language. 10 | 11 | [Try it](https://0xr.github.io/swagger-to-graphql-web/) online! You can paste in 12 | the url to your own Swagger schema. There are also public OpenAPI schemas 13 | available in the [APIs.guru OpenAPI directory](https://apis.guru/browse-apis/). 14 | 15 | ## Features 16 | 17 | - Swagger (OpenAPI 2) and OpenAPI 3 support 18 | - Bring you own HTTP client 19 | - Typescript types included 20 | - Runs in the browser 21 | - Formdata request body 22 | - Custom request headers 23 | 24 | # Usage 25 | 26 | ## Basic server 27 | 28 | This library will fetch your swagger schema, convert it to a GraphQL schema and 29 | convert GraphQL parameters to REST parameters. From there you are control of 30 | making the actual REST call. This means you can reuse your existing HTTP client, 31 | use existing authentication schemes and override any part of the REST call. You 32 | can override the REST host, proxy incoming request headers along to your REST 33 | backend, add caching etc. 34 | 35 | ```typescript 36 | import express, { Request } from 'express'; 37 | import graphqlHTTP from 'express-graphql'; 38 | import { createSchema, CallBackendArguments } from 'swagger-to-graphql'; 39 | 40 | const app = express(); 41 | 42 | // Define your own http client here 43 | async function callBackend({ 44 | context, 45 | requestOptions, 46 | }: CallBackendArguments) { 47 | return 'Not implemented'; 48 | } 49 | 50 | createSchema({ 51 | swaggerSchema: `./petstore.yaml`, 52 | callBackend, 53 | }) 54 | .then(schema => { 55 | app.use( 56 | '/graphql', 57 | graphqlHTTP(() => { 58 | return { 59 | schema, 60 | graphiql: true, 61 | }; 62 | }), 63 | ); 64 | 65 | app.listen(3009, 'localhost', () => { 66 | console.info('http://localhost:3009/graphql'); 67 | }); 68 | }) 69 | .catch(e => { 70 | console.log(e); 71 | }); 72 | ``` 73 | 74 | Constructor (graphQLSchema) arguments: 75 | 76 | ```typescript 77 | export interface Options { 78 | swaggerSchema: string | JSONSchema; 79 | callBackend: (args: CallBackendArguments) => Promise; 80 | } 81 | ``` 82 | 83 | - `swaggerUrl` (string) is a path or URL to your swagger schema file. _required_ 84 | - `callBackend` (async function) is called with all parameters needed to make a 85 | REST call as well as the GraphQL context. 86 | 87 | ## CLI usage 88 | 89 | You can use the library just to convert schemas without actually running server 90 | 91 | ``` 92 | npx swagger-to-graphql --swagger-schema=/path/to/swagger_schema.json > ./types.graphql 93 | ``` 94 | 95 | ## [Apollo Federation](https://www.apollographql.com/docs/apollo-server/federation/introduction/) 96 | 97 | Apollo federation support can be added by using 98 | [graphql-transform-federation](https://github.com/0xR/graphql-transform-federation). 99 | You can extend your swagger-to-graphql schema with other federated schemas or 100 | the other way around. See the 101 | [demo with a transformed schema](https://github.com/0xR/graphql-transform-federation-blog) 102 | for a working example. 103 | 104 | ## Defining your HTTP client 105 | 106 | This repository has: 107 | 108 | - [node-fetch example](./example/node-fetch.ts). Read more about 109 | [node-fetch](https://github.com/bitinn/node-fetch). 110 | - [request-promise example](./example/request-promise.ts). Read more about 111 | [request](https://github.com/request/request). 112 | 113 | To get started install `node-fetch` and copy the 114 | [node-fetch example](./example/node-fetch.ts) into your server. 115 | 116 | ```sh 117 | npm install node-fetch --save 118 | ``` 119 | 120 | ### Implementing your own HTTP client 121 | 122 | There a [unit test](./test/http-adapters-test.ts) for our HTTP client example, 123 | it might be useful when implementing your own client as well. 124 | 125 | The function `callBackend` is called with 2 parameters: 126 | 127 | - `context` is your GraphQL context. For `express-graphql` this is the incoming 128 | `request` object by default. 129 | [Read more](https://github.com/graphql/express-graphql#options). Use this if 130 | you want to proxy headers like `authorization`. For example 131 | `const authorizationHeader = context.get('authorization')`. 132 | - `requestOptions` includes everything you need to make a REST call. 133 | 134 | ```typescript 135 | export interface CallBackendArguments { 136 | context: TContext; 137 | requestOptions: RequestOptions; 138 | } 139 | ``` 140 | 141 | ### RequestOptions 142 | 143 | ```typescript 144 | export interface RequestOptions { 145 | baseUrl?: string; 146 | path: string; 147 | method: string; 148 | headers?: { 149 | [key: string]: string; 150 | }; 151 | query?: { 152 | [key: string]: string | string[]; 153 | }; 154 | body?: any; 155 | bodyType: 'json' | 'formData'; 156 | } 157 | ``` 158 | 159 | - `baseUrl` like defined in your swagger schema: `http://my-backend/v2` 160 | - `path` the next part of the url: `/widgets` 161 | - `method` HTTP verb: `get` 162 | - `headers` HTTP headers which are filled using GraphQL parameters: 163 | `{ api_key: 'xxxx-xxxx' }`. Note these are not the http headers sent to the 164 | GraphQL server itself. Those will be on the `context` parameter 165 | - `query` Query parameters for this calls: `{ id: 123 }`. Note this can be an 166 | array. You can find some examples on how to deal with arrays in query 167 | parameters in the 168 | [qs documentation](https://github.com/ljharb/qs#stringifying). 169 | - `body` the request payload to send with this REST call. 170 | - `bodyType` how to encode your request payload. When the `bodyType` is 171 | `formData` the request should be URL encoded form data. Ensure your HTTP 172 | client sends the right `Content-Type` headers. 173 | 174 | # Resources 175 | 176 | - Blogpost v3: 177 | [Start with GraphQL today by converting your Swagger schema](https://xebia.com/blog/start-with-graphql-today-by-converting-your-swagger-schema/) 178 | - Blogpost: 179 | [Moving existing API from REST to GraphQL](https://medium.com/@raxwunter/moving-existing-api-from-rest-to-graphql-205bab22c184) 180 | - Video: 181 | [O.J. Sousa Rodrigues at Vienna.JS](https://www.youtube.com/watch?v=551gKWJEsK0&feature=youtu.be&t=1269") 182 | -------------------------------------------------------------------------------- /test/integration-test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/camelcase */ 2 | import nock from 'nock'; 3 | import request from 'supertest'; 4 | import express, { Express } from 'express'; 5 | import graphqlHTTP from 'express-graphql'; 6 | import graphQLSchema from '../src'; 7 | import { createTestOptions } from './createTestOptions'; 8 | 9 | const createServer = async ( 10 | ...args: Parameters 11 | ): Promise => { 12 | const app = express(); 13 | const schema = await graphQLSchema(...args); 14 | app.use( 15 | '/graphql', 16 | graphqlHTTP(() => ({ 17 | schema, 18 | graphiql: true, 19 | })), 20 | ); 21 | return app; 22 | }; 23 | 24 | describe('swagger-to-graphql', () => { 25 | beforeEach(() => { 26 | nock.disableNetConnect(); 27 | nock.enableNetConnect('127.0.0.1'); 28 | }); 29 | 30 | afterEach(() => { 31 | nock.cleanAll(); 32 | nock.enableNetConnect(); 33 | }); 34 | 35 | describe('openapi 3', () => { 36 | it('should work with formdata', async () => { 37 | const nockScope = nock('http://petstore.swagger.io/v2') 38 | .post('/pet/1111', 'name=new%20name&status=new%20status') 39 | .reply(200, 'mock result'); 40 | 41 | await request( 42 | await createServer( 43 | createTestOptions( 44 | require.resolve('./fixtures/petstore-openapi3.yaml'), 45 | ), 46 | ), 47 | ) 48 | .post('/graphql') 49 | .send({ 50 | query: ` 51 | mutation { 52 | updatePetWithForm( 53 | petId: "1111" 54 | name: "new name" 55 | status: "new status" 56 | ) 57 | } 58 | `, 59 | }) 60 | .expect({ 61 | data: { 62 | updatePetWithForm: 'mock result', 63 | }, 64 | }); 65 | 66 | nockScope.done(); 67 | }); 68 | }); 69 | 70 | describe('special parameter names', () => { 71 | it('should convert graphql parameter names back to swagger parameter names', async () => { 72 | const nockScope = nock('http://mock.api.com/v2', { 73 | reqheaders: { 74 | 'dashed-request-header': 'mock request header', 75 | }, 76 | }) 77 | .get('/path/mock%20path') 78 | .query({ 79 | 'dashed-query-param': 'mock query param', 80 | }) 81 | .reply(200, { result: 'mocked' }); 82 | 83 | await request( 84 | await createServer( 85 | createTestOptions( 86 | require.resolve('./fixtures/special-parameters.json'), 87 | ), 88 | ), 89 | ) 90 | .post('/graphql') 91 | .send({ 92 | query: ` 93 | query { 94 | get_path_dashed_path_param( 95 | dashed_request_header: "mock request header" 96 | dashed_query_param: "mock query param" 97 | dashed_path_param:"mock path" 98 | ) { 99 | result 100 | } 101 | } 102 | `, 103 | }) 104 | .expect({ 105 | data: { 106 | get_path_dashed_path_param: { 107 | result: 'mocked', 108 | }, 109 | }, 110 | }); 111 | 112 | nockScope.done(); 113 | }); 114 | }); 115 | 116 | describe('simple swagger schema', () => { 117 | const getMockPathQuery = ` 118 | query { 119 | get_mock_path { 120 | result 121 | } 122 | } 123 | `; 124 | it('should make a simple rest call', async () => { 125 | const nockScope = nock('http://mock-host') 126 | .get('/mock-basepath/mock-path') 127 | .reply(200, { result: 'mock result' }); 128 | 129 | await request( 130 | await createServer( 131 | createTestOptions(require.resolve('./fixtures/simple.json')), 132 | ), 133 | ) 134 | .post('/graphql') 135 | .send({ 136 | query: getMockPathQuery, 137 | }) 138 | .expect({ 139 | data: { 140 | get_mock_path: { 141 | result: 'mock result', 142 | }, 143 | }, 144 | }); 145 | 146 | nockScope.done(); 147 | }); 148 | }); 149 | 150 | describe('return-scalar', () => { 151 | const getMockPathQuery = ` 152 | query { 153 | get_mock_path 154 | } 155 | `; 156 | it('should return scalars', async () => { 157 | const nockScope = nock('http://mock-host') 158 | .get('/mock-basepath/mock-path') 159 | .reply(200, 'mock result'); 160 | 161 | await request( 162 | await createServer( 163 | createTestOptions(require.resolve('./fixtures/return-scalar.json')), 164 | ), 165 | ) 166 | .post('/graphql') 167 | .send({ 168 | query: getMockPathQuery, 169 | }) 170 | .expect({ 171 | data: { get_mock_path: 'mock result' }, 172 | }); 173 | 174 | nockScope.done(); 175 | }); 176 | }); 177 | 178 | describe('unsupported types', () => { 179 | it('should return the raw json when response object has no properties', async () => { 180 | const nockScope = nock('http://mock-host') 181 | .get('/mock-basepath/mock-path') 182 | .reply(200, { some: 'json' }); 183 | await request( 184 | await createServer( 185 | createTestOptions({ 186 | host: 'mock-host', 187 | basePath: '/mock-basepath', 188 | paths: { 189 | '/mock-path': { 190 | get: { 191 | operationId: 'getInventory', 192 | responses: { 193 | '200': { 194 | description: 'successful operation', 195 | schema: { 196 | type: 'object', 197 | }, 198 | }, 199 | }, 200 | }, 201 | }, 202 | }, 203 | }), 204 | ), 205 | ) 206 | .post('/graphql') 207 | .send({ 208 | query: `query { 209 | getInventory 210 | }`, 211 | }) 212 | .expect({ 213 | data: { 214 | getInventory: { 215 | some: 'json', 216 | }, 217 | }, 218 | }); 219 | nockScope.done(); 220 | }); 221 | 222 | it('should return the raw json when there is no reponse type', async () => { 223 | const nockScope = nock('http://mock-host') 224 | .get('/mock-basepath/mock-path') 225 | .reply(200, { some: 'json' }); 226 | await request( 227 | await createServer( 228 | createTestOptions({ 229 | host: 'mock-host', 230 | basePath: '/mock-basepath', 231 | paths: { 232 | '/mock-path': { 233 | get: { 234 | operationId: 'getInventory', 235 | responses: {}, 236 | }, 237 | }, 238 | }, 239 | }), 240 | ), 241 | ) 242 | .post('/graphql') 243 | .send({ 244 | query: `query { 245 | getInventory 246 | }`, 247 | }) 248 | .expect({ 249 | data: { 250 | getInventory: { 251 | some: 'json', 252 | }, 253 | }, 254 | }); 255 | nockScope.done(); 256 | }); 257 | }); 258 | }); 259 | -------------------------------------------------------------------------------- /src/typeMap.ts: -------------------------------------------------------------------------------- 1 | // TODO: fix no-param-reassign 2 | /* eslint-disable no-param-reassign */ 3 | import { 4 | GraphQLBoolean, 5 | GraphQLFieldConfigArgumentMap, 6 | GraphQLFieldConfigMap, 7 | GraphQLFloat, 8 | GraphQLInputFieldConfigMap, 9 | GraphQLInputObjectType, 10 | GraphQLInputType, 11 | GraphQLInt, 12 | GraphQLList, 13 | GraphQLNonNull, 14 | GraphQLObjectType, 15 | GraphQLOutputType, 16 | GraphQLScalarType, 17 | GraphQLString, 18 | Thunk, 19 | } from 'graphql'; 20 | import { 21 | isArrayType, 22 | isBodyType, 23 | isObjectType, 24 | JSONSchemaType, 25 | } from './json-schema'; 26 | import { EndpointParam } from './getRequestOptions'; 27 | 28 | export type GraphQLType = GraphQLOutputType | GraphQLInputType; 29 | 30 | export interface GraphQLTypeMap { 31 | [typeName: string]: GraphQLType; 32 | } 33 | const primitiveTypes = { 34 | string: GraphQLString, 35 | date: GraphQLString, 36 | integer: GraphQLInt, 37 | number: GraphQLFloat, 38 | boolean: GraphQLBoolean, 39 | }; 40 | 41 | const jsonType = new GraphQLScalarType({ 42 | name: 'JSON', 43 | serialize(value) { 44 | return value; 45 | }, 46 | }); 47 | 48 | function getPrimitiveType( 49 | format: string | undefined, 50 | type: keyof typeof primitiveTypes, 51 | ): GraphQLScalarType { 52 | const primitiveTypeName = format === 'int64' ? 'string' : type; 53 | const primitiveType = primitiveTypes[primitiveTypeName]; 54 | if (!primitiveType) { 55 | return primitiveTypes.string; 56 | } 57 | return primitiveType; 58 | } 59 | 60 | export const jsonSchemaTypeToGraphQL = ( 61 | title: string, 62 | jsonSchema: JSONSchemaType, 63 | propertyName: string, 64 | isInputType: IsInputType, 65 | gqlTypes: GraphQLTypeMap, 66 | required: boolean, 67 | ): IsInputType extends true ? GraphQLInputType : GraphQLOutputType => { 68 | const baseType = ((): GraphQLType => { 69 | if (isBodyType(jsonSchema)) { 70 | return jsonSchemaTypeToGraphQL( 71 | title, 72 | jsonSchema.schema, 73 | propertyName, 74 | isInputType, 75 | gqlTypes, 76 | required, 77 | ); 78 | } 79 | if (isObjectType(jsonSchema) || isArrayType(jsonSchema)) { 80 | // eslint-disable-next-line no-use-before-define,@typescript-eslint/no-use-before-define 81 | return createGraphQLType( 82 | jsonSchema, 83 | `${title}_${propertyName}`, 84 | isInputType, 85 | gqlTypes, 86 | ); 87 | } 88 | 89 | if (jsonSchema.type === 'file') { 90 | // eslint-disable-next-line no-use-before-define,@typescript-eslint/no-use-before-define 91 | return createGraphQLType( 92 | { 93 | type: 'object', 94 | required: [], 95 | properties: { unsupported: { type: 'string' } }, 96 | }, 97 | `${title}_${propertyName}`, 98 | isInputType, 99 | gqlTypes, 100 | ); 101 | } 102 | 103 | if (jsonSchema.type) { 104 | return getPrimitiveType(jsonSchema.format, jsonSchema.type); 105 | } 106 | throw new Error( 107 | `Don't know how to handle schema ${JSON.stringify( 108 | jsonSchema, 109 | )} without type and schema`, 110 | ); 111 | })(); 112 | return (required 113 | ? GraphQLNonNull(baseType) 114 | : baseType) as IsInputType extends true 115 | ? GraphQLInputType 116 | : GraphQLOutputType; 117 | }; 118 | 119 | const makeValidName = (name: string): string => 120 | name.replace(/[^_0-9A-Za-z]/g, '_'); 121 | 122 | export const getTypeFields = ( 123 | jsonSchema: JSONSchemaType, 124 | title: string, 125 | isInputType: boolean, 126 | gqlTypes: GraphQLTypeMap, 127 | ): 128 | | Thunk 129 | | Thunk> => { 130 | return () => { 131 | const properties: { [name: string]: JSONSchemaType } = {}; 132 | if (isObjectType(jsonSchema)) { 133 | Object.keys(jsonSchema.properties).forEach(key => { 134 | properties[makeValidName(key)] = jsonSchema.properties[key]; 135 | }); 136 | } 137 | return Object.keys(properties).reduce( 138 | ( 139 | prev: { [propertyName: string]: { description: string; type: string } }, 140 | propertyName, 141 | ) => { 142 | const propertySchema = properties[propertyName]; 143 | const type = jsonSchemaTypeToGraphQL( 144 | title, 145 | propertySchema, 146 | propertyName, 147 | isInputType, 148 | gqlTypes, 149 | !!( 150 | isObjectType(jsonSchema) && 151 | jsonSchema.required && 152 | jsonSchema.required.includes(propertyName) 153 | ), 154 | ); 155 | return { 156 | ...prev, 157 | [propertyName]: { 158 | description: propertySchema.description, 159 | type, 160 | }, 161 | }; 162 | }, 163 | {}, 164 | ); 165 | }; 166 | }; 167 | 168 | export const createGraphQLType = ( 169 | jsonSchema: JSONSchemaType | undefined, 170 | title: string, 171 | isInputType: boolean, 172 | gqlTypes: GraphQLTypeMap, 173 | ): GraphQLType => { 174 | title = (jsonSchema && jsonSchema.title) || title; 175 | title = makeValidName(title); 176 | 177 | if (isInputType && !title.endsWith('Input')) { 178 | title += 'Input'; 179 | } 180 | 181 | if (title in gqlTypes) { 182 | return gqlTypes[title]; 183 | } 184 | 185 | if (!jsonSchema) { 186 | jsonSchema = { 187 | type: 'object', 188 | properties: {}, 189 | required: [], 190 | description: '', 191 | title, 192 | }; 193 | } else if (!jsonSchema.title) { 194 | jsonSchema = { ...jsonSchema, title }; 195 | } 196 | 197 | if (isArrayType(jsonSchema)) { 198 | const itemsSchema = Array.isArray(jsonSchema.items) 199 | ? jsonSchema.items[0] 200 | : jsonSchema.items; 201 | if (isObjectType(itemsSchema) || isArrayType(itemsSchema)) { 202 | return new GraphQLList( 203 | GraphQLNonNull( 204 | createGraphQLType( 205 | itemsSchema, 206 | `${title}_items`, 207 | isInputType, 208 | gqlTypes, 209 | ), 210 | ), 211 | ); 212 | } 213 | 214 | if (itemsSchema.type === 'file') { 215 | // eslint-disable-next-line no-use-before-define,@typescript-eslint/no-use-before-define 216 | return new GraphQLList( 217 | GraphQLNonNull( 218 | createGraphQLType( 219 | { 220 | type: 'object', 221 | required: [], 222 | properties: { unsupported: { type: 'string' } }, 223 | }, 224 | title, 225 | isInputType, 226 | gqlTypes, 227 | ), 228 | ), 229 | ); 230 | } 231 | const primitiveType = getPrimitiveType( 232 | itemsSchema.format, 233 | itemsSchema.type, 234 | ); 235 | return new GraphQLList(GraphQLNonNull(primitiveType)); 236 | } 237 | 238 | if ( 239 | isObjectType(jsonSchema) && 240 | !Object.keys(jsonSchema.properties || {}).length 241 | ) { 242 | return jsonType; 243 | } 244 | 245 | const { description } = jsonSchema; 246 | const fields = getTypeFields(jsonSchema, title, isInputType, gqlTypes); 247 | let result; 248 | if (isInputType) { 249 | result = new GraphQLInputObjectType({ 250 | name: title, 251 | description, 252 | fields: fields as GraphQLInputFieldConfigMap, 253 | }); 254 | } else { 255 | result = new GraphQLObjectType({ 256 | name: title, 257 | description, 258 | fields: fields as GraphQLFieldConfigMap, 259 | }); 260 | } 261 | gqlTypes[title] = result; 262 | return result; 263 | }; 264 | 265 | export const mapParametersToFields = ( 266 | parameters: EndpointParam[], 267 | typeName: string, 268 | gqlTypes: GraphQLTypeMap, 269 | ): GraphQLFieldConfigArgumentMap => { 270 | return parameters.reduce((res: GraphQLFieldConfigArgumentMap, param) => { 271 | const type = jsonSchemaTypeToGraphQL( 272 | `param_${typeName}`, 273 | param.jsonSchema, 274 | param.name, 275 | true, 276 | gqlTypes, 277 | param.required, 278 | ); 279 | res[param.name] = { 280 | type, 281 | }; 282 | return res; 283 | }, {}); 284 | }; 285 | -------------------------------------------------------------------------------- /src/swagger.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchemaType, JSONSchemaTypes, isObjectType } from './json-schema'; 2 | import { 3 | EndpointParam, 4 | getRequestOptions, 5 | RequestOptions, 6 | } from './getRequestOptions'; 7 | 8 | export interface GraphQLParameters { 9 | [key: string]: any; 10 | } 11 | 12 | const replaceOddChars = (str: string): string => 13 | str.replace(/[^_a-zA-Z0-9]/g, '_'); 14 | 15 | const getGQLTypeNameFromURL = (method: string, url: string): string => { 16 | const fromUrl = replaceOddChars(url.replace(/[{}]+/g, '')); 17 | return `${method}${fromUrl}`; 18 | }; 19 | 20 | export interface Responses { 21 | [key: string]: { 22 | schema?: JSONSchemaType; 23 | content?: { 24 | 'application/json': { schema: JSONSchemaType }; 25 | }; 26 | type?: 'file'; 27 | }; 28 | } 29 | 30 | export const getSuccessResponse = ( 31 | responses: Responses, 32 | ): JSONSchemaType | undefined => { 33 | const successCode = Object.keys(responses).find(code => { 34 | return code[0] === '2'; 35 | }); 36 | 37 | if (!successCode) { 38 | return undefined; 39 | } 40 | 41 | const successResponse = responses[successCode]; 42 | if (!successResponse) { 43 | throw new Error(`Expected responses[${successCode}] to be defined`); 44 | } 45 | if (successResponse.schema) { 46 | return successResponse.schema; 47 | } 48 | 49 | if (successResponse.content) { 50 | return successResponse.content['application/json'].schema; 51 | } 52 | 53 | return undefined; 54 | }; 55 | 56 | export interface BodyParam { 57 | name: string; 58 | required?: boolean; 59 | schema: JSONSchemaType; 60 | in: 'body'; 61 | } 62 | 63 | export interface Oa2NonBodyParam { 64 | name: string; 65 | type: JSONSchemaTypes; 66 | in: 'header' | 'query' | 'formData' | 'path'; 67 | required?: boolean; 68 | } 69 | 70 | export interface Oa3Param { 71 | name: string; 72 | in: 'header' | 'query' | 'formData' | 'path'; 73 | required?: boolean; 74 | schema: JSONSchemaType; 75 | } 76 | 77 | export type NonBodyParam = Oa2NonBodyParam | Oa3Param; 78 | 79 | export type Param = BodyParam | NonBodyParam; 80 | 81 | export interface OA3BodyParam { 82 | content: { 83 | 'application/json'?: { 84 | schema: JSONSchemaType; 85 | }; 86 | 'application/x-www-form-urlencoded'?: { 87 | schema: JSONSchemaType; 88 | }; 89 | }; 90 | description?: string; 91 | required: boolean; 92 | } 93 | 94 | export const isOa3Param = (param: Param): param is Oa3Param => { 95 | return !!(param as Oa3Param).schema; 96 | }; 97 | 98 | export function addTitlesToJsonSchemas(schema: SwaggerSchema): SwaggerSchema { 99 | const requestBodies = (schema.components || {}).requestBodies || {}; 100 | Object.keys(requestBodies).forEach(requestBodyName => { 101 | const { content } = requestBodies[requestBodyName]; 102 | (Object.keys(content) as (keyof OA3BodyParam['content'])[]).forEach( 103 | contentKey => { 104 | const contentValue = content[contentKey]; 105 | if (contentValue) { 106 | contentValue.schema.title = 107 | contentValue.schema.title || requestBodyName; 108 | } 109 | }, 110 | ); 111 | }); 112 | 113 | const jsonSchemas = (schema.components || {}).schemas || {}; 114 | Object.keys(jsonSchemas).forEach(schemaName => { 115 | const jsonSchema = jsonSchemas[schemaName]; 116 | jsonSchema.title = jsonSchema.title || schemaName; 117 | }); 118 | 119 | const definitions = schema.definitions || {}; 120 | Object.keys(definitions).forEach(definitionName => { 121 | const jsonSchema = definitions[definitionName]; 122 | jsonSchema.title = jsonSchema.title || definitionName; 123 | }); 124 | 125 | return schema; 126 | } 127 | 128 | export const getServerPath = (schema: SwaggerSchema): string | undefined => { 129 | const server = 130 | schema.servers && Array.isArray(schema.servers) 131 | ? schema.servers[0] 132 | : schema.host 133 | ? [ 134 | (schema.schemes && schema.schemes[0]) || 'http', 135 | '://', 136 | schema.host, 137 | schema.basePath, 138 | ] 139 | .filter(Boolean) 140 | .join('') 141 | : undefined; 142 | if (!server) { 143 | return undefined; 144 | } 145 | if (typeof server === 'string') { 146 | return server; 147 | } 148 | const { url, variables } = server; 149 | return variables 150 | ? Object.keys(server.variables).reduce((acc, variableName) => { 151 | const variable = server.variables[variableName]; 152 | const value = 153 | typeof variable === 'string' 154 | ? variable 155 | : variable.default || variable.enum[0]; 156 | return acc.replace(`{${variableName}}`, value); 157 | }, url) 158 | : url; 159 | }; 160 | 161 | export const getParamDetails = (param: Param): EndpointParam => { 162 | const name = replaceOddChars(param.name); 163 | const swaggerName = param.name; 164 | if (isOa3Param(param)) { 165 | const { schema, required, in: type } = param as Oa3Param; 166 | return { 167 | name, 168 | swaggerName, 169 | type, 170 | required: !!required, 171 | jsonSchema: schema, 172 | }; 173 | } 174 | 175 | return { 176 | name, 177 | swaggerName, 178 | type: param.in, 179 | required: !!param.required, 180 | jsonSchema: param, 181 | }; 182 | }; 183 | 184 | const contentTypeFormData = 'application/x-www-form-urlencoded'; 185 | export const getParamDetailsFromRequestBody = ( 186 | requestBody: OA3BodyParam, 187 | ): EndpointParam[] => { 188 | const formData = requestBody.content[contentTypeFormData]; 189 | function getSchemaFromRequestBody(): JSONSchemaType { 190 | if (requestBody.content['application/json']) { 191 | return requestBody.content['application/json'].schema; 192 | } 193 | throw new Error( 194 | `Unsupported content type(s) found: ${Object.keys( 195 | requestBody.content, 196 | ).join(', ')}`, 197 | ); 198 | } 199 | if (formData) { 200 | const formdataSchema = formData.schema; 201 | if (!isObjectType(formdataSchema)) { 202 | throw new Error( 203 | `RequestBody is formData, expected an object schema, got "${JSON.stringify( 204 | formdataSchema, 205 | )}"`, 206 | ); 207 | } 208 | return Object.entries(formdataSchema.properties).map( 209 | ([name, schema]) => ({ 210 | name: replaceOddChars(name), 211 | swaggerName: name, 212 | type: 'formData', 213 | required: formdataSchema.required 214 | ? formdataSchema.required.includes(name) 215 | : false, 216 | jsonSchema: schema, 217 | }), 218 | ); 219 | } 220 | return [ 221 | { 222 | name: 'body', 223 | swaggerName: 'requestBody', 224 | type: 'body', 225 | required: !!requestBody.required, 226 | jsonSchema: getSchemaFromRequestBody(), 227 | }, 228 | ]; 229 | }; 230 | 231 | function isFormdataRequest(requestBody: OA3BodyParam): boolean { 232 | return !!requestBody.content[contentTypeFormData]; 233 | } 234 | 235 | export interface Endpoint { 236 | parameters: EndpointParam[]; 237 | description?: string; 238 | response: JSONSchemaType | undefined; 239 | getRequestOptions: (args: GraphQLParameters) => RequestOptions; 240 | mutation: boolean; 241 | } 242 | 243 | export interface Endpoints { 244 | [operationId: string]: Endpoint; 245 | } 246 | 247 | export interface OperationObject { 248 | requestBody?: OA3BodyParam; 249 | description?: string; 250 | operationId?: string; 251 | parameters?: Param[]; 252 | responses: Responses; 253 | consumes?: string[]; 254 | } 255 | 256 | export type PathObject = { 257 | parameters?: Param[]; 258 | } & { 259 | [operation: string]: OperationObject; 260 | }; 261 | 262 | export interface Variable { 263 | default?: string; 264 | enum: string[]; 265 | } 266 | 267 | export interface ServerObject { 268 | url: string; 269 | description?: string; 270 | variables: { 271 | [key: string]: string | Variable; 272 | }; 273 | } 274 | 275 | export interface SwaggerSchema { 276 | host?: string; 277 | basePath?: string; 278 | schemes?: [string]; 279 | servers?: ServerObject[]; 280 | paths: { 281 | [pathUrl: string]: PathObject; 282 | }; 283 | components?: { 284 | requestBodies?: { 285 | [name: string]: OA3BodyParam; 286 | }; 287 | schemas?: { 288 | [name: string]: JSONSchemaType; 289 | }; 290 | }; 291 | definitions?: { 292 | [name: string]: JSONSchemaType; 293 | }; 294 | } 295 | 296 | /** 297 | * Go through schema and grab routes 298 | */ 299 | export const getAllEndPoints = (schema: SwaggerSchema): Endpoints => { 300 | const allOperations: Endpoints = {}; 301 | const serverPath = getServerPath(schema); 302 | Object.keys(schema.paths).forEach(path => { 303 | const route = schema.paths[path]; 304 | Object.keys(route).forEach(method => { 305 | if (method === 'parameters' || method === 'servers') { 306 | return; 307 | } 308 | const operationObject: OperationObject = route[method] as OperationObject; 309 | const isMutation = 310 | ['post', 'put', 'patch', 'delete'].indexOf(method) !== -1; 311 | const operationId = operationObject.operationId 312 | ? replaceOddChars(operationObject.operationId) 313 | : getGQLTypeNameFromURL(method, path); 314 | 315 | // [FIX] for when parameters is a child of route and not route[method] 316 | if (route.parameters) { 317 | if (operationObject.parameters) { 318 | operationObject.parameters = route.parameters.concat( 319 | operationObject.parameters, 320 | ); 321 | } else { 322 | operationObject.parameters = route.parameters; 323 | } 324 | } 325 | 326 | const bodyParams = operationObject.requestBody 327 | ? getParamDetailsFromRequestBody(operationObject.requestBody) 328 | : []; 329 | 330 | const parameterDetails = [ 331 | ...(operationObject.parameters 332 | ? operationObject.parameters.map(param => getParamDetails(param)) 333 | : []), 334 | ...bodyParams, 335 | ]; 336 | 337 | const endpoint: Endpoint = { 338 | parameters: parameterDetails, 339 | description: operationObject.description, 340 | response: getSuccessResponse(operationObject.responses), 341 | getRequestOptions: (parameterValues: GraphQLParameters) => { 342 | return getRequestOptions({ 343 | parameterDetails, 344 | parameterValues, 345 | baseUrl: serverPath, 346 | path, 347 | method, 348 | formData: operationObject.consumes 349 | ? !operationObject.consumes.includes('application/json') 350 | : operationObject.requestBody 351 | ? isFormdataRequest(operationObject.requestBody) 352 | : false, 353 | }); 354 | }, 355 | mutation: isMutation, 356 | }; 357 | allOperations[operationId] = endpoint; 358 | }); 359 | }); 360 | return allOperations; 361 | }; 362 | -------------------------------------------------------------------------------- /test/fixtures/petstore.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | swagger: '2.0' 3 | info: 4 | description: 'This is a sample server Petstore server. You can find out more about 5 | Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For 6 | this sample, you can use the api key `special-key` to test the authorization filters.' 7 | version: 1.0.0 8 | title: Swagger Petstore 9 | termsOfService: http://swagger.io/terms/ 10 | contact: 11 | email: apiteam@swagger.io 12 | license: 13 | name: Apache 2.0 14 | url: http://www.apache.org/licenses/LICENSE-2.0.html 15 | host: petstore.swagger.io 16 | basePath: "/v2" 17 | tags: 18 | - name: pet 19 | description: Everything about your Pets 20 | externalDocs: 21 | description: Find out more 22 | url: http://swagger.io 23 | - name: store 24 | description: Access to Petstore orders 25 | - name: user 26 | description: Operations about user 27 | externalDocs: 28 | description: Find out more about our store 29 | url: http://swagger.io 30 | schemes: 31 | - http 32 | paths: 33 | "/pet": 34 | post: 35 | tags: 36 | - pet 37 | summary: Add a new pet to the store 38 | description: '' 39 | operationId: addPet 40 | consumes: 41 | - application/json 42 | - application/xml 43 | produces: 44 | - application/xml 45 | - application/json 46 | parameters: 47 | - in: body 48 | name: body 49 | description: Pet object that needs to be added to the store 50 | required: true 51 | schema: 52 | "$ref": "#/definitions/Pet" 53 | responses: 54 | '405': 55 | description: Invalid input 56 | security: 57 | - petstore_auth: 58 | - write:pets 59 | - read:pets 60 | put: 61 | tags: 62 | - pet 63 | summary: Update an existing pet 64 | description: '' 65 | operationId: updatePet 66 | consumes: 67 | - application/json 68 | - application/xml 69 | produces: 70 | - application/xml 71 | - application/json 72 | parameters: 73 | - in: body 74 | name: body 75 | description: Pet object that needs to be added to the store 76 | required: true 77 | schema: 78 | "$ref": "#/definitions/Pet" 79 | responses: 80 | '400': 81 | description: Invalid ID supplied 82 | '404': 83 | description: Pet not found 84 | '405': 85 | description: Validation exception 86 | security: 87 | - petstore_auth: 88 | - write:pets 89 | - read:pets 90 | "/pet/findByStatus": 91 | get: 92 | tags: 93 | - pet 94 | summary: Finds Pets by status 95 | description: Multiple status values can be provided with comma separated strings 96 | operationId: findPetsByStatus 97 | produces: 98 | - application/xml 99 | - application/json 100 | parameters: 101 | - name: status 102 | in: query 103 | description: Status values that need to be considered for filter 104 | required: true 105 | type: array 106 | items: 107 | type: string 108 | enum: 109 | - available 110 | - pending 111 | - sold 112 | default: available 113 | collectionFormat: multi 114 | responses: 115 | '200': 116 | description: successful operation 117 | schema: 118 | type: array 119 | items: 120 | "$ref": "#/definitions/Pet" 121 | '400': 122 | description: Invalid status value 123 | security: 124 | - petstore_auth: 125 | - write:pets 126 | - read:pets 127 | "/pet/findByTags": 128 | get: 129 | tags: 130 | - pet 131 | summary: Finds Pets by tags 132 | description: Muliple tags can be provided with comma separated strings. Use 133 | tag1, tag2, tag3 for testing. 134 | operationId: findPetsByTags 135 | produces: 136 | - application/xml 137 | - application/json 138 | parameters: 139 | - name: tags 140 | in: query 141 | description: Tags to filter by 142 | required: true 143 | type: array 144 | items: 145 | type: string 146 | collectionFormat: multi 147 | responses: 148 | '200': 149 | description: successful operation 150 | schema: 151 | type: array 152 | items: 153 | "$ref": "#/definitions/Pet" 154 | '400': 155 | description: Invalid tag value 156 | security: 157 | - petstore_auth: 158 | - write:pets 159 | - read:pets 160 | deprecated: true 161 | "/pet/{petId}": 162 | get: 163 | tags: 164 | - pet 165 | summary: Find pet by ID 166 | description: Returns a single pet 167 | operationId: getPetById 168 | produces: 169 | - application/xml 170 | - application/json 171 | parameters: 172 | - name: petId 173 | in: path 174 | description: ID of pet to return 175 | required: true 176 | type: integer 177 | format: int64 178 | responses: 179 | '200': 180 | description: successful operation 181 | schema: 182 | "$ref": "#/definitions/Pet" 183 | '400': 184 | description: Invalid ID supplied 185 | '404': 186 | description: Pet not found 187 | security: 188 | - api_key: [] 189 | post: 190 | tags: 191 | - pet 192 | summary: Updates a pet in the store with form data 193 | description: '' 194 | operationId: updatePetWithForm 195 | consumes: 196 | - application/x-www-form-urlencoded 197 | produces: 198 | - application/xml 199 | - application/json 200 | parameters: 201 | - name: petId 202 | in: path 203 | description: ID of pet that needs to be updated 204 | required: true 205 | type: integer 206 | format: int64 207 | - name: name 208 | in: formData 209 | description: Updated name of the pet 210 | required: false 211 | type: string 212 | - name: status 213 | in: formData 214 | description: Updated status of the pet 215 | required: false 216 | type: string 217 | responses: 218 | '405': 219 | description: Invalid input 220 | security: 221 | - petstore_auth: 222 | - write:pets 223 | - read:pets 224 | delete: 225 | tags: 226 | - pet 227 | summary: Deletes a pet 228 | description: '' 229 | operationId: deletePet 230 | produces: 231 | - application/xml 232 | - application/json 233 | parameters: 234 | - name: api_key 235 | in: header 236 | required: false 237 | type: string 238 | - name: petId 239 | in: path 240 | description: Pet id to delete 241 | required: true 242 | type: integer 243 | format: int64 244 | responses: 245 | '400': 246 | description: Invalid ID supplied 247 | '404': 248 | description: Pet not found 249 | security: 250 | - petstore_auth: 251 | - write:pets 252 | - read:pets 253 | "/store/inventory": 254 | get: 255 | tags: 256 | - store 257 | summary: Returns pet inventories by status 258 | description: Returns a map of status codes to quantities 259 | operationId: getInventory 260 | produces: 261 | - application/json 262 | parameters: [] 263 | responses: 264 | '200': 265 | description: successful operation 266 | schema: 267 | type: object 268 | additionalProperties: 269 | type: integer 270 | format: int32 271 | security: 272 | - api_key: [] 273 | "/store/order": 274 | post: 275 | tags: 276 | - store 277 | summary: Place an order for a pet 278 | description: '' 279 | operationId: placeOrder 280 | produces: 281 | - application/xml 282 | - application/json 283 | parameters: 284 | - in: body 285 | name: body 286 | description: order placed for purchasing the pet 287 | required: true 288 | schema: 289 | "$ref": "#/definitions/Order" 290 | responses: 291 | '200': 292 | description: successful operation 293 | schema: 294 | "$ref": "#/definitions/Order" 295 | '400': 296 | description: Invalid Order 297 | "/store/order/{orderId}": 298 | get: 299 | tags: 300 | - store 301 | summary: Find purchase order by ID 302 | description: For valid response try integer IDs with value >= 1 and <= 10. Other 303 | values will generated exceptions 304 | operationId: getOrderById 305 | produces: 306 | - application/xml 307 | - application/json 308 | parameters: 309 | - name: orderId 310 | in: path 311 | description: ID of pet that needs to be fetched 312 | required: true 313 | type: integer 314 | maximum: 10 315 | minimum: 1 316 | format: int64 317 | responses: 318 | '200': 319 | description: successful operation 320 | schema: 321 | "$ref": "#/definitions/Order" 322 | '400': 323 | description: Invalid ID supplied 324 | '404': 325 | description: Order not found 326 | delete: 327 | tags: 328 | - store 329 | summary: Delete purchase order by ID 330 | description: For valid response try integer IDs with positive integer value. 331 | Negative or non-integer values will generate API errors 332 | operationId: deleteOrder 333 | produces: 334 | - application/xml 335 | - application/json 336 | parameters: 337 | - name: orderId 338 | in: path 339 | description: ID of the order that needs to be deleted 340 | required: true 341 | type: integer 342 | minimum: 1 343 | format: int64 344 | responses: 345 | '400': 346 | description: Invalid ID supplied 347 | '404': 348 | description: Order not found 349 | "/user": 350 | post: 351 | tags: 352 | - user 353 | summary: Create user 354 | description: This can only be done by the logged in user. 355 | operationId: createUser 356 | produces: 357 | - application/xml 358 | - application/json 359 | parameters: 360 | - in: body 361 | name: body 362 | description: Created user object 363 | required: true 364 | schema: 365 | "$ref": "#/definitions/User" 366 | responses: 367 | default: 368 | description: successful operation 369 | "/user/createWithArray": 370 | post: 371 | tags: 372 | - user 373 | summary: Creates list of users with given input array 374 | description: '' 375 | operationId: createUsersWithArrayInput 376 | produces: 377 | - application/xml 378 | - application/json 379 | parameters: 380 | - in: body 381 | name: body 382 | description: List of user object 383 | required: true 384 | schema: 385 | type: array 386 | items: 387 | "$ref": "#/definitions/User" 388 | responses: 389 | default: 390 | description: successful operation 391 | "/user/createWithList": 392 | post: 393 | tags: 394 | - user 395 | summary: Creates list of users with given input array 396 | description: '' 397 | operationId: createUsersWithListInput 398 | produces: 399 | - application/xml 400 | - application/json 401 | parameters: 402 | - in: body 403 | name: body 404 | description: List of user object 405 | required: true 406 | schema: 407 | type: array 408 | items: 409 | "$ref": "#/definitions/User" 410 | responses: 411 | default: 412 | description: successful operation 413 | "/user/login": 414 | get: 415 | tags: 416 | - user 417 | summary: Logs user into the system 418 | description: '' 419 | operationId: loginUser 420 | produces: 421 | - application/xml 422 | - application/json 423 | parameters: 424 | - name: username 425 | in: query 426 | description: The user name for login 427 | required: true 428 | type: string 429 | - name: password 430 | in: query 431 | description: The password for login in clear text 432 | required: true 433 | type: string 434 | responses: 435 | '200': 436 | description: successful operation 437 | schema: 438 | type: string 439 | headers: 440 | X-Rate-Limit: 441 | type: integer 442 | format: int32 443 | description: calls per hour allowed by the user 444 | X-Expires-After: 445 | type: string 446 | format: date-time 447 | description: date in UTC when token expires 448 | '400': 449 | description: Invalid username/password supplied 450 | "/user/logout": 451 | get: 452 | tags: 453 | - user 454 | summary: Logs out current logged in user session 455 | description: '' 456 | operationId: logoutUser 457 | produces: 458 | - application/xml 459 | - application/json 460 | parameters: [] 461 | responses: 462 | default: 463 | description: successful operation 464 | "/user/{username}": 465 | get: 466 | tags: 467 | - user 468 | summary: Get user by user name 469 | description: '' 470 | operationId: getUserByName 471 | produces: 472 | - application/xml 473 | - application/json 474 | parameters: 475 | - $ref: '#/parameters/username' 476 | responses: 477 | '200': 478 | description: successful operation 479 | schema: 480 | "$ref": "#/definitions/User" 481 | '400': 482 | description: Invalid username supplied 483 | '404': 484 | description: User not found 485 | put: 486 | tags: 487 | - user 488 | summary: Updated user 489 | description: This can only be done by the logged in user. 490 | operationId: updateUser 491 | produces: 492 | - application/xml 493 | - application/json 494 | parameters: 495 | - name: username 496 | in: path 497 | description: name that need to be updated 498 | required: true 499 | type: string 500 | - in: body 501 | name: body 502 | description: Updated user object 503 | required: true 504 | schema: 505 | "$ref": "#/definitions/User" 506 | responses: 507 | '400': 508 | description: Invalid user supplied 509 | '404': 510 | description: User not found 511 | delete: 512 | tags: 513 | - user 514 | summary: Delete user 515 | description: This can only be done by the logged in user. 516 | operationId: deleteUser 517 | produces: 518 | - application/xml 519 | - application/json 520 | parameters: 521 | - name: username 522 | in: path 523 | description: The name that needs to be deleted 524 | required: true 525 | type: string 526 | responses: 527 | '400': 528 | description: Invalid username supplied 529 | '404': 530 | description: User not found 531 | securityDefinitions: 532 | petstore_auth: 533 | type: oauth2 534 | authorizationUrl: http://petstore.swagger.io/oauth/dialog 535 | flow: implicit 536 | scopes: 537 | write:pets: modify pets in your account 538 | read:pets: read your pets 539 | api_key: 540 | type: apiKey 541 | name: api_key 542 | in: header 543 | parameters: 544 | username: 545 | name: username 546 | in: path 547 | description: 'The name that needs to be fetched. Use user1 for testing.' 548 | required: true 549 | type: string 550 | definitions: 551 | Order: 552 | type: object 553 | properties: 554 | id: 555 | type: integer 556 | format: int64 557 | petId: 558 | type: integer 559 | format: int64 560 | quantity: 561 | type: integer 562 | format: int32 563 | shipDate: 564 | type: string 565 | format: date-time 566 | status: 567 | type: string 568 | description: Order Status 569 | enum: 570 | - placed 571 | - approved 572 | - delivered 573 | complete: 574 | type: boolean 575 | default: false 576 | xml: 577 | name: Order 578 | Category: 579 | type: object 580 | properties: 581 | id: 582 | type: integer 583 | format: int64 584 | name: 585 | type: string 586 | xml: 587 | name: Category 588 | User: 589 | type: object 590 | properties: 591 | id: 592 | type: integer 593 | format: int64 594 | username: 595 | type: string 596 | firstName: 597 | type: string 598 | lastName: 599 | type: string 600 | email: 601 | type: string 602 | password: 603 | type: string 604 | phone: 605 | type: string 606 | userStatus: 607 | type: integer 608 | format: int32 609 | description: User Status 610 | xml: 611 | name: User 612 | Tag: 613 | type: object 614 | properties: 615 | id: 616 | type: integer 617 | format: int64 618 | name: 619 | type: string 620 | xml: 621 | name: Tag 622 | Pet: 623 | type: object 624 | required: 625 | - name 626 | - photoUrls 627 | properties: 628 | id: 629 | type: integer 630 | format: int64 631 | category: 632 | "$ref": "#/definitions/Category" 633 | name: 634 | type: string 635 | example: doggie 636 | photoUrls: 637 | type: array 638 | xml: 639 | name: photoUrl 640 | wrapped: true 641 | items: 642 | type: string 643 | tags: 644 | type: array 645 | xml: 646 | name: tag 647 | wrapped: true 648 | items: 649 | "$ref": "#/definitions/Tag" 650 | status: 651 | type: string 652 | description: pet status in the store 653 | enum: 654 | - available 655 | - pending 656 | - sold 657 | xml: 658 | name: Pet 659 | ApiResponse: 660 | type: object 661 | properties: 662 | code: 663 | type: integer 664 | format: int32 665 | type: 666 | type: string 667 | message: 668 | type: string 669 | externalDocs: 670 | description: Find out more about Swagger 671 | url: http://swagger.io 672 | -------------------------------------------------------------------------------- /test/fixtures/petstore-openapi3.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | description: "This is a sample server Petstore server. You can find out more about 4 | Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, 5 | #swagger](http://swagger.io/irc/). For this sample, you can use the api key 6 | `special-key` to test the authorization filters." 7 | version: 1.0.0 8 | title: Swagger Petstore 9 | termsOfService: http://swagger.io/terms/ 10 | contact: 11 | email: apiteam@swagger.io 12 | license: 13 | name: Apache 2.0 14 | url: http://www.apache.org/licenses/LICENSE-2.0.html 15 | tags: 16 | - name: pet 17 | description: Everything about your Pets 18 | externalDocs: 19 | description: Find out more 20 | url: http://swagger.io 21 | - name: store 22 | description: Access to Petstore orders 23 | - name: user 24 | description: Operations about user 25 | externalDocs: 26 | description: Find out more about our store 27 | url: http://swagger.io 28 | paths: 29 | /pet: 30 | post: 31 | tags: 32 | - pet 33 | summary: Add a new pet to the store 34 | description: "" 35 | operationId: addPet 36 | requestBody: 37 | $ref: "#/components/requestBodies/Pet" 38 | responses: 39 | "405": 40 | description: Invalid input 41 | security: 42 | - petstore_auth: 43 | - write:pets 44 | - read:pets 45 | put: 46 | tags: 47 | - pet 48 | summary: Update an existing pet 49 | description: "" 50 | operationId: updatePet 51 | requestBody: 52 | $ref: "#/components/requestBodies/Pet" 53 | responses: 54 | "400": 55 | description: Invalid ID supplied 56 | "404": 57 | description: Pet not found 58 | "405": 59 | description: Validation exception 60 | security: 61 | - petstore_auth: 62 | - write:pets 63 | - read:pets 64 | /pet/findByStatus: 65 | get: 66 | tags: 67 | - pet 68 | summary: Finds Pets by status 69 | description: Multiple status values can be provided with comma separated strings 70 | operationId: findPetsByStatus 71 | parameters: 72 | - name: status 73 | in: query 74 | description: Status values that need to be considered for filter 75 | required: true 76 | explode: true 77 | schema: 78 | type: array 79 | items: 80 | type: string 81 | enum: 82 | - available 83 | - pending 84 | - sold 85 | default: available 86 | responses: 87 | "200": 88 | description: successful operation 89 | content: 90 | application/xml: 91 | schema: 92 | type: array 93 | items: 94 | $ref: "#/components/schemas/Pet" 95 | application/json: 96 | schema: 97 | type: array 98 | items: 99 | $ref: "#/components/schemas/Pet" 100 | "400": 101 | description: Invalid status value 102 | security: 103 | - petstore_auth: 104 | - write:pets 105 | - read:pets 106 | /pet/findByTags: 107 | get: 108 | tags: 109 | - pet 110 | summary: Finds Pets by tags 111 | description: Muliple tags can be provided with comma separated strings. Use tag1, 112 | tag2, tag3 for testing. 113 | operationId: findPetsByTags 114 | parameters: 115 | - name: tags 116 | in: query 117 | description: Tags to filter by 118 | required: true 119 | explode: true 120 | schema: 121 | type: array 122 | items: 123 | type: string 124 | responses: 125 | "200": 126 | description: successful operation 127 | content: 128 | application/xml: 129 | schema: 130 | type: array 131 | items: 132 | $ref: "#/components/schemas/Pet" 133 | application/json: 134 | schema: 135 | type: array 136 | items: 137 | $ref: "#/components/schemas/Pet" 138 | "400": 139 | description: Invalid tag value 140 | security: 141 | - petstore_auth: 142 | - write:pets 143 | - read:pets 144 | deprecated: true 145 | "/pet/{petId}": 146 | get: 147 | tags: 148 | - pet 149 | summary: Find pet by ID 150 | description: Returns a single pet 151 | operationId: getPetById 152 | parameters: 153 | - name: petId 154 | in: path 155 | description: ID of pet to return 156 | required: true 157 | schema: 158 | type: integer 159 | format: int64 160 | responses: 161 | "200": 162 | description: successful operation 163 | content: 164 | application/xml: 165 | schema: 166 | $ref: "#/components/schemas/Pet" 167 | application/json: 168 | schema: 169 | $ref: "#/components/schemas/Pet" 170 | "400": 171 | description: Invalid ID supplied 172 | "404": 173 | description: Pet not found 174 | security: 175 | - api_key: [] 176 | post: 177 | tags: 178 | - pet 179 | summary: Updates a pet in the store with form data 180 | description: "" 181 | operationId: updatePetWithForm 182 | parameters: 183 | - name: petId 184 | in: path 185 | description: ID of pet that needs to be updated 186 | required: true 187 | schema: 188 | type: integer 189 | format: int64 190 | requestBody: 191 | content: 192 | application/x-www-form-urlencoded: 193 | schema: 194 | type: object 195 | properties: 196 | name: 197 | description: Updated name of the pet 198 | type: string 199 | status: 200 | description: Updated status of the pet 201 | type: string 202 | responses: 203 | "405": 204 | description: Invalid input 205 | security: 206 | - petstore_auth: 207 | - write:pets 208 | - read:pets 209 | delete: 210 | tags: 211 | - pet 212 | summary: Deletes a pet 213 | description: "" 214 | operationId: deletePet 215 | parameters: 216 | - name: api_key 217 | in: header 218 | required: false 219 | schema: 220 | type: string 221 | - name: petId 222 | in: path 223 | description: Pet id to delete 224 | required: true 225 | schema: 226 | type: integer 227 | format: int64 228 | responses: 229 | "400": 230 | description: Invalid ID supplied 231 | "404": 232 | description: Pet not found 233 | security: 234 | - petstore_auth: 235 | - write:pets 236 | - read:pets 237 | /store/inventory: 238 | get: 239 | tags: 240 | - store 241 | summary: Returns pet inventories by status 242 | description: Returns a map of status codes to quantities 243 | operationId: getInventory 244 | responses: 245 | "200": 246 | description: successful operation 247 | content: 248 | application/json: 249 | schema: 250 | type: object 251 | additionalProperties: 252 | type: integer 253 | format: int32 254 | security: 255 | - api_key: [] 256 | /store/order: 257 | post: 258 | tags: 259 | - store 260 | summary: Place an order for a pet 261 | description: "" 262 | operationId: placeOrder 263 | requestBody: 264 | content: 265 | application/json: 266 | schema: 267 | $ref: "#/components/schemas/Order" 268 | description: order placed for purchasing the pet 269 | required: true 270 | responses: 271 | "200": 272 | description: successful operation 273 | content: 274 | application/xml: 275 | schema: 276 | $ref: "#/components/schemas/Order" 277 | application/json: 278 | schema: 279 | $ref: "#/components/schemas/Order" 280 | "400": 281 | description: Invalid Order 282 | "/store/order/{orderId}": 283 | get: 284 | tags: 285 | - store 286 | summary: Find purchase order by ID 287 | description: For valid response try integer IDs with value >= 1 and <= 10. Other 288 | values will generated exceptions 289 | operationId: getOrderById 290 | parameters: 291 | - name: orderId 292 | in: path 293 | description: ID of pet that needs to be fetched 294 | required: true 295 | schema: 296 | type: integer 297 | format: int64 298 | minimum: 1 299 | maximum: 10 300 | responses: 301 | "200": 302 | description: successful operation 303 | content: 304 | application/xml: 305 | schema: 306 | $ref: "#/components/schemas/Order" 307 | application/json: 308 | schema: 309 | $ref: "#/components/schemas/Order" 310 | "400": 311 | description: Invalid ID supplied 312 | "404": 313 | description: Order not found 314 | delete: 315 | tags: 316 | - store 317 | summary: Delete purchase order by ID 318 | description: For valid response try integer IDs with positive integer value. Negative 319 | or non-integer values will generate API errors 320 | operationId: deleteOrder 321 | parameters: 322 | - name: orderId 323 | in: path 324 | description: ID of the order that needs to be deleted 325 | required: true 326 | schema: 327 | type: integer 328 | format: int64 329 | minimum: 1 330 | responses: 331 | "400": 332 | description: Invalid ID supplied 333 | "404": 334 | description: Order not found 335 | /user: 336 | post: 337 | tags: 338 | - user 339 | summary: Create user 340 | description: This can only be done by the logged in user. 341 | operationId: createUser 342 | requestBody: 343 | content: 344 | application/json: 345 | schema: 346 | $ref: "#/components/schemas/User" 347 | description: Created user object 348 | required: true 349 | responses: 350 | default: 351 | description: successful operation 352 | /user/createWithArray: 353 | post: 354 | tags: 355 | - user 356 | summary: Creates list of users with given input array 357 | description: "" 358 | operationId: createUsersWithArrayInput 359 | requestBody: 360 | $ref: "#/components/requestBodies/UserArray" 361 | responses: 362 | default: 363 | description: successful operation 364 | /user/createWithList: 365 | post: 366 | tags: 367 | - user 368 | summary: Creates list of users with given input array 369 | description: "" 370 | operationId: createUsersWithListInput 371 | requestBody: 372 | $ref: "#/components/requestBodies/UserArray" 373 | responses: 374 | default: 375 | description: successful operation 376 | /user/login: 377 | get: 378 | tags: 379 | - user 380 | summary: Logs user into the system 381 | description: "" 382 | operationId: loginUser 383 | parameters: 384 | - name: username 385 | in: query 386 | description: The user name for login 387 | required: true 388 | schema: 389 | type: string 390 | - name: password 391 | in: query 392 | description: The password for login in clear text 393 | required: true 394 | schema: 395 | type: string 396 | responses: 397 | "200": 398 | description: successful operation 399 | headers: 400 | X-Rate-Limit: 401 | description: calls per hour allowed by the user 402 | schema: 403 | type: integer 404 | format: int32 405 | X-Expires-After: 406 | description: date in UTC when token expires 407 | schema: 408 | type: string 409 | format: date-time 410 | content: 411 | application/xml: 412 | schema: 413 | type: string 414 | application/json: 415 | schema: 416 | type: string 417 | "400": 418 | description: Invalid username/password supplied 419 | /user/logout: 420 | get: 421 | tags: 422 | - user 423 | summary: Logs out current logged in user session 424 | description: "" 425 | operationId: logoutUser 426 | responses: 427 | default: 428 | description: successful operation 429 | "/user/{username}": 430 | get: 431 | tags: 432 | - user 433 | summary: Get user by user name 434 | description: "" 435 | operationId: getUserByName 436 | parameters: 437 | - $ref: "#/components/parameters/username" 438 | responses: 439 | "200": 440 | description: successful operation 441 | content: 442 | application/xml: 443 | schema: 444 | $ref: "#/components/schemas/User" 445 | application/json: 446 | schema: 447 | $ref: "#/components/schemas/User" 448 | "400": 449 | description: Invalid username supplied 450 | "404": 451 | description: User not found 452 | put: 453 | tags: 454 | - user 455 | summary: Updated user 456 | description: This can only be done by the logged in user. 457 | operationId: updateUser 458 | parameters: 459 | - name: username 460 | in: path 461 | description: name that need to be updated 462 | required: true 463 | schema: 464 | type: string 465 | requestBody: 466 | content: 467 | application/json: 468 | schema: 469 | $ref: "#/components/schemas/User" 470 | description: Updated user object 471 | required: true 472 | responses: 473 | "400": 474 | description: Invalid user supplied 475 | "404": 476 | description: User not found 477 | delete: 478 | tags: 479 | - user 480 | summary: Delete user 481 | description: This can only be done by the logged in user. 482 | operationId: deleteUser 483 | parameters: 484 | - name: username 485 | in: path 486 | description: The name that needs to be deleted 487 | required: true 488 | schema: 489 | type: string 490 | responses: 491 | "400": 492 | description: Invalid username supplied 493 | "404": 494 | description: User not found 495 | externalDocs: 496 | description: Find out more about Swagger 497 | url: http://swagger.io 498 | servers: 499 | - url: http://petstore.swagger.io/v2 500 | components: 501 | parameters: 502 | username: 503 | name: username 504 | in: path 505 | description: The name that needs to be fetched. Use user1 for testing. 506 | required: true 507 | schema: 508 | type: string 509 | requestBodies: 510 | UserArray: 511 | content: 512 | application/json: 513 | schema: 514 | type: array 515 | items: 516 | $ref: "#/components/schemas/User" 517 | description: List of user object 518 | required: true 519 | Pet: 520 | content: 521 | application/json: 522 | schema: 523 | $ref: "#/components/schemas/Pet" 524 | application/xml: 525 | schema: 526 | $ref: "#/components/schemas/Pet" 527 | description: Pet object that needs to be added to the store 528 | required: true 529 | securitySchemes: 530 | petstore_auth: 531 | type: oauth2 532 | flows: 533 | implicit: 534 | authorizationUrl: http://petstore.swagger.io/oauth/dialog 535 | scopes: 536 | write:pets: modify pets in your account 537 | read:pets: read your pets 538 | api_key: 539 | type: apiKey 540 | name: api_key 541 | in: header 542 | schemas: 543 | Order: 544 | type: object 545 | properties: 546 | id: 547 | type: integer 548 | format: int64 549 | petId: 550 | type: integer 551 | format: int64 552 | quantity: 553 | type: integer 554 | format: int32 555 | shipDate: 556 | type: string 557 | format: date-time 558 | status: 559 | type: string 560 | description: Order Status 561 | enum: 562 | - placed 563 | - approved 564 | - delivered 565 | complete: 566 | type: boolean 567 | default: false 568 | xml: 569 | name: Order 570 | Category: 571 | type: object 572 | properties: 573 | id: 574 | type: integer 575 | format: int64 576 | name: 577 | type: string 578 | xml: 579 | name: Category 580 | User: 581 | type: object 582 | properties: 583 | id: 584 | type: integer 585 | format: int64 586 | username: 587 | type: string 588 | firstName: 589 | type: string 590 | lastName: 591 | type: string 592 | email: 593 | type: string 594 | password: 595 | type: string 596 | phone: 597 | type: string 598 | userStatus: 599 | type: integer 600 | format: int32 601 | description: User Status 602 | xml: 603 | name: User 604 | Tag: 605 | type: object 606 | properties: 607 | id: 608 | type: integer 609 | format: int64 610 | name: 611 | type: string 612 | xml: 613 | name: Tag 614 | Pet: 615 | type: object 616 | required: 617 | - name 618 | - photoUrls 619 | properties: 620 | id: 621 | type: integer 622 | format: int64 623 | category: 624 | $ref: "#/components/schemas/Category" 625 | name: 626 | type: string 627 | example: doggie 628 | photoUrls: 629 | type: array 630 | xml: 631 | name: photoUrl 632 | wrapped: true 633 | items: 634 | type: string 635 | tags: 636 | type: array 637 | xml: 638 | name: tag 639 | wrapped: true 640 | items: 641 | $ref: "#/components/schemas/Tag" 642 | status: 643 | type: string 644 | description: pet status in the store 645 | enum: 646 | - available 647 | - pending 648 | - sold 649 | xml: 650 | name: Pet 651 | ApiResponse: 652 | type: object 653 | properties: 654 | code: 655 | type: integer 656 | format: int32 657 | type: 658 | type: string 659 | message: 660 | type: string 661 | -------------------------------------------------------------------------------- /test/fixtures/petstore.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "description": "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.", 5 | "version": "1.0.0", 6 | "title": "Swagger Petstore", 7 | "termsOfService": "http://swagger.io/terms/", 8 | "contact": { 9 | "email": "apiteam@swagger.io" 10 | }, 11 | "license": { 12 | "name": "Apache 2.0", 13 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 14 | } 15 | }, 16 | "host": "petstore.swagger.io", 17 | "basePath": "/v2", 18 | "tags": [ 19 | { 20 | "name": "pet", 21 | "description": "Everything about your Pets", 22 | "externalDocs": { 23 | "description": "Find out more", 24 | "url": "http://swagger.io" 25 | } 26 | }, 27 | { 28 | "name": "store", 29 | "description": "Access to Petstore orders" 30 | }, 31 | { 32 | "name": "user", 33 | "description": "Operations about user", 34 | "externalDocs": { 35 | "description": "Find out more about our store", 36 | "url": "http://swagger.io" 37 | } 38 | } 39 | ], 40 | "schemes": ["http"], 41 | "paths": { 42 | "/pet": { 43 | "post": { 44 | "tags": ["pet"], 45 | "summary": "Add a new pet to the store", 46 | "description": "", 47 | "operationId": "addPet", 48 | "consumes": ["application/json", "application/xml"], 49 | "produces": ["application/xml", "application/json"], 50 | "parameters": [ 51 | { 52 | "in": "body", 53 | "name": "body", 54 | "description": "Pet object that needs to be added to the store", 55 | "required": true, 56 | "schema": { 57 | "$ref": "#/definitions/Pet" 58 | } 59 | } 60 | ], 61 | "responses": { 62 | "405": { 63 | "description": "Invalid input" 64 | } 65 | }, 66 | "security": [ 67 | { 68 | "petstore_auth": ["write:pets", "read:pets"] 69 | } 70 | ] 71 | }, 72 | "put": { 73 | "tags": ["pet"], 74 | "summary": "Update an existing pet", 75 | "description": "", 76 | "operationId": "updatePet", 77 | "consumes": ["application/json", "application/xml"], 78 | "produces": ["application/xml", "application/json"], 79 | "parameters": [ 80 | { 81 | "in": "body", 82 | "name": "body", 83 | "description": "Pet object that needs to be added to the store", 84 | "required": true, 85 | "schema": { 86 | "$ref": "#/definitions/Pet" 87 | } 88 | } 89 | ], 90 | "responses": { 91 | "400": { 92 | "description": "Invalid ID supplied" 93 | }, 94 | "404": { 95 | "description": "Pet not found" 96 | }, 97 | "405": { 98 | "description": "Validation exception" 99 | } 100 | }, 101 | "security": [ 102 | { 103 | "petstore_auth": ["write:pets", "read:pets"] 104 | } 105 | ] 106 | } 107 | }, 108 | "/pet/findByStatus": { 109 | "get": { 110 | "tags": ["pet"], 111 | "summary": "Finds Pets by status", 112 | "description": "Multiple status values can be provided with comma separated strings", 113 | "operationId": "findPetsByStatus", 114 | "produces": ["application/xml", "application/json"], 115 | "parameters": [ 116 | { 117 | "name": "status", 118 | "in": "query", 119 | "description": "Status values that need to be considered for filter", 120 | "required": true, 121 | "type": "array", 122 | "items": { 123 | "type": "string", 124 | "enum": ["available", "pending", "sold"], 125 | "default": "available" 126 | }, 127 | "collectionFormat": "multi" 128 | } 129 | ], 130 | "responses": { 131 | "200": { 132 | "description": "successful operation", 133 | "schema": { 134 | "type": "array", 135 | "items": { 136 | "$ref": "#/definitions/Pet" 137 | } 138 | } 139 | }, 140 | "400": { 141 | "description": "Invalid status value" 142 | } 143 | }, 144 | "security": [ 145 | { 146 | "petstore_auth": ["write:pets", "read:pets"] 147 | } 148 | ] 149 | } 150 | }, 151 | "/pet/findByTags": { 152 | "get": { 153 | "tags": ["pet"], 154 | "summary": "Finds Pets by tags", 155 | "description": "Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", 156 | "operationId": "findPetsByTags", 157 | "produces": ["application/xml", "application/json"], 158 | "parameters": [ 159 | { 160 | "name": "tags", 161 | "in": "query", 162 | "description": "Tags to filter by", 163 | "required": true, 164 | "type": "array", 165 | "items": { 166 | "type": "string" 167 | }, 168 | "collectionFormat": "multi" 169 | } 170 | ], 171 | "responses": { 172 | "200": { 173 | "description": "successful operation", 174 | "schema": { 175 | "type": "array", 176 | "items": { 177 | "$ref": "#/definitions/Pet" 178 | } 179 | } 180 | }, 181 | "400": { 182 | "description": "Invalid tag value" 183 | } 184 | }, 185 | "security": [ 186 | { 187 | "petstore_auth": ["write:pets", "read:pets"] 188 | } 189 | ], 190 | "deprecated": true 191 | } 192 | }, 193 | "/pet/{petId}": { 194 | "get": { 195 | "tags": ["pet"], 196 | "summary": "Find pet by ID", 197 | "description": "Returns a single pet", 198 | "operationId": "getPetById", 199 | "produces": ["application/xml", "application/json"], 200 | "parameters": [ 201 | { 202 | "name": "petId", 203 | "in": "path", 204 | "description": "ID of pet to return", 205 | "required": true, 206 | "type": "integer", 207 | "format": "int64" 208 | } 209 | ], 210 | "responses": { 211 | "200": { 212 | "description": "successful operation", 213 | "schema": { 214 | "$ref": "#/definitions/Pet" 215 | } 216 | }, 217 | "400": { 218 | "description": "Invalid ID supplied" 219 | }, 220 | "404": { 221 | "description": "Pet not found" 222 | } 223 | }, 224 | "security": [ 225 | { 226 | "api_key": [] 227 | } 228 | ] 229 | }, 230 | "post": { 231 | "tags": ["pet"], 232 | "summary": "Updates a pet in the store with form data", 233 | "description": "", 234 | "operationId": "updatePetWithForm", 235 | "consumes": ["application/x-www-form-urlencoded"], 236 | "produces": ["application/xml", "application/json"], 237 | "parameters": [ 238 | { 239 | "name": "petId", 240 | "in": "path", 241 | "description": "ID of pet that needs to be updated", 242 | "required": true, 243 | "type": "integer", 244 | "format": "int64" 245 | }, 246 | { 247 | "name": "name", 248 | "in": "formData", 249 | "description": "Updated name of the pet", 250 | "required": false, 251 | "type": "string" 252 | }, 253 | { 254 | "name": "status", 255 | "in": "formData", 256 | "description": "Updated status of the pet", 257 | "required": false, 258 | "type": "string" 259 | } 260 | ], 261 | "responses": { 262 | "405": { 263 | "description": "Invalid input" 264 | } 265 | }, 266 | "security": [ 267 | { 268 | "petstore_auth": ["write:pets", "read:pets"] 269 | } 270 | ] 271 | }, 272 | "delete": { 273 | "tags": ["pet"], 274 | "summary": "Deletes a pet", 275 | "description": "", 276 | "operationId": "deletePet", 277 | "produces": ["application/xml", "application/json"], 278 | "parameters": [ 279 | { 280 | "name": "api_key", 281 | "in": "header", 282 | "required": false, 283 | "type": "string" 284 | }, 285 | { 286 | "name": "petId", 287 | "in": "path", 288 | "description": "Pet id to delete", 289 | "required": true, 290 | "type": "integer", 291 | "format": "int64" 292 | } 293 | ], 294 | "responses": { 295 | "400": { 296 | "description": "Invalid ID supplied" 297 | }, 298 | "404": { 299 | "description": "Pet not found" 300 | } 301 | }, 302 | "security": [ 303 | { 304 | "petstore_auth": ["write:pets", "read:pets"] 305 | } 306 | ] 307 | } 308 | }, 309 | "/store/inventory": { 310 | "get": { 311 | "tags": ["store"], 312 | "summary": "Returns pet inventories by status", 313 | "description": "Returns a map of status codes to quantities", 314 | "operationId": "getInventory", 315 | "produces": ["application/json"], 316 | "parameters": [], 317 | "responses": { 318 | "200": { 319 | "description": "successful operation", 320 | "schema": { 321 | "type": "object", 322 | "additionalProperties": { 323 | "type": "integer", 324 | "format": "int32" 325 | } 326 | } 327 | } 328 | }, 329 | "security": [ 330 | { 331 | "api_key": [] 332 | } 333 | ] 334 | } 335 | }, 336 | "/store/order": { 337 | "post": { 338 | "tags": ["store"], 339 | "summary": "Place an order for a pet", 340 | "description": "", 341 | "operationId": "placeOrder", 342 | "produces": ["application/xml", "application/json"], 343 | "parameters": [ 344 | { 345 | "in": "body", 346 | "name": "body", 347 | "description": "order placed for purchasing the pet", 348 | "required": true, 349 | "schema": { 350 | "$ref": "#/definitions/Order" 351 | } 352 | } 353 | ], 354 | "responses": { 355 | "200": { 356 | "description": "successful operation", 357 | "schema": { 358 | "$ref": "#/definitions/Order" 359 | } 360 | }, 361 | "400": { 362 | "description": "Invalid Order" 363 | } 364 | } 365 | } 366 | }, 367 | "/store/order/{orderId}": { 368 | "get": { 369 | "tags": ["store"], 370 | "summary": "Find purchase order by ID", 371 | "description": "For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions", 372 | "operationId": "getOrderById", 373 | "produces": ["application/xml", "application/json"], 374 | "parameters": [ 375 | { 376 | "name": "orderId", 377 | "in": "path", 378 | "description": "ID of pet that needs to be fetched", 379 | "required": true, 380 | "type": "integer", 381 | "maximum": 10, 382 | "minimum": 1, 383 | "format": "int64" 384 | } 385 | ], 386 | "responses": { 387 | "200": { 388 | "description": "successful operation", 389 | "schema": { 390 | "$ref": "#/definitions/Order" 391 | } 392 | }, 393 | "400": { 394 | "description": "Invalid ID supplied" 395 | }, 396 | "404": { 397 | "description": "Order not found" 398 | } 399 | } 400 | }, 401 | "delete": { 402 | "tags": ["store"], 403 | "summary": "Delete purchase order by ID", 404 | "description": "For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors", 405 | "operationId": "deleteOrder", 406 | "produces": ["application/xml", "application/json"], 407 | "parameters": [ 408 | { 409 | "name": "orderId", 410 | "in": "path", 411 | "description": "ID of the order that needs to be deleted", 412 | "required": true, 413 | "type": "integer", 414 | "minimum": 1, 415 | "format": "int64" 416 | } 417 | ], 418 | "responses": { 419 | "400": { 420 | "description": "Invalid ID supplied" 421 | }, 422 | "404": { 423 | "description": "Order not found" 424 | } 425 | } 426 | } 427 | }, 428 | "/user": { 429 | "post": { 430 | "tags": ["user"], 431 | "summary": "Create user", 432 | "description": "This can only be done by the logged in user.", 433 | "operationId": "createUser", 434 | "produces": ["application/xml", "application/json"], 435 | "parameters": [ 436 | { 437 | "in": "body", 438 | "name": "body", 439 | "description": "Created user object", 440 | "required": true, 441 | "schema": { 442 | "$ref": "#/definitions/User" 443 | } 444 | } 445 | ], 446 | "responses": { 447 | "default": { 448 | "description": "successful operation" 449 | } 450 | } 451 | } 452 | }, 453 | "/user/createWithArray": { 454 | "post": { 455 | "tags": ["user"], 456 | "summary": "Creates list of users with given input array", 457 | "description": "", 458 | "operationId": "createUsersWithArrayInput", 459 | "produces": ["application/xml", "application/json"], 460 | "parameters": [ 461 | { 462 | "in": "body", 463 | "name": "body", 464 | "description": "List of user object", 465 | "required": true, 466 | "schema": { 467 | "type": "array", 468 | "items": { 469 | "$ref": "#/definitions/User" 470 | } 471 | } 472 | } 473 | ], 474 | "responses": { 475 | "default": { 476 | "description": "successful operation" 477 | } 478 | } 479 | } 480 | }, 481 | "/user/createWithList": { 482 | "post": { 483 | "tags": ["user"], 484 | "summary": "Creates list of users with given input array", 485 | "description": "", 486 | "operationId": "createUsersWithListInput", 487 | "produces": ["application/xml", "application/json"], 488 | "parameters": [ 489 | { 490 | "in": "body", 491 | "name": "body", 492 | "description": "List of user object", 493 | "required": true, 494 | "schema": { 495 | "type": "array", 496 | "items": { 497 | "$ref": "#/definitions/User" 498 | } 499 | } 500 | } 501 | ], 502 | "responses": { 503 | "default": { 504 | "description": "successful operation" 505 | } 506 | } 507 | } 508 | }, 509 | "/user/login": { 510 | "get": { 511 | "tags": ["user"], 512 | "summary": "Logs user into the system", 513 | "description": "", 514 | "operationId": "loginUser", 515 | "produces": ["application/xml", "application/json"], 516 | "parameters": [ 517 | { 518 | "name": "username", 519 | "in": "query", 520 | "description": "The user name for login", 521 | "required": true, 522 | "type": "string" 523 | }, 524 | { 525 | "name": "password", 526 | "in": "query", 527 | "description": "The password for login in clear text", 528 | "required": true, 529 | "type": "string" 530 | } 531 | ], 532 | "responses": { 533 | "200": { 534 | "description": "successful operation", 535 | "schema": { 536 | "type": "string" 537 | }, 538 | "headers": { 539 | "X-Rate-Limit": { 540 | "type": "integer", 541 | "format": "int32", 542 | "description": "calls per hour allowed by the user" 543 | }, 544 | "X-Expires-After": { 545 | "type": "string", 546 | "format": "date-time", 547 | "description": "date in UTC when token expires" 548 | } 549 | } 550 | }, 551 | "400": { 552 | "description": "Invalid username/password supplied" 553 | } 554 | } 555 | } 556 | }, 557 | "/user/logout": { 558 | "get": { 559 | "tags": ["user"], 560 | "summary": "Logs out current logged in user session", 561 | "description": "", 562 | "operationId": "logoutUser", 563 | "produces": ["application/xml", "application/json"], 564 | "parameters": [], 565 | "responses": { 566 | "default": { 567 | "description": "successful operation" 568 | } 569 | } 570 | } 571 | }, 572 | "/user/{username}": { 573 | "get": { 574 | "tags": ["user"], 575 | "summary": "Get user by user name", 576 | "description": "", 577 | "operationId": "getUserByName", 578 | "produces": ["application/xml", "application/json"], 579 | "parameters": [ 580 | { 581 | "$ref": "#/parameters/username" 582 | } 583 | ], 584 | "responses": { 585 | "200": { 586 | "description": "successful operation", 587 | "schema": { 588 | "$ref": "#/definitions/User" 589 | } 590 | }, 591 | "400": { 592 | "description": "Invalid username supplied" 593 | }, 594 | "404": { 595 | "description": "User not found" 596 | } 597 | } 598 | }, 599 | "put": { 600 | "tags": ["user"], 601 | "summary": "Updated user", 602 | "description": "This can only be done by the logged in user.", 603 | "operationId": "updateUser", 604 | "produces": ["application/xml", "application/json"], 605 | "parameters": [ 606 | { 607 | "name": "username", 608 | "in": "path", 609 | "description": "name that need to be updated", 610 | "required": true, 611 | "type": "string" 612 | }, 613 | { 614 | "in": "body", 615 | "name": "body", 616 | "description": "Updated user object", 617 | "required": true, 618 | "schema": { 619 | "$ref": "#/definitions/User" 620 | } 621 | } 622 | ], 623 | "responses": { 624 | "400": { 625 | "description": "Invalid user supplied" 626 | }, 627 | "404": { 628 | "description": "User not found" 629 | } 630 | } 631 | }, 632 | "delete": { 633 | "tags": ["user"], 634 | "summary": "Delete user", 635 | "description": "This can only be done by the logged in user.", 636 | "operationId": "deleteUser", 637 | "produces": ["application/xml", "application/json"], 638 | "parameters": [ 639 | { 640 | "name": "username", 641 | "in": "path", 642 | "description": "The name that needs to be deleted", 643 | "required": true, 644 | "type": "string" 645 | } 646 | ], 647 | "responses": { 648 | "400": { 649 | "description": "Invalid username supplied" 650 | }, 651 | "404": { 652 | "description": "User not found" 653 | } 654 | } 655 | } 656 | } 657 | }, 658 | "securityDefinitions": { 659 | "petstore_auth": { 660 | "type": "oauth2", 661 | "authorizationUrl": "http://petstore.swagger.io/oauth/dialog", 662 | "flow": "implicit", 663 | "scopes": { 664 | "write:pets": "modify pets in your account", 665 | "read:pets": "read your pets" 666 | } 667 | }, 668 | "api_key": { 669 | "type": "apiKey", 670 | "name": "api_key", 671 | "in": "header" 672 | } 673 | }, 674 | "parameters": { 675 | "username": { 676 | "name": "username", 677 | "in": "path", 678 | "description": "The name that needs to be fetched. Use user1 for testing.", 679 | "required": true, 680 | "type": "string" 681 | } 682 | }, 683 | "definitions": { 684 | "Order": { 685 | "type": "object", 686 | "properties": { 687 | "id": { 688 | "type": "integer", 689 | "format": "int64" 690 | }, 691 | "petId": { 692 | "type": "integer", 693 | "format": "int64" 694 | }, 695 | "quantity": { 696 | "type": "integer", 697 | "format": "int32" 698 | }, 699 | "shipDate": { 700 | "type": "string", 701 | "format": "date-time" 702 | }, 703 | "status": { 704 | "type": "string", 705 | "description": "Order Status", 706 | "enum": ["placed", "approved", "delivered"] 707 | }, 708 | "complete": { 709 | "type": "boolean", 710 | "default": false 711 | } 712 | }, 713 | "xml": { 714 | "name": "Order" 715 | } 716 | }, 717 | "Category": { 718 | "type": "object", 719 | "properties": { 720 | "id": { 721 | "type": "integer", 722 | "format": "int64" 723 | }, 724 | "name": { 725 | "type": "string" 726 | } 727 | }, 728 | "xml": { 729 | "name": "Category" 730 | } 731 | }, 732 | "User": { 733 | "type": "object", 734 | "properties": { 735 | "id": { 736 | "type": "integer", 737 | "format": "int64" 738 | }, 739 | "username": { 740 | "type": "string" 741 | }, 742 | "firstName": { 743 | "type": "string" 744 | }, 745 | "lastName": { 746 | "type": "string" 747 | }, 748 | "email": { 749 | "type": "string" 750 | }, 751 | "password": { 752 | "type": "string" 753 | }, 754 | "phone": { 755 | "type": "string" 756 | }, 757 | "userStatus": { 758 | "type": "integer", 759 | "format": "int32", 760 | "description": "User Status" 761 | } 762 | }, 763 | "xml": { 764 | "name": "User" 765 | } 766 | }, 767 | "Tag": { 768 | "type": "object", 769 | "properties": { 770 | "id": { 771 | "type": "integer", 772 | "format": "int64" 773 | }, 774 | "name": { 775 | "type": "string" 776 | } 777 | }, 778 | "xml": { 779 | "name": "Tag" 780 | } 781 | }, 782 | "Pet": { 783 | "type": "object", 784 | "required": ["name", "photoUrls"], 785 | "properties": { 786 | "id": { 787 | "type": "integer", 788 | "format": "int64" 789 | }, 790 | "category": { 791 | "$ref": "#/definitions/Category" 792 | }, 793 | "name": { 794 | "type": "string", 795 | "example": "doggie" 796 | }, 797 | "photoUrls": { 798 | "type": "array", 799 | "xml": { 800 | "name": "photoUrl", 801 | "wrapped": true 802 | }, 803 | "items": { 804 | "type": "string" 805 | } 806 | }, 807 | "tags": { 808 | "type": "array", 809 | "xml": { 810 | "name": "tag", 811 | "wrapped": true 812 | }, 813 | "items": { 814 | "$ref": "#/definitions/Tag" 815 | } 816 | }, 817 | "status": { 818 | "type": "string", 819 | "description": "pet status in the store", 820 | "enum": ["available", "pending", "sold"] 821 | } 822 | }, 823 | "xml": { 824 | "name": "Pet" 825 | } 826 | }, 827 | "ApiResponse": { 828 | "type": "object", 829 | "properties": { 830 | "code": { 831 | "type": "integer", 832 | "format": "int32" 833 | }, 834 | "type": { 835 | "type": "string" 836 | }, 837 | "message": { 838 | "type": "string" 839 | } 840 | } 841 | } 842 | }, 843 | "externalDocs": { 844 | "description": "Find out more about Swagger", 845 | "url": "http://swagger.io" 846 | } 847 | } 848 | --------------------------------------------------------------------------------