Skip to content

Commit

Permalink
refactor: Splitting Matcher into Graph and GraphMatcher
Browse files Browse the repository at this point in the history
  • Loading branch information
aholstenson committed Sep 29, 2019
1 parent 44f5808 commit eba7270
Show file tree
Hide file tree
Showing 95 changed files with 1,047 additions and 846 deletions.
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ module.exports = {
globals: {
"ts-jest": {
"diagnostics": {
"ignoreCodes": "TS2531"
"ignoreCodes": [ "TS2531", "TS2532" ]
}
}
}
Expand Down
36 changes: 20 additions & 16 deletions src/ActionsBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,22 @@ import { Matcher, EncounterOptions } from './graph/matching';
import { ResolvedIntent } from './resolver/ResolvedIntent';
import { ResolvedIntents } from './resolver/ResolvedIntents';

export type Action = (item: ResolvedIntent) => void;
export type Action<Values extends object> = (item: ResolvedIntent<Values>) => void;

export class ActionsBuilder {
private language: Language;
private builder: IntentsBuilder;
private handlers: Map<string, any>;
private id: number;

constructor(lang: Language) {
this.language = lang;
this.builder = new IntentsBuilder(lang);
this.handlers = new Map();
this.id = 0;
}

public action(id?: string): ActionBuilder {
public action(id?: string): ActionBuilder<{}> {
// Auto assign an id
const actualId = id ? id : id = ('__auto__' + ++this.id);

Expand All @@ -28,7 +30,7 @@ export class ActionsBuilder {
return {
value(valueId, type) {
builder.value(valueId, type);
return this;
return this as any;
},

add(...args) {
Expand All @@ -49,35 +51,37 @@ export class ActionsBuilder {
}

public build() {
return new Actions(this.builder.build(), this.handlers);
return new Actions(this.language, this.builder.build(), this.handlers);
}
}

export interface ActionBuilder {
value(id: string, type: Value): this;
export interface ActionBuilder<Values extends object> {
value<I extends string, V>(id: I, type: Value<V>): ActionBuilder<Values & { [K in I]: V }>;

add(...args: string[]): this;

handler(func: Action): this;
handler(func: Action<Values>): this;

done(): ActionsBuilder;
}

export class Actions {
public readonly language: Language;
private handlers: Map<string, any>;
private matcher: Matcher<ResolvedIntents>;

constructor(matcher: Matcher<ResolvedIntents>, handlers: Map<string, any>) {
private matcher: Matcher<ResolvedIntents<any>>;

constructor(
language: Language,
matcher: Matcher<ResolvedIntents<any>>,
handlers: Map<string, any>
) {
this.language = language;
this.matcher = matcher;
this.handlers = handlers;
}

get language() {
return this.matcher.language;
}

public match(expression: string, options: EncounterOptions): Promise<ResolvedActions> {
const map = (item: ResolvedIntent): ResolvedAction => {
const map = (item: ResolvedIntent<any>): ResolvedAction => {
const result = item as any;
result.activate = (...args: any[]) => {
const handler = this.handlers.get(item.intent);
Expand All @@ -98,7 +102,7 @@ export class Actions {
}
}

export interface ResolvedAction extends ResolvedIntent {
export interface ResolvedAction extends ResolvedIntent<any> {
activate: () => void;
}

Expand Down
10 changes: 6 additions & 4 deletions src/IntentsBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ResolverBuilder } from './resolver/ResolverBuilder';
import { Language } from './language/Language';
import { Value } from './values/base';
import { Matcher } from './graph/matching';
import { ResolvedIntents } from './resolver/ResolvedIntents';

export class IntentsBuilder {
private language: Language;
Expand All @@ -24,7 +26,7 @@ export class IntentsBuilder {
const self = this;
const instance = new ResolverBuilder(this.language, id);
return {
value(valueId: string, type: Value) {
value(valueId: string, type: Value<any>) {
instance.value(valueId, type);
return this;
},
Expand All @@ -41,13 +43,13 @@ export class IntentsBuilder {
};
}

public build() {
return this.builder.build();
public build(): Matcher<ResolvedIntents<any>> {
return this.builder.toMatcher();
}
}

export interface IntentBuilder {
value(id: string, type: Value): this;
value(id: string, type: Value<any>): this;

add(...args: string[]): this;

Expand Down
25 changes: 25 additions & 0 deletions src/graph/Graph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Node } from './Node';
import { MatchingState } from './matching';
import { GraphOptions } from './GraphOptions';

/**
* Graph that has been built via GraphBuilder. Graphs are a collection of
* outgoing nodes that can parse an expression.
*/
export interface Graph<DataType> {
/**
* The outgoing nodes of this graph.
*/
nodes: Node[];

/**
* Options to apply during matching of this graph.
*/
options: GraphOptions;

/**
* Internal state of this matcher that is accessed if it is used as a
* sub graph.
*/
matchingState: MatchingState;
}
94 changes: 27 additions & 67 deletions src/graph/GraphBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { SubNode } from './SubNode';
import { CollectorNode, Collectable } from './CollectorNode';
import { CustomNode, TokenValidator } from './CustomNode';

import { Matcher } from './matching/Matcher';
import { DefaultMatcher, MatcherOptions, MatchReductionEncounter } from './matching/DefaultMatcher';
import { Language } from '../language/Language';
import { Encounter, Match } from './matching';
import { emptyState } from './matching';
import { Predicate } from '../utils/predicates';
import { Graph } from './Graph';
import { GraphOptions } from './GraphOptions';

/**
* Object that can be mapped into another one.
Expand All @@ -18,7 +18,7 @@ export interface MappableObject<V> {
[x: string]: V;
}

export type GraphBuildable<V> = string | RegExp | Node | Matcher<any> | ((builder: GraphBuilder<any, any, any>) => Node);
export type GraphBuildable<V> = string | RegExp | Node | Graph<any> | ((builder: GraphBuilder<any>) => Node);

export type GraphBuildableArray<V> = GraphBuildable<V> | GraphBuildable<V>[];

Expand All @@ -27,10 +27,10 @@ export type GraphBuildableArray<V> = GraphBuildable<V> | GraphBuildable<V>[];
* be used within the graph or standalone to match expressions.
*
*/
export class GraphBuilder<V, M=V, R=M[]> {
export class GraphBuilder<V> {
protected language: Language;
private nodes: Node[];
private options: MatcherOptions<R>;
protected options: GraphOptions;

constructor(language: Language) {
this.language = language;
Expand Down Expand Up @@ -152,12 +152,12 @@ export class GraphBuilder<V, M=V, R=M[]> {
return push(result);
} else if(n instanceof RegExp) {
return push(new RegExpNode(n));
} else if(n instanceof DefaultMatcher) {
return push(new SubNode(n, n.options));
} else if(n instanceof Node) {
return push(n);
} else if(typeof n === 'string') {
return push(this.parse(n));
} else if('nodes' in n) {
return push(new SubNode(n, n.options));
} else {
throw new Error('Invalid node');
}
Expand Down Expand Up @@ -204,77 +204,37 @@ export class GraphBuilder<V, M=V, R=M[]> {
return this;
}

/**
* Setup a mapper that turns the intermediate representation into the
* public facing value type.
*/
public mapResults<N>(mapper: (result: V, encounter: Encounter) => N): GraphBuilder<V, N> {
const self = this as unknown as GraphBuilder<V, N>;
self.options.mapper = mapper;
return self;
}

/**
* Reduce the results down to a new object. This can be used to perform
* a transformation on all of the results at once.
*
* @param func
*/
public reducer<NewR>(func: (results: MatchReductionEncounter<V, M>) => NewR): GraphBuilder<V, M, NewR> {
const self = this as unknown as GraphBuilder<V, M, NewR>;
self.options.reducer = func;
return self;
}

/**
* Reduce the results down so only the best match is returned when this
* matcher is queried.
*/
public onlyBest(): GraphBuilder<V, M, M | null> {
return this.reducer(({ results, map }) => {
const match = results.first();
if(match) {
return map(match.data);
} else {
return null;
}
});
}

/**
* Build this graph and turn it into a matcher.
*/
public toMatcher(): Matcher<R> {
return this.createMatcher(this.language, this.nodes, this.options);
}

protected createMatcher<C>(lang: Language, nodes: Node[], options: MatcherOptions<C>): Matcher<C> {
return new DefaultMatcher(lang, nodes, options);
public build(): Graph<V> {
return {
nodes: this.nodes,
options: this.options,
matchingState: emptyState()
};
}

public static result<V>(matcher?: Matcher<any> | Predicate<V>, validator?: Predicate<V>): (builder: GraphBuilder<V, any>) => Node {
public static result<V>(graph?: Graph<any> | Predicate<V>, validator?: Predicate<V>): (builder: GraphBuilder<V>) => Node {
if(typeof validator === 'undefined') {
if(typeof matcher === 'function') {
validator = matcher;
matcher = undefined;
} else if(matcher) {
throw new Error('Expected graph or a validation function, got ' + matcher);
if(typeof graph === 'function') {
validator = graph;
graph = undefined;
} else if(graph) {
throw new Error('Expected graph or a validation function, got ' + graph);
}
}

if(matcher && ! (matcher instanceof DefaultMatcher)) {
throw new Error('matcher is not actually an instance of Matcher');
if(graph && ! ('nodes' in graph)) {
throw new Error('Given graph is not valid');
}

return function(builder: GraphBuilder<V>) {
const sub = matcher instanceof DefaultMatcher
? new SubNode(matcher, matcher.options, validator)
const sub = graph && 'nodes' in graph
? new SubNode(graph, graph.options, validator)
: new SubNode(builder.nodes, builder.options as any, validator);

sub.recursive = ! matcher;
sub.recursive = ! graph;

if(validator) {
let name = matcher instanceof DefaultMatcher ? matcher.options.name : builder.options.name;
let name = (graph && 'nodes' in graph) ? graph.options.name : builder.options.name;
if(validator.name) {
if(name) {
name += ':' + validator.name;
Expand All @@ -283,7 +243,7 @@ export class GraphBuilder<V, M=V, R=M[]> {
}
}
sub.name = name || 'unknown';
} else if(! matcher) {
} else if(! graph) {
sub.name = builder.options.name + ':self';
}

Expand Down
5 changes: 5 additions & 0 deletions src/graph/GraphOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { EncounterOptions } from './matching';

export interface GraphOptions extends EncounterOptions {
name?: string;
}
48 changes: 48 additions & 0 deletions src/graph/KnownGraphs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { NumberData } from '../numbers/NumberData';
import { DateTimeData } from '../time/DateTimeData';
import { OrdinalData } from '../numbers/OrdinalData';

/**
* Graphs that are known to be available for a language. This enum used when
* fetching a graph from a language and to lookup the type the graph uses.
*
* This allows for type-safety within the values.
*/
export enum KnownGraphs {
Boolean = 'boolean',
DateDuration = 'date-duration',
Date = 'date',
DateInterval = 'date-interval',
DateTimeDuration = 'date-time-duration',
DateTime = 'date-time',
Integer = 'integer',
Month = 'month',
Number = 'number',
Ordinal = 'ordinal',
Quarter = 'quarter',
TimeDuration = 'time-duration',
Time = 'time',
Week = 'week',
Year = 'year'
}

/**
* Types for `KnownGraphs`.
*/
export interface KnownGraphsDataTypes {
[KnownGraphs.Boolean]: boolean;
[KnownGraphs.DateDuration]: DateTimeData;
[KnownGraphs.Date]: DateTimeData;
[KnownGraphs.DateInterval]: DateTimeData;
[KnownGraphs.DateTimeDuration]: DateTimeData;
[KnownGraphs.DateTime]: DateTimeData;
[KnownGraphs.Integer]: NumberData;
[KnownGraphs.Month]: DateTimeData;
[KnownGraphs.Number]: NumberData;
[KnownGraphs.Ordinal]: OrdinalData;
[KnownGraphs.Quarter]: DateTimeData;
[KnownGraphs.TimeDuration]: DateTimeData;
[KnownGraphs.Time]: DateTimeData;
[KnownGraphs.Week]: DateTimeData;
[KnownGraphs.Year]: DateTimeData;
}
Loading

0 comments on commit eba7270

Please sign in to comment.