From 2d8695051c1db6f82c693211195ea6b5088eb5c6 Mon Sep 17 00:00:00 2001 From: Roman Hotsiy Date: Mon, 23 Jan 2017 23:29:52 +0200 Subject: [PATCH] Basic Indexing + search --- lib/components/JsonSchema/json-schema.ts | 2 +- lib/components/Search/redoc-search.html | 8 +- lib/components/Search/redoc-search.scss | 19 ++++ lib/components/Search/redoc-search.ts | 27 ++++-- lib/services/menu.service.ts | 4 + lib/services/search.service.ts | 115 ++++++++++++++++++++++- lib/utils/helpers.ts | 16 ++++ lib/utils/index.ts | 1 + package.json | 1 + 9 files changed, 178 insertions(+), 15 deletions(-) diff --git a/lib/components/JsonSchema/json-schema.ts b/lib/components/JsonSchema/json-schema.ts index 9e2058a4..d7d41493 100644 --- a/lib/components/JsonSchema/json-schema.ts +++ b/lib/components/JsonSchema/json-schema.ts @@ -159,7 +159,7 @@ export class JsonSchema extends BaseSearchableComponent implements OnInit { propName = relative[1]; } let prop = props.find(p => p._name === propName); - if (!prop) return; + if (!prop || prop.isTrivial) return; prop.expanded = true; this.cdr.markForCheck(); this.cdr.detectChanges(); diff --git a/lib/components/Search/redoc-search.html b/lib/components/Search/redoc-search.html index 598afffc..43ee86d9 100644 --- a/lib/components/Search/redoc-search.html +++ b/lib/components/Search/redoc-search.html @@ -1,2 +1,6 @@ - - + + diff --git a/lib/components/Search/redoc-search.scss b/lib/components/Search/redoc-search.scss index 5d4e87f3..0d2c890a 100644 --- a/lib/components/Search/redoc-search.scss +++ b/lib/components/Search/redoc-search.scss @@ -1,3 +1,22 @@ :host { display: block; + padding: 20px; + background: silver; +} + +input { + width: 100%; + box-sizing: border-box; + padding: 5px; +} + +.search-results { + margin: 10px 0 0; + padding: 0; + list-style: none; + + > li { + display: block; + cursor: pointer; + } } diff --git a/lib/components/Search/redoc-search.ts b/lib/components/Search/redoc-search.ts index 00db55f8..6f38f4e7 100644 --- a/lib/components/Search/redoc-search.ts +++ b/lib/components/Search/redoc-search.ts @@ -1,6 +1,6 @@ 'use strict'; import { Component, ChangeDetectionStrategy, OnInit, HostBinding } from '@angular/core'; -import { Marker, SearchService } from '../../services/'; +import { Marker, SearchService, MenuService } from '../../services/'; @Component({ selector: 'redoc-search', @@ -10,27 +10,34 @@ import { Marker, SearchService } from '../../services/'; }) export class RedocSearch implements OnInit { logo:any = {}; + items: any[] = []; - constructor(private marker: Marker, public search: SearchService) { + constructor(private marker: Marker, public search: SearchService, public menu: MenuService) { } init() { - + this.search.indexAll(); } update(val) { + let searchRes = this.search.search(val); + this.items = Object.keys(searchRes).map(id => ({ + menuItem: this.menu.getItemById(id), + pointers: searchRes[id].map(el => el.pointer) + })); this.marker.mark(val); } - tmpSearch() { - this.search.ensureSearchVisible([ - '/paths/~1pet~1findByStatus/get/responses/200/schema/items/properties/category/properties/sub', - '/paths/~1pet~1findByStatus/get/responses/200/schema/items/properties/tags', - '/paths/~1pet/post/parameters/0/schema/properties/tags' - ]); + clickSearch(item) { + this.search.ensureSearchVisible( + item.pointers + ); + this.marker.remark(); + this.menu.activate(item.menuItem.flatIdx); + this.menu.scrollToActive(); } ngOnInit() { - + this.init(); } } diff --git a/lib/services/menu.service.ts b/lib/services/menu.service.ts index ab959a6f..5d6ad8b0 100644 --- a/lib/services/menu.service.ts +++ b/lib/services/menu.service.ts @@ -417,6 +417,10 @@ export class MenuService { return res; } + getItemById(id: string):MenuItem { + return this.flatItems.find(item => item.id === id); + } + destroy() { this._hashSubscription.unsubscribe(); this._scrollSubscription.unsubscribe(); diff --git a/lib/services/search.service.ts b/lib/services/search.service.ts index 15813a89..f16f72c3 100644 --- a/lib/services/search.service.ts +++ b/lib/services/search.service.ts @@ -1,13 +1,124 @@ import { Injectable } from '@angular/core'; import { AppStateService } from './app-state.service'; +import { JsonPointer, groupBy, SpecManager, StringMap} from '../utils/'; +import * as lunr from 'lunr'; + +interface IndexElement { + menuPtr: string; + title: string; + body: string; + pointer: string; +} + +const index = lunr(function () { + this.field('menuPtr', {boost: 0}); + this.field('title', {boost: 1.5}); + this.field('body'); + this.ref('pointer'); +}) + +const store:StringMap = {}; @Injectable() export class SearchService { - constructor(private app: AppStateService) { - window['locator'] = this; + constructor(private app: AppStateService, private spec: SpecManager) { } + ensureSearchVisible(containingPointers: string[]) { this.app.searchContainingPointers.next(containingPointers); } + + indexAll() { + const swagger = this.spec.schema; + + this.indexPaths(swagger); + } + + search(q):StringMap { + const res:IndexElement[] = index.search(q).map(res => { + return store[res.ref]; + }); + const grouped = groupBy(res, 'menuPtr'); + return grouped; + } + + index(element: IndexElement) { + index.add(element); + store[element.pointer] = element; + } + + indexPaths(swagger:any) { + const paths = swagger.paths + const basePtr = '#/paths'; + Object.keys(paths).forEach(path => { + let opearations = paths[path]; + Object.keys(opearations).forEach(verb => { + const opearation = opearations[verb]; + const ptr = JsonPointer.join(basePtr, [path, verb]); + this.indexOperation(opearation, ptr); + }); + }); + } + + indexOperation(operation:any, opPtr:string) { + this.index({ + pointer: opPtr, + menuPtr: opPtr, + title: operation.summary, + body: operation.description + }); + this.indexOperationResponses(operation, opPtr); + } + + indexOperationResponses(operation:any, 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, + menuPtr: operationPtr, + title: code, + body: resp.description + }); + + if (resp.schema) { + this.indexSchema(resp.schema, '', JsonPointer.join(respPtr, 'schema'), operationPtr); + } + }); + } + + indexSchema(_schema:any, name: string, absolutePointer: string, menuPointer: string) { + if (!_schema) return; + let schema = _schema; + let title = name; + if (schema.$ref) { + schema = this.spec.byPointer(_schema.$ref); + title = name + ' ' + JsonPointer.baseName(_schema.$ref); + } + + let body = schema.description; // TODO: defaults, examples, etc... + + if (schema.type === 'array') { + this.indexSchema(schema.items, title, JsonPointer.join(absolutePointer, ['items']), menuPointer); + return; + } + + this.index({ + pointer: absolutePointer, + menuPtr: menuPointer, + title, + body + }) + + // TODO: allof etc + if (schema.properties) { + Object.keys(schema.properties).forEach(propName => { + let propPtr = JsonPointer.join(absolutePointer, ['properties', propName]); + this.indexSchema(schema.properties[propName], propName, propPtr, menuPointer); + }) + } + } } diff --git a/lib/utils/helpers.ts b/lib/utils/helpers.ts index 4366388e..00cf4fd7 100644 --- a/lib/utils/helpers.ts +++ b/lib/utils/helpers.ts @@ -1,5 +1,9 @@ 'use strict'; +export interface StringMap { + [key: string]: T; +} + export function stringify(obj:any) { return JSON.stringify(obj); } @@ -16,6 +20,18 @@ export function isBlank(obj: any): boolean { return obj == undefined; } +const hasOwnProperty = Object.prototype.hasOwnProperty; +export function groupBy(array: T[], key:string):StringMap { + return array.reduce>(function(res, value) { + if (hasOwnProperty.call(res, value[key])) { + res[value[key]].push(value); + } else { + res[value[key]] = [value]; + } + return res; + }, {}); +} + export function statusCodeType(statusCode) { if (statusCode < 100 || statusCode > 599) { throw new Error('invalid HTTP code'); diff --git a/lib/utils/index.ts b/lib/utils/index.ts index 79d57372..06c03491 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -1,5 +1,6 @@ export * from './custom-error-handler'; export * from './helpers'; export * from './md-renderer'; +export * from './spec-manager'; export { default as JsonPointer } from './JsonPointer'; diff --git a/package.json b/package.json index dd0c9b3d..477b0e76 100644 --- a/package.json +++ b/package.json @@ -111,6 +111,7 @@ "hint.css": "^2.3.2", "json-pointer": "^0.6.0", "json-schema-ref-parser": "^3.1.2", + "lunr": "^0.7.2", "mark.js": "github:julmot/mark.js", "openapi-sampler": "^0.3.3", "prismjs": "^1.5.1",