fix(mqtt): key tag definitions by RawPath, not the TagConfig blob
Task 2 review follow-up. The plan specified MqttEquipmentTagParser.TryParse(reference)
with def.Name = the TagConfig blob, and told us to mirror a type named
ModbusEquipmentTagParser. Both are plan defects:
- EquipmentTagRefResolver documents that a v3 driver reference "is now always a
RawPath" and that the blob-parse fallback is retired. Keying Name by the blob
would make OnDataChange publish under a reference that never matches the
RawPath-keyed fan-out in DriverHostActor - silently dead in production with
every unit test still green.
- ModbusEquipmentTagParser does not exist. The six sibling drivers all use
<Driver>TagDefinitionFactory.FromTagConfig(tagConfig, rawPath, out def).
Changes:
- Rename MqttEquipmentTagParser -> MqttTagDefinitionFactory; TryParse(reference,
out def) -> FromTagConfig(tagConfig, rawPath, out def) setting Name: rawPath,
matching ModbusTagDefinitionFactory's structure, param docs and guard order.
- Pin the identity contract with a dedicated test so a regression to blob-keying
goes red.
- Read qos with the same strictness as the enums: a present-but-invalid qos
("high" / 1.5 / 5 / null) now rejects the tag and is warned by Inspect,
instead of being silently absorbed into the driver-level default and handing
the operator a weaker delivery guarantee than the one they authored.
- No ToTagConfig inverse: the siblings carry one solely for their Driver.<X>.Cli
project, the MQTT plan defines none, and the AdminUI editor template
references no driver factory. Recorded as an explicit YAGNI call in the type
doc rather than added speculatively.
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
@@ -1,164 +0,0 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Mqtt;
|
||||
|
||||
/// <summary>
|
||||
/// Turns an authored equipment tag's opaque wire <c>reference</c> — its <c>TagConfig</c> JSON blob
|
||||
/// — into a typed <see cref="MqttTagDefinition"/>. Mirrors <c>ModbusTagDefinitionFactory</c>: a
|
||||
/// leading <c>{</c> marks a TagConfig blob, enum fields are read STRICTLY, and the two entry
|
||||
/// points carry deliberately different strictness contracts.
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="TryParse"/> is the <b>runtime</b> path. It never throws; anything
|
||||
/// malformed returns <see langword="false"/> so the driver surfaces
|
||||
/// <c>BadNodeIdUnknown</c> rather than a misleading <c>Good</c> off a wrong-typed
|
||||
/// default.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="Inspect"/> is the <b>deploy-time</b> path. It returns human-readable
|
||||
/// warnings so a bad tag config surfaces at deploy instead of silently going dark at
|
||||
/// runtime.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// Sparkplug-descriptor parsing (<c>groupId</c> / <c>edgeNodeId</c> / <c>deviceId</c> /
|
||||
/// <c>metricName</c>) is a deliberate P1 stub — the fields exist on
|
||||
/// <see cref="MqttTagDefinition"/> but are never populated here until the P2 tasks fill them.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class MqttEquipmentTagParser
|
||||
{
|
||||
/// <summary>The JSONPath applied when a Json-format blob omits <c>jsonPath</c>: the document root.</summary>
|
||||
private const string RootJsonPath = "$";
|
||||
|
||||
/// <summary>The MQTT wildcard characters — illegal in a <em>tag's</em> (concrete) subscription topic.</summary>
|
||||
private static readonly char[] TopicWildcards = ['+', '#'];
|
||||
|
||||
/// <summary>
|
||||
/// Parses a driver wire <paramref name="reference"/> into a typed definition. Returns
|
||||
/// <see langword="false"/> — never throws — for a null/blank reference, a reference that is not
|
||||
/// a TagConfig blob (no leading <c>{</c>), unparseable JSON, a missing or blank <c>topic</c>, a
|
||||
/// <c>qos</c> outside 0–2, or a present-but-invalid (typo'd) <c>payloadFormat</c> /
|
||||
/// <c>dataType</c>. The produced definition's <see cref="MqttTagDefinition.Name"/> is
|
||||
/// <paramref name="reference"/> itself, which is the key published values are routed on.
|
||||
/// <para>
|
||||
/// A <b>wildcard</b> topic (<c>+</c> / <c>#</c>) is deliberately NOT rejected here: it is
|
||||
/// ambiguous rather than unparseable, and <see cref="Inspect"/> is the operator-visible
|
||||
/// surface for it. Rejecting it at runtime would turn an authoring mistake into a silent
|
||||
/// <c>BadNodeIdUnknown</c> with no stated cause.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="reference">The tag's wire reference (its authored <c>TagConfig</c> JSON).</param>
|
||||
/// <param name="def">The parsed definition when this returns <see langword="true"/>; otherwise <see langword="null"/>.</param>
|
||||
/// <returns><see langword="true"/> when <paramref name="reference"/> is a valid MQTT tag config.</returns>
|
||||
public static bool TryParse(string reference, [NotNullWhen(true)] out MqttTagDefinition? def)
|
||||
{
|
||||
def = null;
|
||||
if (string.IsNullOrWhiteSpace(reference) || reference[0] != '{') return false;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(reference);
|
||||
var root = doc.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object) return false;
|
||||
|
||||
// topic is the whole point of a Plain-mode tag — without one there is nothing to subscribe
|
||||
// to, so an absent/blank value is a hard reject rather than a defaulted empty subscription.
|
||||
var topic = ReadString(root, "topic");
|
||||
if (string.IsNullOrWhiteSpace(topic)) return false;
|
||||
|
||||
// Strict enum reads: a present-but-invalid (typo'd) value rejects the whole tag
|
||||
// (→ BadNodeIdUnknown) instead of silently defaulting to a wrong-typed Good.
|
||||
if (!TagConfigJson.TryReadEnumStrict(root, "payloadFormat", MqttPayloadFormat.Json, out var payloadFormat))
|
||||
return false;
|
||||
if (!TagConfigJson.TryReadEnumStrict(root, "dataType", DriverDataType.String, out var dataType))
|
||||
return false;
|
||||
|
||||
// qos is an MQTT protocol field with exactly three legal values; anything else would be
|
||||
// rejected by the broker at SUBSCRIBE time, so reject it here where the cause is visible.
|
||||
var qos = ReadOptionalInt(root, "qos");
|
||||
if (qos is { } q && q is < 0 or > 2) return false;
|
||||
|
||||
var jsonPath = ReadString(root, "jsonPath");
|
||||
if (string.IsNullOrEmpty(jsonPath)) jsonPath = RootJsonPath;
|
||||
|
||||
def = new MqttTagDefinition(
|
||||
Name: reference,
|
||||
Topic: topic,
|
||||
PayloadFormat: payloadFormat,
|
||||
JsonPath: jsonPath,
|
||||
DataType: dataType,
|
||||
Qos: qos,
|
||||
RetainSeed: ReadBoolOrDefault(root, "retainSeed", defaultValue: true));
|
||||
return true;
|
||||
}
|
||||
catch (JsonException) { return false; }
|
||||
catch (FormatException) { return false; }
|
||||
catch (InvalidOperationException) { return false; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deploy-time inspection of an equipment-tag <c>TagConfig</c> blob. Returns human-readable
|
||||
/// warnings for a structurally unparseable blob (which the runtime turns into a silent
|
||||
/// <c>BadNodeIdUnknown</c>), for present-but-invalid <c>payloadFormat</c> / <c>dataType</c>
|
||||
/// enum values, and for a <b>wildcard</b> tag topic (a tag bound to <c>+</c> / <c>#</c> would
|
||||
/// receive values from many topics — ambiguous, and almost never what the operator meant).
|
||||
/// Empty when the blob is clean or is not a TagConfig object at all. Never throws.
|
||||
/// </summary>
|
||||
/// <param name="reference">The equipment tag's TagConfig JSON.</param>
|
||||
/// <returns>The warnings; empty when clean.</returns>
|
||||
public static IReadOnlyList<string> Inspect(string reference)
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
if (string.IsNullOrWhiteSpace(reference) || reference[0] != '{') return warnings;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(reference);
|
||||
var root = doc.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
warnings.Add("Mqtt TagConfig root is not a JSON object — the tag will not resolve (BadNodeIdUnknown).");
|
||||
return warnings;
|
||||
}
|
||||
foreach (var w in new[]
|
||||
{
|
||||
TagConfigJson.DescribeInvalidEnum<MqttPayloadFormat>(root, "payloadFormat"),
|
||||
TagConfigJson.DescribeInvalidEnum<DriverDataType>(root, "dataType"),
|
||||
})
|
||||
{
|
||||
if (w is not null) warnings.Add(w);
|
||||
}
|
||||
var topic = ReadString(root, "topic");
|
||||
if (topic.IndexOfAny(TopicWildcards) >= 0)
|
||||
{
|
||||
warnings.Add(
|
||||
$"topic '{topic}' contains an MQTT wildcard (+ or #); a tag's topic must be concrete, " +
|
||||
"or the tag will be fed by every matching topic.");
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
warnings.Add("Mqtt TagConfig is not valid JSON — the tag will not resolve (BadNodeIdUnknown).");
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
|
||||
private static string ReadString(JsonElement o, string name)
|
||||
=> o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.String
|
||||
? e.GetString() ?? ""
|
||||
: "";
|
||||
|
||||
private static int? ReadOptionalInt(JsonElement o, string name)
|
||||
=> o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.Number
|
||||
&& e.TryGetInt32(out var v) ? v : null;
|
||||
|
||||
private static bool ReadBoolOrDefault(JsonElement o, string name, bool defaultValue)
|
||||
=> o.TryGetProperty(name, out var e) && e.ValueKind is JsonValueKind.True or JsonValueKind.False
|
||||
? e.GetBoolean()
|
||||
: defaultValue;
|
||||
}
|
||||
@@ -3,10 +3,10 @@ using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Mqtt;
|
||||
|
||||
/// <summary>
|
||||
/// The driver's internal per-tag descriptor — the parsed form of an authored equipment tag's
|
||||
/// <c>TagConfig</c> JSON, produced by <see cref="MqttEquipmentTagParser.TryParse"/> and looked up
|
||||
/// through the shared <see cref="EquipmentTagRefResolver{TDef}"/> exactly as Modbus does. See
|
||||
/// <c>docs/plans/2026-07-15-mqtt-sparkplug-driver-design.md</c> §5.2 / §5.3.
|
||||
/// The driver's internal per-tag descriptor — the parsed form of an authored raw tag's
|
||||
/// <c>TagConfig</c> JSON, produced by <see cref="MqttTagDefinitionFactory.FromTagConfig"/> and
|
||||
/// looked up through the shared <see cref="EquipmentTagRefResolver{TDef}"/> exactly as Modbus does.
|
||||
/// See <c>docs/plans/2026-07-15-mqtt-sparkplug-driver-design.md</c> §5.2 / §5.3.
|
||||
/// <para>
|
||||
/// The record carries <b>both</b> ingest shapes: the Plain-MQTT fields
|
||||
/// (<see cref="Topic"/> … <see cref="RetainSeed"/>), populated in P1, and the Sparkplug B
|
||||
@@ -18,9 +18,12 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Mqtt;
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="Name">
|
||||
/// The definition's identity: the exact wire <c>reference</c> string the driver was handed
|
||||
/// (the tag's authored <c>TagConfig</c> blob). Published values key the forward router on this,
|
||||
/// so it must round-trip byte-for-byte.
|
||||
/// The definition's identity: the tag's <b>RawPath</b> — the cluster-scoped slash path that is the
|
||||
/// v3 driver wire reference. This is the key <see cref="EquipmentTagRefResolver{TDef}"/> resolves
|
||||
/// on, the key <c>OnDataChange</c> must publish under, and the key <c>DriverHostActor</c> fans out
|
||||
/// to the raw + UNS NodeIds. It is emphatically <b>not</b> the authored <c>TagConfig</c> blob: the
|
||||
/// pre-v3 blob-as-reference shape is retired, and publishing under a blob key would silently miss
|
||||
/// the RawPath-keyed fan-out while every unit test still passed.
|
||||
/// </param>
|
||||
/// <param name="Topic">The concrete MQTT topic this tag subscribes to (Plain mode).</param>
|
||||
/// <param name="PayloadFormat">How the received payload is decoded (Plain mode).</param>
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
using System.Text.Json;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Mqtt;
|
||||
|
||||
/// <summary>
|
||||
/// v3 pure mapper: turns an authored raw tag's <c>TagConfig</c> JSON (the shape produced by the
|
||||
/// AdminUI <c>MqttTagConfigModel</c>) into an <see cref="MqttTagDefinition"/>. Under v3 a tag's
|
||||
/// identity is its <b>RawPath</b> (a cluster-scoped slash path), not the address blob — so the
|
||||
/// produced definition's <see cref="MqttTagDefinition.Name"/> is the RawPath the driver was handed,
|
||||
/// which is exactly the wire reference the driver's <c>RawPath → def</c> resolver keys on. The driver
|
||||
/// builds that table by mapping each <see cref="RawTagEntry"/> the deploy artifact delivers through
|
||||
/// <see cref="FromTagConfig"/>.
|
||||
/// <para>
|
||||
/// Two entry points carry deliberately different strictness contracts.
|
||||
/// <see cref="FromTagConfig"/> is the <b>runtime</b> path: it never throws, and anything
|
||||
/// malformed returns <see langword="false"/> so the driver surfaces <c>BadNodeIdUnknown</c>
|
||||
/// rather than a misleading <c>Good</c> off a wrong-typed default. <see cref="Inspect"/> is the
|
||||
/// <b>deploy-time</b> path: it returns human-readable warnings so a bad tag config surfaces at
|
||||
/// deploy instead of silently going dark at runtime.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Sparkplug-descriptor parsing (<c>groupId</c> / <c>edgeNodeId</c> / <c>deviceId</c> /
|
||||
/// <c>metricName</c>) is a deliberate P1 stub — the fields exist on
|
||||
/// <see cref="MqttTagDefinition"/> but are never populated here until the P2 tasks fill them.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>No <c>ToTagConfig</c> inverse — a deliberate YAGNI call.</b> The six sibling factories
|
||||
/// each carry one solely to serve their <c>Driver.<X>.Cli</c> project, which synthesises
|
||||
/// <see cref="RawTagEntry"/> blobs from operator flags; the MQTT plan defines no
|
||||
/// <c>Mqtt.Cli</c>. The AdminUI typed editor does not need one either — the
|
||||
/// <c><Driver>TagConfigModel</c> template serialises through its own preserved
|
||||
/// <c>JsonObject</c> key bag and references no driver factory (verified: no
|
||||
/// <c>TagDefinitionFactory</c> reference exists anywhere under the AdminUI project). Add the
|
||||
/// inverse when a real caller appears, not before.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class MqttTagDefinitionFactory
|
||||
{
|
||||
/// <summary>The JSONPath applied when a Json-format blob omits <c>jsonPath</c>: the document root.</summary>
|
||||
private const string RootJsonPath = "$";
|
||||
|
||||
/// <summary>The MQTT wildcard characters — illegal in a <em>tag's</em> (concrete) subscription topic.</summary>
|
||||
private static readonly char[] TopicWildcards = ['+', '#'];
|
||||
|
||||
/// <summary>
|
||||
/// Maps an authored <c>TagConfig</c> object to a typed definition keyed by <paramref name="rawPath"/>.
|
||||
/// The input is always an authored TagConfig JSON object (there is no name-vs-blob heuristic in v3).
|
||||
/// Enum fields (<c>payloadFormat</c> / <c>dataType</c>) and <c>qos</c> are read STRICTLY — a
|
||||
/// present-but-invalid (typo'd) value rejects the whole tag (returns <see langword="false"/> ⇒ the
|
||||
/// driver surfaces <c>BadNodeIdUnknown</c>) rather than silently defaulting to a wrong-typed Good or
|
||||
/// a downgraded delivery guarantee. Never throws.
|
||||
/// <para>
|
||||
/// A <b>wildcard</b> topic (<c>+</c> / <c>#</c>) is deliberately NOT rejected here: it is
|
||||
/// ambiguous rather than unparseable, and <see cref="Inspect"/> is the operator-visible surface
|
||||
/// for it. Rejecting it at runtime would turn an authoring mistake into a silent
|
||||
/// <c>BadNodeIdUnknown</c> with no stated cause.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="tagConfig">The authored equipment-tag TagConfig JSON.</param>
|
||||
/// <param name="rawPath">The tag's RawPath — becomes the definition's identity (<c>Name</c>).</param>
|
||||
/// <param name="def">The mapped definition when this returns <see langword="true"/>.</param>
|
||||
/// <returns><see langword="true"/> when <paramref name="tagConfig"/> is a valid MQTT tag-config object.</returns>
|
||||
public static bool FromTagConfig(string tagConfig, string rawPath, out MqttTagDefinition def)
|
||||
{
|
||||
def = null!;
|
||||
if (string.IsNullOrWhiteSpace(tagConfig) || tagConfig[0] != '{') return false;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(tagConfig);
|
||||
var root = doc.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object) return false;
|
||||
|
||||
// topic is the whole point of a Plain-mode tag — without one there is nothing to subscribe
|
||||
// to, so an absent/blank value is a hard reject rather than a defaulted empty subscription.
|
||||
var topic = ReadString(root, "topic");
|
||||
if (string.IsNullOrWhiteSpace(topic)) return false;
|
||||
|
||||
// Strict enum reads: a present-but-invalid (typo'd) value rejects the whole tag
|
||||
// (→ BadNodeIdUnknown) instead of silently defaulting to a wrong-typed Good.
|
||||
if (!TagConfigJson.TryReadEnumStrict(root, "payloadFormat", MqttPayloadFormat.Json, out var payloadFormat))
|
||||
return false;
|
||||
if (!TagConfigJson.TryReadEnumStrict(root, "dataType", DriverDataType.String, out var dataType))
|
||||
return false;
|
||||
|
||||
// qos is read with the same strictness, for the same reason: silently absorbing a malformed
|
||||
// "qos":"high" / "qos":1.5 / "qos":5 into the driver-level default would hand the operator a
|
||||
// WEAKER delivery guarantee than the one they authored, with nothing to show for it.
|
||||
if (!TryReadQosStrict(root, out var qos)) return false;
|
||||
|
||||
var jsonPath = ReadString(root, "jsonPath");
|
||||
if (string.IsNullOrEmpty(jsonPath)) jsonPath = RootJsonPath;
|
||||
|
||||
def = new MqttTagDefinition(
|
||||
Name: rawPath,
|
||||
Topic: topic,
|
||||
PayloadFormat: payloadFormat,
|
||||
JsonPath: jsonPath,
|
||||
DataType: dataType,
|
||||
Qos: qos,
|
||||
RetainSeed: ReadBoolOrDefault(root, "retainSeed", defaultValue: true));
|
||||
return true;
|
||||
}
|
||||
catch (JsonException) { return false; }
|
||||
catch (FormatException) { return false; }
|
||||
catch (InvalidOperationException) { return false; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deploy-time inspection of an equipment-tag <c>TagConfig</c> blob. Returns human-readable
|
||||
/// warnings for a structurally unparseable blob (which the runtime turns into a silent
|
||||
/// <c>BadNodeIdUnknown</c>), for present-but-invalid <c>payloadFormat</c> / <c>dataType</c> /
|
||||
/// <c>qos</c> values, and for a <b>wildcard</b> tag topic (a tag bound to <c>+</c> / <c>#</c>
|
||||
/// would receive values from many topics — ambiguous, and almost never what the operator meant).
|
||||
/// Empty when the blob is clean or is not an equipment-tag TagConfig object. Never throws.
|
||||
/// </summary>
|
||||
/// <param name="reference">The equipment tag's TagConfig JSON.</param>
|
||||
/// <returns>The warnings; empty when clean.</returns>
|
||||
public static IReadOnlyList<string> Inspect(string reference)
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
if (string.IsNullOrWhiteSpace(reference) || reference[0] != '{') return warnings;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(reference);
|
||||
var root = doc.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
warnings.Add("Mqtt TagConfig root is not a JSON object — the tag will not resolve (BadNodeIdUnknown).");
|
||||
return warnings;
|
||||
}
|
||||
foreach (var w in new[]
|
||||
{
|
||||
TagConfigJson.DescribeInvalidEnum<MqttPayloadFormat>(root, "payloadFormat"),
|
||||
TagConfigJson.DescribeInvalidEnum<DriverDataType>(root, "dataType"),
|
||||
DescribeInvalidQos(root),
|
||||
})
|
||||
{
|
||||
if (w is not null) warnings.Add(w);
|
||||
}
|
||||
var topic = ReadString(root, "topic");
|
||||
if (topic.IndexOfAny(TopicWildcards) >= 0)
|
||||
{
|
||||
warnings.Add(
|
||||
$"value '{topic}' for 'topic' contains an MQTT wildcard (+ or #); a tag's topic must be " +
|
||||
"concrete, or the tag will be fed by every matching topic.");
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
warnings.Add("Mqtt TagConfig is not valid JSON — the tag will not resolve (BadNodeIdUnknown).");
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strict <c>qos</c> read, mirroring <see cref="TagConfigJson.TryReadEnumStrict{TEnum}"/>'s
|
||||
/// absent / valid / present-but-invalid split: absent ⇒ <see langword="null"/> (the driver-level
|
||||
/// <see cref="MqttPlainOptions.DefaultQos"/> wins); a JSON integer in 0–2 ⇒ that value; anything
|
||||
/// else present (non-number, non-integer, or out of range) ⇒ the read FAILS.
|
||||
/// </summary>
|
||||
/// <param name="o">The TagConfig root object.</param>
|
||||
/// <param name="qos">The parsed QoS, or <see langword="null"/> when the field is absent.</param>
|
||||
/// <returns><see langword="false"/> only when <c>qos</c> is present but not a legal MQTT QoS.</returns>
|
||||
private static bool TryReadQosStrict(JsonElement o, out int? qos)
|
||||
{
|
||||
qos = null;
|
||||
if (!o.TryGetProperty("qos", out var e)) return true; // absent
|
||||
if (e.ValueKind != JsonValueKind.Number || !e.TryGetInt32(out var v)) return false;
|
||||
if (v is < 0 or > 2) return false;
|
||||
qos = v;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A human-readable warning for a present-but-invalid <c>qos</c> field, or <see langword="null"/>
|
||||
/// when it is absent or valid — the <c>qos</c> counterpart of
|
||||
/// <see cref="TagConfigJson.DescribeInvalidEnum{TEnum}"/>.
|
||||
/// </summary>
|
||||
/// <param name="o">The TagConfig root object.</param>
|
||||
/// <returns>The warning text, or <see langword="null"/>.</returns>
|
||||
private static string? DescribeInvalidQos(JsonElement o)
|
||||
{
|
||||
if (!o.TryGetProperty("qos", out var e)) return null;
|
||||
if (e.ValueKind == JsonValueKind.Number && e.TryGetInt32(out var v) && v is >= 0 and <= 2) return null;
|
||||
return $"value '{e.GetRawText()}' for 'qos' is not a valid MQTT QoS; valid: 0, 1, 2";
|
||||
}
|
||||
|
||||
private static string ReadString(JsonElement o, string name)
|
||||
=> o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.String
|
||||
? e.GetString() ?? ""
|
||||
: "";
|
||||
|
||||
private static bool ReadBoolOrDefault(JsonElement o, string name, bool defaultValue)
|
||||
=> o.TryGetProperty(name, out var e) && e.ValueKind is JsonValueKind.True or JsonValueKind.False
|
||||
? e.GetBoolean()
|
||||
: defaultValue;
|
||||
}
|
||||
Reference in New Issue
Block a user