Skip to content

Commit

Permalink
feat(predicates): implement TypeScript type guards (#3289)
Browse files Browse the repository at this point in the history
Refs #3280
  • Loading branch information
char0n authored Oct 18, 2023
1 parent 61f7c62 commit 0cae70a
Show file tree
Hide file tree
Showing 19 changed files with 267 additions and 168 deletions.
1 change: 1 addition & 0 deletions packages/apidom-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export {
includesSymbols,
includesClasses,
} from './predicates/index';
export type { ElementPredicate } from './predicates/helpers';
export { default as createPredicate } from './predicates/helpers';

export { filter, reject, find, findAtOffset, some, traverse, parents } from './traversal/index';
Expand Down
80 changes: 64 additions & 16 deletions packages/apidom-core/src/predicates/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,4 @@
const hasMethod = (name: string, obj: Record<string, unknown>): boolean =>
typeof obj?.[name] === 'function';

const hasBasicElementProps = (element: any) =>
element != null &&
Object.prototype.hasOwnProperty.call(element, '_storedElement') &&
Object.prototype.hasOwnProperty.call(element, '_content');

const primitiveEq = (val: unknown, obj: any): boolean => obj?.primitive?.() === val;

const hasClass = (cls: string, obj: any): boolean => obj?.classes?.includes?.(cls) || false;

export const isElementType = (name: string, element: any): boolean => element?.element === name;
import { Element, ArrayElement } from 'minim';

interface PredicateHelpers {
hasMethod: typeof hasMethod;
Expand All @@ -20,10 +8,70 @@ interface PredicateHelpers {
hasClass: typeof hasClass;
}

type PredicateCreator = (helpers: PredicateHelpers) => (element: any) => boolean;
interface ElementBasicsTrait {
_storedElement: string;
_content: unknown;
}

interface ElementPrimitiveBehavior {
primitive: () => unknown;
}

interface ElementTypeTrait<T = string> {
element: T;
}

interface ElementClassesTrait {
classes: ArrayElement | Array<string>;
}

type PredicateCreator<T extends Element> = (helpers: PredicateHelpers) => ElementPredicate<T>;

export type ElementPredicate<T extends Element> = (element: unknown) => element is T;

const hasMethod = <T extends string>(
name: T,
element: unknown,
): element is { [key in T]: (...args: unknown[]) => unknown } => {
return (
typeof element === 'object' &&
element !== null &&
name in element &&
typeof (element as Record<string, unknown>)[name] === 'function'
);
};

const hasBasicElementProps = (element: unknown): element is ElementBasicsTrait =>
typeof element === 'object' &&
element != null &&
'_storedElement' in element &&
typeof element._storedElement === 'string' && // eslint-disable-line no-underscore-dangle
'_content' in element;

const primitiveEq = (val: unknown, element: unknown): element is ElementPrimitiveBehavior => {
if (typeof element === 'object' && element !== null && 'primitive' in element) {
return typeof element.primitive === 'function' && element.primitive() === val;
}
return false;
};

const hasClass = (cls: string, element: unknown): element is ElementClassesTrait => {
return (
typeof element === 'object' &&
element !== null &&
'classes' in element &&
(Array.isArray(element.classes) || element.classes instanceof ArrayElement) &&
element.classes.includes(cls)
);
};

export const isElementType = (name: string, element: unknown): element is ElementTypeTrait =>
typeof element === 'object' &&
element !== null &&
'element' in element &&
element.element === name;

const createPredicate = (predicateCreator: PredicateCreator) => {
// @ts-ignore
const createPredicate = <T extends Element>(predicateCreator: PredicateCreator<T>) => {
return predicateCreator({
hasMethod,
hasBasicElementProps,
Expand Down
46 changes: 29 additions & 17 deletions packages/apidom-core/src/predicates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,40 +18,41 @@ import CommentElement from '../elements/Comment';
import ParserResultElement from '../elements/ParseResult';
import SourceMapElement from '../elements/SourceMap';
import createPredicate, { isElementType as isElementTypeHelper } from './helpers';
import type { ElementPredicate } from './helpers';

export const isElement = createPredicate(({ hasBasicElementProps, primitiveEq }) => {
return (element: any) =>
return (element: unknown): element is Element =>
element instanceof Element ||
(hasBasicElementProps(element) && primitiveEq(undefined, element));
});

export const isStringElement = createPredicate(({ hasBasicElementProps, primitiveEq }) => {
return (element: any) =>
return (element: unknown): element is StringElement =>
element instanceof StringElement ||
(hasBasicElementProps(element) && primitiveEq('string', element));
});

export const isNumberElement = createPredicate(({ hasBasicElementProps, primitiveEq }) => {
return (element: any) =>
return (element: unknown): element is NumberElement =>
element instanceof NumberElement ||
(hasBasicElementProps(element) && primitiveEq('number', element));
});

export const isNullElement = createPredicate(({ hasBasicElementProps, primitiveEq }) => {
return (element: any) =>
return (element: unknown): element is NullElement =>
element instanceof NullElement ||
(hasBasicElementProps(element) && primitiveEq('null', element));
});

export const isBooleanElement = createPredicate(({ hasBasicElementProps, primitiveEq }) => {
return (element: any) =>
return (element: unknown): element is BooleanElement =>
element instanceof BooleanElement ||
(hasBasicElementProps(element) && primitiveEq('boolean', element));
});

export const isObjectElement = createPredicate(
({ hasBasicElementProps, primitiveEq, hasMethod }) => {
return (element: any) =>
return (element: unknown): element is ObjectElement =>
element instanceof ObjectElement ||
(hasBasicElementProps(element) &&
primitiveEq('object', element) &&
Expand All @@ -63,7 +64,7 @@ export const isObjectElement = createPredicate(

export const isArrayElement = createPredicate(
({ hasBasicElementProps, primitiveEq, hasMethod }) => {
return (element: any) =>
return (element: unknown): element is ArrayElement =>
(element instanceof ArrayElement && !(element instanceof ObjectElement)) ||
(hasBasicElementProps(element) &&
primitiveEq('array', element) &&
Expand All @@ -76,7 +77,7 @@ export const isArrayElement = createPredicate(

export const isMemberElement = createPredicate(
({ hasBasicElementProps, isElementType, primitiveEq }) => {
return (element: any) =>
return (element: unknown): element is MemberElement =>
element instanceof MemberElement ||
(hasBasicElementProps(element) &&
isElementType('member', element) &&
Expand All @@ -86,7 +87,7 @@ export const isMemberElement = createPredicate(

export const isLinkElement = createPredicate(
({ hasBasicElementProps, isElementType, primitiveEq }) => {
return (element: any) =>
return (element: unknown): element is LinkElement =>
element instanceof LinkElement ||
(hasBasicElementProps(element) &&
isElementType('link', element) &&
Expand All @@ -96,7 +97,7 @@ export const isLinkElement = createPredicate(

export const isRefElement = createPredicate(
({ hasBasicElementProps, isElementType, primitiveEq }) => {
return (element: any) =>
return (element: unknown): element is RefElement =>
element instanceof RefElement ||
(hasBasicElementProps(element) &&
isElementType('ref', element) &&
Expand All @@ -106,7 +107,7 @@ export const isRefElement = createPredicate(

export const isAnnotationElement = createPredicate(
({ hasBasicElementProps, isElementType, primitiveEq }) => {
return (element: any) =>
return (element: unknown): element is AnnotationElement =>
element instanceof AnnotationElement ||
(hasBasicElementProps(element) &&
isElementType('annotation', element) &&
Expand All @@ -116,7 +117,7 @@ export const isAnnotationElement = createPredicate(

export const isCommentElement = createPredicate(
({ hasBasicElementProps, isElementType, primitiveEq }) => {
return (element: any) =>
return (element: unknown): element is CommentElement =>
element instanceof CommentElement ||
(hasBasicElementProps(element) &&
isElementType('comment', element) &&
Expand All @@ -126,7 +127,7 @@ export const isCommentElement = createPredicate(

export const isParseResultElement = createPredicate(
({ hasBasicElementProps, isElementType, primitiveEq }) => {
return (element: any) =>
return (element: unknown): element is ParserResultElement =>
element instanceof ParserResultElement ||
(hasBasicElementProps(element) &&
isElementType('parseResult', element) &&
Expand All @@ -136,15 +137,26 @@ export const isParseResultElement = createPredicate(

export const isSourceMapElement = createPredicate(
({ hasBasicElementProps, isElementType, primitiveEq }) => {
return (element: any) =>
return (element: unknown): element is SourceMapElement =>
element instanceof SourceMapElement ||
(hasBasicElementProps(element) &&
isElementType('sourceMap', element) &&
primitiveEq('array', element));
},
);

export const isPrimitiveElement = (element: any): boolean => {
type PrimitiveElement =
| ObjectElement
| ArrayElement
| BooleanElement
| NumberElement
| StringElement
| NullElement
| MemberElement;

export const isPrimitiveElement: ElementPredicate<PrimitiveElement> = (
element: unknown,
): element is PrimitiveElement => {
return (
isElementTypeHelper('object', element) ||
isElementTypeHelper('array', element) ||
Expand All @@ -156,8 +168,8 @@ export const isPrimitiveElement = (element: any): boolean => {
);
};

export const hasElementSourceMap = (element: any): boolean => {
return isSourceMapElement(element?.meta?.get?.('sourceMap'));
export const hasElementSourceMap = <T extends Element>(element: T): boolean => {
return isSourceMapElement(element.meta.get('sourceMap'));
};

export const includesSymbols = <T extends Element>(symbols: string[], element: T): boolean => {
Expand Down
2 changes: 1 addition & 1 deletion packages/apidom-core/test/predicates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ describe('predicates', function () {

specify('should support duck-typing', function () {
const elementDuck = {
_storedElement: undefined,
_storedElement: 'element',
_content: undefined,
primitive() {
return undefined;
Expand Down
13 changes: 10 additions & 3 deletions packages/apidom-core/test/traversal/filter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { assert } from 'chai';

import { filter, createNamespace, isMemberElement, ArraySlice, ObjectElement } from '../../src';
import {
filter,
createNamespace,
isMemberElement,
isElement,
ArraySlice,
ObjectElement,
} from '../../src';

const namespace = createNamespace();

Expand All @@ -17,8 +24,8 @@ describe('traversal', function () {
});

specify('should find content matching the predicate', function () {
const predicate = (element: any): boolean =>
isMemberElement(element) && element.key.equals('a');
const predicate = (element: unknown): boolean =>
isMemberElement(element) && isElement(element.key) && element.key.equals('a');
const filtered = filter(predicate, objElement);

assert.lengthOf(filtered, 1);
Expand Down
6 changes: 3 additions & 3 deletions packages/apidom-core/test/traversal/find.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { assert } from 'chai';
import { F as stubFalse } from 'ramda';

import { createNamespace, find, isMemberElement, MemberElement } from '../../src';
import { createNamespace, find, isMemberElement, isElement, MemberElement } from '../../src';

const namespace = createNamespace();

Expand All @@ -12,8 +12,8 @@ describe('traversal', function () {
const objElement = new namespace.elements.Object({ a: 'b', c: 'd' });

specify('should return first match', function () {
const predicate = (element: any): boolean =>
isMemberElement(element) && element.key.equals('c');
const predicate = (element: unknown): boolean =>
isMemberElement(element) && isElement(element.key) && element.key.equals('c');
// @ts-ignore
const found = find(predicate, objElement) as MemberElement;

Expand Down
16 changes: 8 additions & 8 deletions packages/apidom-ns-api-design-systems/src/predicates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import StandardIdentifierElement from './elements/StandardIdentifier';

export const isMainElement = createPredicate(
({ hasBasicElementProps, isElementType, primitiveEq }) => {
return (element: any) =>
return (element: unknown): element is MainElement =>
element instanceof MainElement ||
(hasBasicElementProps(element) &&
isElementType('main', element) &&
Expand All @@ -21,7 +21,7 @@ export const isMainElement = createPredicate(

export const isInfoElement = createPredicate(
({ hasBasicElementProps, isElementType, primitiveEq }) => {
return (element: any) =>
return (element: unknown): element is InfoElement =>
element instanceof InfoElement ||
(hasBasicElementProps(element) &&
isElementType('info', element) &&
Expand All @@ -31,7 +31,7 @@ export const isInfoElement = createPredicate(

export const isPrincipleElement = createPredicate(
({ hasBasicElementProps, isElementType, primitiveEq }) => {
return (element: any) =>
return (element: unknown): element is PrincipleElement =>
element instanceof PrincipleElement ||
(hasBasicElementProps(element) &&
isElementType('principle', element) &&
Expand All @@ -41,7 +41,7 @@ export const isPrincipleElement = createPredicate(

export const isRequirementElement = createPredicate(
({ hasBasicElementProps, isElementType, primitiveEq }) => {
return (element: any) =>
return (element: unknown): element is RequirementElement =>
element instanceof RequirementElement ||
(hasBasicElementProps(element) &&
isElementType('requirement', element) &&
Expand All @@ -51,7 +51,7 @@ export const isRequirementElement = createPredicate(

export const isRequirementLevelElement = createPredicate(
({ hasBasicElementProps, isElementType, primitiveEq }) => {
return (element: any) =>
return (element: unknown): element is RequirementLevelElement =>
element instanceof RequirementLevelElement ||
(hasBasicElementProps(element) &&
isElementType('requirementLevel', element) &&
Expand All @@ -61,7 +61,7 @@ export const isRequirementLevelElement = createPredicate(

export const isScenarioElement = createPredicate(
({ hasBasicElementProps, isElementType, primitiveEq }) => {
return (element: any) =>
return (element: unknown): element is ScenarioElement =>
element instanceof ScenarioElement ||
(hasBasicElementProps(element) &&
isElementType('scenario', element) &&
Expand All @@ -71,7 +71,7 @@ export const isScenarioElement = createPredicate(

export const isStandardElement = createPredicate(
({ hasBasicElementProps, isElementType, primitiveEq }) => {
return (element: any) =>
return (element: unknown): element is StandardElement =>
element instanceof StandardElement ||
(hasBasicElementProps(element) &&
isElementType('standard', element) &&
Expand All @@ -81,7 +81,7 @@ export const isStandardElement = createPredicate(

export const isStandardIdentifierElement = createPredicate(
({ hasBasicElementProps, isElementType, primitiveEq }) => {
return (element: any) =>
return (element: unknown): element is StandardIdentifierElement =>
element instanceof StandardIdentifierElement ||
(hasBasicElementProps(element) &&
isElementType('standardIdentifier', element) &&
Expand Down
Loading

0 comments on commit 0cae70a

Please sign in to comment.