2017-01-19 00:48:55 +03:00
|
|
|
import { Injectable } from '@angular/core';
|
|
|
|
import { AppStateService } from './app-state.service';
|
2017-01-26 17:46:28 +03:00
|
|
|
import { SchemaNormalizer } from './schema-normalizer.service';
|
2017-01-30 19:59:57 +03:00
|
|
|
import { JsonPointer, groupBy, SpecManager, StringMap, snapshot, MarkdownHeading } from '../utils/';
|
|
|
|
import { methods as swaggerMethods } from '../utils/swagger-defs';
|
2017-01-28 19:47:12 +03:00
|
|
|
import * as slugify from 'slugify';
|
|
|
|
|
|
|
|
import {
|
2017-01-30 17:11:14 +03:00
|
|
|
SwaggerSpec,
|
|
|
|
SwaggerOperation,
|
|
|
|
SwaggerSchema,
|
|
|
|
SwaggerBodyParameter,
|
|
|
|
SwaggerResponse
|
|
|
|
} from '../utils/swagger-typings';
|
2017-01-19 00:48:55 +03:00
|
|
|
|
2017-01-24 00:29:52 +03:00
|
|
|
import * as lunr from 'lunr';
|
|
|
|
|
|
|
|
interface IndexElement {
|
2017-01-26 17:46:28 +03:00
|
|
|
menuId: string;
|
2017-01-24 00:29:52 +03:00
|
|
|
title: string;
|
|
|
|
body: string;
|
|
|
|
pointer: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
const index = lunr(function () {
|
|
|
|
this.field('title', {boost: 1.5});
|
|
|
|
this.field('body');
|
|
|
|
this.ref('pointer');
|
2017-01-28 16:57:22 +03:00
|
|
|
});
|
2017-01-24 00:29:52 +03:00
|
|
|
|
|
|
|
const store:StringMap<IndexElement> = {};
|
2017-01-19 00:48:55 +03:00
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
export class SearchService {
|
2017-01-26 17:46:28 +03:00
|
|
|
normalizer: SchemaNormalizer;
|
2017-01-24 00:29:52 +03:00
|
|
|
constructor(private app: AppStateService, private spec: SpecManager) {
|
2017-01-26 17:46:28 +03:00
|
|
|
this.normalizer = new SchemaNormalizer(spec);
|
2017-01-19 00:48:55 +03:00
|
|
|
}
|
2017-01-24 00:29:52 +03:00
|
|
|
|
2017-01-28 19:47:12 +03:00
|
|
|
ensureSearchVisible(containingPointers: string|null[]) {
|
2017-01-19 00:48:55 +03:00
|
|
|
this.app.searchContainingPointers.next(containingPointers);
|
|
|
|
}
|
2017-01-24 00:29:52 +03:00
|
|
|
|
|
|
|
indexAll() {
|
2017-01-30 17:11:14 +03:00
|
|
|
console.time('Indexing');
|
2017-01-28 14:03:51 +03:00
|
|
|
this.indexPaths(this.spec.schema);
|
2017-01-28 19:47:12 +03:00
|
|
|
this.indexTags(this.spec.schema);
|
2017-01-30 19:59:57 +03:00
|
|
|
this.indexDescriptionHeadings(this.spec.schema.info['x-redoc-markdown-headers']);
|
2017-01-30 17:11:14 +03:00
|
|
|
console.time('Indexing end');
|
2017-01-24 00:29:52 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
search(q):StringMap<IndexElement[]> {
|
2017-01-28 14:03:51 +03:00
|
|
|
var items = {};
|
2017-01-24 00:29:52 +03:00
|
|
|
const res:IndexElement[] = index.search(q).map(res => {
|
2017-01-28 14:03:51 +03:00
|
|
|
items[res.menuId] = res;
|
2017-01-24 00:29:52 +03:00
|
|
|
return store[res.ref];
|
|
|
|
});
|
2017-01-26 17:46:28 +03:00
|
|
|
const grouped = groupBy(res, 'menuId');
|
2017-01-24 00:29:52 +03:00
|
|
|
return grouped;
|
|
|
|
}
|
|
|
|
|
|
|
|
index(element: IndexElement) {
|
2017-01-26 19:21:24 +03:00
|
|
|
// don't reindex same pointers (for discriminator)
|
|
|
|
if (store[element.pointer]) return;
|
2017-01-24 00:29:52 +03:00
|
|
|
index.add(element);
|
|
|
|
store[element.pointer] = element;
|
|
|
|
}
|
|
|
|
|
2017-01-30 19:59:57 +03:00
|
|
|
indexDescriptionHeadings(headings:StringMap<MarkdownHeading>) {
|
|
|
|
if (!headings) return;
|
|
|
|
Object.keys(headings).forEach(k => {
|
|
|
|
let heading = headings[k];
|
|
|
|
this.index({
|
|
|
|
menuId: heading.id,
|
|
|
|
title: heading.title,
|
|
|
|
body: heading.content,
|
|
|
|
pointer: '/heading/' + heading.id
|
|
|
|
});
|
|
|
|
|
|
|
|
this.indexDescriptionHeadings(heading.children);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-01-28 19:47:12 +03:00
|
|
|
indexTags(swagger:SwaggerSpec) {
|
|
|
|
let tags = swagger.tags;
|
2017-02-03 00:35:06 +03:00
|
|
|
if (!tags) return;
|
2017-01-28 19:47:12 +03:00
|
|
|
for (let tag of tags) {
|
|
|
|
if (tag['x-traitTag']) continue;
|
|
|
|
let id = `tag/${slugify(tag.name)}`;
|
|
|
|
this.index({
|
|
|
|
menuId: id,
|
|
|
|
title: tag.name,
|
|
|
|
body: tag.description,
|
|
|
|
pointer: id
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
indexPaths(swagger:SwaggerSpec) {
|
2017-01-28 16:57:22 +03:00
|
|
|
const paths = swagger.paths;
|
2017-01-24 00:29:52 +03:00
|
|
|
const basePtr = '#/paths';
|
|
|
|
Object.keys(paths).forEach(path => {
|
|
|
|
let opearations = paths[path];
|
|
|
|
Object.keys(opearations).forEach(verb => {
|
2017-01-30 19:59:57 +03:00
|
|
|
if (!swaggerMethods.has(verb)) return;
|
2017-01-24 00:29:52 +03:00
|
|
|
const opearation = opearations[verb];
|
|
|
|
const ptr = JsonPointer.join(basePtr, [path, verb]);
|
2017-01-30 19:59:57 +03:00
|
|
|
|
2017-01-24 00:29:52 +03:00
|
|
|
this.indexOperation(opearation, ptr);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-01-28 19:47:12 +03:00
|
|
|
indexOperation(operation:SwaggerOperation, operationPointer:string) {
|
2017-01-24 00:29:52 +03:00
|
|
|
this.index({
|
2017-01-26 17:46:28 +03:00
|
|
|
pointer: operationPointer,
|
|
|
|
menuId: operationPointer,
|
2017-01-24 00:29:52 +03:00
|
|
|
title: operation.summary,
|
|
|
|
body: operation.description
|
|
|
|
});
|
2017-01-26 17:46:28 +03:00
|
|
|
this.indexOperationResponses(operation, operationPointer);
|
2017-01-28 16:57:22 +03:00
|
|
|
this.indexOperationParameters(operation, operationPointer);
|
2017-01-26 17:46:28 +03:00
|
|
|
}
|
|
|
|
|
2017-01-28 19:47:12 +03:00
|
|
|
indexOperationParameters(operation: SwaggerOperation, operationPointer: string) {
|
2017-01-30 17:11:14 +03:00
|
|
|
const parameters = this.spec.getMethodParams(operationPointer);
|
2017-01-28 14:03:51 +03:00
|
|
|
if (!parameters) return;
|
2017-01-26 17:46:28 +03:00
|
|
|
for (let i=0; i<parameters.length; ++i) {
|
|
|
|
const param = parameters[i];
|
|
|
|
const paramPointer = JsonPointer.join(operationPointer, ['parameters', i]);
|
|
|
|
this.index({
|
|
|
|
pointer: paramPointer,
|
|
|
|
menuId: operationPointer,
|
|
|
|
title: param.in === 'body' ? '' : param.name,
|
|
|
|
body: param.description
|
|
|
|
});
|
|
|
|
|
|
|
|
if (param.in === 'body') {
|
2017-01-28 14:03:51 +03:00
|
|
|
this.normalizer.reset();
|
2017-01-30 17:11:14 +03:00
|
|
|
this.indexSchema((<SwaggerBodyParameter>param).schema,
|
2017-01-28 19:47:12 +03:00
|
|
|
'', JsonPointer.join(paramPointer, ['schema']), operationPointer);
|
2017-01-26 17:46:28 +03:00
|
|
|
}
|
|
|
|
}
|
2017-01-24 00:29:52 +03:00
|
|
|
}
|
|
|
|
|
2017-01-28 19:47:12 +03:00
|
|
|
indexOperationResponses(operation:SwaggerOperation, operationPtr:string) {
|
2017-01-24 00:29:52 +03:00
|
|
|
const responses = operation.responses;
|
|
|
|
if (!responses) return;
|
|
|
|
Object.keys(responses).forEach(code => {
|
|
|
|
const resp = responses[code];
|
|
|
|
const respPtr = JsonPointer.join(operationPtr, ['responses', code]);
|
|
|
|
this.index({
|
|
|
|
pointer: respPtr,
|
2017-01-26 17:46:28 +03:00
|
|
|
menuId: operationPtr,
|
2017-01-24 00:29:52 +03:00
|
|
|
title: code,
|
|
|
|
body: resp.description
|
|
|
|
});
|
|
|
|
|
|
|
|
if (resp.schema) {
|
2017-01-28 14:03:51 +03:00
|
|
|
this.normalizer.reset();
|
2017-01-24 00:29:52 +03:00
|
|
|
this.indexSchema(resp.schema, '', JsonPointer.join(respPtr, 'schema'), operationPtr);
|
|
|
|
}
|
2017-01-30 17:11:14 +03:00
|
|
|
if (resp.headers) {
|
|
|
|
this.indexOperationResponseHeaders(resp, respPtr, operationPtr);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
indexOperationResponseHeaders(response: SwaggerResponse, responsePtr: string, operationPtr: string, ) {
|
|
|
|
let headers = response.headers || [];
|
|
|
|
Object.keys(headers).forEach(headerName => {
|
|
|
|
let header = headers[headerName];
|
|
|
|
this.index({
|
|
|
|
pointer: `${responsePtr}/${headerName}`,
|
|
|
|
menuId: operationPtr,
|
|
|
|
title: headerName,
|
|
|
|
body: header.description
|
|
|
|
});
|
2017-01-24 00:29:52 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-01-30 17:11:14 +03:00
|
|
|
indexSchema(_schema:SwaggerSchema, name: string, absolutePointer: string,
|
2017-01-28 19:47:12 +03:00
|
|
|
menuPointer: string, parent?: string) {
|
2017-01-24 00:29:52 +03:00
|
|
|
if (!_schema) return;
|
|
|
|
let schema = _schema;
|
|
|
|
let title = name;
|
2017-01-28 14:03:51 +03:00
|
|
|
schema = this.normalizer.normalize(schema, schema._pointer || absolutePointer, { childFor: parent });
|
2017-01-24 00:29:52 +03:00
|
|
|
|
|
|
|
let body = schema.description; // TODO: defaults, examples, etc...
|
|
|
|
|
|
|
|
if (schema.type === 'array') {
|
|
|
|
this.indexSchema(schema.items, title, JsonPointer.join(absolutePointer, ['items']), menuPointer);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-01-28 14:03:51 +03:00
|
|
|
if (schema.discriminator) {
|
2017-01-26 19:21:24 +03:00
|
|
|
let derived = this.spec.findDerivedDefinitions(schema._pointer, schema);
|
|
|
|
for (let defInfo of derived ) {
|
|
|
|
let subSpec = this.spec.getDescendant(defInfo, schema);
|
2017-01-28 14:03:51 +03:00
|
|
|
this.indexSchema(snapshot(subSpec), '', absolutePointer, menuPointer, schema._pointer);
|
2017-01-26 19:21:24 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-02 23:03:47 +03:00
|
|
|
if (schema.type === 'string' && schema.enum) {
|
|
|
|
body += ' ' + schema.enum.join(' ');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!parent) {
|
|
|
|
// redoc doesn't display top level descriptions and titles
|
|
|
|
this.index({
|
|
|
|
pointer: absolutePointer,
|
|
|
|
menuId: menuPointer,
|
|
|
|
title,
|
|
|
|
body
|
|
|
|
});
|
|
|
|
}
|
2017-01-24 00:29:52 +03:00
|
|
|
|
|
|
|
if (schema.properties) {
|
|
|
|
Object.keys(schema.properties).forEach(propName => {
|
|
|
|
let propPtr = JsonPointer.join(absolutePointer, ['properties', propName]);
|
|
|
|
this.indexSchema(schema.properties[propName], propName, propPtr, menuPointer);
|
2017-01-28 16:57:22 +03:00
|
|
|
});
|
2017-01-24 00:29:52 +03:00
|
|
|
}
|
|
|
|
}
|
2017-01-19 00:48:55 +03:00
|
|
|
}
|