Skip to content

Commit

Permalink
feat: Introducing Matcher interface for public use
Browse files Browse the repository at this point in the history
  • Loading branch information
aholstenson committed Sep 29, 2019
1 parent d5de736 commit 78fb265
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 103 deletions.
15 changes: 8 additions & 7 deletions src/graph/GraphBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { SubNode } from './SubNode';
import { CollectorNode, Collectable } from './CollectorNode';
import { CustomNode, TokenValidator } from './CustomNode';

import { Matcher, MatcherOptions, MatchReductionEncounter } from './matching/Matcher';
import { Matcher } from './matching/Matcher';
import { DefaultMatcher, MatcherOptions, MatchReductionEncounter } from './matching/DefaultMatcher';
import { Language } from '../language/Language';
import { Encounter, Match } from './matching';
import { Predicate } from '../utils/predicates';
Expand Down Expand Up @@ -151,7 +152,7 @@ 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 Matcher) {
} else if(n instanceof DefaultMatcher) {
return push(new SubNode(n, n.options));
} else if(n instanceof Node) {
return push(n);
Expand Down Expand Up @@ -247,8 +248,8 @@ export class GraphBuilder<V, M=V, R=M[]> {
return this.createMatcher(this.language, this.nodes, this.options);
}

protected createMatcher<C>(lang: Language, nodes: Node[], options: MatcherOptions<C>) {
return new Matcher(lang, nodes, options);
protected createMatcher<C>(lang: Language, nodes: Node[], options: MatcherOptions<C>): Matcher<C> {
return new DefaultMatcher(lang, nodes, options);
}

public static result<V>(matcher?: Matcher<any> | Predicate<V>, validator?: Predicate<V>): (builder: GraphBuilder<V, any>) => Node {
Expand All @@ -261,19 +262,19 @@ export class GraphBuilder<V, M=V, R=M[]> {
}
}

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

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

sub.recursive = ! matcher;

if(validator) {
let name = matcher instanceof Matcher ? matcher.options.name : builder.options.name;
let name = matcher instanceof DefaultMatcher ? matcher.options.name : builder.options.name;
if(validator.name) {
if(name) {
name += ':' + validator.name;
Expand Down
6 changes: 3 additions & 3 deletions src/graph/SubNode.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Node } from './Node';
import { Matcher, MatcherOptions, Encounter, Match, MatchingState, emptyState } from './matching';
import { DefaultMatcher, Matcher, MatcherOptions, Encounter, Match, MatchingState, emptyState } from './matching';
import { Predicate, alwaysTruePredicate } from '../utils/predicates';

/*
Expand Down Expand Up @@ -49,13 +49,13 @@ export class SubNode<V> extends Node {
*/
public partialFallback?: any;

constructor(roots: Matcher<V> | Node[], options: MatcherOptions<V>, filter?: Predicate<V>) {
constructor(roots: DefaultMatcher<V> | Node[], options: MatcherOptions<V>, filter?: Predicate<V>) {
super();

this.recursive = false;
this.filter = filter || alwaysTruePredicate;

if(roots instanceof Matcher) {
if(roots instanceof DefaultMatcher) {
// Roots is actually a matcher, copy the graph from the matcher
this.roots = roots.nodes;
this.state = roots.matchingState;
Expand Down
99 changes: 99 additions & 0 deletions src/graph/matching/DefaultMatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Encounter } from './Encounter';
import { Language } from '../../language/Language';
import { Node } from '../Node';

import { MatchingState, emptyState } from './MatchingState';
import { MatchSet } from './MatchSet';
import { MatchOptions } from './MatchOptions';
import { EncounterOptions } from './EncounterOptions';

export interface MatchReductionEncounter<RawData, MappedData> {
encounter: Encounter;

results: MatchSet<RawData>;

map: (object: RawData) => MappedData;
}

export interface MatcherOptions<V> extends EncounterOptions {
name?: string;

reducer?: (reduction: MatchReductionEncounter<any, any>) => V;

mapper?: (result: any, encounter: Encounter) => any;
}

/**
* Matcher that can match expressions against a graph.
*/
export class DefaultMatcher<V> {
public readonly language: Language;
public readonly nodes: Node[];
public readonly options: MatcherOptions<V>;

/**
* Internal state of this matcher that is accessed if it is used as a
* sub graph.
*/
public matchingState: MatchingState;

constructor(language: Language, nodes: Node[], options: MatcherOptions<V>) {
this.language = language;
this.nodes = nodes;

this.options = options;
this.matchingState = emptyState();
}

/**
* Match against the given expression.
*
* @param {string} expression
* @param {object} options
* @return {Promise}
*/
public match(expression: string, options: MatchOptions={}): Promise<V> {
if(typeof expression !== 'string') {
throw new Error('Can only match against string expressions');
}

const resolvedOptions = Object.assign({
onlyComplete: true
}, this.options, options);

const tokens = this.language.tokenize(expression);
const encounter = new Encounter(tokens, resolvedOptions);
encounter.outgoing = this.nodes;

const promise = encounter.next(0, 0)
.then(() => {
return encounter.matches;
});

if(this.options.reducer) {
const reducer = this.options.reducer;
return promise.then((results: MatchSet<any>) => reducer({
results,
encounter,
map: (object: any) => this.options.mapper
? this.options.mapper(object, encounter)
: object
}
));
} else {
return promise.then((results: MatchSet<any>) => {
const asArray = results.toArray();
let mapped;
if(this.options.mapper) {
const mapper = this.options.mapper;
mapped = asArray.map(match => mapper(match.data, encounter));
} else {
mapped = asArray.map(match => match.data);
}

// Forcefully convert into V type
return mapped as unknown as V;
});
}
}
}
94 changes: 5 additions & 89 deletions src/graph/matching/Matcher.ts
Original file line number Diff line number Diff line change
@@ -1,99 +1,15 @@
import { Encounter } from './Encounter';
import { Language } from '../../language/Language';
import { Node } from '../Node';

import { MatchingState, emptyState } from './MatchingState';
import { MatchSet } from './MatchSet';
import { MatchOptions } from './MatchOptions';
import { EncounterOptions } from './EncounterOptions';

export interface MatchReductionEncounter<RawData, MappedData> {
encounter: Encounter;

results: MatchSet<RawData>;

map: (object: RawData) => MappedData;
}

export interface MatcherOptions<V> extends EncounterOptions {
name?: string;

reducer?: (reduction: MatchReductionEncounter<any, any>) => V;

mapper?: (result: any, encounter: Encounter) => any;
}

/**
* Matcher that can match expressions against a graph.
*/
export class Matcher<V> {
public readonly language: Language;
public readonly nodes: Node[];
public readonly options: MatcherOptions<V>;

/**
* Internal state of this matcher that is accessed if it is used as a
* sub graph.
*/
public matchingState: MatchingState;

constructor(language: Language, nodes: Node[], options: MatcherOptions<V>) {
this.language = language;
this.nodes = nodes;

this.options = options;
this.matchingState = emptyState();
}

export interface Matcher<V> {
/**
* Match against the given expression.
*
* @param {string} expression
* @param {object} options
* @return {Promise}
* @param expression
* @param options
* @return
*/
public match(expression: string, options: MatchOptions={}): Promise<V> {
if(typeof expression !== 'string') {
throw new Error('Can only match against string expressions');
}

const resolvedOptions = Object.assign({
onlyComplete: true
}, this.options, options);

const tokens = this.language.tokenize(expression);
const encounter = new Encounter(tokens, resolvedOptions);
encounter.outgoing = this.nodes;

const promise = encounter.next(0, 0)
.then(() => {
return encounter.matches;
});

if(this.options.reducer) {
const reducer = this.options.reducer;
return promise.then((results: MatchSet<any>) => reducer({
results,
encounter,
map: (object: any) => this.options.mapper
? this.options.mapper(object, encounter)
: object
}
));
} else {
return promise.then((results: MatchSet<any>) => {
const asArray = results.toArray();
let mapped;
if(this.options.mapper) {
const mapper = this.options.mapper;
mapped = asArray.map(match => mapper(match.data, encounter));
} else {
mapped = asArray.map(match => match.data);
}

// Forcefully convert into V type
return mapped as unknown as V;
});
}
}
match(expression: string, options?: MatchOptions): Promise<V>;
}
1 change: 1 addition & 0 deletions src/graph/matching/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './Encounter';
export * from './EncounterOptions';
export * from './DefaultMatcher';
export * from './Match';
export * from './Matcher';
export * from './MatchHandler';
Expand Down
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { Language } from './language/Language';
import { IntentsBuilder } from './IntentsBuilder';
import { ActionsBuilder } from './ActionsBuilder';

export {
Matcher,
MatchOptions
} from './graph/matching';

export * from './IntentsBuilder';
export * from './ActionsBuilder';

Expand Down
4 changes: 2 additions & 2 deletions src/resolver/ResolverBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ResolvedIntent } from './ResolvedIntent';
import { Language } from '../language/Language';
import { LanguageSpecificValue, NodeConvertable } from '../values/base';
import { Collectable } from '../graph/CollectorNode';
import { Match } from '../graph/matching';
import { Match, DefaultMatcher } from '../graph/matching';
import { ResolvedIntents } from './ResolvedIntents';
import { GraphBuildable } from '../graph/GraphBuilder';

Expand Down Expand Up @@ -52,7 +52,7 @@ export class ResolverBuilder {
}

public add(...args: GraphBuildable<any>[]) {
if(args[0] instanceof Matcher) {
if(args[0] instanceof DefaultMatcher) {
/**
* If adding another parser for resolving intent just copy all
* of its nodes as they should work just fine with our own parser.
Expand Down
4 changes: 2 additions & 2 deletions src/resolver/ResolverParser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { GraphBuilder } from '../graph/GraphBuilder';
import { Matcher, MatcherOptions, EncounterOptions } from '../graph/matching';
import { MatcherOptions, EncounterOptions, DefaultMatcher } from '../graph/matching';

import { TokenNode } from '../graph/TokenNode';
import { ValueNode } from './ValueNode';
Expand Down Expand Up @@ -107,7 +107,7 @@ export class ResolverParser<V, M=V[]> extends GraphBuilder<V, M> {
}
}

class ResolvingMatcher<V> extends Matcher<V> {
class ResolvingMatcher<V> extends DefaultMatcher<V> {

public match(expression: string, options: EncounterOptions={}) {
options.matchIsEqual = options.partial
Expand Down

0 comments on commit 78fb265

Please sign in to comment.