import { Injectable } from '@angular/core'; import { AppStateService } from './app-state.service'; import { SchemaNormalizer } from './schema-normalizer.service'; import { JsonPointer, groupBy, SpecManager, StringMap, snapshot, MarkdownHeading } from '../utils/'; import { methods as swaggerMethods } from '../utils/swagger-defs'; import * as slugify from 'slugify'; import { SwaggerSpec, SwaggerOperation, SwaggerSchema, SwaggerBodyParameter, SwaggerResponse } from '../utils/swagger-typings'; import * as lunr from 'lunr'; interface IndexElement { menuId: string; title: string; body: string; pointer: string; } const index = lunr(function () { this.field('title', {boost: 1.5}); this.field('body'); this.ref('pointer'); }); const store:StringMap = {}; @Injectable() export class SearchService { normalizer: SchemaNormalizer; constructor(private app: AppStateService, private spec: SpecManager) { this.normalizer = new SchemaNormalizer(spec); } ensureSearchVisible(containingPointers: string|null[]) { this.app.searchContainingPointers.next(containingPointers); } indexAll() { console.time('Indexing'); this.indexPaths(this.spec.schema); this.indexTags(this.spec.schema); this.indexDescriptionHeadings(this.spec.schema.info['x-redoc-markdown-headers']); console.time('Indexing end'); } search(q):StringMap { var items = {}; const res:IndexElement[] = index.search(q).map(res => { items[res.menuId] = res; return store[res.ref]; }); const grouped = groupBy(res, 'menuId'); return grouped; } index(element: IndexElement) { // don't reindex same pointers (for discriminator) if (store[element.pointer]) return; index.add(element); store[element.pointer] = element; } indexDescriptionHeadings(headings:StringMap) { 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); }); } indexTags(swagger:SwaggerSpec) { let tags = swagger.tags; 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) { const paths = swagger.paths; const basePtr = '#/paths'; Object.keys(paths).forEach(path => { let opearations = paths[path]; Object.keys(opearations).forEach(verb => { if (!swaggerMethods.has(verb)) return; const opearation = opearations[verb]; const ptr = JsonPointer.join(basePtr, [path, verb]); this.indexOperation(opearation, ptr); }); }); } indexOperation(operation:SwaggerOperation, operationPointer:string) { this.index({ pointer: operationPointer, menuId: operationPointer, title: operation.summary, body: operation.description }); this.indexOperationResponses(operation, operationPointer); this.indexOperationParameters(operation, operationPointer); } indexOperationParameters(operation: SwaggerOperation, operationPointer: string) { const parameters = this.spec.getMethodParams(operationPointer); if (!parameters) return; for (let i=0; iparam).schema, '', JsonPointer.join(paramPointer, ['schema']), operationPointer); } } } indexOperationResponses(operation:SwaggerOperation, operationPtr:string) { 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, menuId: operationPtr, title: code, body: resp.description }); if (resp.schema) { this.normalizer.reset(); this.indexSchema(resp.schema, '', JsonPointer.join(respPtr, 'schema'), operationPtr); } 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 }); }); } indexSchema(_schema:SwaggerSchema, name: string, absolutePointer: string, menuPointer: string, parent?: string) { if (!_schema) return; let schema = _schema; let title = name; schema = this.normalizer.normalize(schema, schema._pointer || absolutePointer, { childFor: parent }); let body = schema.description; // TODO: defaults, examples, etc... if (schema.type === 'array') { this.indexSchema(schema.items, title, JsonPointer.join(absolutePointer, ['items']), menuPointer); return; } if (schema.discriminator) { let derived = this.spec.findDerivedDefinitions(schema._pointer, schema); for (let defInfo of derived ) { let subSpec = this.spec.getDescendant(defInfo, schema); this.indexSchema(snapshot(subSpec), '', absolutePointer, menuPointer, schema._pointer); } } 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 }); } if (schema.properties) { Object.keys(schema.properties).forEach(propName => { let propPtr = JsonPointer.join(absolutePointer, ['properties', propName]); this.indexSchema(schema.properties[propName], propName, propPtr, menuPointer); }); } } }