chore: refactor, simplify AppStore

This commit is contained in:
Roman Hotsiy 2018-07-26 17:34:44 +03:00
parent 757a92e425
commit 9361ead8c4
No known key found for this signature in database
GPG Key ID: 5CB7B3ACABA57CB0
13 changed files with 193 additions and 163 deletions

View File

@ -47,7 +47,7 @@ export class OneOfSchema extends React.Component<SchemaProps> {
<OneOfLabel> {schema.oneOfType} </OneOfLabel> <OneOfLabel> {schema.oneOfType} </OneOfLabel>
<OneOfList> <OneOfList>
{oneOf.map((subSchema, idx) => ( {oneOf.map((subSchema, idx) => (
<OneOfButton key={subSchema._$ref} schema={schema} subSchema={subSchema} idx={idx} /> <OneOfButton key={subSchema.pointer} schema={schema} subSchema={subSchema} idx={idx} />
))} ))}
</OneOfList> </OneOfList>
<Schema {...this.props} schema={oneOf[schema.activeOneOf]} /> <Schema {...this.props} schema={oneOf[schema.activeOneOf]} />

View File

@ -61,15 +61,11 @@ export class OAuthFlow extends React.PureComponent<OAuthFlowProps> {
} }
export interface SecurityDefsProps { export interface SecurityDefsProps {
securitySchemes?: SecuritySchemesModel; securitySchemes: SecuritySchemesModel;
} }
export class SecurityDefs extends React.PureComponent<SecurityDefsProps> { export class SecurityDefs extends React.PureComponent<SecurityDefsProps> {
render() { render() {
if (!this.props.securitySchemes) {
return null;
}
return ( return (
<div> <div>
{this.props.securitySchemes.schemes.map(scheme => ( {this.props.securitySchemes.schemes.map(scheme => (

View File

@ -15,7 +15,6 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"name": "packSize", "name": "packSize",
"required": false, "required": false,
"schema": SchemaModel { "schema": SchemaModel {
"_$ref": "#/components/schemas/Dog/properties/packSize/schema",
"activeOneOf": 0, "activeOneOf": 0,
"constraints": Array [], "constraints": Array [],
"default": undefined, "default": undefined,
@ -31,6 +30,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"nullable": false, "nullable": false,
"options": "<<<filtered>>>", "options": "<<<filtered>>>",
"pattern": undefined, "pattern": undefined,
"pointer": "#/components/schemas/Dog/properties/packSize",
"rawSchema": Object { "rawSchema": Object {
"default": undefined, "default": undefined,
"type": "number", "type": "number",
@ -63,7 +63,6 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"name": "type", "name": "type",
"required": true, "required": true,
"schema": SchemaModel { "schema": SchemaModel {
"_$ref": "#/components/schemas/Dog/properties/type/schema",
"activeOneOf": 0, "activeOneOf": 0,
"constraints": Array [], "constraints": Array [],
"default": undefined, "default": undefined,
@ -79,6 +78,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"nullable": false, "nullable": false,
"options": "<<<filtered>>>", "options": "<<<filtered>>>",
"pattern": undefined, "pattern": undefined,
"pointer": "#/components/schemas/Dog/properties/type",
"rawSchema": Object { "rawSchema": Object {
"default": undefined, "default": undefined,
"type": "string", "type": "string",

View File

@ -90,7 +90,32 @@ export class AppStore {
this.updateMarkOnMenu(this.menu.activeItemIdx); this.updateMarkOnMenu(this.menu.activeItemIdx);
} }
updateMarkOnMenu(idx: number) { dispose() {
this.scroll.dispose();
this.menu.dispose();
this.disposer();
}
/**
* serializes store
* **SUPER HACKY AND NOT OPTIMAL IMPLEMENTATION**
*/
// TODO: improve
async toJS(): Promise<StoreState> {
return {
menu: {
activeItemIdx: this.menu.activeItemIdx,
},
spec: {
url: this.spec.parser.specUrl,
data: this.spec.parser.spec,
},
searchIndex: this.search ? await this.search.toJS() : undefined,
options: this.rawOptions,
};
}
private updateMarkOnMenu(idx: number) {
const start = Math.max(0, idx); const start = Math.max(0, idx);
const end = Math.min(this.menu.flatItems.length, start + 5); const end = Math.min(this.menu.flatItems.length, start + 5);
@ -111,29 +136,4 @@ export class AppStore {
this.marker.addOnly(elements); this.marker.addOnly(elements);
this.marker.mark(); this.marker.mark();
} }
dispose() {
this.scroll.dispose();
this.menu.dispose();
this.disposer();
}
/**
* serializes store
* **SUPER HACKY AND NOT OPTIMAL IMPLEMENTATION**
*/
// TODO:
async toJS(): Promise<StoreState> {
return {
menu: {
activeItemIdx: this.menu.activeItemIdx,
},
spec: {
url: this.spec.parser.specUrl,
data: this.spec.parser.spec,
},
searchIndex: this.search ? await this.search.toJS() : undefined,
options: this.rawOptions,
};
}
} }

View File

@ -1,5 +1,5 @@
import { OpenAPIOperation, OpenAPIParameter, OpenAPISpec, OpenAPITag, Referenced } from '../types'; import { OpenAPIOperation, OpenAPIParameter, OpenAPISpec, OpenAPITag, Referenced } from '../types';
import { isOperationName, JsonPointer } from '../utils'; import { isOperationName } from '../utils';
import { MarkdownRenderer } from './MarkdownRenderer'; import { MarkdownRenderer } from './MarkdownRenderer';
import { GroupModel, OperationModel } from './models'; import { GroupModel, OperationModel } from './models';
import { OpenAPIParser } from './OpenAPIParser'; import { OpenAPIParser } from './OpenAPIParser';
@ -11,7 +11,7 @@ export type TagInfo = OpenAPITag & {
}; };
export type ExtendedOpenAPIOperation = { export type ExtendedOpenAPIOperation = {
_$ref: string; pathName: string;
httpVerb: string; httpVerb: string;
pathParameters: Array<Referenced<OpenAPIParameter>>; pathParameters: Array<Referenced<OpenAPIParameter>>;
} & OpenAPIOperation; } & OpenAPIOperation;
@ -190,7 +190,7 @@ export class MenuBuilder {
// empty tag // empty tag
operationTags = ['']; operationTags = [''];
} }
const operationPointer = JsonPointer.compile(['paths', pathName, operationName]);
for (const tagName of operationTags) { for (const tagName of operationTags) {
let tag = tags[tagName]; let tag = tags[tagName];
if (tag === undefined) { if (tag === undefined) {
@ -205,7 +205,7 @@ export class MenuBuilder {
} }
tag.operations.push({ tag.operations.push({
...operationInfo, ...operationInfo,
_$ref: operationPointer, pathName,
httpVerb: operationName, httpVerb: operationName,
pathParameters: path.parameters || [], pathParameters: path.parameters || [],
}); });

View File

@ -1,6 +1,6 @@
import { action, computed, observable } from 'mobx'; import { action, observable } from 'mobx';
import { querySelector } from '../utils/dom'; import { querySelector } from '../utils/dom';
import { GroupModel, OperationModel, SpecStore } from './models'; import { SpecStore } from './models';
import { HistoryService } from './HistoryService'; import { HistoryService } from './HistoryService';
import { ScrollService } from './ScrollService'; import { ScrollService } from './ScrollService';
@ -55,20 +55,31 @@ export class MenuStore {
*/ */
@observable sideBarOpened: boolean = false; @observable sideBarOpened: boolean = false;
items: IMenuItem[];
flatItems: IMenuItem[];
/** /**
* cached flattened menu items to support absolute indexing * cached flattened menu items to support absolute indexing
*/ */
private _unsubscribe: () => void; private _unsubscribe: () => void;
private _hashUnsubscribe: () => void; private _hashUnsubscribe: () => void;
private _items?: Array<GroupModel | OperationModel>;
/** /**
* *
* @param spec [SpecStore](#SpecStore) which contains page content structure * @param spec [SpecStore](#SpecStore) which contains page content structure
* @param _scrollService scroll service instance used by this menu * @param scroll scroll service instance used by this menu
*/ */
constructor(private spec: SpecStore, private _scrollService: ScrollService) { constructor(spec: SpecStore, public scroll: ScrollService) {
this._unsubscribe = _scrollService.subscribe(this.updateOnScroll); this.items = spec.operationGroups;
this.flatItems = flattenByProp(this.items || [], 'items');
this.flatItems.forEach((item, idx) => (item.absoluteIdx = idx));
this.subscribe();
}
subscribe() {
this._unsubscribe = this.scroll.subscribe(this.updateOnScroll);
this._hashUnsubscribe = HistoryService.subscribe(this.updateOnHash); this._hashUnsubscribe = HistoryService.subscribe(this.updateOnHash);
} }
@ -82,23 +93,11 @@ export class MenuStore {
this.sideBarOpened = false; this.sideBarOpened = false;
} }
/**
* top level menu items (not flattened)
*/
@computed
get items(): IMenuItem[] {
if (!this._items) {
this._items = this.spec.operationGroups;
}
return this._items;
}
/** /**
* update active items on scroll * update active items on scroll
* @param isScrolledDown whether last scroll was downside * @param isScrolledDown whether last scroll was downside
*/ */
@action.bound updateOnScroll = (isScrolledDown: boolean): void => {
updateOnScroll(isScrolledDown: boolean): void {
const step = isScrolledDown ? 1 : -1; const step = isScrolledDown ? 1 : -1;
let itemIdx = this.activeItemIdx; let itemIdx = this.activeItemIdx;
while (true) { while (true) {
@ -112,12 +111,12 @@ export class MenuStore {
if (isScrolledDown) { if (isScrolledDown) {
const el = this.getElementAt(itemIdx + 1); const el = this.getElementAt(itemIdx + 1);
if (this._scrollService.isElementBellow(el)) { if (this.scroll.isElementBellow(el)) {
break; break;
} }
} else { } else {
const el = this.getElementAt(itemIdx); const el = this.getElementAt(itemIdx);
if (this._scrollService.isElementAbove(el)) { if (this.scroll.isElementAbove(el)) {
break; break;
} }
} }
@ -125,14 +124,13 @@ export class MenuStore {
} }
this.activate(this.flatItems[itemIdx], true, true); this.activate(this.flatItems[itemIdx], true, true);
} };
/** /**
* update active items on hash change * update active items on hash change
* @param hash current hash * @param hash current hash
*/ */
@action.bound updateOnHash = (hash: string = HistoryService.hash): boolean => {
updateOnHash(hash: string = HistoryService.hash): boolean {
if (!hash) { if (!hash) {
return false; return false;
} }
@ -143,10 +141,10 @@ export class MenuStore {
if (item) { if (item) {
this.activateAndScroll(item, false); this.activateAndScroll(item, false);
} else { } else {
this._scrollService.scrollIntoViewBySelector(`[${SECTION_ATTR}="${hash}"]`); this.scroll.scrollIntoViewBySelector(`[${SECTION_ATTR}="${hash}"]`);
} }
return item !== undefined; return item !== undefined;
} };
/** /**
* get section/operation DOM Node related to the item or null if it doesn't exist * get section/operation DOM Node related to the item or null if it doesn't exist
@ -168,16 +166,6 @@ export class MenuStore {
return this.flatItems.find(item => item.id === id); return this.flatItems.find(item => item.id === id);
}; };
/**
* flattened items as they appear in the tree depth-first (top to bottom in the view)
*/
@computed
get flatItems(): IMenuItem[] {
const flatItems = flattenByProp(this._items || [], 'items');
flatItems.forEach((item, idx) => (item.absoluteIdx = idx));
return flatItems;
}
/** /**
* activate menu item * activate menu item
* @param item item to activate * @param item item to activate
@ -246,7 +234,7 @@ export class MenuStore {
* scrolls to active section * scrolls to active section
*/ */
scrollToActive(): void { scrollToActive(): void {
this._scrollService.scrollIntoView(this.getElementAt(this.activeItemIdx)); this.scroll.scrollIntoView(this.getElementAt(this.activeItemIdx));
} }
dispose() { dispose() {

View File

@ -1,4 +1,3 @@
import { observable } from 'mobx';
import { resolve as urlResolve } from 'url'; import { resolve as urlResolve } from 'url';
import { OpenAPIRef, OpenAPISchema, OpenAPISpec, Referenced } from '../types'; import { OpenAPIRef, OpenAPISchema, OpenAPISpec, Referenced } from '../types';
@ -39,8 +38,8 @@ class RefCounter {
* Loads and keeps spec. Provides raw spec operations * Loads and keeps spec. Provides raw spec operations
*/ */
export class OpenAPIParser { export class OpenAPIParser {
@observable specUrl?: string; specUrl?: string;
@observable.ref spec: OpenAPISpec; spec: OpenAPISpec;
private _refCounter: RefCounter = new RefCounter(); private _refCounter: RefCounter = new RefCounter();

View File

@ -1,7 +1,6 @@
import { computed, observable } from 'mobx'; import { OpenAPIExternalDocumentation, OpenAPISpec } from '../types';
import { OpenAPISpec } from '../types';
import { MenuBuilder } from './MenuBuilder'; import { ContentItemModel, MenuBuilder } from './MenuBuilder';
import { ApiInfoModel } from './models/ApiInfo'; import { ApiInfoModel } from './models/ApiInfo';
import { SecuritySchemesModel } from './models/SecuritySchemes'; import { SecuritySchemesModel } from './models/SecuritySchemes';
import { OpenAPIParser } from './OpenAPIParser'; import { OpenAPIParser } from './OpenAPIParser';
@ -10,7 +9,12 @@ import { RedocNormalizedOptions } from './RedocNormalizedOptions';
* Store that containts all the specification related information in the form of tree * Store that containts all the specification related information in the form of tree
*/ */
export class SpecStore { export class SpecStore {
@observable.ref parser: OpenAPIParser; parser: OpenAPIParser;
info: ApiInfoModel;
externalDocs?: OpenAPIExternalDocumentation;
operationGroups: ContentItemModel[];
securitySchemes: SecuritySchemesModel;
constructor( constructor(
spec: OpenAPISpec, spec: OpenAPISpec,
@ -18,26 +22,10 @@ export class SpecStore {
private options: RedocNormalizedOptions, private options: RedocNormalizedOptions,
) { ) {
this.parser = new OpenAPIParser(spec, specUrl, options); this.parser = new OpenAPIParser(spec, specUrl, options);
} this.info = new ApiInfoModel(this.parser);
this.externalDocs = this.parser.spec.externalDocs;
this.operationGroups = MenuBuilder.buildStructure(this.parser, this.options);
@computed this.securitySchemes = new SecuritySchemesModel(this.parser);
get info(): ApiInfoModel {
return new ApiInfoModel(this.parser);
}
@computed
get externalDocs() {
return this.parser.spec.externalDocs;
}
@computed
get operationGroups() {
return MenuBuilder.buildStructure(this.parser, this.options);
}
@computed
get securitySchemes() {
const schemes = this.parser.spec.components && this.parser.spec.components.securitySchemes;
return schemes && new SecuritySchemesModel(this.parser);
} }
} }

View File

@ -32,8 +32,7 @@ export class FieldModel {
this.name = infoOrRef.name || info.name; this.name = infoOrRef.name || info.name;
this.in = info.in; this.in = info.in;
this.required = !!info.required; this.required = !!info.required;
const schemaPointer = (parser.isRef(infoOrRef) ? infoOrRef.$ref : pointer) + '/schema'; this.schema = new SchemaModel(parser, info.schema || {}, pointer, options);
this.schema = new SchemaModel(parser, info.schema || {}, schemaPointer, options);
this.description = this.description =
info.description === undefined ? this.schema.description || '' : info.description; info.description === undefined ? this.schema.description || '' : info.description;
this.example = info.example || this.schema.example; this.example = info.example || this.schema.example;

View File

@ -11,6 +11,7 @@ import {
getStatusCodeType, getStatusCodeType,
isStatusCode, isStatusCode,
JsonPointer, JsonPointer,
memoize,
mergeParams, mergeParams,
normalizeServers, normalizeServers,
sortByRequired, sortByRequired,
@ -22,7 +23,6 @@ import { FieldModel } from './Field';
import { RequestBodyModel } from './RequestBody'; import { RequestBodyModel } from './RequestBody';
import { ResponseModel } from './Response'; import { ResponseModel } from './Response';
import { CodeSample } from './types'; import { CodeSample } from './types';
/** /**
* Operation model ready to be used by components * Operation model ready to be used by components
*/ */
@ -44,80 +44,41 @@ export class OperationModel implements IMenuItem {
@observable active: boolean = false; @observable active: boolean = false;
//#endregion //#endregion
_$ref: string; pointer: string;
operationId?: string; operationId?: string;
httpVerb: string; httpVerb: string;
deprecated: boolean; deprecated: boolean;
requestBody?: RequestBodyModel;
parameters: FieldModel[];
responses: ResponseModel[];
path: string; path: string;
servers: OpenAPIServer[]; servers: OpenAPIServer[];
security: SecurityRequirementModel[]; security: SecurityRequirementModel[];
codeSamples: CodeSample[]; codeSamples: CodeSample[];
constructor( constructor(
parser: OpenAPIParser, private parser: OpenAPIParser,
operationSpec: ExtendedOpenAPIOperation, private operationSpec: ExtendedOpenAPIOperation,
parent: GroupModel | undefined, parent: GroupModel | undefined,
options: RedocNormalizedOptions, private options: RedocNormalizedOptions,
) { ) {
this.pointer = JsonPointer.compile(['paths', operationSpec.pathName, operationSpec.httpVerb]);
this.id = this.id =
operationSpec.operationId !== undefined operationSpec.operationId !== undefined
? 'operation/' + operationSpec.operationId ? 'operation/' + operationSpec.operationId
: parent !== undefined : parent !== undefined
? parent.id + operationSpec._$ref ? parent.id + this.pointer
: operationSpec._$ref; : this.pointer;
this.name = getOperationSummary(operationSpec); this.name = getOperationSummary(operationSpec);
this.description = operationSpec.description; this.description = operationSpec.description;
this.parent = parent; this.parent = parent;
this.externalDocs = operationSpec.externalDocs; this.externalDocs = operationSpec.externalDocs;
this._$ref = operationSpec._$ref;
this.deprecated = !!operationSpec.deprecated; this.deprecated = !!operationSpec.deprecated;
this.httpVerb = operationSpec.httpVerb; this.httpVerb = operationSpec.httpVerb;
this.deprecated = !!operationSpec.deprecated; this.deprecated = !!operationSpec.deprecated;
this.operationId = operationSpec.operationId; this.operationId = operationSpec.operationId;
this.requestBody =
operationSpec.requestBody && new RequestBodyModel(parser, operationSpec.requestBody, options);
this.codeSamples = operationSpec['x-code-samples'] || []; this.codeSamples = operationSpec['x-code-samples'] || [];
this.path = JsonPointer.baseName(this._$ref, 2); this.path = operationSpec.pathName;
this.parameters = mergeParams(
parser,
operationSpec.pathParameters,
operationSpec.parameters,
).map(paramOrRef => new FieldModel(parser, paramOrRef, this._$ref, options));
if (options.requiredPropsFirst) {
sortByRequired(this.parameters);
}
let hasSuccessResponses = false;
this.responses = Object.keys(operationSpec.responses || [])
.filter(code => {
if (code === 'default') {
return true;
}
if (getStatusCodeType(code) === 'success') {
hasSuccessResponses = true;
}
return isStatusCode(code);
}) // filter out other props (e.g. x-props)
.map(code => {
return new ResponseModel(
parser,
code,
hasSuccessResponses,
operationSpec.responses[code],
options,
);
});
this.servers = normalizeServers( this.servers = normalizeServers(
parser.specUrl, parser.specUrl,
operationSpec.servers || parser.spec.servers || [], operationSpec.servers || parser.spec.servers || [],
@ -143,4 +104,53 @@ export class OperationModel implements IMenuItem {
deactivate() { deactivate() {
this.active = false; this.active = false;
} }
@memoize
get requestBody() {
return (
this.operationSpec.requestBody &&
new RequestBodyModel(this.parser, this.operationSpec.requestBody, this.options)
);
}
@memoize
get parameters() {
const _parameters = mergeParams(
this.parser,
this.operationSpec.pathParameters,
this.operationSpec.parameters,
// TODO: fix pointer
).map(paramOrRef => new FieldModel(this.parser, paramOrRef, this.pointer, this.options));
if (this.options.requiredPropsFirst) {
sortByRequired(_parameters);
}
return _parameters;
}
@memoize
get responses() {
let hasSuccessResponses = false;
return Object.keys(this.operationSpec.responses || [])
.filter(code => {
if (code === 'default') {
return true;
}
if (getStatusCodeType(code) === 'success') {
hasSuccessResponses = true;
}
return isStatusCode(code);
}) // filter out other props (e.g. x-props)
.map(code => {
return new ResponseModel(
this.parser,
code,
hasSuccessResponses,
this.operationSpec.responses[code],
this.options,
);
});
}
} }

View File

@ -18,7 +18,7 @@ import {
// TODO: refactor this model, maybe use getters instead of copying all the values // TODO: refactor this model, maybe use getters instead of copying all the values
export class SchemaModel { export class SchemaModel {
_$ref: string; pointer: string;
type: string; type: string;
displayType: string; displayType: string;
@ -60,13 +60,13 @@ export class SchemaModel {
constructor( constructor(
parser: OpenAPIParser, parser: OpenAPIParser,
schemaOrRef: Referenced<OpenAPISchema>, schemaOrRef: Referenced<OpenAPISchema>,
$ref: string, pointer: string,
private options: RedocNormalizedOptions, private options: RedocNormalizedOptions,
isChild: boolean = false, isChild: boolean = false,
) { ) {
this._$ref = schemaOrRef.$ref || $ref || ''; this.pointer = schemaOrRef.$ref || pointer || '';
this.rawSchema = parser.deref(schemaOrRef); this.rawSchema = parser.deref(schemaOrRef);
this.schema = parser.mergeAllOf(this.rawSchema, this._$ref, isChild); this.schema = parser.mergeAllOf(this.rawSchema, this.pointer, isChild);
this.init(parser, isChild); this.init(parser, isChild);
parser.exitRef(schemaOrRef); parser.exitRef(schemaOrRef);
@ -91,7 +91,7 @@ export class SchemaModel {
this.isCircular = schema['x-circular-ref']; this.isCircular = schema['x-circular-ref'];
this.title = this.title =
schema.title || (isNamedDefinition(this._$ref) && JsonPointer.baseName(this._$ref)) || ''; schema.title || (isNamedDefinition(this.pointer) && JsonPointer.baseName(this.pointer)) || '';
this.description = schema.description || ''; this.description = schema.description || '';
this.type = schema.type || detectType(schema); this.type = schema.type || detectType(schema);
this.format = schema.format; this.format = schema.format;
@ -123,7 +123,7 @@ export class SchemaModel {
this.oneOfType = 'One of'; this.oneOfType = 'One of';
if (schema.anyOf !== undefined) { if (schema.anyOf !== undefined) {
console.warn( console.warn(
`oneOf and anyOf are not supported on the same level. Skipping anyOf at ${this._$ref}`, `oneOf and anyOf are not supported on the same level. Skipping anyOf at ${this.pointer}`,
); );
} }
return; return;
@ -136,9 +136,9 @@ export class SchemaModel {
} }
if (this.type === 'object') { if (this.type === 'object') {
this.fields = buildFields(parser, schema, this._$ref, this.options); this.fields = buildFields(parser, schema, this.pointer, this.options);
} else if (this.type === 'array' && schema.items) { } else if (this.type === 'array' && schema.items) {
this.items = new SchemaModel(parser, schema.items, this._$ref + '/items', this.options); this.items = new SchemaModel(parser, schema.items, this.pointer + '/items', this.options);
this.displayType = this.items.displayType; this.displayType = this.items.displayType;
this.displayFormat = this.items.format; this.displayFormat = this.items.format;
this.typePrefix = this.items.typePrefix + 'Array of '; this.typePrefix = this.items.typePrefix + 'Array of ';
@ -162,7 +162,7 @@ export class SchemaModel {
// merge base schema into each of oneOf's subschemas // merge base schema into each of oneOf's subschemas
allOf: [variant, { ...this.schema, oneOf: undefined, anyOf: undefined }], allOf: [variant, { ...this.schema, oneOf: undefined, anyOf: undefined }],
} as OpenAPISchema, } as OpenAPISchema,
this._$ref + '/oneOf/' + idx, this.pointer + '/oneOf/' + idx,
this.options, this.options,
), ),
); );
@ -177,7 +177,7 @@ export class SchemaModel {
) { ) {
const discriminator = getDiscriminator(schema)!; const discriminator = getDiscriminator(schema)!;
this.discriminatorProp = discriminator.propertyName; this.discriminatorProp = discriminator.propertyName;
const derived = parser.findDerived([...(schema.parentRefs || []), this._$ref]); const derived = parser.findDerived([...(schema.parentRefs || []), this.pointer]);
if (schema.oneOf) { if (schema.oneOf) {
for (const variant of schema.oneOf) { for (const variant of schema.oneOf) {

View File

@ -7,3 +7,4 @@ export * from './loadAndBundleSpec';
export * from './dom'; export * from './dom';
export * from './decorators'; export * from './decorators';
export * from './debug'; export * from './debug';
export * from './memoize';

49
src/utils/memoize.ts Normal file
View File

@ -0,0 +1,49 @@
// source: https://github.com/andreypopp/memoize-decorator
const SENTINEL = {};
export function memoize<T>(target: any, name: string, descriptor: TypedPropertyDescriptor<T>) {
if (typeof descriptor.value === 'function') {
return (_memoizeMethod(target, name, descriptor) as any) as TypedPropertyDescriptor<T>;
} else if (typeof descriptor.get === 'function') {
return _memoizeGetter(target, name, descriptor) as TypedPropertyDescriptor<T>;
} else {
throw new Error(
'@memoize decorator can be applied to methods or getters, got ' +
String(descriptor.value) +
' instead',
);
}
}
function _memoizeGetter(target: any, name: string, descriptor: PropertyDescriptor) {
const memoizedName = `_memoized_${name}`;
const get = descriptor.get!;
target[memoizedName] = SENTINEL;
return {
...descriptor,
get() {
if (this[memoizedName] === SENTINEL) {
this[memoizedName] = get.call(this);
}
return this[memoizedName];
},
};
}
function _memoizeMethod<T>(target: any, name: string, descriptor: TypedPropertyDescriptor<T>) {
if (!descriptor.value || (descriptor.value as any).length > 0) {
throw new Error('@memoize decorator can only be applied to methods of zero arguments');
}
const memoizedName = `_memoized_${name}`;
const value = descriptor.value;
target[memoizedName] = SENTINEL;
return {
...descriptor,
value() {
if (this[memoizedName] === SENTINEL) {
this[memoizedName] = (value as any).call(this);
}
return this[memoizedName] as any;
},
};
}