mirror of
https://github.com/dotnet/MQTTnet.git
synced 2026-10-16 09:13:02 +08:00
Utility class supporting dynamic topics using topic templates (#1932)
* 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>
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
// 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.Threading;
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard1.3;netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0;net6.0;net7.0</TargetFrameworks>
|
||||
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">$(TargetFrameworks);net452;net461;net48</TargetFrameworks>
|
||||
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' AND '$(MSBuildRuntimeType)' != 'Core' ">$(TargetFrameworks);uap10.0</TargetFrameworks>
|
||||
|
||||
<AssemblyName>MQTTnet.Extensions.TopicTemplate</AssemblyName>
|
||||
<RootNamespace>MQTTnet.Extensions.TopicTemplate</RootNamespace>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
<Company>The contributors of MQTTnet</Company>
|
||||
<Product>MQTTnet</Product>
|
||||
<Description>
|
||||
This is an extension library which provides mqtt topic templating logic to support dispatch,
|
||||
routing and similar functionality based on the well known moustache syntax, aka
|
||||
AsyncAPI dynamic channel address.
|
||||
</Description>
|
||||
<Authors>The contributors of MQTTnet</Authors>
|
||||
<PackageId>MQTTnet.Extensions.TopicTemplate</PackageId>
|
||||
<SignAssembly>false</SignAssembly>
|
||||
<DelaySign>false</DelaySign>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<Copyright>Copyright (c) .NET Foundation and Contributors</Copyright>
|
||||
<PackageProjectUrl>https://github.com/dotnet/MQTTnet</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/dotnet/MQTTnet.git</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageTags>MQTT Message Queue Telemetry MQTTClient Messaging Routing AsyncAPI Template</PackageTags>
|
||||
<NeutralLanguage>en-US</NeutralLanguage>
|
||||
<PackageIcon>nuget.png</PackageIcon>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageReleaseNotes>For release notes please go to MQTTnet release notes (https://www.nuget.org/packages/MQTTnet/).</PackageReleaseNotes>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>1591;NETSDK1138</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(TargetFramework)' == 'uap10.0'">
|
||||
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
|
||||
<NugetTargetMoniker>UAP,Version=v10.0</NugetTargetMoniker>
|
||||
<TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
|
||||
<TargetPlatformVersion>10.0.18362.0</TargetPlatformVersion>
|
||||
<TargetPlatformMinVersion>10.0.10240.0</TargetPlatformMinVersion>
|
||||
<TargetFrameworkIdentifier>.NETCore</TargetFrameworkIdentifier>
|
||||
<TargetFrameworkVersion>v5.0</TargetFrameworkVersion>
|
||||
<DefineConstants>$(DefineConstants);WINDOWS_UWP</DefineConstants>
|
||||
<DefaultLanguage>en</DefaultLanguage>
|
||||
<LanguageTargets>$(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets</LanguageTargets>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\Images\nuget.png">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MQTTnet\MQTTnet.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,334 @@
|
||||
// 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
// 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.Linq;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Extensions.TopicTemplate
|
||||
{
|
||||
public static class TopicTemplateExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Modify this message builder to respond to a given message. The
|
||||
/// message's response topic and correlation data are included
|
||||
/// in the message builder.
|
||||
/// </summary>
|
||||
/// <param
|
||||
/// name="builder">
|
||||
/// a message builder
|
||||
/// </param>
|
||||
/// <param
|
||||
/// name="message">
|
||||
/// a message with a response topic
|
||||
/// </param>
|
||||
/// <returns>a message builder</returns>
|
||||
/// <see
|
||||
/// cref="BuildResponse" />
|
||||
public static MqttApplicationMessageBuilder AsResponseTo(this MqttApplicationMessageBuilder builder, MqttApplicationMessage message)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(message.ResponseTopic))
|
||||
{
|
||||
throw new ArgumentException("message does not have a response topic");
|
||||
}
|
||||
|
||||
return builder.WithTopic(message.ResponseTopic).WithCorrelationData(message.CorrelationData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the filter topic according to the template, with
|
||||
/// remaining template parameters substituted by single-level
|
||||
/// wildcard.
|
||||
/// </summary>
|
||||
/// <param
|
||||
/// name="builder">
|
||||
/// a filter builder
|
||||
/// </param>
|
||||
/// <param
|
||||
/// name="topicTemplate">
|
||||
/// a topic template
|
||||
/// </param>
|
||||
/// <param
|
||||
/// name="subscribeTreeRoot">
|
||||
/// whether to subscribe to the whole topic tree
|
||||
/// </param>
|
||||
/// <returns>the modified topic filter</returns>
|
||||
public static MqttTopicFilterBuilder BuildFilter(this MqttTopicTemplate topicTemplate, bool subscribeTreeRoot = false)
|
||||
{
|
||||
return new MqttTopicFilterBuilder().WithTopicTemplate(topicTemplate, subscribeTreeRoot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return a message builder to respond to this message. The
|
||||
/// message's response topic and correlation data are included
|
||||
/// in the response message builder.
|
||||
/// </summary>
|
||||
/// <param
|
||||
/// name="message">
|
||||
/// a message with a response topic
|
||||
/// </param>
|
||||
/// <returns>a message builder</returns>
|
||||
/// <see
|
||||
/// cref="AsResponseTo" />
|
||||
public static MqttApplicationMessageBuilder BuildResponse(this MqttApplicationMessage message)
|
||||
{
|
||||
return new MqttApplicationMessageBuilder().AsResponseTo(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return whether the message matches the given topic template.
|
||||
/// </summary>
|
||||
/// <param
|
||||
/// name="message">
|
||||
/// a message
|
||||
/// </param>
|
||||
/// <param
|
||||
/// name="topicTemplate">
|
||||
/// a topic template
|
||||
/// </param>
|
||||
/// <param
|
||||
/// name="subtree">
|
||||
/// whether to include the topic subtree
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
public static bool MatchesTopicTemplate(this MqttApplicationMessage message, MqttTopicTemplate topicTemplate, bool subtree = false)
|
||||
{
|
||||
return topicTemplate.MatchesTopic(message.Topic, subtree);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the filter topic according to the template, with
|
||||
/// template parameters substituted by a single-level
|
||||
/// wildcard.
|
||||
/// </summary>
|
||||
/// <param
|
||||
/// name="builder">
|
||||
/// a filter builder
|
||||
/// </param>
|
||||
/// <param
|
||||
/// name="topicTemplate">
|
||||
/// a topic template
|
||||
/// </param>
|
||||
/// <param
|
||||
/// name="subscribeTreeRoot">
|
||||
/// whether to subscribe to the whole topic tree
|
||||
/// </param>
|
||||
/// <returns>the modified topic filter</returns>
|
||||
public static MqttTopicFilterBuilder WithTopicTemplate(this MqttTopicFilterBuilder builder, MqttTopicTemplate topicTemplate, bool subscribeTreeRoot = false)
|
||||
{
|
||||
return builder.WithTopic(subscribeTreeRoot ? topicTemplate.TopicTreeRootFilter : topicTemplate.TopicFilter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the publication topic according to the topic template. The template
|
||||
/// must not have remaining (unset) parameters or contain wildcards.
|
||||
/// </summary>
|
||||
/// <param
|
||||
/// name="builder">
|
||||
/// a message builder
|
||||
/// </param>
|
||||
/// <param
|
||||
/// name="topicTemplate">
|
||||
/// a parameterless topic template
|
||||
/// </param>
|
||||
/// <returns>the modified message builder</returns>
|
||||
/// <exception
|
||||
/// cref="ArgumentException">
|
||||
/// if the topic template has parameters
|
||||
/// </exception>
|
||||
public static MqttApplicationMessageBuilder WithTopicTemplate(this MqttApplicationMessageBuilder builder, MqttTopicTemplate topicTemplate)
|
||||
{
|
||||
if (topicTemplate.Parameters.Any())
|
||||
{
|
||||
throw new ArgumentException("topic templates must be parameter-less when sending " + topicTemplate.Template);
|
||||
}
|
||||
|
||||
MqttTopicValidator.ThrowIfInvalid(topicTemplate.Template);
|
||||
return builder.WithTopic(topicTemplate.Template);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// 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 Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using MQTTnet.Extensions.TopicTemplate;
|
||||
|
||||
namespace MQTTnet.Tests.Extensions
|
||||
{
|
||||
[TestClass]
|
||||
public sealed class MqttTopicTemplate_Tests
|
||||
{
|
||||
[TestMethod]
|
||||
public void BasicOps()
|
||||
{
|
||||
var template = new MqttTopicTemplate("A/B/{param}/D");
|
||||
Assert.AreEqual("A/B/C/D", template.WithParameter("param", "D"));
|
||||
Assert.AreEqual("A/B/+/D", template.WithoutParameter("param"));
|
||||
Assert.AreEqual("A/B/+/D", template.TopicFilter);
|
||||
Assert.AreEqual("A/B/+/D/#", template.TopicTreeRootFilter);
|
||||
|
||||
// corner cases
|
||||
Assert.AreEqual("", new MqttTopicTemplate("").TopicFilter);
|
||||
Assert.AreEqual("#", new MqttTopicTemplate("#").TopicFilter);
|
||||
Assert.AreEqual("+", new MqttTopicTemplate("+").TopicFilter);
|
||||
Assert.AreEqual("/", new MqttTopicTemplate("/").TopicFilter);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void BasicOpsWithHash()
|
||||
{
|
||||
var template = new MqttTopicTemplate("A/B/{param}/D/#");
|
||||
Assert.AreEqual("A/B/C/D/#", template.WithParameter("param", "D"));
|
||||
Assert.AreEqual("A/B/+/D/#", template.WithoutParameter("param"));
|
||||
Assert.AreEqual("A/B/+/D/#", template.TopicFilter);
|
||||
Assert.AreEqual("A/B/+/D/#", template.TopicTreeRootFilter);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void RejectsReservedChars1()
|
||||
{
|
||||
var template = new MqttTopicTemplate("A/B/{foo}/D");
|
||||
template.WithParameter("foo", "a#");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void RejectsReservedChars2()
|
||||
{
|
||||
var template = new MqttTopicTemplate("A/B/{foo}/D");
|
||||
template.WithParameter("foo", "e/f");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DynamicRoutingSupport()
|
||||
{
|
||||
var v1template = new MqttTopicTemplate("A/v1/{id}/F");
|
||||
var v2template = new MqttTopicTemplate("A/v2/foo/{id}/{bar}");
|
||||
var incoming_v1_topic = "A/v1/32/F";
|
||||
|
||||
var compat_v2_topic = v2template
|
||||
.WithParameterValuesFrom(
|
||||
v1template.ParseParameterValues(incoming_v1_topic))
|
||||
.WithParameter("bar", "unknown");
|
||||
|
||||
// id: 32 was relocated and bar: unknown added
|
||||
Assert.AreEqual("A/v2/foo/32/unknown", compat_v2_topic.Template);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SubscriptionSupport()
|
||||
{
|
||||
var template = new MqttTopicTemplate("A/v1/{param}/F");
|
||||
var filter = template.BuildFilter()
|
||||
.WithAtLeastOnceQoS()
|
||||
.WithNoLocal()
|
||||
.Build();
|
||||
Assert.AreEqual("A/v1/+/F", filter.Topic);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CanonicalPrefixFilter()
|
||||
{
|
||||
var template1 = new MqttTopicTemplate("A/v1/{param}/F");
|
||||
var template2 = new MqttTopicTemplate("A/v1/E/F");
|
||||
var template3 = new MqttTopicTemplate("A/v1/E/F/G/H");
|
||||
var canonicalFilter = MqttTopicTemplate.FindCanonicalPrefix(new[] { template1, template2, template3 });
|
||||
Assert.AreEqual("A/v1/+/+", canonicalFilter.TopicFilter);
|
||||
Assert.AreEqual("A/v1/+/+/#", canonicalFilter.TopicTreeRootFilter);
|
||||
|
||||
var template4 = new MqttTopicTemplate("A/v2/{prefix}");
|
||||
var canonicalFilter2 = MqttTopicTemplate.FindCanonicalPrefix(new[] { template1, template2, template3, template4 });
|
||||
Assert.AreEqual("A/+/+", canonicalFilter2.TopicFilter);
|
||||
Assert.AreEqual("A/+/#", canonicalFilter2.TopicTreeRootFilter);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
<ProjectReference Include="..\..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj" />
|
||||
<ProjectReference Include="..\..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj" />
|
||||
<ProjectReference Include="..\..\Source\MQTTnet\MQTTnet.csproj" />
|
||||
<ProjectReference Include="..\MQTTnet.Extensions.TopicTemplate\MQTTnet.Extensions.TopicTemplate.csproj" />
|
||||
<ProjectReference Include="..\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user