diff --git a/src/graph/GraphBuilder.ts b/src/graph/GraphBuilder.ts index 3e09fac..4f7f3f8 100644 --- a/src/graph/GraphBuilder.ts +++ b/src/graph/GraphBuilder.ts @@ -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'; @@ -151,7 +152,7 @@ export class GraphBuilder { 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); @@ -247,8 +248,8 @@ export class GraphBuilder { return this.createMatcher(this.language, this.nodes, this.options); } - protected createMatcher(lang: Language, nodes: Node[], options: MatcherOptions) { - return new Matcher(lang, nodes, options); + protected createMatcher(lang: Language, nodes: Node[], options: MatcherOptions): Matcher { + return new DefaultMatcher(lang, nodes, options); } public static result(matcher?: Matcher | Predicate, validator?: Predicate): (builder: GraphBuilder) => Node { @@ -261,19 +262,19 @@ export class GraphBuilder { } } - 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) { - 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; diff --git a/src/graph/SubNode.ts b/src/graph/SubNode.ts index ef728bf..949c03e 100644 --- a/src/graph/SubNode.ts +++ b/src/graph/SubNode.ts @@ -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'; /* @@ -49,13 +49,13 @@ export class SubNode extends Node { */ public partialFallback?: any; - constructor(roots: Matcher | Node[], options: MatcherOptions, filter?: Predicate) { + constructor(roots: DefaultMatcher | Node[], options: MatcherOptions, filter?: Predicate) { 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; diff --git a/src/graph/matching/DefaultMatcher.ts b/src/graph/matching/DefaultMatcher.ts new file mode 100644 index 0000000..0f727f6 --- /dev/null +++ b/src/graph/matching/DefaultMatcher.ts @@ -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 { + encounter: Encounter; + + results: MatchSet; + + map: (object: RawData) => MappedData; +} + +export interface MatcherOptions extends EncounterOptions { + name?: string; + + reducer?: (reduction: MatchReductionEncounter) => V; + + mapper?: (result: any, encounter: Encounter) => any; +} + +/** + * Matcher that can match expressions against a graph. + */ +export class DefaultMatcher { + public readonly language: Language; + public readonly nodes: Node[]; + public readonly options: MatcherOptions; + + /** + * 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) { + 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 { + 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) => reducer({ + results, + encounter, + map: (object: any) => this.options.mapper + ? this.options.mapper(object, encounter) + : object + } + )); + } else { + return promise.then((results: MatchSet) => { + 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; + }); + } + } +} diff --git a/src/graph/matching/Matcher.ts b/src/graph/matching/Matcher.ts index ea57c4a..662d377 100644 --- a/src/graph/matching/Matcher.ts +++ b/src/graph/matching/Matcher.ts @@ -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 { - encounter: Encounter; - - results: MatchSet; - - map: (object: RawData) => MappedData; -} - -export interface MatcherOptions extends EncounterOptions { - name?: string; - - reducer?: (reduction: MatchReductionEncounter) => V; - - mapper?: (result: any, encounter: Encounter) => any; -} /** * Matcher that can match expressions against a graph. */ -export class Matcher { - public readonly language: Language; - public readonly nodes: Node[]; - public readonly options: MatcherOptions; - - /** - * 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) { - this.language = language; - this.nodes = nodes; - - this.options = options; - this.matchingState = emptyState(); - } - +export interface Matcher { /** * 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 { - 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) => reducer({ - results, - encounter, - map: (object: any) => this.options.mapper - ? this.options.mapper(object, encounter) - : object - } - )); - } else { - return promise.then((results: MatchSet) => { - 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; } diff --git a/src/graph/matching/index.ts b/src/graph/matching/index.ts index 90817e3..e29c55d 100644 --- a/src/graph/matching/index.ts +++ b/src/graph/matching/index.ts @@ -1,5 +1,6 @@ export * from './Encounter'; export * from './EncounterOptions'; +export * from './DefaultMatcher'; export * from './Match'; export * from './Matcher'; export * from './MatchHandler'; diff --git a/src/index.ts b/src/index.ts index 0c73763..8aa10cb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/resolver/ResolverBuilder.ts b/src/resolver/ResolverBuilder.ts index 628f477..d5b196f 100644 --- a/src/resolver/ResolverBuilder.ts +++ b/src/resolver/ResolverBuilder.ts @@ -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'; @@ -52,7 +52,7 @@ export class ResolverBuilder { } public add(...args: GraphBuildable[]) { - 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. diff --git a/src/resolver/ResolverParser.ts b/src/resolver/ResolverParser.ts index 955e447..abd1d4f 100644 --- a/src/resolver/ResolverParser.ts +++ b/src/resolver/ResolverParser.ts @@ -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'; @@ -107,7 +107,7 @@ export class ResolverParser extends GraphBuilder { } } -class ResolvingMatcher extends Matcher { +class ResolvingMatcher extends DefaultMatcher { public match(expression: string, options: EncounterOptions={}) { options.matchIsEqual = options.partial