mirror of
https://github.com/dotnet/MQTTnet.git
synced 2026-10-15 00:15:15 +08:00
* Add MqttTopicTemplate
The topic template class is a way to deal with dynamic topics that
is less cumbersome and error-prone than String.Format or
other string fu one might use to handle dynamic topics.
Example:
MyCity/{street}/{crossing}/lights
'street' and 'crossing' would be parameters.
The class and its extension methods are intended to support
dynamic subscription/publication, message-topic matching and
routing. The template syntax is modelled after the well-known
moustache syntax, which conincidentally is also published
as the 'AsyncAPI channel address expressions'.
* Add MQTTnet.Extensions.TopicTemplate to solution
* Apply code style from team shared resharper settings.
Add missing file headers.
* Update release notes
---------
Co-authored-by: christian <6939810+chkr1011@users.noreply.github.com>
334 lines
12 KiB
C#
334 lines
12 KiB
C#
// Licensed to the .NET Foundation under one or more agreements.
|
|
// The .NET Foundation licenses this file to you under the MIT license.
|
|
// See the LICENSE file in the project root for more information.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading;
|
|
using MQTTnet.Protocol;
|
|
|
|
namespace MQTTnet.Extensions.TopicTemplate
|
|
{
|
|
/// <summary>
|
|
/// A topic template is an MQTT topic filter string that may contain
|
|
/// segments in curly braces called parameters. This well-known
|
|
/// 'moustache' syntax also matches AsyncAPI Channel Address Expressions.
|
|
/// The topic template is designed to support dynamic subscription/publication,
|
|
/// message-topic matching and routing. It is intended to be more convenient
|
|
/// than String.Format() for aforementioned purposes.
|
|
/// </summary>
|
|
/// <example>
|
|
/// topic/subtopic/{parameter}/{otherParameter}
|
|
/// </example>
|
|
public sealed class MqttTopicTemplate : IEquatable<MqttTopicTemplate>
|
|
{
|
|
static readonly Regex MoustacheRegex = new Regex("{([^/]+?)}", RegexOptions.Compiled);
|
|
|
|
readonly string[] _parameterSegments;
|
|
|
|
string _topicFilter;
|
|
|
|
/// <summary>
|
|
/// Create a topic template from an mqtt topic filter with moustache placeholders.
|
|
/// </summary>
|
|
/// <param
|
|
/// name="topicTemplate">
|
|
/// </param>
|
|
/// <exception
|
|
/// cref="ArgumentNullException">
|
|
/// </exception>
|
|
public MqttTopicTemplate(string topicTemplate)
|
|
{
|
|
if (topicTemplate == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(topicTemplate));
|
|
}
|
|
|
|
MqttTopicValidator.ThrowIfInvalidSubscribe(topicTemplate);
|
|
|
|
Template = topicTemplate;
|
|
_parameterSegments = topicTemplate.Split(MqttTopicFilterComparer.LevelSeparator)
|
|
.Select(segment => MoustacheRegex.Match(segment).Groups[1].Value)
|
|
.Select(s => s.Length > 0 ? s : null)
|
|
.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Yield the template parameter names.
|
|
/// </summary>
|
|
public IEnumerable<string> Parameters => _parameterSegments.Where(s => s != null);
|
|
|
|
/// <summary>
|
|
/// The topic template string representation, e.g. A/B/{foo}/D.
|
|
/// </summary>
|
|
public string Template { get; }
|
|
|
|
/// <summary>
|
|
/// The topic template as an MQTT topic filter (+ substituted for all parameters). If the template
|
|
/// ends with a multi-level wildcard (hash), this will be reflected here.
|
|
/// </summary>
|
|
public string TopicFilter
|
|
{
|
|
get
|
|
{
|
|
LazyInitializer.EnsureInitialized(ref _topicFilter, () => MoustacheRegex.Replace(Template, MqttTopicFilterComparer.SingleLevelWildcard.ToString()));
|
|
return _topicFilter;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return the topic filter of this template, ending with a multi-level wildcard (hash).
|
|
/// </summary>
|
|
public string TopicTreeRootFilter
|
|
{
|
|
get
|
|
{
|
|
var filter = TopicFilter;
|
|
// append slash if neccessary
|
|
if (filter.Length > 0 && !filter.EndsWith(MqttTopicFilterComparer.LevelSeparator.ToString()) &&
|
|
!filter.EndsWith(MqttTopicFilterComparer.MultiLevelWildcard.ToString()))
|
|
{
|
|
filter += MqttTopicFilterComparer.LevelSeparator;
|
|
}
|
|
|
|
// append hash if neccessary
|
|
if (!filter.EndsWith(MqttTopicFilterComparer.MultiLevelWildcard.ToString()))
|
|
{
|
|
filter += MqttTopicFilterComparer.MultiLevelWildcard;
|
|
}
|
|
|
|
return filter;
|
|
}
|
|
}
|
|
|
|
public bool Equals(MqttTopicTemplate other)
|
|
{
|
|
return other != null && Template == other.Template;
|
|
}
|
|
|
|
public override bool Equals(object obj)
|
|
{
|
|
if (ReferenceEquals(null, obj))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (ReferenceEquals(this, obj))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (obj.GetType() != GetType())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return Equals((MqttTopicTemplate)obj);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determine the shortest common prefix of the given templates. Partial segments
|
|
/// are not returned.
|
|
/// </summary>
|
|
/// <param
|
|
/// name="templates">
|
|
/// topic templates
|
|
/// </param>
|
|
/// <returns></returns>
|
|
public static MqttTopicTemplate FindCanonicalPrefix(IEnumerable<MqttTopicTemplate> templates)
|
|
{
|
|
string root = null;
|
|
|
|
string CommonPrefix(string a, string b)
|
|
{
|
|
var maxIndex = Math.Min(a.Length, b.Length) - 1;
|
|
for (var i = 0; i <= maxIndex; i++)
|
|
{
|
|
if (a[i] != b[i])
|
|
{
|
|
return a.Substring(0, i);
|
|
}
|
|
}
|
|
|
|
return a.Substring(0, maxIndex);
|
|
}
|
|
|
|
foreach (var topic in from template in templates select template.Template)
|
|
{
|
|
root = root == null ? topic : CommonPrefix(root, topic);
|
|
}
|
|
|
|
root = root ?? "";
|
|
|
|
return new MqttTopicTemplate(root.Substring(0, root.LastIndexOf('/')));
|
|
}
|
|
|
|
public override int GetHashCode()
|
|
{
|
|
return Template.GetHashCode();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test if this topic template matches a given topic.
|
|
/// </summary>
|
|
/// <param
|
|
/// name="topic">
|
|
/// a fully specified topic
|
|
/// </param>
|
|
/// <param
|
|
/// name="subtree">
|
|
/// true to match including the subtree (multi-level wildcard)
|
|
/// </param>
|
|
/// <returns>true iff the topic matches the template's filter</returns>
|
|
/// <exception
|
|
/// cref="InvalidOperationException">
|
|
/// </exception>
|
|
/// <exception
|
|
/// cref="ArgumentException">
|
|
/// if the topic is invalid
|
|
/// </exception>
|
|
public bool MatchesTopic(string topic, bool subtree = false)
|
|
{
|
|
var comparison = MqttTopicFilterComparer.Compare(topic, subtree ? TopicTreeRootFilter : TopicFilter);
|
|
if (comparison == MqttTopicFilterCompareResult.FilterInvalid)
|
|
{
|
|
throw new InvalidOperationException("Invalid filter");
|
|
}
|
|
|
|
if (comparison == MqttTopicFilterCompareResult.TopicInvalid)
|
|
{
|
|
throw new ArgumentException("Invalid topic", nameof(topic));
|
|
}
|
|
|
|
return comparison == MqttTopicFilterCompareResult.IsMatch;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extract the parameter values from a topic corresponding to the template
|
|
/// parameters. The topic has to match this template.
|
|
/// </summary>
|
|
/// <param
|
|
/// name="topic">
|
|
/// the topic
|
|
/// </param>
|
|
/// <returns>an enumeration of (parameter, index, value)</returns>
|
|
public IEnumerable<(string parameter, int index, string value)> ParseParameterValues(string topic)
|
|
{
|
|
if (!MatchesTopic(topic))
|
|
{
|
|
throw new ArgumentException("the topic has to match this template", nameof(topic));
|
|
}
|
|
|
|
return parseParameterValuesInternal(topic);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extract the parameter values from the message topic corresponding to the template
|
|
/// parameters. The message topic has to match this topic template.
|
|
/// </summary>
|
|
/// <param
|
|
/// name="message">
|
|
/// the message
|
|
/// </param>
|
|
/// <returns>an enumeration of (parameter, index, value)</returns>
|
|
public IEnumerable<(string parameter, int index, string value)> ParseParameterValues(MqttApplicationMessage message)
|
|
{
|
|
return ParseParameterValues(message.Topic);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Try to set a parameter to a given value. If the parameter is not present,
|
|
/// this is returned. The value must not contain slashes.
|
|
/// </summary>
|
|
/// <param
|
|
/// name="parameter">
|
|
/// a template parameter
|
|
/// </param>
|
|
/// <param
|
|
/// name="value">
|
|
/// a string
|
|
/// </param>
|
|
/// <returns></returns>
|
|
public MqttTopicTemplate TrySetParameter(string parameter, string value)
|
|
{
|
|
if (parameter != null && _parameterSegments.Contains(parameter))
|
|
{
|
|
return WithParameter(parameter, value);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Replace the given parameter with a single-level wildcard (plus sign).
|
|
/// </summary>
|
|
/// <param
|
|
/// name="parameter">
|
|
/// parameter name
|
|
/// </param>
|
|
/// <returns>the topic template (without the parameter)</returns>
|
|
public MqttTopicTemplate WithoutParameter(string parameter)
|
|
{
|
|
return WithParameter(parameter, MqttTopicFilterComparer.SingleLevelWildcard.ToString());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Substitute a parameter with a given value, thus removing the parameter. If the parameter is not present,
|
|
/// the method trows. The value must not contain slashes.
|
|
/// </summary>
|
|
/// <param
|
|
/// name="parameter">
|
|
/// a template parameter
|
|
/// </param>
|
|
/// <param
|
|
/// name="value">
|
|
/// a string
|
|
/// </param>
|
|
/// <exception
|
|
/// cref="ArgumentException">
|
|
/// when the parameter is not present
|
|
/// </exception>
|
|
/// <returns>the topic template (without the parameter)</returns>
|
|
public MqttTopicTemplate WithParameter(string parameter, string value)
|
|
{
|
|
if (value == null || string.IsNullOrEmpty(parameter) || !_parameterSegments.Contains(parameter) || value.Contains(MqttTopicFilterComparer.LevelSeparator) ||
|
|
value.Contains(MqttTopicFilterComparer.MultiLevelWildcard))
|
|
{
|
|
throw new ArgumentException("parameter must exist and value must not contain slashes.");
|
|
}
|
|
|
|
var moustache = "{" + parameter + "}";
|
|
return new MqttTopicTemplate(Template.Replace(moustache, value));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reuse parameters as they are extracted using another topic template on this template
|
|
/// when the parameter name matches. Useful
|
|
/// for compatibility routing.
|
|
/// </summary>
|
|
/// <param
|
|
/// name="parameters">
|
|
/// </param>
|
|
/// <returns></returns>
|
|
public MqttTopicTemplate WithParameterValuesFrom(IEnumerable<(string parameter, int index, string value)> parameters)
|
|
{
|
|
return parameters.Aggregate(this, (t, p) => t.TrySetParameter(p.parameter, p.value));
|
|
}
|
|
|
|
IEnumerable<(string parameter, int index, string value)> parseParameterValuesInternal(string topic)
|
|
{
|
|
// because we have a match, we know the segment array is at least the template's length
|
|
var segments = topic.Split(MqttTopicFilterComparer.LevelSeparator);
|
|
for (var i = 0; i < _parameterSegments.Length; i++)
|
|
{
|
|
var name = _parameterSegments[i];
|
|
if (name != null)
|
|
{
|
|
yield return (name, i, segments[i]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |