@airtasker/spot
Advanced tools
Comparing version 0.1.27 to 0.1.28
@@ -20,2 +20,3 @@ "use strict"; | ||
let { api: apiPath, language, generator, out: outDir } = flags; | ||
const apiFileName = path.basename(apiPath, ".ts"); | ||
const api = await file_parser_1.parsePath(apiPath); | ||
@@ -61,3 +62,6 @@ if (!generator) { | ||
const generatedFiles = generators[generator][language](api); | ||
for (const [relativePath, content] of Object.entries(generatedFiles)) { | ||
for (let [relativePath, content] of Object.entries(generatedFiles)) { | ||
if (relativePath.indexOf("*") !== -1) { | ||
relativePath = relativePath.replace(/\*/g, apiFileName); | ||
} | ||
output_1.outputFile(outDir, relativePath, content); | ||
@@ -107,6 +111,6 @@ } | ||
json: api => ({ | ||
"types.json": json_schema_1.generateJsonSchema(api, "json") | ||
"*.json": json_schema_1.generateJsonSchema(api, "json") | ||
}), | ||
yaml: api => ({ | ||
"types.yml": json_schema_1.generateJsonSchema(api, "yaml") | ||
"*.yml": json_schema_1.generateJsonSchema(api, "yaml") | ||
}) | ||
@@ -116,6 +120,6 @@ }, | ||
json: api => ({ | ||
"api.json": openapi2_1.generateOpenApiV2(api, "json") | ||
"*.json": openapi2_1.generateOpenApiV2(api, "json") | ||
}), | ||
yaml: api => ({ | ||
"api.yml": openapi2_1.generateOpenApiV2(api, "yaml") | ||
"*.yml": openapi2_1.generateOpenApiV2(api, "yaml") | ||
}) | ||
@@ -125,6 +129,6 @@ }, | ||
json: api => ({ | ||
"api.json": openapi3_1.generateOpenApiV3(api, "json") | ||
"*.json": openapi3_1.generateOpenApiV3(api, "json") | ||
}), | ||
yaml: api => ({ | ||
"api.yml": openapi3_1.generateOpenApiV3(api, "yaml") | ||
"*.yml": openapi3_1.generateOpenApiV3(api, "yaml") | ||
}) | ||
@@ -131,0 +135,0 @@ }, |
@@ -20,2 +20,3 @@ "use strict"; | ||
let { api: apiPath, language, generator, out: outDir } = flags; | ||
const apiFileName = path.basename(apiPath, ".ts"); | ||
const api = await file_parser_1.parsePath(apiPath); | ||
@@ -61,3 +62,6 @@ if (!generator) { | ||
const generatedFiles = generators[generator][language](api); | ||
for (const [relativePath, content] of Object.entries(generatedFiles)) { | ||
for (let [relativePath, content] of Object.entries(generatedFiles)) { | ||
if (relativePath.indexOf("*") !== -1) { | ||
relativePath = relativePath.replace(/\*/g, apiFileName); | ||
} | ||
output_1.outputFile(outDir, relativePath, content); | ||
@@ -107,6 +111,6 @@ } | ||
json: api => ({ | ||
"types.json": json_schema_1.generateJsonSchema(api, "json") | ||
"*.json": json_schema_1.generateJsonSchema(api, "json") | ||
}), | ||
yaml: api => ({ | ||
"types.yml": json_schema_1.generateJsonSchema(api, "yaml") | ||
"*.yml": json_schema_1.generateJsonSchema(api, "yaml") | ||
}) | ||
@@ -116,6 +120,6 @@ }, | ||
json: api => ({ | ||
"api.json": openapi2_1.generateOpenApiV2(api, "json") | ||
"*.json": openapi2_1.generateOpenApiV2(api, "json") | ||
}), | ||
yaml: api => ({ | ||
"api.yml": openapi2_1.generateOpenApiV2(api, "yaml") | ||
"*.yml": openapi2_1.generateOpenApiV2(api, "yaml") | ||
}) | ||
@@ -125,6 +129,6 @@ }, | ||
json: api => ({ | ||
"api.json": openapi3_1.generateOpenApiV3(api, "json") | ||
"*.json": openapi3_1.generateOpenApiV3(api, "json") | ||
}), | ||
yaml: api => ({ | ||
"api.yml": openapi3_1.generateOpenApiV3(api, "yaml") | ||
"*.yml": openapi3_1.generateOpenApiV3(api, "yaml") | ||
}) | ||
@@ -131,0 +135,0 @@ }, |
@@ -8,2 +8,3 @@ "use strict"; | ||
const compact = require("lodash/compact"); | ||
const uniqBy = require("lodash/uniqBy"); | ||
const defaultTo = require("lodash/defaultTo"); | ||
@@ -25,10 +26,7 @@ function generateOpenApiV2(api, format) { | ||
swagger: "2.0", | ||
tags: [ | ||
{ | ||
name: "TODO" | ||
} | ||
], | ||
tags: getTags(api), | ||
info: { | ||
version: "0.0.0", | ||
title: "TODO", | ||
title: api.description.name, | ||
description: api.description.description, | ||
contact: { | ||
@@ -47,5 +45,5 @@ name: "TODO" | ||
operationId: endpointName, | ||
description: "TODO", | ||
description: endpoint.description, | ||
consumes: consumes(api, endpoint), | ||
tags: ["TODO"], | ||
tags: endpoint.tags, | ||
parameters: getParameters(api, endpoint), | ||
@@ -70,2 +68,12 @@ responses: { | ||
exports.openApiV2 = openApiV2; | ||
function getTags(api) { | ||
return uniqBy(Object.entries(api.endpoints).reduce((acc, [endpointName, endpoint]) => { | ||
if (endpoint.tags) { | ||
acc = acc.concat(endpoint.tags.map(tag => { | ||
return { name: tag }; | ||
})); | ||
} | ||
return acc; | ||
}, []), "name"); | ||
} | ||
function getParameters(api, endpoint) { | ||
@@ -72,0 +80,0 @@ const parameters = endpoint.path |
@@ -9,2 +9,3 @@ "use strict"; | ||
const compact = require("lodash/compact"); | ||
const uniqBy = require("lodash/uniqBy"); | ||
const pickBy = require("lodash/pickBy"); | ||
@@ -27,10 +28,7 @@ const defaultTo = require("lodash/defaultTo"); | ||
openapi: "3.0.0", | ||
tags: [ | ||
{ | ||
name: "TODO" | ||
} | ||
], | ||
tags: getTags(api), | ||
info: { | ||
version: "0.0.0", | ||
title: "TODO", | ||
title: api.description.name, | ||
description: api.description.description, | ||
contact: { | ||
@@ -49,4 +47,4 @@ name: "TODO" | ||
operationId: endpointName, | ||
description: "TODO", | ||
tags: ["TODO"], | ||
description: endpoint.description, | ||
tags: endpoint.tags, | ||
parameters: getParameters(api, endpoint), | ||
@@ -78,2 +76,12 @@ ...pickBy({ | ||
exports.openApiV3 = openApiV3; | ||
function getTags(api) { | ||
return uniqBy(Object.entries(api.endpoints).reduce((acc, [endpointName, endpoint]) => { | ||
if (endpoint.tags) { | ||
acc = acc.concat(endpoint.tags.map(tag => { | ||
return { name: tag }; | ||
})); | ||
} | ||
return acc; | ||
}, []), "name"); | ||
} | ||
function getParameters(api, endpoint) { | ||
@@ -80,0 +88,0 @@ const parameters = endpoint.path |
@@ -126,3 +126,3 @@ "use strict"; | ||
ts.createPropertyAssignment("responseType", ts.createStringLiteral("json")), | ||
ts.createPropertyAssignment("headers", ts.createObjectLiteral(Object.entries(endpoint.headers).map(([headerName, header]) => ts.createPropertyAssignment(header.headerFieldName, ts.createIdentifier(headerName))))), | ||
ts.createPropertyAssignment("headers", ts.createObjectLiteral(Object.entries(endpoint.headers).map(([headerName, header]) => ts.createPropertyAssignment(ts.createStringLiteral(header.headerFieldName), ts.createIdentifier(headerName))))), | ||
...(includeRequest | ||
@@ -129,0 +129,0 @@ ? [ |
@@ -1,3 +0,5 @@ | ||
export declare function api(description?: ApiDescription): (constructor: Function) => void; | ||
export declare function api(description: ApiDescription): (constructor: Function) => void; | ||
export interface ApiDescription { | ||
name: string; | ||
description: string; | ||
} | ||
@@ -8,4 +10,6 @@ export declare function endpoint(description: EndpointDescription): (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void; | ||
path: string; | ||
description?: string; | ||
requestContentType?: HttpContentType; | ||
successStatusCode?: number; | ||
tags?: string[]; | ||
} | ||
@@ -12,0 +16,0 @@ export declare function genericError<T>(): (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
function api(description = {}) { | ||
function api(description) { | ||
return (constructor) => { }; | ||
@@ -5,0 +5,0 @@ } |
@@ -8,2 +8,3 @@ "use strict"; | ||
const compact = require("lodash/compact"); | ||
const uniqBy = require("lodash/uniqBy"); | ||
const defaultTo = require("lodash/defaultTo"); | ||
@@ -25,10 +26,7 @@ function generateOpenApiV2(api, format) { | ||
swagger: "2.0", | ||
tags: [ | ||
{ | ||
name: "TODO" | ||
} | ||
], | ||
tags: getTags(api), | ||
info: { | ||
version: "0.0.0", | ||
title: "TODO", | ||
title: api.description.name, | ||
description: api.description.description, | ||
contact: { | ||
@@ -47,5 +45,5 @@ name: "TODO" | ||
operationId: endpointName, | ||
description: "TODO", | ||
description: endpoint.description, | ||
consumes: consumes(api, endpoint), | ||
tags: ["TODO"], | ||
tags: endpoint.tags, | ||
parameters: getParameters(api, endpoint), | ||
@@ -70,2 +68,12 @@ responses: { | ||
exports.openApiV2 = openApiV2; | ||
function getTags(api) { | ||
return uniqBy(Object.entries(api.endpoints).reduce((acc, [endpointName, endpoint]) => { | ||
if (endpoint.tags) { | ||
acc = acc.concat(endpoint.tags.map(tag => { | ||
return { name: tag }; | ||
})); | ||
} | ||
return acc; | ||
}, []), "name"); | ||
} | ||
function getParameters(api, endpoint) { | ||
@@ -72,0 +80,0 @@ const parameters = endpoint.path |
@@ -9,2 +9,3 @@ "use strict"; | ||
const compact = require("lodash/compact"); | ||
const uniqBy = require("lodash/uniqBy"); | ||
const pickBy = require("lodash/pickBy"); | ||
@@ -27,10 +28,7 @@ const defaultTo = require("lodash/defaultTo"); | ||
openapi: "3.0.0", | ||
tags: [ | ||
{ | ||
name: "TODO" | ||
} | ||
], | ||
tags: getTags(api), | ||
info: { | ||
version: "0.0.0", | ||
title: "TODO", | ||
title: api.description.name, | ||
description: api.description.description, | ||
contact: { | ||
@@ -49,4 +47,4 @@ name: "TODO" | ||
operationId: endpointName, | ||
description: "TODO", | ||
tags: ["TODO"], | ||
description: endpoint.description, | ||
tags: endpoint.tags, | ||
parameters: getParameters(api, endpoint), | ||
@@ -78,2 +76,12 @@ ...pickBy({ | ||
exports.openApiV3 = openApiV3; | ||
function getTags(api) { | ||
return uniqBy(Object.entries(api.endpoints).reduce((acc, [endpointName, endpoint]) => { | ||
if (endpoint.tags) { | ||
acc = acc.concat(endpoint.tags.map(tag => { | ||
return { name: tag }; | ||
})); | ||
} | ||
return acc; | ||
}, []), "name"); | ||
} | ||
function getParameters(api, endpoint) { | ||
@@ -80,0 +88,0 @@ const parameters = endpoint.path |
@@ -126,3 +126,3 @@ "use strict"; | ||
ts.createPropertyAssignment("responseType", ts.createStringLiteral("json")), | ||
ts.createPropertyAssignment("headers", ts.createObjectLiteral(Object.entries(endpoint.headers).map(([headerName, header]) => ts.createPropertyAssignment(header.headerFieldName, ts.createIdentifier(headerName))))), | ||
ts.createPropertyAssignment("headers", ts.createObjectLiteral(Object.entries(endpoint.headers).map(([headerName, header]) => ts.createPropertyAssignment(ts.createStringLiteral(header.headerFieldName), ts.createIdentifier(headerName))))), | ||
...(includeRequest | ||
@@ -129,0 +129,0 @@ ? [ |
@@ -1,3 +0,5 @@ | ||
export declare function api(description?: ApiDescription): (constructor: Function) => void; | ||
export declare function api(description: ApiDescription): (constructor: Function) => void; | ||
export interface ApiDescription { | ||
name: string; | ||
description: string; | ||
} | ||
@@ -8,4 +10,6 @@ export declare function endpoint(description: EndpointDescription): (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void; | ||
path: string; | ||
description?: string; | ||
requestContentType?: HttpContentType; | ||
successStatusCode?: number; | ||
tags?: string[]; | ||
} | ||
@@ -12,0 +16,0 @@ export declare function genericError<T>(): (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
function api(description = {}) { | ||
function api(description) { | ||
return (constructor) => { }; | ||
@@ -5,0 +5,0 @@ } |
@@ -1,2 +0,2 @@ | ||
import { HttpContentType, HttpMethod } from "./lib"; | ||
import { ApiDescription, HttpContentType, HttpMethod } from "./lib"; | ||
export interface Api { | ||
@@ -9,2 +9,3 @@ endpoints: { | ||
}; | ||
description: ApiDescription; | ||
} | ||
@@ -14,4 +15,6 @@ export interface Endpoint { | ||
path: PathComponent[]; | ||
description: string; | ||
headers: Headers; | ||
queryParams: QueryParamComponent[]; | ||
tags?: string[]; | ||
requestContentType?: HttpContentType; | ||
@@ -18,0 +21,0 @@ requestType: Type; |
@@ -11,2 +11,4 @@ "use strict"; | ||
const type_parser_1 = require("./type-parser"); | ||
const endpoint_method_1 = require("./nodes/endpoint-method"); | ||
const merge = require("lodash/merge"); | ||
/** | ||
@@ -18,7 +20,3 @@ * Parses a TypeScript source file, as well as any other TypeScript files it imports recursively. | ||
async function parsePath(sourcePath) { | ||
const api = { | ||
endpoints: {}, | ||
types: {} | ||
}; | ||
await parseFileRecursively(api, new Set(), sourcePath); | ||
const api = parseRootFile(sourcePath); | ||
const errors = validator_1.validate(api); | ||
@@ -31,5 +29,5 @@ if (errors.length > 0) { | ||
exports.parsePath = parsePath; | ||
async function parseFileRecursively(api, visitedPaths, sourcePath) { | ||
if (!(await fs.existsSync(sourcePath))) { | ||
if (await fs.existsSync(sourcePath + ".ts")) { | ||
function extractSourceFile(sourcePath) { | ||
if (!fs.existsSync(sourcePath)) { | ||
if (fs.existsSync(sourcePath + ".ts")) { | ||
sourcePath += ".ts"; | ||
@@ -41,11 +39,9 @@ } | ||
} | ||
if (visitedPaths.has(sourcePath) || | ||
path.resolve(sourcePath).startsWith(__dirname)) { | ||
return; | ||
} | ||
else { | ||
visitedPaths.add(sourcePath); | ||
} | ||
const fileContent = await fs.readFile(sourcePath, "utf8"); | ||
const fileContent = fs.readFileSync(sourcePath, "utf8"); | ||
const sourceFile = ts.createSourceFile(path.basename(sourcePath), fileContent, ts.ScriptTarget.Latest); | ||
return sourceFile; | ||
} | ||
function getPathsRecursively(sourcePath) { | ||
const importPaths = new Set(); | ||
const sourceFile = extractSourceFile(sourcePath); | ||
for (const statement of sourceFile.statements) { | ||
@@ -61,9 +57,79 @@ if (ts.isImportDeclaration(statement)) { | ||
} | ||
await parseFileRecursively(api, visitedPaths, path.join(sourcePath, "..", importPath)); | ||
if (!(importPaths.has(importPath) || | ||
path.resolve(importPath).startsWith(__dirname))) { | ||
importPaths.add(path.join(sourcePath, "..", importPath)); | ||
} | ||
} | ||
else if (ts.isClassDeclaration(statement)) { | ||
} | ||
return new Set([ | ||
...importPaths, | ||
...[...importPaths].reduce((acc, importPath) => { | ||
return new Set([...acc, ...getPathsRecursively(importPath)]); | ||
}, new Set()) | ||
]); | ||
} | ||
function parseRootFile(sourcePath) { | ||
const importPaths = getPathsRecursively(sourcePath); | ||
const api = { | ||
endpoints: {}, | ||
types: {}, | ||
description: { | ||
name: "", | ||
description: "" | ||
} | ||
}; | ||
if (containsApiDeclaration(sourcePath)) { | ||
const sourceFile = extractSourceFile(sourcePath); | ||
for (const statement of sourceFile.statements) { | ||
if (ts.isClassDeclaration(statement)) { | ||
const apiDescription = api_class_1.parseApiClass(sourceFile, statement); | ||
if (apiDescription) { | ||
api.description = apiDescription; | ||
} | ||
api.endpoints = parseEndpoints(statement, sourceFile); | ||
} | ||
else if (ts.isTypeAliasDeclaration(statement)) { | ||
const name = statement.name.getText(sourceFile); | ||
api.types[name] = type_parser_1.extractType(sourceFile, statement.type); | ||
} | ||
else if (ts.isInterfaceDeclaration(statement)) { | ||
const name = statement.name.getText(sourceFile); | ||
api.types[name] = type_parser_1.extractObjectType(sourceFile, statement); | ||
} | ||
} | ||
return [...importPaths].reduce((acc, path) => { | ||
return merge(acc, parseFile(path)); | ||
}, api); | ||
} | ||
else { | ||
throw panic_1.panic(`No @api declaration found at ${sourcePath}`); | ||
} | ||
} | ||
function parseEndpoints(statement, sourceFile) { | ||
const endpoints = {}; | ||
for (const member of statement.members) { | ||
if (ts.isMethodDeclaration(member)) { | ||
// Each endpoint must be defined only once. | ||
const endpointName = member.name.getText(sourceFile); | ||
if (endpoints[endpointName]) { | ||
throw panic_1.panic(`Found multiple definitions of the same endpoint ${endpointName}`); | ||
} | ||
endpoints[endpointName] = endpoint_method_1.parseEndpointMethod(sourceFile, member); | ||
} | ||
} | ||
return endpoints; | ||
} | ||
function parseFile(sourcePath) { | ||
const api = { | ||
endpoints: {}, | ||
types: {} | ||
}; | ||
const sourceFile = extractSourceFile(sourcePath); | ||
for (const statement of sourceFile.statements) { | ||
if (ts.isClassDeclaration(statement)) { | ||
const apiDecorator = decorators_1.extractSingleDecorator(sourceFile, statement, "api"); | ||
if (apiDecorator) { | ||
api_class_1.parseApiClass(sourceFile, statement, api); | ||
throw `@api cannot be defined more than once at ${sourcePath}`; | ||
} | ||
api.endpoints = parseEndpoints(statement, sourceFile); | ||
} | ||
@@ -79,2 +145,15 @@ else if (ts.isTypeAliasDeclaration(statement)) { | ||
} | ||
return api; | ||
} | ||
function containsApiDeclaration(sourcePath) { | ||
const sourceFile = extractSourceFile(sourcePath); | ||
for (const statement of sourceFile.statements) { | ||
if (ts.isClassDeclaration(statement)) { | ||
const apiDecorator = decorators_1.extractSingleDecorator(sourceFile, statement, "api"); | ||
if (apiDecorator) { | ||
return true; | ||
} | ||
} | ||
} | ||
return false; | ||
} |
import * as ts from "typescript"; | ||
import { Api } from "../../models"; | ||
import { ApiDescription } from "@airtasker/spot"; | ||
/** | ||
* Parses a top-level API class definition and the endpoints it defines, such as: | ||
* ``` | ||
* @api() | ||
* @api({ | ||
* name: "My API", | ||
* description: "A really cool API" | ||
* }) | ||
* class Api { | ||
@@ -18,2 +21,2 @@ * @endpoint({ | ||
*/ | ||
export declare function parseApiClass(sourceFile: ts.SourceFile, classDeclaration: ts.ClassDeclaration, api: Api): void; | ||
export declare function parseApiClass(sourceFile: ts.SourceFile, classDeclaration: ts.ClassDeclaration): ApiDescription; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const ts = require("typescript"); | ||
const endpoint_method_1 = require("./endpoint-method"); | ||
const decorators_1 = require("../decorators"); | ||
const panic_1 = require("../panic"); | ||
const literal_parser_1 = require("../literal-parser"); | ||
/** | ||
* Parses a top-level API class definition and the endpoints it defines, such as: | ||
* ``` | ||
* @api() | ||
* @api({ | ||
* name: "My API", | ||
* description: "A really cool API" | ||
* }) | ||
* class Api { | ||
@@ -20,9 +24,30 @@ * @endpoint({ | ||
*/ | ||
function parseApiClass(sourceFile, classDeclaration, api) { | ||
for (const member of classDeclaration.members) { | ||
if (ts.isMethodDeclaration(member)) { | ||
endpoint_method_1.parseEndpointMethod(sourceFile, member, api); | ||
} | ||
function parseApiClass(sourceFile, classDeclaration) { | ||
const apiDecorator = decorators_1.extractSingleDecorator(sourceFile, classDeclaration, "api"); | ||
if (!apiDecorator) { | ||
throw panic_1.panic("@api() decorator not found"); | ||
} | ||
if (apiDecorator.arguments.length !== 1) { | ||
throw panic_1.panic(`Expected exactly one argument for @api(), got ${apiDecorator.arguments.length}`); | ||
} | ||
const apiDescription = apiDecorator.arguments[0]; | ||
if (!literal_parser_1.isObjectLiteral(apiDescription)) { | ||
throw panic_1.panic(`@api() expects an object literal, got this instead: ${classDeclaration.getText(sourceFile)}`); | ||
} | ||
return extractApiInfo(sourceFile, classDeclaration, apiDescription); | ||
} | ||
exports.parseApiClass = parseApiClass; | ||
function extractApiInfo(sourceFile, classDeclaration, apiDescription) { | ||
const nameLiteral = apiDescription.properties["name"]; | ||
if (!literal_parser_1.isStringLiteral(nameLiteral)) { | ||
throw panic_1.panic(`Invalid name in api description: ${classDeclaration.getText(sourceFile)}`); | ||
} | ||
const descriptionLiteral = apiDescription.properties["description"]; | ||
if (!literal_parser_1.isStringLiteral(descriptionLiteral)) { | ||
throw panic_1.panic(`Invalid name in api description: ${classDeclaration.getText(sourceFile)}`); | ||
} | ||
return { | ||
name: nameLiteral.text, | ||
description: descriptionLiteral.text | ||
}; | ||
} |
import * as ts from "typescript"; | ||
import { Api } from "../../models"; | ||
import { Endpoint } from "../../models"; | ||
/** | ||
@@ -17,2 +17,2 @@ * Parses a method of an API class definition, such as: | ||
*/ | ||
export declare function parseEndpointMethod(sourceFile: ts.SourceFile, methodDeclaration: ts.MethodDeclaration, api: Api): void; | ||
export declare function parseEndpointMethod(sourceFile: ts.SourceFile, methodDeclaration: ts.MethodDeclaration): Endpoint; |
@@ -16,2 +16,4 @@ "use strict"; | ||
const success_status_code_1 = require("../properties/success-status-code"); | ||
const endpoint_description_1 = require("../properties/endpoint-description"); | ||
const tags_1 = require("../properties/tags"); | ||
/** | ||
@@ -31,13 +33,8 @@ * Parses a method of an API class definition, such as: | ||
*/ | ||
function parseEndpointMethod(sourceFile, methodDeclaration, api) { | ||
function parseEndpointMethod(sourceFile, methodDeclaration) { | ||
// A method must have an @endpoint() decorator to qualify as an endpoint definition. | ||
const endpointDecorator = decorators_1.extractSingleDecorator(sourceFile, methodDeclaration, "endpoint"); | ||
if (!endpointDecorator) { | ||
return; | ||
throw panic_1.panic("Expected to have @endpoint() for the method"); | ||
} | ||
// Each endpoint must be defined only once. | ||
const endpointName = methodDeclaration.name.getText(sourceFile); | ||
if (api.endpoints[endpointName]) { | ||
throw panic_1.panic(`Found multiple definitions of the same endpoint ${endpointName}`); | ||
} | ||
if (endpointDecorator.arguments.length !== 1) { | ||
@@ -51,6 +48,8 @@ throw panic_1.panic(`Expected exactly one argument for @endpoint(), got ${endpointDecorator.arguments.length}`); | ||
} | ||
const endpoint = { | ||
return { | ||
method: method_1.extractMethod(sourceFile, methodDeclaration, endpointDescription), | ||
path: path_1.extractPath(sourceFile, methodDeclaration, endpointDescription), | ||
description: endpoint_description_1.extractEndpointDescription(sourceFile, methodDeclaration, endpointDescription), | ||
requestContentType: request_content_type_1.extractRequestContentType(sourceFile, methodDeclaration, endpointDescription), | ||
tags: tags_1.extractTags(sourceFile, methodDeclaration, endpointDescription), | ||
headers: headers_1.extractHeaders(sourceFile, methodDeclaration), | ||
@@ -64,4 +63,3 @@ queryParams: query_parameters_1.extractQueryParams(sourceFile, methodDeclaration), | ||
}; | ||
api.endpoints[endpointName] = endpoint; | ||
} | ||
exports.parseEndpointMethod = parseEndpointMethod; |
@@ -1,2 +0,2 @@ | ||
import { HttpContentType, HttpMethod } from "./lib"; | ||
import { ApiDescription, HttpContentType, HttpMethod } from "./lib"; | ||
export interface Api { | ||
@@ -9,2 +9,3 @@ endpoints: { | ||
}; | ||
description: ApiDescription; | ||
} | ||
@@ -14,4 +15,6 @@ export interface Endpoint { | ||
path: PathComponent[]; | ||
description: string; | ||
headers: Headers; | ||
queryParams: QueryParamComponent[]; | ||
tags?: string[]; | ||
requestContentType?: HttpContentType; | ||
@@ -18,0 +21,0 @@ requestType: Type; |
@@ -11,2 +11,4 @@ "use strict"; | ||
const type_parser_1 = require("./type-parser"); | ||
const endpoint_method_1 = require("./nodes/endpoint-method"); | ||
const merge = require("lodash/merge"); | ||
/** | ||
@@ -18,7 +20,3 @@ * Parses a TypeScript source file, as well as any other TypeScript files it imports recursively. | ||
async function parsePath(sourcePath) { | ||
const api = { | ||
endpoints: {}, | ||
types: {} | ||
}; | ||
await parseFileRecursively(api, new Set(), sourcePath); | ||
const api = parseRootFile(sourcePath); | ||
const errors = validator_1.validate(api); | ||
@@ -31,5 +29,5 @@ if (errors.length > 0) { | ||
exports.parsePath = parsePath; | ||
async function parseFileRecursively(api, visitedPaths, sourcePath) { | ||
if (!(await fs.existsSync(sourcePath))) { | ||
if (await fs.existsSync(sourcePath + ".ts")) { | ||
function extractSourceFile(sourcePath) { | ||
if (!fs.existsSync(sourcePath)) { | ||
if (fs.existsSync(sourcePath + ".ts")) { | ||
sourcePath += ".ts"; | ||
@@ -41,11 +39,9 @@ } | ||
} | ||
if (visitedPaths.has(sourcePath) || | ||
path.resolve(sourcePath).startsWith(__dirname)) { | ||
return; | ||
} | ||
else { | ||
visitedPaths.add(sourcePath); | ||
} | ||
const fileContent = await fs.readFile(sourcePath, "utf8"); | ||
const fileContent = fs.readFileSync(sourcePath, "utf8"); | ||
const sourceFile = ts.createSourceFile(path.basename(sourcePath), fileContent, ts.ScriptTarget.Latest); | ||
return sourceFile; | ||
} | ||
function getPathsRecursively(sourcePath) { | ||
const importPaths = new Set(); | ||
const sourceFile = extractSourceFile(sourcePath); | ||
for (const statement of sourceFile.statements) { | ||
@@ -61,9 +57,79 @@ if (ts.isImportDeclaration(statement)) { | ||
} | ||
await parseFileRecursively(api, visitedPaths, path.join(sourcePath, "..", importPath)); | ||
if (!(importPaths.has(importPath) || | ||
path.resolve(importPath).startsWith(__dirname))) { | ||
importPaths.add(path.join(sourcePath, "..", importPath)); | ||
} | ||
} | ||
else if (ts.isClassDeclaration(statement)) { | ||
} | ||
return new Set([ | ||
...importPaths, | ||
...[...importPaths].reduce((acc, importPath) => { | ||
return new Set([...acc, ...getPathsRecursively(importPath)]); | ||
}, new Set()) | ||
]); | ||
} | ||
function parseRootFile(sourcePath) { | ||
const importPaths = getPathsRecursively(sourcePath); | ||
const api = { | ||
endpoints: {}, | ||
types: {}, | ||
description: { | ||
name: "", | ||
description: "" | ||
} | ||
}; | ||
if (containsApiDeclaration(sourcePath)) { | ||
const sourceFile = extractSourceFile(sourcePath); | ||
for (const statement of sourceFile.statements) { | ||
if (ts.isClassDeclaration(statement)) { | ||
const apiDescription = api_class_1.parseApiClass(sourceFile, statement); | ||
if (apiDescription) { | ||
api.description = apiDescription; | ||
} | ||
api.endpoints = parseEndpoints(statement, sourceFile); | ||
} | ||
else if (ts.isTypeAliasDeclaration(statement)) { | ||
const name = statement.name.getText(sourceFile); | ||
api.types[name] = type_parser_1.extractType(sourceFile, statement.type); | ||
} | ||
else if (ts.isInterfaceDeclaration(statement)) { | ||
const name = statement.name.getText(sourceFile); | ||
api.types[name] = type_parser_1.extractObjectType(sourceFile, statement); | ||
} | ||
} | ||
return [...importPaths].reduce((acc, path) => { | ||
return merge(acc, parseFile(path)); | ||
}, api); | ||
} | ||
else { | ||
throw panic_1.panic(`No @api declaration found at ${sourcePath}`); | ||
} | ||
} | ||
function parseEndpoints(statement, sourceFile) { | ||
const endpoints = {}; | ||
for (const member of statement.members) { | ||
if (ts.isMethodDeclaration(member)) { | ||
// Each endpoint must be defined only once. | ||
const endpointName = member.name.getText(sourceFile); | ||
if (endpoints[endpointName]) { | ||
throw panic_1.panic(`Found multiple definitions of the same endpoint ${endpointName}`); | ||
} | ||
endpoints[endpointName] = endpoint_method_1.parseEndpointMethod(sourceFile, member); | ||
} | ||
} | ||
return endpoints; | ||
} | ||
function parseFile(sourcePath) { | ||
const api = { | ||
endpoints: {}, | ||
types: {} | ||
}; | ||
const sourceFile = extractSourceFile(sourcePath); | ||
for (const statement of sourceFile.statements) { | ||
if (ts.isClassDeclaration(statement)) { | ||
const apiDecorator = decorators_1.extractSingleDecorator(sourceFile, statement, "api"); | ||
if (apiDecorator) { | ||
api_class_1.parseApiClass(sourceFile, statement, api); | ||
throw `@api cannot be defined more than once at ${sourcePath}`; | ||
} | ||
api.endpoints = parseEndpoints(statement, sourceFile); | ||
} | ||
@@ -79,2 +145,15 @@ else if (ts.isTypeAliasDeclaration(statement)) { | ||
} | ||
return api; | ||
} | ||
function containsApiDeclaration(sourcePath) { | ||
const sourceFile = extractSourceFile(sourcePath); | ||
for (const statement of sourceFile.statements) { | ||
if (ts.isClassDeclaration(statement)) { | ||
const apiDecorator = decorators_1.extractSingleDecorator(sourceFile, statement, "api"); | ||
if (apiDecorator) { | ||
return true; | ||
} | ||
} | ||
} | ||
return false; | ||
} |
import * as ts from "typescript"; | ||
import { Api } from "../../models"; | ||
import { ApiDescription } from "@airtasker/spot"; | ||
/** | ||
* Parses a top-level API class definition and the endpoints it defines, such as: | ||
* ``` | ||
* @api() | ||
* @api({ | ||
* name: "My API", | ||
* description: "A really cool API" | ||
* }) | ||
* class Api { | ||
@@ -18,2 +21,2 @@ * @endpoint({ | ||
*/ | ||
export declare function parseApiClass(sourceFile: ts.SourceFile, classDeclaration: ts.ClassDeclaration, api: Api): void; | ||
export declare function parseApiClass(sourceFile: ts.SourceFile, classDeclaration: ts.ClassDeclaration): ApiDescription; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const ts = require("typescript"); | ||
const endpoint_method_1 = require("./endpoint-method"); | ||
const decorators_1 = require("../decorators"); | ||
const panic_1 = require("../panic"); | ||
const literal_parser_1 = require("../literal-parser"); | ||
/** | ||
* Parses a top-level API class definition and the endpoints it defines, such as: | ||
* ``` | ||
* @api() | ||
* @api({ | ||
* name: "My API", | ||
* description: "A really cool API" | ||
* }) | ||
* class Api { | ||
@@ -20,9 +24,30 @@ * @endpoint({ | ||
*/ | ||
function parseApiClass(sourceFile, classDeclaration, api) { | ||
for (const member of classDeclaration.members) { | ||
if (ts.isMethodDeclaration(member)) { | ||
endpoint_method_1.parseEndpointMethod(sourceFile, member, api); | ||
} | ||
function parseApiClass(sourceFile, classDeclaration) { | ||
const apiDecorator = decorators_1.extractSingleDecorator(sourceFile, classDeclaration, "api"); | ||
if (!apiDecorator) { | ||
throw panic_1.panic("@api() decorator not found"); | ||
} | ||
if (apiDecorator.arguments.length !== 1) { | ||
throw panic_1.panic(`Expected exactly one argument for @api(), got ${apiDecorator.arguments.length}`); | ||
} | ||
const apiDescription = apiDecorator.arguments[0]; | ||
if (!literal_parser_1.isObjectLiteral(apiDescription)) { | ||
throw panic_1.panic(`@api() expects an object literal, got this instead: ${classDeclaration.getText(sourceFile)}`); | ||
} | ||
return extractApiInfo(sourceFile, classDeclaration, apiDescription); | ||
} | ||
exports.parseApiClass = parseApiClass; | ||
function extractApiInfo(sourceFile, classDeclaration, apiDescription) { | ||
const nameLiteral = apiDescription.properties["name"]; | ||
if (!literal_parser_1.isStringLiteral(nameLiteral)) { | ||
throw panic_1.panic(`Invalid name in api description: ${classDeclaration.getText(sourceFile)}`); | ||
} | ||
const descriptionLiteral = apiDescription.properties["description"]; | ||
if (!literal_parser_1.isStringLiteral(descriptionLiteral)) { | ||
throw panic_1.panic(`Invalid name in api description: ${classDeclaration.getText(sourceFile)}`); | ||
} | ||
return { | ||
name: nameLiteral.text, | ||
description: descriptionLiteral.text | ||
}; | ||
} |
import * as ts from "typescript"; | ||
import { Api } from "../../models"; | ||
import { Endpoint } from "../../models"; | ||
/** | ||
@@ -17,2 +17,2 @@ * Parses a method of an API class definition, such as: | ||
*/ | ||
export declare function parseEndpointMethod(sourceFile: ts.SourceFile, methodDeclaration: ts.MethodDeclaration, api: Api): void; | ||
export declare function parseEndpointMethod(sourceFile: ts.SourceFile, methodDeclaration: ts.MethodDeclaration): Endpoint; |
@@ -16,2 +16,4 @@ "use strict"; | ||
const success_status_code_1 = require("../properties/success-status-code"); | ||
const endpoint_description_1 = require("../properties/endpoint-description"); | ||
const tags_1 = require("../properties/tags"); | ||
/** | ||
@@ -31,13 +33,8 @@ * Parses a method of an API class definition, such as: | ||
*/ | ||
function parseEndpointMethod(sourceFile, methodDeclaration, api) { | ||
function parseEndpointMethod(sourceFile, methodDeclaration) { | ||
// A method must have an @endpoint() decorator to qualify as an endpoint definition. | ||
const endpointDecorator = decorators_1.extractSingleDecorator(sourceFile, methodDeclaration, "endpoint"); | ||
if (!endpointDecorator) { | ||
return; | ||
throw panic_1.panic("Expected to have @endpoint() for the method"); | ||
} | ||
// Each endpoint must be defined only once. | ||
const endpointName = methodDeclaration.name.getText(sourceFile); | ||
if (api.endpoints[endpointName]) { | ||
throw panic_1.panic(`Found multiple definitions of the same endpoint ${endpointName}`); | ||
} | ||
if (endpointDecorator.arguments.length !== 1) { | ||
@@ -51,6 +48,8 @@ throw panic_1.panic(`Expected exactly one argument for @endpoint(), got ${endpointDecorator.arguments.length}`); | ||
} | ||
const endpoint = { | ||
return { | ||
method: method_1.extractMethod(sourceFile, methodDeclaration, endpointDescription), | ||
path: path_1.extractPath(sourceFile, methodDeclaration, endpointDescription), | ||
description: endpoint_description_1.extractEndpointDescription(sourceFile, methodDeclaration, endpointDescription), | ||
requestContentType: request_content_type_1.extractRequestContentType(sourceFile, methodDeclaration, endpointDescription), | ||
tags: tags_1.extractTags(sourceFile, methodDeclaration, endpointDescription), | ||
headers: headers_1.extractHeaders(sourceFile, methodDeclaration), | ||
@@ -64,4 +63,3 @@ queryParams: query_parameters_1.extractQueryParams(sourceFile, methodDeclaration), | ||
}; | ||
api.endpoints[endpointName] = endpoint; | ||
} | ||
exports.parseEndpointMethod = parseEndpointMethod; |
@@ -1,1 +0,1 @@ | ||
{"version":"0.1.27","commands":{"generate":{"id":"generate","description":"Runs a generator on an API. Used to produce client libraries, server boilerplates and well-known API contract formats such as OpenAPI.","pluginName":"@airtasker/spot","pluginType":"core","aliases":[],"examples":["$ api generate --language typescript --generator axios-client --out src/\nGenerated the following files:\n- src/types.ts\n- src/validators.ts\n- src/client.ts\n"],"flags":{"help":{"name":"help","type":"boolean","char":"h","description":"show CLI help","allowNo":false},"api":{"name":"api","type":"option","char":"a","description":"Path to a TypeScript API definition","required":true},"language":{"name":"language","type":"option","char":"l","description":"Language to generate"},"generator":{"name":"generator","type":"option","char":"g","description":"Generator to run"},"out":{"name":"out","type":"option","char":"o","description":"Directory in which to output generated files"}},"args":[]},"init":{"id":"init","description":"Generates the boilerplate for an API.","pluginName":"@airtasker/spot","pluginType":"core","aliases":[],"examples":["$ api init\nGenerated the following files:\n- api.ts\n- tsconfig.json\n- package.json\n"],"flags":{"help":{"name":"help","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]}}} | ||
{"version":"0.1.28","commands":{"generate":{"id":"generate","description":"Runs a generator on an API. Used to produce client libraries, server boilerplates and well-known API contract formats such as OpenAPI.","pluginName":"@airtasker/spot","pluginType":"core","aliases":[],"examples":["$ api generate --language typescript --generator axios-client --out src/\nGenerated the following files:\n- src/types.ts\n- src/validators.ts\n- src/client.ts\n"],"flags":{"help":{"name":"help","type":"boolean","char":"h","description":"show CLI help","allowNo":false},"api":{"name":"api","type":"option","char":"a","description":"Path to a TypeScript API definition","required":true},"language":{"name":"language","type":"option","char":"l","description":"Language to generate"},"generator":{"name":"generator","type":"option","char":"g","description":"Generator to run"},"out":{"name":"out","type":"option","char":"o","description":"Directory in which to output generated files"}},"args":[]},"init":{"id":"init","description":"Generates the boilerplate for an API.","pluginName":"@airtasker/spot","pluginType":"core","aliases":[],"examples":["$ api init\nGenerated the following files:\n- api.ts\n- tsconfig.json\n- package.json\n"],"flags":{"help":{"name":"help","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]}}} |
{ | ||
"name": "@airtasker/spot", | ||
"version": "0.1.27", | ||
"version": "0.1.28", | ||
"author": "Francois Wouts", | ||
@@ -62,4 +62,6 @@ "bin": { | ||
"test": "jest", | ||
"prettier:list": "prettier --list-different \"**/*.js\" \"**/*.jsx\" \"**/*.ts\" \"**/*.tsx\"", | ||
"prettier:write": "prettier --write \"**/*.js\" \"**/*.jsx\" \"**/*.ts\" \"**/*.tsx\"", | ||
"release": "yarn version && oclif-dev readme && git add README.md && git commit README.md -m \"Update README\" && git push && npm publish --access=public" | ||
} | ||
} |
@@ -104,3 +104,3 @@ Spot | ||
_See code: [build/cli/src/commands/generate.js](https://github.com/airtasker/spot/blob/v0.1.27/build/cli/src/commands/generate.js)_ | ||
_See code: [build/cli/src/commands/generate.js](https://github.com/airtasker/spot/blob/v0.1.28/build/cli/src/commands/generate.js)_ | ||
@@ -143,3 +143,3 @@ ## `spot help [COMMAND]` | ||
_See code: [build/cli/src/commands/init.js](https://github.com/airtasker/spot/blob/v0.1.27/build/cli/src/commands/init.js)_ | ||
_See code: [build/cli/src/commands/init.js](https://github.com/airtasker/spot/blob/v0.1.28/build/cli/src/commands/init.js)_ | ||
<!-- commandsstop --> |
323036
160
8260