Created
November 21, 2016 08:54
-
-
Save anhldbk/253e4772189c1dba6aada3c49a7ade3e to your computer and use it in GitHub Desktop.
Module for matching MQTT-like topics
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict' | |
const _ = require('lodash'), | |
path = require('path') | |
/** | |
* Class for matching MQTT-like topics | |
*/ | |
class EventTree { | |
constructor() { | |
this.root = new TopicNode() | |
} | |
/** | |
* Add an event | |
* @param {String} event The event. For example: `+/system/+`, `system/data`... | |
* | |
* An event is valid if it follow below rules: | |
* - Rule #1: If any part of the event is not `+` or `#`, then it must not contain `+` and '#' | |
* - Rule #2: Part `#` must be located at the end | |
* | |
* @param {Object} context Any data associated with the event | |
* @return {Boolean} If the event is valid, returns true. Otherwise, returns false. | |
*/ | |
add(event, context) { | |
var parts = event.split('/'), | |
i = 0 | |
// validate topics | |
i = _.findIndex(parts, (p) => { | |
if (p.length == 0) { // empty part | |
return true | |
} | |
if ('+' !== p) { | |
if ('#' === p) { | |
if (i != parts.length - 1) { | |
// for Rule #2 | |
return true | |
} | |
} else { | |
if (_.findIndex(p, m => (m === '#' | m === '+' | m === '')) != -1) { | |
return true | |
} | |
} | |
} | |
i += 1 | |
}) | |
if (i != -1) { | |
return false | |
} | |
_.reduce(parts, (node, part) => node.addChildNode(part, context), this.root) | |
return true | |
} | |
/** | |
* Match an event with pre-added topics | |
* @param {String} event The event which does NOT contain characters `+` and `#` | |
* @return {Array} If the event is valid, returns array of matched topics | |
* Otherwise, returns an empty array | |
*/ | |
match(event) { | |
var result = this._getNodes(event) | |
return _.map(result, node => node.path) | |
} | |
/** | |
* Get nodes associated with an event | |
* @param {String} event Event name | |
* @return {Array} Array of TreeNode | |
*/ | |
_getNodes(event) { | |
var parts = event.split('/'), | |
valid = _.findIndex(parts, m => (m === '#' | m === '+' | m === '')), | |
result = [] | |
if (!valid) { | |
//Invalid event. Must not contain characters `#` or `+`' | |
return [] | |
} | |
// recursively check | |
result = _.reduce( | |
parts, | |
(nodes, part) => { | |
var result = _.flatten( | |
_.map( | |
nodes, | |
(node) => node.isAny() ? node : node.matchChildNodes(part) | |
) | |
) | |
return result | |
}, [this.root] | |
) | |
// keep only leaf nodes | |
result = _.filter(result, node => node.isLeaf()) | |
return result | |
} | |
/** | |
* Get contexts associated with a specific event | |
* @param {String} event Event name | |
* @return {Array} Array of contexts | |
*/ | |
getContexts(event) { | |
var result = this._getNodes(event) | |
return _.flatten( | |
_.map(result, node => node.contexts) | |
) | |
} | |
} | |
class TopicNode { | |
constructor(label, parent, context) { | |
(_.isNil(label)) && (label = '') | |
if (parent instanceof TopicNode) { | |
this.path = path.join(parent.path, label) | |
} else { | |
this.path = label | |
} | |
this.label = label | |
this._isAny = (label == '#') | |
this.children = {} | |
if (_.isNil(context)) { | |
context = [] | |
} | |
if (!_.isArray(context)) { | |
context = [context] | |
} | |
this.contexts = context | |
} | |
/** | |
* Add a new child node | |
* @param {String} label Its label | |
* @param {Object} context Its context | |
* @return {TopicNode} If there's a child having the same label, returns that one. | |
* We also add more context to that node. | |
* Otherwise, a new node will be created and return | |
*/ | |
addChildNode(label, context) { | |
if (!_.isString(label) || label.length === 0) { | |
throw new Error('Invalid label') | |
} | |
var node = _.get(this.children, label) | |
if (_.isNil(node)) { | |
node = new TopicNode(label, this, context) | |
this.children[node.label] = node | |
} else { | |
(!_.isNil(context)) && (node.contexts.push(context)) | |
} | |
return node | |
} | |
matchChildNodes(label) { | |
var candidate = ['#', '+'], | |
result = [] | |
if (_.indexOf(candidate, label) == -1) { | |
candidate.push(label) | |
} | |
_.forEach(candidate, (c) => { | |
if (_.has(this.children, c)) { | |
result.push(this.children[c]) | |
} | |
}) | |
return result | |
} | |
isLeaf() { | |
return _.values(this.children).length == 0 | |
} | |
isAny() { | |
return this._isAny | |
} | |
} | |
module.exports = EventTree |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment