Skip to content

Commit

Permalink
feat: add ability to specify custom kinds (#118)
Browse files Browse the repository at this point in the history
  • Loading branch information
prevwong authored Dec 18, 2023
1 parent 6bc28c2 commit 52c4c8d
Show file tree
Hide file tree
Showing 15 changed files with 149 additions and 9 deletions.
7 changes: 7 additions & 0 deletions .changeset/calm-points-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@rekajs/parser': patch
'@rekajs/types': patch
'@rekajs/core': patch
---

Add ability to specify custom kinds
14 changes: 14 additions & 0 deletions packages/core/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as t from '@rekajs/types';

import { ExtensionDefinition } from './extension';
import { Reka } from './reka';
import { KindFieldValidators } from './utils';

export type StateExternalFunction = (opts: Record<string, any>) => any;
export type StateExternalFunctions = Array<t.ExternalFunc>;
Expand All @@ -23,7 +24,20 @@ export type RekaExternalsFactory = {
components: t.Component[];
};

export type CustomKindConfig = {
fallback?: any;
// Note: this is currently just decorative and doesn't do anything
// TODO: enforce validation in evaluator
validate: (field: typeof KindFieldValidators) => t.Validator;
};

export type CustomKindDefinition = {
fallback: any;
validator: t.Validator;
};

export type RekaOpts = {
kinds?: Record<string, CustomKindConfig>;
externals?: Partial<RekaExternalsFactory>;
extensions?: ExtensionDefinition<any>[];
};
Expand Down
38 changes: 37 additions & 1 deletion packages/core/src/reka.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,14 @@ import { ExtensionDefinition, ExtensionRegistry } from './extension';
import { Externals } from './externals';
import { Frame, FrameOpts } from './frame';
import { Head } from './head';
import { RekaOpts, StateSubscriberOpts } from './interfaces';
import {
CustomKindDefinition,
RekaOpts,
StateSubscriberOpts,
} from './interfaces';
import { ChangeListenerSubscriber, Observer } from './observer';
import { ExtensionVolatileStateKey, ExternalVolatileStateKey } from './symbols';
import { KindFieldValidators } from './utils';

export class Reka {
id: string;
Expand All @@ -35,6 +40,8 @@ export class Reka {
/// @internal
declare head: Head;

private declare kinds: Record<string, CustomKindDefinition>;

private declare observer: Observer<t.State>;
private declare extensions: ExtensionRegistry;

Expand Down Expand Up @@ -62,6 +69,8 @@ export class Reka {
[ExternalVolatileStateKey]: {},
};

this.createCustomKindsDefinition();

this.externals = new Externals(this, opts?.externals);

makeObservable(this, {
Expand All @@ -72,6 +81,33 @@ export class Reka {
});
}

private createCustomKindsDefinition() {
if (!this.opts || !this.opts.kinds) {
return;
}

const kinds = this.opts.kinds;

this.kinds = Object.keys(kinds).reduce((accum, name) => {
// Validate Custom kind names
invariant(
name[0] === name[0].toUpperCase(),
`Custom kind "${name}" must start with a capital letter.`
);

accum[name] = {
fallback: kinds[name].fallback ?? '',
validator: kinds[name].validate(KindFieldValidators),
};

return accum;
}, {});
}

getCustomKind(name: string) {
return this.kinds[name] ?? null;
}

getExternalState(key: string) {
return this.externals.getStateValue(key);
}
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,8 @@ export const defer = (fn: () => void) => {

// eslint-disable-next-line @typescript-eslint/no-empty-function
export const noop = () => {};

export const KindFieldValidators = {
string: (validate?: (value: string) => boolean) =>
t.assertions.type('string', validate),
};
8 changes: 7 additions & 1 deletion packages/parser/src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ class _Parser extends Lexer {
}

private parseKindType() {
const kindType = this.consume(TokenType.KIND_TYPE).value;
const kindType = this.consume(TokenType.KIND_TYPE).value as string;

switch (kindType) {
case 'string': {
Expand Down Expand Up @@ -395,6 +395,12 @@ class _Parser extends Lexer {
});
}
default: {
if (kindType.length > 0 && kindType[0] === kindType[0].toUpperCase()) {
return t.customKind({
name: kindType,
});
}

return t.anyKind();
}
}
Expand Down
5 changes: 5 additions & 0 deletions packages/parser/src/stringifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ class _Stringifier {
return;
}

if (t.is(input, t.CustomKind)) {
this.writer.write(input.name);
return;
}

this.writer.write('any');
};

Expand Down
23 changes: 23 additions & 0 deletions packages/parser/src/tests/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,29 @@ describe('Parser', () => {
],
});
});
it('should be able to parse variable with custom kind', () => {
expect(
Parser.parseProgram(`
val color: Color = "#000";
`)
).toMatchObject({
type: 'Program',
globals: [
{
type: 'Val',
name: 'color',
init: {
type: 'Literal',
value: '#000',
},
kind: {
type: 'CustomKind',
name: 'Color',
},
},
],
});
});
it('should be able to parse negative values', () => {
expect(Parser.parseExpression('-1')).toMatchObject({
operator: '-',
Expand Down
4 changes: 2 additions & 2 deletions packages/parser/src/tests/stringifier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,11 +159,11 @@ describe('Stringifier', () => {
Stringifier.toString(
t.val({
name: 'color',
kind: t.stringKind(),
kind: t.customKind({ name: 'Color' }),
init: t.literal({ value: 'blue' }),
})
)
).toEqual(`val color:string = "blue"`);
).toEqual(`val color:Color = "blue"`);

expect(
Stringifier.toString(
Expand Down
3 changes: 3 additions & 0 deletions packages/types/src/generated/builder.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export const arrayKind = (...args: ConstructorParameters<typeof t.ArrayKind>) =>
export const optionKind = (
...args: ConstructorParameters<typeof t.OptionKind>
) => new t.OptionKind(...args);
export const customKind = (
...args: ConstructorParameters<typeof t.CustomKind>
) => new t.CustomKind(...args);
export const literal = (...args: ConstructorParameters<typeof t.Literal>) =>
new t.Literal(...args);
export const identifier = (
Expand Down
19 changes: 19 additions & 0 deletions packages/types/src/generated/types.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,23 @@ export class OptionKind extends Kind {

Schema.register('OptionKind', OptionKind);

type CustomKindParameters = {
name: string;
};

export class CustomKind extends Kind {
// Type Hack: in order to accurately use type predicates via the .is() util method
// @ts-ignore
private declare __isCustomKind?: string;

declare name: string;
constructor(value: CustomKindParameters) {
super('CustomKind', value);
}
}

Schema.register('CustomKind', CustomKind);

type ExpressionParameters = {
meta?: Record<string, any>;
};
Expand Down Expand Up @@ -1095,6 +1112,7 @@ export type Any =
| BooleanKind
| ArrayKind
| OptionKind
| CustomKind
| Expression
| Identifiable
| Variable
Expand Down Expand Up @@ -1150,6 +1168,7 @@ export type Visitor = {
BooleanKind: (node: BooleanKind) => any;
ArrayKind: (node: ArrayKind) => any;
OptionKind: (node: OptionKind) => any;
CustomKind: (node: CustomKind) => any;
Expression: (node: Expression) => any;
Identifiable: (node: Identifiable) => any;
Variable: (node: Variable) => any;
Expand Down
7 changes: 7 additions & 0 deletions packages/types/src/types.definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ Schema.define('OptionKind', {
}),
});

Schema.define('CustomKind', {
extends: 'Kind',
fields: (t) => ({
name: t.string,
}),
});

Schema.define('Expression', {
extends: 'ASTNode',
abstract: true,
Expand Down
3 changes: 2 additions & 1 deletion packages/types/src/validators/assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
} from './definitions';
import { Validator } from './validator';

export const type = (type: string) => new TypeValidator(type);
export const type = (type: string, validate?: (value: any) => boolean) =>
new TypeValidator(type, validate);
export const node = (node: string, isRef?: boolean) =>
new NodeValidator(node, isRef);
export const union = (...validators: Validator[]) =>
Expand Down
12 changes: 10 additions & 2 deletions packages/types/src/validators/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { Schema, Type } from '../schema';

export class TypeValidator extends Validator {
type: string;
validateFn?: (value: any) => boolean;

constructor(type: string) {
constructor(type: string, validateFn?: (value: any) => boolean) {
super('type');
this.type = type;
this.validateFn = validateFn;
}

validate(value: any) {
Expand All @@ -19,7 +21,13 @@ export class TypeValidator extends Validator {
return Object.getPrototypeOf(value) === Object.prototype;
}

return typeof value === this.type.toLowerCase();
const isValid = typeof value === this.type.toLowerCase();

if (!this.validateFn) {
return isValid;
}

return this.validateFn(value);
}
}

Expand Down
3 changes: 1 addition & 2 deletions site/constants/encoded-dummy-program.ts

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions site/editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@ export class Editor {
});

this.reka = Reka.create({
kinds: {
Color: {
validate(field) {
return field.string((value) => value.startsWith('#'));
},
},
},
...createSharedStateGlobals({
externals: {
components: [
Expand Down

1 comment on commit 52c4c8d

@vercel
Copy link

@vercel vercel bot commented on 52c4c8d Dec 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

reka – ./

rekajs.vercel.app
reka-prevwong.vercel.app
reka.js.org
reka-git-main-prevwong.vercel.app

Please sign in to comment.