mirror of
https://github.com/dotnet/MQTTnet.git
synced 2026-10-15 00:15:15 +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:
@@ -2,3 +2,4 @@
|
|||||||
* [Core] The package inspector is now fully async (#1941).
|
* [Core] The package inspector is now fully async (#1941).
|
||||||
* [Client] Added a dedicated exception when the client is not connected (#1954, thanks to @marcpiulachs).
|
* [Client] Added a dedicated exception when the client is not connected (#1954, thanks to @marcpiulachs).
|
||||||
* [ManagedClient] Added a new event (SubscriptionsChangedAsync) which is fired when a subscription or unsubscription was made (#1894, thanks to @pdufrene).
|
* [ManagedClient] Added a new event (SubscriptionsChangedAsync) which is fired when a subscription or unsubscription was made (#1894, thanks to @pdufrene).
|
||||||
|
* [TopicTemplate] Added new extension which provides a template engine for topics (#1932, thanks to @simonthum).
|
||||||
|
|||||||
+13
-9
@@ -7,9 +7,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet", "Source\MQTTnet\M
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B3F60ECB-45BA-4C66-8903-8BB89CA67998}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B3F60ECB-45BA-4C66-8903-8BB89CA67998}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
CODE-OF-CONDUCT.md = CODE-OF-CONDUCT.md
|
||||||
LICENSE = LICENSE
|
LICENSE = LICENSE
|
||||||
README.md = README.md
|
README.md = README.md
|
||||||
CODE-OF-CONDUCT.md = CODE-OF-CONDUCT.md
|
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.AspNetCore", "Source\MQTTnet.AspnetCore\MQTTnet.AspNetCore.csproj", "{F10C4060-F7EE-4A83-919F-FF723E72F94A}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.AspNetCore", "Source\MQTTnet.AspnetCore\MQTTnet.AspNetCore.csproj", "{F10C4060-F7EE-4A83-919F-FF723E72F94A}"
|
||||||
@@ -20,17 +20,19 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Extensions.ManagedC
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Extensions.WebSocket4Net", "Source\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj", "{2BD01D53-4CA5-4142-BE8D-313876395E3E}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Extensions.WebSocket4Net", "Source\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj", "{2BD01D53-4CA5-4142-BE8D-313876395E3E}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.Samples", "Samples\MQTTnet.Samples.csproj", "{71CF35F5-3327-4A91-AAF4-5340F6701771}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Samples", "Samples\MQTTnet.Samples.csproj", "{71CF35F5-3327-4A91-AAF4-5340F6701771}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.Tests", "Source\MQTTnet.Tests\MQTTnet.Tests.csproj", "{B270F32A-9F3E-42EE-A989-813E35E29ADB}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Tests", "Source\MQTTnet.Tests\MQTTnet.Tests.csproj", "{B270F32A-9F3E-42EE-A989-813E35E29ADB}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.AspNetCore.Tests", "Source\MQTTnet.AspNetCore.Tests\MQTTnet.AspNetCore.Tests.csproj", "{A238BBBF-C75F-482D-9CC3-BB34ABA9B675}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.AspNetCore.Tests", "Source\MQTTnet.AspNetCore.Tests\MQTTnet.AspNetCore.Tests.csproj", "{A238BBBF-C75F-482D-9CC3-BB34ABA9B675}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.Benchmarks", "Source\MQTTnet.Benchmarks\MQTTnet.Benchmarks.csproj", "{2F516E76-AAC4-4219-B7D1-34CDD3CFF381}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Benchmarks", "Source\MQTTnet.Benchmarks\MQTTnet.Benchmarks.csproj", "{2F516E76-AAC4-4219-B7D1-34CDD3CFF381}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.TestApp", "Source\MQTTnet.TestApp\MQTTnet.TestApp.csproj", "{175D5340-CC5B-4542-939D-4E7D15A0BC8D}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.TestApp", "Source\MQTTnet.TestApp\MQTTnet.TestApp.csproj", "{175D5340-CC5B-4542-939D-4E7D15A0BC8D}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.AspTestApp", "Source\MQTTnet.AspTestApp\MQTTnet.AspTestApp.csproj", "{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.AspTestApp", "Source\MQTTnet.AspTestApp\MQTTnet.AspTestApp.csproj", "{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Extensions.TopicTemplate", "Source\MQTTnet.Extensions.TopicTemplate\MQTTnet.Extensions.TopicTemplate.csproj", "{374D5861-F7A6-42A2-8CBE-040846000EF9}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@@ -82,12 +84,14 @@ Global
|
|||||||
{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Release|Any CPU.Build.0 = Release|Any CPU
|
{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{374D5861-F7A6-42A2-8CBE-040846000EF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{374D5861-F7A6-42A2-8CBE-040846000EF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{374D5861-F7A6-42A2-8CBE-040846000EF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{374D5861-F7A6-42A2-8CBE-040846000EF9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {07536672-5CBC-4BE3-ACE0-708A431A7894}
|
SolutionGuid = {07536672-5CBC-4BE3-ACE0-708A431A7894}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
|
|||||||
@@ -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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
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.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj" />
|
||||||
<ProjectReference Include="..\..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj" />
|
<ProjectReference Include="..\..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj" />
|
||||||
<ProjectReference Include="..\..\Source\MQTTnet\MQTTnet.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" />
|
<ProjectReference Include="..\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user