Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(predicates): implement TypeScript type guards #3289

Merged
merged 2 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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