mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 06:31:35 +08:00
2de64d7d93
# Description A reference in a NodeSet2 input whose target is a node of another model was dropped without a diagnostic when that model is supplied by a referenced assembly rather than by an AdditionalFiles input. The dependency metadata of a referenced assembly carries types only, so the importer could not resolve an instance such as DI's DeviceSet and skipped every reference to it: a device organized by DeviceSet (inverse Organizes) lost that reference, and so did any non-hierarchical reference to such a node. When the target of a reference is in another, non-zero namespace and no loaded design defines it, the importer now creates a stand-in that carries only the NodeId the generated AddReference call needs. The stand-in is not generated. References whose reference type is unknown are still skipped. Tests generate a DI-style NodeSet2 whose device is organized by DeviceSet against DI supplied as a referenced assembly and as an AdditionalFile, and compile the result against the stack. ## Related Issues None. ## Checklist - [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [x] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
3843 lines
154 KiB
C#
3843 lines
154 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Xml;
|
|
using Opc.Ua.SourceGeneration;
|
|
using Opc.Ua.Types;
|
|
|
|
namespace Opc.Ua.Schema.Model
|
|
{
|
|
/// <summary>
|
|
/// Defines where to add nullable annotations
|
|
/// </summary>
|
|
internal enum NullableAnnotation
|
|
{
|
|
/// <summary>
|
|
/// Types should be non nullable
|
|
/// </summary>
|
|
NonNullable,
|
|
|
|
/// <summary>
|
|
/// Types are nullable except for data types
|
|
/// </summary>
|
|
NullableExceptDataTypes,
|
|
|
|
/// <summary>
|
|
/// All types are nullable
|
|
/// </summary>
|
|
Nullable
|
|
}
|
|
|
|
/// <summary>
|
|
/// Identifies one generated method-argument symbol scope and the names
|
|
/// reserved by that scope's emitted scaffolding.
|
|
/// </summary>
|
|
internal sealed class MethodArgumentCodeNameScope
|
|
{
|
|
public MethodArgumentCodeNameScope(params string[] reservedNames)
|
|
: this(null, reservedNames)
|
|
{
|
|
}
|
|
|
|
public MethodArgumentCodeNameScope(
|
|
Func<string, bool, IEnumerable<string>>? getCollisionIdentifiers,
|
|
params string[] reservedNames)
|
|
{
|
|
ReservedNames = reservedNames == null ? [] : [.. reservedNames];
|
|
m_getCollisionIdentifiers = getCollisionIdentifiers;
|
|
}
|
|
|
|
public IReadOnlyCollection<string> ReservedNames { get; }
|
|
|
|
public IEnumerable<string> GetCollisionIdentifiers(
|
|
string identifier,
|
|
bool output)
|
|
{
|
|
return m_getCollisionIdentifiers?.Invoke(identifier, output) ?? [identifier];
|
|
}
|
|
|
|
private readonly Func<string, bool, IEnumerable<string>>? m_getCollisionIdentifiers;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dotnet code generation support.
|
|
/// </summary>
|
|
internal static class ModelDesignExtensions
|
|
{
|
|
/// <summary>
|
|
/// Get basic data type from data type design.
|
|
/// </summary>
|
|
/// <param name="dataType"></param>
|
|
/// <returns></returns>
|
|
public static BasicDataType DetermineBasicDataType(this DataTypeDesign? dataType)
|
|
{
|
|
if (dataType == null)
|
|
{
|
|
return BasicDataType.BaseDataType;
|
|
}
|
|
if (dataType.IsOptionSet)
|
|
|
|
{
|
|
return BasicDataType.Enumeration;
|
|
}
|
|
// check if it is a built in data type.
|
|
if (dataType.IsBasicDataType(out BasicDataType basicDataType))
|
|
{
|
|
return basicDataType;
|
|
}
|
|
|
|
// recursively search hierarchy if not base data type yet
|
|
BasicDataType basicType = DetermineBasicDataType(
|
|
dataType.BaseTypeNode as DataTypeDesign);
|
|
|
|
// data type is user defined if a sub-type of structure.
|
|
if (basicType == BasicDataType.Structure)
|
|
{
|
|
return BasicDataType.UserDefined;
|
|
}
|
|
return basicType;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get type node state class name
|
|
/// </summary>
|
|
public static string GetNodeStateClassName(
|
|
this TypeDesign type,
|
|
Namespace[] namespaces)
|
|
{
|
|
string className = type.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
|
|
return CoreUtils.Format("{0}State", className);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the class name to use when creating an instance of the type.
|
|
/// </summary>
|
|
public static string GetNodeStateClassName(
|
|
this InstanceDesign? instance,
|
|
string targetNamespace,
|
|
Namespace[] namespaces,
|
|
bool asFactory = false,
|
|
bool applyStandardFallback = true)
|
|
{
|
|
if (instance is MethodDesign method)
|
|
{
|
|
string className;
|
|
XmlQualifiedName methodStateIdentity =
|
|
MethodDesignArgumentResolver.ResolveMethodStateIdentity(method);
|
|
if (methodStateIdentity != null)
|
|
{
|
|
className =
|
|
methodStateIdentity.AsFullyQualifiedTypeSymbol(namespaces);
|
|
|
|
if (className.EndsWith("MethodType", StringComparison.Ordinal))
|
|
{
|
|
className = className[..^"MethodType".Length];
|
|
}
|
|
else if (className.EndsWith("Type", StringComparison.Ordinal))
|
|
{
|
|
className = className[..^"Type".Length];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
className = method.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
|
|
}
|
|
|
|
if (MethodDesignArgumentResolver.HasMethodArguments(method))
|
|
{
|
|
// Only method types have a typed class. A reference to a
|
|
// method that declares its arguments inline uses the base
|
|
// MethodState; declaration names keep the typed name.
|
|
if (applyStandardFallback &&
|
|
!MethodDesignArgumentResolver.HasTypedMethodState(method))
|
|
{
|
|
return CoreUtils.Format(
|
|
"{0}global::Opc.Ua.MethodState",
|
|
asFactory ? "new " : string.Empty);
|
|
}
|
|
|
|
string typedClassName = CoreUtils.Format(
|
|
"{0}{1}MethodState",
|
|
asFactory ? "new " : string.Empty,
|
|
className);
|
|
// Degrade a reference to a standard global::Opc.Ua.*MethodState
|
|
// to the base MethodState when a curated Opc.Ua.Core omits the
|
|
// typed class and the current model pass does not declare it
|
|
// (e.g. the FileDirectoryType methods on a Robotics Programs
|
|
// object). Model-generation-scoped; see
|
|
// StandardMethodStateFallback.
|
|
if (applyStandardFallback &&
|
|
StandardMethodStateFallback.ShouldFallBackToBase(typedClassName))
|
|
{
|
|
return CoreUtils.Format(
|
|
"{0}global::Opc.Ua.MethodState",
|
|
asFactory ? "new " : string.Empty);
|
|
}
|
|
return typedClassName;
|
|
}
|
|
|
|
return CoreUtils.Format(
|
|
"{0}global::Opc.Ua.MethodState",
|
|
asFactory ? "new " : string.Empty);
|
|
}
|
|
|
|
if (instance is not VariableDesign variable)
|
|
|
|
{
|
|
// No state class is emitted for an excluded ObjectType: use
|
|
// the nearest emitted supertype's.
|
|
TypeDesign? typeDefinition = instance!.TypeDefinitionNode;
|
|
while (typeDefinition is ObjectTypeDesign { IsExcludedFromGeneration: true } &&
|
|
typeDefinition.BaseTypeNode != null)
|
|
{
|
|
typeDefinition = typeDefinition.BaseTypeNode;
|
|
}
|
|
return GetNodeStateNameSimple(typeDefinition!);
|
|
}
|
|
var variableType = instance.TypeDefinitionNode as VariableTypeDesign;
|
|
|
|
// A VariableType materialised from a referenced assembly's
|
|
// dependency payload used to arrive without its DataType
|
|
// restriction, leaving DataTypeNode null and crashing here with a
|
|
// bare NullReferenceException that named neither the variable nor
|
|
// the type. The payload now carries the restriction; this check
|
|
// keeps any remaining gap diagnosable instead of unreadable.
|
|
if (variableType?.DataTypeNode == null)
|
|
{
|
|
throw new InvalidDataException(
|
|
$"The data type of variable type " +
|
|
$"'{instance.TypeDefinition}' could not be resolved, so the " +
|
|
$"node state class of '{instance.SymbolicId?.Name ?? instance.BrowseName}' " +
|
|
"cannot be determined. The type is most likely supplied by a " +
|
|
"referenced assembly whose dependency payload predates the " +
|
|
"VariableType data type entry - rebuild that assembly.");
|
|
}
|
|
|
|
// check if the variable type restricted the datatype to eliminate the
|
|
// need for a template parameter.
|
|
if (variableType.DataTypeNode.IsTemplateParameterRequired(variableType.ValueRank))
|
|
{
|
|
return GetNodeStateNameSimple(variableType);
|
|
}
|
|
|
|
// check if the variable instance did not restrict the datatype.
|
|
if (!variable.DataTypeNode!.IsTemplateParameterRequired(variable.ValueRank))
|
|
{
|
|
return GetNodeStateNameSimple(variableType);
|
|
}
|
|
|
|
// instance restricted the datatype but the type did not.
|
|
string scalarName = GetDotNetTypeName(
|
|
variable.DataTypeNode,
|
|
targetNamespace,
|
|
namespaces,
|
|
nullable: NullableAnnotation.NonNullable,
|
|
true);
|
|
|
|
if (variable.ValueRank == ValueRank.Array)
|
|
{
|
|
if (asFactory)
|
|
{
|
|
return CoreUtils.Format(
|
|
"{0}State<global::Opc.Ua.ArrayOf<{1}>>.With<{2}>",
|
|
GetClassName(variableType, namespaces),
|
|
scalarName,
|
|
GetVariantBuilder(variable.DataTypeNode, scalarName));
|
|
}
|
|
return CoreUtils.Format(
|
|
"{0}State<global::Opc.Ua.ArrayOf<{1}>>",
|
|
GetClassName(variableType, namespaces),
|
|
scalarName);
|
|
}
|
|
|
|
if (variable.ValueRank == ValueRank.OneOrMoreDimensions &&
|
|
variable.DataTypeNode!.SupportsMatrixOf() &&
|
|
variable.DataTypeNode!.BasicDataType
|
|
is not BasicDataType.BaseDataType
|
|
and not BasicDataType.Number
|
|
and not BasicDataType.UInteger
|
|
and not BasicDataType.Integer)
|
|
{
|
|
if (asFactory)
|
|
{
|
|
return CoreUtils.Format(
|
|
"{0}State<global::Opc.Ua.MatrixOf<{1}>>.With<{2}>",
|
|
GetClassName(variableType, namespaces),
|
|
scalarName,
|
|
GetVariantBuilder(variable.DataTypeNode, scalarName));
|
|
}
|
|
return CoreUtils.Format(
|
|
"{0}State<global::Opc.Ua.MatrixOf<{1}>>",
|
|
GetClassName(variableType, namespaces),
|
|
scalarName);
|
|
}
|
|
|
|
if (IsIndeterminateType(variable))
|
|
{
|
|
return GetNodeStateNameSimple(variableType);
|
|
}
|
|
|
|
// add hack for TwoStateDiscreteType which always has to be bool.
|
|
if (variableType.SymbolicName ==
|
|
new XmlQualifiedName("TwoStateDiscreteType", Namespaces.OpcUa))
|
|
{
|
|
return GetNodeStateNameSimple(variableType);
|
|
}
|
|
|
|
if (asFactory)
|
|
{
|
|
return CoreUtils.Format(
|
|
"{0}State<{1}>.With<{2}>",
|
|
GetClassName(variableType, namespaces),
|
|
scalarName,
|
|
GetVariantBuilder(variable.DataTypeNode, scalarName));
|
|
}
|
|
|
|
return CoreUtils.Format(
|
|
"{0}State<{1}>",
|
|
GetClassName(variableType, namespaces),
|
|
scalarName);
|
|
|
|
string GetNodeStateNameSimple(TypeDesign variableType)
|
|
{
|
|
return CoreUtils.Format(
|
|
"{0}{1}State",
|
|
asFactory ? "new " : string.Empty,
|
|
GetClassName(variableType, namespaces));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the variant builder to use to access or mutate a variant of type T
|
|
/// </summary>
|
|
public static string GetVariantBuilder(
|
|
this DataTypeDesign? datatype,
|
|
string typeName)
|
|
{
|
|
switch (datatype!.BasicDataType)
|
|
{
|
|
case BasicDataType.UserDefined:
|
|
if (datatype.IsEnumeration)
|
|
{
|
|
return CoreUtils.Format("global::Opc.Ua.EnumerationBuilder<{0}>", typeName);
|
|
}
|
|
return CoreUtils.Format("global::Opc.Ua.StructureBuilder<{0}>", typeName);
|
|
case BasicDataType.Enumeration:
|
|
if (datatype.SymbolicId ==
|
|
new XmlQualifiedName("Enumeration", Namespaces.OpcUa))
|
|
{
|
|
return "global::Opc.Ua.VariantBuilder";
|
|
}
|
|
if (datatype.IsOptionSet)
|
|
{
|
|
return GetVariantBuilder((DataTypeDesign)datatype.BaseTypeNode!, typeName);
|
|
}
|
|
return CoreUtils.Format("global::Opc.Ua.EnumerationBuilder<{0}>", typeName);
|
|
default:
|
|
return "global::Opc.Ua.VariantBuilder";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the class name to use when creating an instance of the type.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
public static string GetBaseClassName(this TypeDesign type, Namespace[] namespaces)
|
|
{
|
|
if (type?.BaseTypeNode?.SymbolicName == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(type));
|
|
}
|
|
if (type is not DataTypeDesign dataType ||
|
|
dataType.BaseTypeNode is not DataTypeDesign dtd)
|
|
{
|
|
return type.BaseTypeNode.GetClassName(namespaces);
|
|
}
|
|
|
|
if (dtd.BasicDataType == BasicDataType.Structure)
|
|
|
|
{
|
|
return "global::Opc.Ua.IEncodeable";
|
|
}
|
|
return dtd.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fully qualify a symbol
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
/// <exception cref="ArgumentException"></exception>
|
|
public static string AsFullyQualifiedTypeSymbol(
|
|
this XmlQualifiedName symbol,
|
|
Namespace[] namespaces,
|
|
string? alternativeName = null)
|
|
{
|
|
string? name = alternativeName ?? symbol?.Name;
|
|
if (string.IsNullOrEmpty(name))
|
|
{
|
|
throw new ArgumentException("Symbol name is empty");
|
|
}
|
|
string? ns = namespaces.GetNamespacePrefix((symbol?.Namespace)!);
|
|
if (string.IsNullOrEmpty(ns))
|
|
{
|
|
return name;
|
|
}
|
|
return CoreUtils.Format("global::{0}.{1}", ns, name);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the class name to use when creating an instance of the type.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
public static bool IsDerivedDataType(this TypeDesign type)
|
|
{
|
|
if (type is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(type));
|
|
}
|
|
if (type is not DataTypeDesign ||
|
|
type.BaseTypeNode is not DataTypeDesign dtd)
|
|
{
|
|
return true;
|
|
}
|
|
return dtd.BasicDataType != BasicDataType.Structure;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a qualifier for the namespace to use in code.
|
|
/// </summary>
|
|
public static string? GetNamespacePrefix(this Namespace[] namespaces, string namespaceUri)
|
|
{
|
|
if (namespaces != null)
|
|
{
|
|
foreach (Namespace ns in namespaces)
|
|
{
|
|
if (ns.Value == namespaceUri)
|
|
{
|
|
return CoreUtils.Format("{0}", ns.Prefix);
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a constant string for the namespace uri.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
/// <exception cref="ArgumentException"></exception>
|
|
public static string? GetConstantSymbolForNamespace(
|
|
this Namespace[] namespaces,
|
|
string namespaceUri)
|
|
{
|
|
if (namespaces is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(namespaces));
|
|
}
|
|
Namespace? ns = namespaces.GetNamespace(namespaceUri);
|
|
if (ns == null)
|
|
{
|
|
return null;
|
|
}
|
|
if (string.IsNullOrWhiteSpace(ns.Name))
|
|
{
|
|
throw new ArgumentException("Found namespace has no name");
|
|
}
|
|
if (string.IsNullOrWhiteSpace(ns.Prefix)) // TODO: should this work?
|
|
{
|
|
return CoreUtils.Format("Namespaces.{0}", ns.Name);
|
|
}
|
|
return CoreUtils.Format("global::{0}.Namespaces.{1}", ns.Prefix, ns.Name);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the expression for a namespace URI in generated code: the
|
|
/// model's namespace constant when the model declares the namespace,
|
|
/// otherwise the URI as an escaped string literal.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// A namespace the model does not declare can still appear in
|
|
/// generated code: an inherited child keeps the browse namespace of
|
|
/// the model that declared it (DI below a Robotics subtype), and a
|
|
/// reference can target a node of a namespace the NodeSet lists only
|
|
/// in its NamespaceUris.
|
|
/// </remarks>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
/// <exception cref="ArgumentException"></exception>
|
|
public static string GetNamespaceUriAsCode(
|
|
this Namespace[] namespaces,
|
|
string namespaceUri)
|
|
{
|
|
return namespaces.GetConstantSymbolForNamespace(namespaceUri)
|
|
?? namespaceUri.AsStringLiteral();
|
|
}
|
|
|
|
public static bool IsOverriddenWithSameClass(
|
|
this InstanceDesign instance,
|
|
string targetNamespace,
|
|
Namespace[] namespaces)
|
|
{
|
|
if (instance is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(instance));
|
|
}
|
|
if (!instance.IsOverridden())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
string x = instance.GetMergedInstance().GetNodeStateClassName(targetNamespace, namespaces);
|
|
string y = instance.OveriddenNode.GetNodeStateClassName(targetNamespace, namespaces);
|
|
|
|
// Only the exact same class counts. An override that refines the
|
|
// generic argument (BaseDataVariableState<double> overridden as
|
|
// BaseDataVariableState<int>) is a different class and has to be
|
|
// redeclared, or the subtype keeps exposing the base's value type.
|
|
// (A special case treating every BaseDataVariableState<...> pair as
|
|
// equal used to compare against the unqualified class name, which
|
|
// GetNodeStateClassName never returns - it had no effect and this
|
|
// is the behaviour the generated code has always had.)
|
|
return x == y;
|
|
}
|
|
|
|
private static bool IsIndeterminateType(this InstanceDesign instance)
|
|
{
|
|
if (instance is VariableDesign variable)
|
|
{
|
|
if (variable.ValueRank is not ValueRank.Scalar and not ValueRank.Array)
|
|
{
|
|
// Matrix-rank with a concrete data type that supports
|
|
// `MatrixOf<T>` is determinate - the generator emits a
|
|
// typed `State<MatrixOf<T>>` for it. Everything else
|
|
// (ScalarOrArray, ScalarOrOneDimension, Any, or matrix
|
|
// over an abstract numeric base) stays on the Variant
|
|
// fallback.
|
|
return variable.ValueRank != ValueRank.OneOrMoreDimensions ||
|
|
variable.DataTypeNode == null ||
|
|
!variable.DataTypeNode.SupportsMatrixOf() ||
|
|
!(variable.DataTypeNode.BasicDataType
|
|
is not BasicDataType.BaseDataType
|
|
and not BasicDataType.Number
|
|
and not BasicDataType.UInteger
|
|
and not BasicDataType.Integer);
|
|
}
|
|
if (variable.DataType ==
|
|
new XmlQualifiedName(BrowseNames.Enumeration, Namespaces.OpcUa))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public static string? EnsureUniqueEnumName(this Parameter target)
|
|
{
|
|
if (target?.Parent is DataTypeDesign dt && dt.HasFields && dt.Fields != null)
|
|
{
|
|
HashSet<string> names = [];
|
|
|
|
foreach (Parameter field in dt.Fields)
|
|
{
|
|
int count = 1;
|
|
string check = field.Name;
|
|
|
|
if (names.Contains(check))
|
|
{
|
|
check = field.Name + "_" + field.Identifier;
|
|
}
|
|
|
|
while (names.Contains(check))
|
|
{
|
|
check = field.Name + "_v" + count++;
|
|
}
|
|
|
|
if (target.Identifier == field.Identifier)
|
|
|
|
{
|
|
return check;
|
|
}
|
|
names.Add(check);
|
|
}
|
|
}
|
|
|
|
return target?.Name;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the canonical C# identifier for a method argument.
|
|
/// </summary>
|
|
internal static string GetGeneratedCodeIdentifier(
|
|
this Parameter field,
|
|
bool upperCamelCase = false,
|
|
MethodArgumentCodeNameScope? scope = null)
|
|
{
|
|
string? name = field?.Name;
|
|
scope ??= s_defaultMethodArgumentCodeNameScope;
|
|
if (field != null)
|
|
{
|
|
lock (s_generatedCodeNamesLock)
|
|
{
|
|
if (s_generatedCodeNames.TryGetValue(field, out GeneratedCodeNameState? state) &&
|
|
state.TryGetName(scope, out string generatedName) &&
|
|
!string.IsNullOrEmpty(generatedName))
|
|
{
|
|
name = generatedName;
|
|
}
|
|
}
|
|
}
|
|
return name!.ToCSharpIdentifier(upperCamelCase);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Assigns unique generated-code names against the requested scope while
|
|
/// preserving the authored OPC UA names.
|
|
/// </summary>
|
|
internal static void AssignMethodArgumentCodeNames(
|
|
this MethodDesign method,
|
|
MethodArgumentCodeNameScope? scope = null,
|
|
params string[] additionalReservedNames)
|
|
{
|
|
AssignMethodArgumentCodeNames(
|
|
(method?.InputArguments)!,
|
|
(method?.OutputArguments)!,
|
|
scope,
|
|
additionalReservedNames);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Assigns unique generated-code names for an effective method signature.
|
|
/// </summary>
|
|
internal static void AssignMethodArgumentCodeNames(
|
|
Parameter[] inputArguments,
|
|
Parameter[] outputArguments,
|
|
MethodArgumentCodeNameScope? scope = null,
|
|
params string[] additionalReservedNames)
|
|
{
|
|
scope ??= s_defaultMethodArgumentCodeNameScope;
|
|
lock (s_generatedCodeNamesLock)
|
|
{
|
|
ClearGeneratedCodeNames(inputArguments, scope);
|
|
ClearGeneratedCodeNames(outputArguments, scope);
|
|
|
|
var usedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
AddReservedMethodArgumentNames(usedNames, scope.ReservedNames);
|
|
AddReservedMethodArgumentNames(usedNames, additionalReservedNames);
|
|
AssignUniqueGeneratedCodeNames(
|
|
inputArguments,
|
|
usedNames,
|
|
output: false,
|
|
scope);
|
|
AssignUniqueGeneratedCodeNames(
|
|
outputArguments,
|
|
usedNames,
|
|
output: true,
|
|
scope);
|
|
}
|
|
}
|
|
|
|
private static void AddReservedMethodArgumentNames(
|
|
HashSet<string> usedNames,
|
|
IEnumerable<string> reservedNames)
|
|
{
|
|
if (reservedNames == null)
|
|
{
|
|
return;
|
|
}
|
|
foreach (string reservedName in reservedNames)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(reservedName))
|
|
{
|
|
usedNames.Add(GetIdentifierCollisionKey(
|
|
reservedName.ToCSharpIdentifier()));
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void AssignUniqueGeneratedCodeNames(
|
|
Parameter[] arguments,
|
|
HashSet<string> usedNames,
|
|
bool output,
|
|
MethodArgumentCodeNameScope scope)
|
|
{
|
|
if (arguments == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (Parameter argument in arguments)
|
|
{
|
|
if (argument == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
string identifier = argument.Name.ToCSharpIdentifier();
|
|
if (TryReserveGeneratedIdentifier(
|
|
identifier,
|
|
output,
|
|
scope,
|
|
usedNames))
|
|
{
|
|
SetGeneratedCodeName(argument, identifier, scope);
|
|
continue;
|
|
}
|
|
|
|
string stem = identifier.TrimStart('@');
|
|
int suffix = output ? 1 : 2;
|
|
while (true)
|
|
{
|
|
string suffixText;
|
|
if (output)
|
|
{
|
|
suffixText = suffix == 1 ?
|
|
"Out" :
|
|
"Out" + suffix.ToString(CultureInfo.InvariantCulture);
|
|
}
|
|
else
|
|
{
|
|
suffixText = suffix.ToString(CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
string candidate = (stem + suffixText).ToCSharpIdentifier();
|
|
suffix++;
|
|
if (TryReserveGeneratedIdentifier(
|
|
candidate,
|
|
output,
|
|
scope,
|
|
usedNames))
|
|
{
|
|
SetGeneratedCodeName(argument, candidate, scope);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool TryReserveGeneratedIdentifier(
|
|
string identifier,
|
|
bool output,
|
|
MethodArgumentCodeNameScope scope,
|
|
HashSet<string> usedNames)
|
|
{
|
|
string[] collisionKeys =
|
|
[
|
|
.. scope.GetCollisionIdentifiers(identifier, output)
|
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
|
.Select(GetIdentifierCollisionKey)
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
];
|
|
if (collisionKeys.Any(usedNames.Contains))
|
|
{
|
|
return false;
|
|
}
|
|
foreach (string collisionKey in collisionKeys)
|
|
{
|
|
usedNames.Add(collisionKey);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private static void ClearGeneratedCodeNames(
|
|
Parameter[] arguments,
|
|
MethodArgumentCodeNameScope scope)
|
|
{
|
|
if (arguments == null)
|
|
{
|
|
return;
|
|
}
|
|
foreach (Parameter argument in arguments)
|
|
{
|
|
if (argument != null &&
|
|
s_generatedCodeNames.TryGetValue(
|
|
argument,
|
|
out GeneratedCodeNameState? state))
|
|
{
|
|
state.ClearName(scope);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static string GetIdentifierCollisionKey(string identifier)
|
|
{
|
|
return identifier.TrimStart('@');
|
|
}
|
|
|
|
private static void SetGeneratedCodeName(
|
|
Parameter argument,
|
|
string name,
|
|
MethodArgumentCodeNameScope scope)
|
|
{
|
|
s_generatedCodeNames.GetOrCreateValue(argument).SetName(scope, name);
|
|
}
|
|
|
|
private sealed class GeneratedCodeNameState
|
|
{
|
|
public bool TryGetName(
|
|
MethodArgumentCodeNameScope scope,
|
|
out string name)
|
|
{
|
|
return m_names.TryGetValue(scope, out name!);
|
|
}
|
|
|
|
public void SetName(
|
|
MethodArgumentCodeNameScope scope,
|
|
string name)
|
|
{
|
|
m_names[scope] = name;
|
|
}
|
|
|
|
public void ClearName(MethodArgumentCodeNameScope scope)
|
|
{
|
|
m_names.Remove(scope);
|
|
}
|
|
|
|
private readonly Dictionary<MethodArgumentCodeNameScope, string> m_names = [];
|
|
}
|
|
|
|
private static readonly MethodArgumentCodeNameScope
|
|
s_defaultMethodArgumentCodeNameScope = new();
|
|
private static readonly System.Threading.Lock s_generatedCodeNamesLock = new();
|
|
private static readonly ConditionalWeakTable<Parameter, GeneratedCodeNameState>
|
|
s_generatedCodeNames = new();
|
|
|
|
/// <summary>
|
|
/// Returns the field name of a child node.
|
|
/// </summary>
|
|
public static string GetChildFieldName(this Parameter field)
|
|
{
|
|
if (string.IsNullOrEmpty(field?.Name))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
// Derived from the property name, which is sanitized and already
|
|
// disambiguated against its siblings. Mapping the authored name
|
|
// directly is lossy in a way the property mapping is not: "Value Id"
|
|
// and "ValueId" both collapse onto "m_valueId" (CS0102, even though
|
|
// their properties differ), and characters that are legal in a
|
|
// BrowseName but not in an identifier survive into the field name -
|
|
// "$Value" became "m_$Value", which does not compile at all.
|
|
return field.GetPropertyName().TrimStart('@').ToSafeSymbolName(true, "m_");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the C# identifier of the property a structure field is
|
|
/// generated as. Structure field names are authored data, so they can be
|
|
/// C# keywords or collide with the members every generated data type
|
|
/// carries; neither would compile when used verbatim. The wire name is
|
|
/// unaffected - it is emitted from <see cref="Parameter.Name"/>.
|
|
/// </summary>
|
|
public static string GetPropertyName(this Parameter field)
|
|
{
|
|
return GetGeneratedName(field, s_propertyNames, BuildPropertyNames);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Looks a field's generated name up in the map computed once for its
|
|
/// whole structure. Deciding one field at a time cannot work: three
|
|
/// authored names that sanitize alike ("A-", "A?", "A!" all give "A_")
|
|
/// each see only the others' pre-disambiguation names, so the second and
|
|
/// third both pick the same replacement.
|
|
/// </summary>
|
|
private static string GetGeneratedName(
|
|
Parameter field,
|
|
ConditionalWeakTable<DataTypeDesign, string[]> cache,
|
|
ConditionalWeakTable<DataTypeDesign, string[]>.CreateValueCallback build)
|
|
{
|
|
if (string.IsNullOrEmpty(field?.Name))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
if (field.Parent is not DataTypeDesign dataType || dataType.Fields == null)
|
|
{
|
|
return field.Name.ToCSharpIdentifierPreserveCase();
|
|
}
|
|
|
|
int index = IndexOfField(dataType.Fields, field);
|
|
if (index < 0)
|
|
{
|
|
return field.Name.ToCSharpIdentifierPreserveCase();
|
|
}
|
|
return cache.GetValue(dataType, build)[index];
|
|
}
|
|
|
|
private static int IndexOfField(Parameter[] fields, Parameter field)
|
|
{
|
|
for (int ii = 0; ii < fields.Length; ii++)
|
|
{
|
|
if (ReferenceEquals(fields[ii], field))
|
|
{
|
|
return ii;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Assigns every field of a structure its generated property name in one
|
|
/// pass, in declaration order, so each name is unique within the class.
|
|
/// A name is taken by an earlier property, by the backing field of an
|
|
/// earlier property, by a member the templates declare, by the enclosing
|
|
/// type's own name, or by an inherited property.
|
|
/// </summary>
|
|
private static string[] BuildPropertyNames(DataTypeDesign dataType)
|
|
{
|
|
Parameter[] fields = dataType.Fields;
|
|
var names = new string[fields.Length];
|
|
|
|
var taken = new HashSet<string>(StringComparer.Ordinal);
|
|
taken.UnionWith(s_reservedDataTypeMembers);
|
|
taken.UnionWith(s_reservedDataTypeFields);
|
|
if (!string.IsNullOrEmpty(dataType.SymbolicName?.Name))
|
|
{
|
|
// A member may not carry the name of its enclosing type.
|
|
taken.Add(dataType.SymbolicName.Name);
|
|
}
|
|
|
|
// Inherited properties, keyed by generated name. A derived field
|
|
// that carries the *same* authored name as an inherited one is a
|
|
// deliberate redeclaration (the generator emits it as an override),
|
|
// so only a different wire name landing on the same identifier is a
|
|
// collision - that would be CS0108 on the generated class.
|
|
Dictionary<string, string> inherited = CollectInheritedPropertyNames(dataType);
|
|
|
|
for (int ii = 0; ii < fields.Length; ii++)
|
|
{
|
|
Parameter field = fields[ii];
|
|
if (string.IsNullOrEmpty(field?.Name))
|
|
{
|
|
names[ii] = string.Empty;
|
|
continue;
|
|
}
|
|
|
|
string candidate = SanitizeFieldName(field.Name);
|
|
string suffixed = candidate + "Field";
|
|
while (IsTaken(taken, inherited, candidate, field.Name))
|
|
{
|
|
candidate = suffixed;
|
|
suffixed += "_";
|
|
}
|
|
|
|
taken.Add(candidate);
|
|
// Reserve the backing field the property is stored in, so two
|
|
// properties cannot share one ("Value" and "value" both map to
|
|
// "m_value", and a field literally named "m_value" would take
|
|
// the name of another field's store).
|
|
taken.Add(ToBackingFieldName(candidate));
|
|
|
|
// Re-apply the keyword escape: the replacement may no longer be
|
|
// one, and the original may still be.
|
|
names[ii] = candidate.ToCSharpIdentifierPreserveCase();
|
|
}
|
|
|
|
return names;
|
|
}
|
|
|
|
private static bool IsTaken(
|
|
HashSet<string> taken,
|
|
Dictionary<string, string> inherited,
|
|
string candidate,
|
|
string authoredName)
|
|
{
|
|
return taken.Contains(candidate) ||
|
|
taken.Contains(ToBackingFieldName(candidate)) ||
|
|
(inherited.TryGetValue(candidate, out string? inheritedFrom) &&
|
|
!string.Equals(inheritedFrom, authoredName, StringComparison.Ordinal));
|
|
}
|
|
|
|
/// <summary>
|
|
/// The generated property names of every field inherited through the
|
|
/// base type chain, mapped to the authored name each came from.
|
|
/// </summary>
|
|
private static Dictionary<string, string> CollectInheritedPropertyNames(
|
|
DataTypeDesign dataType)
|
|
{
|
|
var result = new Dictionary<string, string>(StringComparer.Ordinal);
|
|
var visited = new HashSet<XmlQualifiedName>();
|
|
|
|
for (var baseType = dataType.BaseTypeNode as DataTypeDesign;
|
|
baseType?.Fields != null;
|
|
baseType = baseType.BaseTypeNode as DataTypeDesign)
|
|
{
|
|
if (baseType.SymbolicId != null && !visited.Add(baseType.SymbolicId))
|
|
{
|
|
// A cyclic base chain is reported elsewhere; do not hang.
|
|
break;
|
|
}
|
|
foreach (Parameter field in baseType.Fields)
|
|
{
|
|
if (string.IsNullOrEmpty(field?.Name))
|
|
{
|
|
continue;
|
|
}
|
|
string name = field.GetPropertyName().TrimStart('@');
|
|
if (!result.ContainsKey(name))
|
|
{
|
|
result[name] = field.Name;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static string SanitizeFieldName(string name)
|
|
{
|
|
return name.ToCSharpIdentifierPreserveCase().TrimStart('@');
|
|
}
|
|
|
|
private static string ToBackingFieldName(string propertyName)
|
|
{
|
|
return propertyName.ToSafeSymbolName(true, "m_");
|
|
}
|
|
|
|
private static readonly ConditionalWeakTable<DataTypeDesign, string[]>
|
|
s_propertyNames = new();
|
|
private static readonly ConditionalWeakTable<DataTypeDesign, string[]>
|
|
s_fieldsEnumMemberNames = new();
|
|
|
|
/// <summary>
|
|
/// Backing fields the data type templates declare themselves, which a
|
|
/// generated property therefore cannot be named after.
|
|
/// </summary>
|
|
private static readonly HashSet<string> s_reservedDataTypeFields =
|
|
new(StringComparer.Ordinal) { "m_FieldNames", "m_pooledSentinel" };
|
|
|
|
/// <summary>
|
|
/// Returns the C# identifier of the member a structure field is given in
|
|
/// the generated <c>{ClassName}Fields</c> enumeration. Same reasoning as
|
|
/// <see cref="GetPropertyName(Parameter)"/>, but the only name already
|
|
/// taken in that scope is <c>None</c>.
|
|
/// </summary>
|
|
public static string GetFieldsEnumMemberName(this Parameter field)
|
|
{
|
|
return GetGeneratedName(
|
|
field, s_fieldsEnumMemberNames, BuildFieldsEnumMemberNames);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Assigns every field its member name in the generated
|
|
/// <c>{ClassName}Fields</c> enumeration, in one pass for the same reason
|
|
/// as the property names. The enumeration is its own scope, so the only
|
|
/// name taken up front is <c>None</c>.
|
|
/// </summary>
|
|
private static string[] BuildFieldsEnumMemberNames(DataTypeDesign dataType)
|
|
{
|
|
Parameter[] fields = dataType.Fields;
|
|
var names = new string[fields.Length];
|
|
var taken = new HashSet<string>(StringComparer.Ordinal) { "None" };
|
|
|
|
for (int ii = 0; ii < fields.Length; ii++)
|
|
{
|
|
Parameter field = fields[ii];
|
|
if (string.IsNullOrEmpty(field?.Name))
|
|
{
|
|
names[ii] = string.Empty;
|
|
continue;
|
|
}
|
|
|
|
string candidate = SanitizeFieldName(field.Name);
|
|
string suffixed = candidate + "Field";
|
|
while (taken.Contains(candidate))
|
|
{
|
|
candidate = suffixed;
|
|
suffixed += "_";
|
|
}
|
|
|
|
taken.Add(candidate);
|
|
names[ii] = candidate.ToCSharpIdentifierPreserveCase();
|
|
}
|
|
|
|
return names;
|
|
}
|
|
|
|
|
|
private static readonly HashSet<string> s_reservedDataTypeMembers =
|
|
new(StringComparer.Ordinal)
|
|
{
|
|
"SwitchField",
|
|
"EncodingMask",
|
|
"EncodingMaskFieldNames",
|
|
"Reuse",
|
|
"ResetForReuse",
|
|
"TypeId",
|
|
"BinaryEncodingId",
|
|
"XmlEncodingId",
|
|
"JsonEncodingId",
|
|
"Encode",
|
|
"Decode",
|
|
"IsEqual",
|
|
"Equals",
|
|
"GetHashCode",
|
|
"GetType",
|
|
"ToString",
|
|
"Clone",
|
|
"MemberwiseClone",
|
|
"Initialize"
|
|
};
|
|
|
|
/// <summary>
|
|
/// Returns the field name of a child node.
|
|
/// </summary>
|
|
public static string GetChildFieldName(this InstanceDesign instance)
|
|
{
|
|
if (string.IsNullOrEmpty(instance?.SymbolicName?.Name))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
string name = instance.SymbolicName.Name.ToSafeSymbolName(true, "m_");
|
|
|
|
if (instance is MethodDesign)
|
|
{
|
|
return CoreUtils.Format("{0}Method", name);
|
|
}
|
|
|
|
return name;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fixes class names for nodes.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
/// <exception cref="ArgumentException"></exception>
|
|
public static string GetClassName(this TypeDesign node, Namespace[] namespaces)
|
|
{
|
|
if (node is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(node));
|
|
}
|
|
if (node is DataTypeDesign)
|
|
|
|
{
|
|
return node.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
|
|
}
|
|
if (node is ObjectTypeDesign objectType &&
|
|
objectType.ClassName == "ObjectSource")
|
|
{
|
|
return "global::Opc.Ua.BaseObject";
|
|
}
|
|
|
|
if (node is VariableTypeDesign variableType &&
|
|
variableType.ClassName is "DataVariable" or "BaseVariable")
|
|
{
|
|
return "global::Opc.Ua.BaseDataVariable";
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(node.ClassName))
|
|
{
|
|
return node.SymbolicName.AsFullyQualifiedTypeSymbol(
|
|
namespaces, node.ClassName);
|
|
}
|
|
return node.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the NodeClass of a Node
|
|
/// </summary>
|
|
public static string GetNodeClassAsString(this NodeDesign node)
|
|
{
|
|
if (node is VariableDesign)
|
|
{
|
|
return "Variable";
|
|
}
|
|
if (node is VariableTypeDesign)
|
|
|
|
{
|
|
return "VariableType";
|
|
}
|
|
if (node is ObjectDesign)
|
|
|
|
{
|
|
return "Object";
|
|
}
|
|
if (node is ObjectTypeDesign)
|
|
|
|
{
|
|
return "ObjectType";
|
|
}
|
|
if (node is ReferenceTypeDesign)
|
|
|
|
{
|
|
return "ReferenceType";
|
|
}
|
|
if (node is DataTypeDesign)
|
|
|
|
{
|
|
return "DataType";
|
|
}
|
|
if (node is MethodDesign)
|
|
|
|
{
|
|
return "Method";
|
|
}
|
|
if (node is ViewDesign)
|
|
|
|
{
|
|
return "View";
|
|
}
|
|
return "Node";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps the event notifier flag onto a string.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
public static string GetEventNotifierAsCode(this ObjectTypeDesign objectType)
|
|
{
|
|
if (objectType is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(objectType));
|
|
}
|
|
if (objectType.SupportsEvents)
|
|
{
|
|
return "global::Opc.Ua.EventNotifiers.SubscribeToEvents";
|
|
}
|
|
return "global::Opc.Ua.EventNotifiers.None";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps the value rank enumeration onto a string.
|
|
/// </summary>
|
|
public static string GetValueRankAsCode(this ValueRank valueRank, string arrayDimensions)
|
|
{
|
|
switch (valueRank)
|
|
{
|
|
case ValueRank.Array:
|
|
return "global::Opc.Ua.ValueRanks.OneDimension";
|
|
case ValueRank.Scalar:
|
|
return "global::Opc.Ua.ValueRanks.Scalar";
|
|
case ValueRank.Any:
|
|
case ValueRank.ScalarOrArray:
|
|
return "global::Opc.Ua.ValueRanks.Any";
|
|
case ValueRank.ScalarOrOneDimension:
|
|
return "global::Opc.Ua.ValueRanks.ScalarOrOneDimension";
|
|
case ValueRank.OneOrMoreDimensions:
|
|
if (string.IsNullOrWhiteSpace(arrayDimensions))
|
|
{
|
|
return "global::Opc.Ua.ValueRanks.OneOrMoreDimensions";
|
|
}
|
|
|
|
// The number of entries in ArrayDimensions is the number of
|
|
// dimensions, which equals the ValueRank for a fixed-size
|
|
// multi-dimensional array (e.g. "3,3" is a two-dimensional
|
|
// array with ValueRank 2).
|
|
string[] dimensions = arrayDimensions.Split([','], StringSplitOptions.RemoveEmptyEntries);
|
|
int dims = dimensions.Length;
|
|
if (dims == 0)
|
|
{
|
|
return "global::Opc.Ua.ValueRanks.OneOrMoreDimensions";
|
|
}
|
|
if (dims == 1)
|
|
{
|
|
return "global::Opc.Ua.ValueRanks.OneDimension";
|
|
}
|
|
if (dims == 2)
|
|
{
|
|
return "global::Opc.Ua.ValueRanks.TwoDimensions";
|
|
}
|
|
return CoreUtils.Format("{0}", dims);
|
|
}
|
|
|
|
return "global::Opc.Ua.ValueRanks.Any";
|
|
}
|
|
|
|
/// <summary>
|
|
/// The bit an OptionSet field mask (the design Identifier) selects.
|
|
/// An OptionSet field names exactly one bit (OPC 10000-3 8.40, 8.52);
|
|
/// false for a zero mask (the "no bits set" value) or a mask of
|
|
/// several bits.
|
|
/// </summary>
|
|
public static bool TryGetOptionSetBit(decimal mask, out int bit)
|
|
{
|
|
bit = 0;
|
|
if (mask <= 0 || decimal.Truncate(mask) != mask)
|
|
{
|
|
return false;
|
|
}
|
|
while (decimal.Remainder(mask, 2) == 0)
|
|
{
|
|
mask /= 2;
|
|
bit++;
|
|
}
|
|
return mask == 1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The bit an OptionSet field names: its explicit
|
|
/// <see cref="Parameter.OptionSetBit"/> (any position, a subtype of
|
|
/// the OptionSet structure has no upper bit, OPC 10000-3 8.40) or
|
|
/// else the bit of its Identifier mask. False for the zero ("no bits
|
|
/// set") value and a mask of several bits.
|
|
/// </summary>
|
|
public static bool TryGetOptionSetBit(this Parameter field, out int bit)
|
|
{
|
|
if (field?.OptionSetBit is int explicitBit && explicitBit >= 0)
|
|
{
|
|
bit = explicitBit;
|
|
return true;
|
|
}
|
|
bit = 0;
|
|
return field != null && TryGetOptionSetBit(field.Identifier, out bit);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The decimal mask of an OptionSet bit, or 0 when the bit is beyond
|
|
/// what a decimal represents exactly (bit 96 and above); such a bit
|
|
/// is carried by <see cref="Parameter.OptionSetBit"/>. Computed in
|
|
/// decimal since a signed shift turns bit 63 into a negative mask.
|
|
/// </summary>
|
|
public static decimal GetOptionSetMask(int bit)
|
|
{
|
|
if (bit is < 0 or >= kMaxDecimalMaskBits)
|
|
{
|
|
return 0;
|
|
}
|
|
decimal mask = 1m;
|
|
for (int ii = 0; ii < bit; ii++)
|
|
{
|
|
mask *= 2;
|
|
}
|
|
return mask;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The hexadecimal BitMask of an OptionSet bit (at least 8 digits),
|
|
/// for any bit position.
|
|
/// </summary>
|
|
public static string GetOptionSetBitMask(int bit)
|
|
{
|
|
string mask = "1248"[bit % 4] + new string('0', bit / 4);
|
|
return mask.Length < 8 ? mask.PadLeft(8, '0') : mask;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The bit a hexadecimal BitMask of any length selects; false when
|
|
/// the mask is not a single bit.
|
|
/// </summary>
|
|
public static bool TryGetOptionSetBitFromBitMask(string bitMask, out int bit)
|
|
{
|
|
bit = 0;
|
|
string? mask = bitMask?.Trim().TrimStart('0');
|
|
if (string.IsNullOrEmpty(mask))
|
|
{
|
|
return false;
|
|
}
|
|
int zeros = 0;
|
|
for (int ii = mask.Length - 1; ii > 0; ii--)
|
|
{
|
|
if (mask[ii] != '0')
|
|
{
|
|
return false;
|
|
}
|
|
zeros++;
|
|
}
|
|
int lead = mask[0] switch
|
|
{
|
|
'1' => 0,
|
|
'2' => 1,
|
|
'4' => 2,
|
|
'8' => 3,
|
|
_ => -1
|
|
};
|
|
if (lead < 0)
|
|
{
|
|
return false;
|
|
}
|
|
bit = (zeros * 4) + lead;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The bits a decimal OptionSet mask can represent exactly.
|
|
/// </summary>
|
|
private const int kMaxDecimalMaskBits = 96;
|
|
|
|
/// <summary>
|
|
/// The ArrayDimensions a StructureField publishes for the field. A
|
|
/// StructureField ValueRank is -1 or >= 1, never 0 (OPC 10000-3
|
|
/// 8.51), and a multi-dimensional field has at least two dimensions
|
|
/// (OPC 10000-6 5.2.5): a <see cref="ValueRank.OneOrMoreDimensions"/>
|
|
/// field without ArrayDimensions is a two dimensional matrix of
|
|
/// unknown lengths. (The validator already normalizes design fields
|
|
/// that way; this also covers fields it did not see.)
|
|
/// </summary>
|
|
public static string GetStructureFieldArrayDimensions(this Parameter field)
|
|
{
|
|
if (field.ValueRank == ValueRank.OneOrMoreDimensions &&
|
|
string.IsNullOrWhiteSpace(field.ArrayDimensions))
|
|
{
|
|
return "0,0";
|
|
}
|
|
return field.ArrayDimensions;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps the MinimumSamplingInterval onto a constant.
|
|
/// </summary>
|
|
public static string GetMinimumSamplingIntervalAsCode(this VariableTypeDesign variableType)
|
|
{
|
|
return variableType.MinimumSamplingInterval switch
|
|
{
|
|
-1 => "global::Opc.Ua.MinimumSamplingIntervals.Indeterminate",
|
|
0 => "global::Opc.Ua.MinimumSamplingIntervals.Continuous",
|
|
_ => variableType.MinimumSamplingInterval.ToString(CultureInfo.InvariantCulture)
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether this is a reference type and implicitly nullable
|
|
/// </summary>
|
|
public static bool IsDotNetReferenceType(
|
|
this DataTypeDesign dataType,
|
|
ValueRank valueRank)
|
|
{
|
|
return !IsDotNetValueType(dataType, valueRank);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether the data type supports equality comparison in .NET.
|
|
/// </summary>
|
|
public static bool IsDotNetEqualityComparable(
|
|
this DataTypeDesign dataType,
|
|
ValueRank valueRank)
|
|
{
|
|
if (IsDotNetValueType(dataType, valueRank))
|
|
{
|
|
return true;
|
|
}
|
|
if (valueRank == ValueRank.Scalar)
|
|
{
|
|
switch (dataType.BasicDataType)
|
|
{
|
|
case BasicDataType.String:
|
|
// Use equality for strings
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// If the data type is a value type in .NET.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
public static bool IsDotNetValueType(
|
|
this DataTypeDesign dataType,
|
|
ValueRank valueRank)
|
|
{
|
|
if (dataType is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(dataType));
|
|
}
|
|
if (valueRank != ValueRank.Scalar)
|
|
{
|
|
return true;
|
|
// ArrayOf/MatrixOf/Variant;
|
|
}
|
|
switch (dataType.BasicDataType)
|
|
{
|
|
case BasicDataType.Boolean:
|
|
case BasicDataType.SByte:
|
|
case BasicDataType.Byte:
|
|
case BasicDataType.Int16:
|
|
case BasicDataType.UInt16:
|
|
case BasicDataType.Int32:
|
|
case BasicDataType.UInt32:
|
|
case BasicDataType.Int64:
|
|
case BasicDataType.UInt64:
|
|
case BasicDataType.Float:
|
|
case BasicDataType.Double:
|
|
case BasicDataType.DateTime:
|
|
case BasicDataType.Guid:
|
|
case BasicDataType.NodeId:
|
|
case BasicDataType.ExpandedNodeId:
|
|
case BasicDataType.QualifiedName:
|
|
case BasicDataType.LocalizedText:
|
|
case BasicDataType.StatusCode:
|
|
case BasicDataType.Structure: // Extension object
|
|
case BasicDataType.Number:
|
|
case BasicDataType.Integer:
|
|
case BasicDataType.UInteger:
|
|
case BasicDataType.BaseDataType: // Variant
|
|
case BasicDataType.Enumeration when !dataType.IsOptionSet:
|
|
case BasicDataType.UserDefined when dataType.IsEnumeration:
|
|
case BasicDataType.ByteString:
|
|
case BasicDataType.XmlElement:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// If the type needs to be cloned
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
public static bool NeedsCloning(this DataTypeDesign dataType)
|
|
{
|
|
if (dataType is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(dataType));
|
|
}
|
|
switch (dataType.BasicDataType)
|
|
{
|
|
case BasicDataType.DataValue:
|
|
case BasicDataType.Structure: // Extension object
|
|
case BasicDataType.BaseDataType: // Variant
|
|
case BasicDataType.UserDefined when !dataType.IsEnumeration:
|
|
case BasicDataType.Enumeration when dataType.IsOptionSet:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether the data type can be expressed as a typed
|
|
/// <see cref="MatrixOf{T}"/> in generated code, i.e. whether
|
|
/// <c>Variant.From(MatrixOf<T>)</c> /
|
|
/// <c>Variant.GetXxxMatrix()</c> round-trip APIs exist.
|
|
/// All BasicDataType values except <see cref="BasicDataType.DiagnosticInfo"/>
|
|
/// (which is not a valid structure field per OPC UA Part 5) are
|
|
/// supported.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
public static bool SupportsMatrixOf(this DataTypeDesign dataType)
|
|
{
|
|
if (dataType is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(dataType));
|
|
}
|
|
return dataType.BasicDataType != BasicDataType.DiagnosticInfo;
|
|
}
|
|
|
|
/// <summary>
|
|
/// True if a field or a nested field of the structure (including the
|
|
/// fields inherited from its base types) is a multi-dimensional array,
|
|
/// i.e. encoded with the inline matrix representation. Such a
|
|
/// structure "is not compatible with the deprecated DataTypeDictionary
|
|
/// mechanism, and shall not be included in a DataTypeDictionary"
|
|
/// (OPC 10000-6 5.2.5): the dictionary has no way to describe values
|
|
/// whose count is the product of the dimensions. A field that allows
|
|
/// subtypes is an ExtensionObject in the dictionary and does not nest.
|
|
/// </summary>
|
|
public static bool HasInlineMatrixField(this DataTypeDesign dataType)
|
|
{
|
|
return HasInlineMatrixField(dataType, []);
|
|
|
|
static bool HasInlineMatrixField(DataTypeDesign dataType, HashSet<DataTypeDesign> visited)
|
|
{
|
|
if (dataType == null || !visited.Add(dataType))
|
|
{
|
|
return false;
|
|
}
|
|
for (DataTypeDesign? type = dataType;
|
|
type != null;
|
|
type = type.BaseTypeNode as DataTypeDesign)
|
|
{
|
|
if (type.Fields == null || type.IsOptionSet || type.IsEnumeration)
|
|
{
|
|
continue;
|
|
}
|
|
foreach (Parameter field in type.Fields)
|
|
{
|
|
if (field == null)
|
|
{
|
|
continue;
|
|
}
|
|
if (field.ValueRank == ValueRank.OneOrMoreDimensions)
|
|
{
|
|
return true;
|
|
}
|
|
if (field.ValueRank is ValueRank.Scalar or ValueRank.Array &&
|
|
!field.AllowSubTypes &&
|
|
field.DataTypeNode is DataTypeDesign fieldType &&
|
|
fieldType.BasicDataType == BasicDataType.UserDefined &&
|
|
!fieldType.IsEnumeration &&
|
|
HasInlineMatrixField(fieldType, visited))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits the default value as a C# expression.
|
|
/// </summary>
|
|
/// <param name="dataType">The data type of the value.</param>
|
|
/// <param name="valueRank">The value rank.</param>
|
|
/// <param name="defaultValue">The authored value, decoded here when
|
|
/// <paramref name="decodedValue"/> is null.</param>
|
|
/// <param name="decodedValue">The already decoded value.</param>
|
|
/// <param name="valueAsVariant">Wrap the expression in a Variant.</param>
|
|
/// <param name="targetNamespace">The target namespace.</param>
|
|
/// <param name="namespaces">The design's namespaces.</param>
|
|
/// <param name="context">The context used to decode
|
|
/// <paramref name="defaultValue"/>.</param>
|
|
/// <param name="onUnknownElement">Fallback for values that have no
|
|
/// expression form.</param>
|
|
/// <param name="decodedValueNamespaceUris">The table the namespace
|
|
/// indexes in <paramref name="decodedValue"/> refer to (the NodeSet's);
|
|
/// null for a value authored in the design.</param>
|
|
/// <param name="namespaceTableVariable">A NamespaceTable expression in
|
|
/// scope where the code is emitted (e.g. <c>context.NamespaceUris</c>),
|
|
/// or null when there is none.</param>
|
|
public static string GetValueAsCode(
|
|
this DataTypeDesign dataType,
|
|
ValueRank valueRank,
|
|
System.Xml.XmlElement? defaultValue,
|
|
object? decodedValue,
|
|
bool valueAsVariant,
|
|
string targetNamespace,
|
|
Namespace[] namespaces,
|
|
IServiceMessageContext context,
|
|
Func<string>? onUnknownElement = null,
|
|
NamespaceTable? decodedValueNamespaceUris = null,
|
|
string? namespaceTableVariable = null)
|
|
{
|
|
TypeInfo decodedValueType = default;
|
|
string defaultString = valueAsVariant ?
|
|
"global::Opc.Ua.Variant.Null" :
|
|
"default";
|
|
|
|
if (decodedValue == null && defaultValue != null)
|
|
{
|
|
// <opc:DefaultValue>
|
|
// <uax:Boolean>true</uax:Boolean>
|
|
// </opc:DefaultValue >
|
|
|
|
// hand-written designs may write empty strings as layout whitespace.
|
|
using var decoder = new XmlDecoder((XmlElement)defaultValue, context)
|
|
{
|
|
TreatWhitespaceOnlyStringsAsEmpty = true
|
|
};
|
|
Variant variant = decoder.ReadVariantValue(null, default);
|
|
decodedValueType = variant.TypeInfo;
|
|
decodedValue = variant.AsBoxedObject(Variant.BoxingBehavior.Legacy);
|
|
// Authored in the design: its indexes are the design's own.
|
|
decodedValueNamespaceUris = null;
|
|
}
|
|
|
|
if (valueRank == ValueRank.Array)
|
|
{
|
|
return dataType.GetArrayValueAsCode(
|
|
defaultValue,
|
|
decodedValue,
|
|
valueAsVariant,
|
|
targetNamespace,
|
|
namespaces,
|
|
onUnknownElement)
|
|
?? defaultString;
|
|
}
|
|
|
|
if (valueRank == ValueRank.Scalar &&
|
|
dataType.BasicDataType != BasicDataType.BaseDataType)
|
|
{
|
|
return dataType.GetScalarValueAsCode(
|
|
defaultValue,
|
|
decodedValue,
|
|
decodedValueType,
|
|
valueAsVariant,
|
|
targetNamespace,
|
|
namespaces,
|
|
onUnknownElement,
|
|
decodedValueNamespaceUris,
|
|
namespaceTableVariable)
|
|
?? defaultString;
|
|
}
|
|
|
|
return onUnknownElement?.Invoke() ?? defaultString;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get default for a scalar type
|
|
/// </summary>
|
|
internal static string? GetScalarValueAsCode(
|
|
this DataTypeDesign dataType,
|
|
System.Xml.XmlElement? defaultValue,
|
|
object? decodedValue,
|
|
TypeInfo decodedValueType,
|
|
bool valueAsVariant,
|
|
string targetNamespace,
|
|
Namespace[] namespaces,
|
|
Func<string>? onUnknownElement = null,
|
|
NamespaceTable? decodedValueNamespaceUris = null,
|
|
string? namespaceTableVariable = null)
|
|
{
|
|
// Note that we return a typed value since we initialize a object/variant
|
|
switch (dataType.BasicDataType)
|
|
{
|
|
case BasicDataType.Boolean:
|
|
if (decodedValue is not bool boolValue)
|
|
{
|
|
boolValue = false;
|
|
}
|
|
return MakeReturnType(boolValue ? "true" : "false");
|
|
case BasicDataType.SByte:
|
|
if (decodedValue is not sbyte sbyteValue)
|
|
{
|
|
sbyteValue = 0;
|
|
}
|
|
return MakeReturnType(CoreUtils.Format("(sbyte){0}", sbyteValue));
|
|
case BasicDataType.Byte:
|
|
if (decodedValue is not byte byteValue)
|
|
{
|
|
byteValue = 0;
|
|
}
|
|
return MakeReturnType(CoreUtils.Format("(byte){0}", byteValue));
|
|
case BasicDataType.Int16:
|
|
if (decodedValue is not short shortValue)
|
|
{
|
|
shortValue = 0;
|
|
}
|
|
return MakeReturnType(CoreUtils.Format("(short){0}", shortValue));
|
|
case BasicDataType.UInt16:
|
|
if (decodedValue is not ushort ushortValue)
|
|
{
|
|
ushortValue = 0;
|
|
}
|
|
return MakeReturnType(CoreUtils.Format("(ushort){0}", ushortValue));
|
|
case BasicDataType.Int32:
|
|
if (decodedValue is not int intValue)
|
|
{
|
|
intValue = 0;
|
|
}
|
|
return MakeReturnType(CoreUtils.Format("(int){0}", intValue));
|
|
case BasicDataType.UInt32:
|
|
if (decodedValue is not uint uintValue)
|
|
{
|
|
uintValue = 0;
|
|
}
|
|
return MakeReturnType(CoreUtils.Format("(uint){0}", uintValue));
|
|
case BasicDataType.Integer:
|
|
case BasicDataType.Int64:
|
|
if (decodedValue is not long longValue)
|
|
{
|
|
longValue = 0;
|
|
}
|
|
return MakeReturnType(CoreUtils.Format("(long){0}", longValue));
|
|
case BasicDataType.UInteger:
|
|
case BasicDataType.UInt64:
|
|
if (decodedValue is not ulong ulongValue)
|
|
{
|
|
ulongValue = 0;
|
|
}
|
|
return MakeReturnType(CoreUtils.Format("(ulong){0}", ulongValue));
|
|
case BasicDataType.Float:
|
|
if (decodedValue is not float floatValue)
|
|
{
|
|
floatValue = 0;
|
|
}
|
|
// NaN and the infinities have no literal form in C# - they
|
|
// format as "NaN" / "Infinity", which would not compile.
|
|
if (float.IsNaN(floatValue))
|
|
{
|
|
return MakeReturnType("float.NaN");
|
|
}
|
|
if (float.IsPositiveInfinity(floatValue))
|
|
{
|
|
return MakeReturnType("float.PositiveInfinity");
|
|
}
|
|
if (float.IsNegativeInfinity(floatValue))
|
|
{
|
|
return MakeReturnType("float.NegativeInfinity");
|
|
}
|
|
return MakeReturnType("(float)" + FormatFloatLiteral(floatValue));
|
|
case BasicDataType.Number:
|
|
case BasicDataType.Double:
|
|
if (decodedValue is not double doubleValue)
|
|
{
|
|
doubleValue = 0;
|
|
}
|
|
if (double.IsNaN(doubleValue))
|
|
{
|
|
return MakeReturnType("double.NaN");
|
|
}
|
|
if (double.IsPositiveInfinity(doubleValue))
|
|
{
|
|
return MakeReturnType("double.PositiveInfinity");
|
|
}
|
|
if (double.IsNegativeInfinity(doubleValue))
|
|
{
|
|
return MakeReturnType("double.NegativeInfinity");
|
|
}
|
|
return MakeReturnType("(double)" + FormatDoubleLiteral(doubleValue));
|
|
case BasicDataType.String:
|
|
if (decodedValue is not string stringValue)
|
|
{
|
|
return MakeReturnType("default(string)");
|
|
}
|
|
return MakeReturnType(stringValue.AsStringLiteral());
|
|
case BasicDataType.DateTime:
|
|
if (decodedValue is DateTimeUtc dtutc)
|
|
{
|
|
if (dtutc.IsNull)
|
|
{
|
|
return MakeReturnType("global::Opc.Ua.DateTimeUtc.MinValue");
|
|
}
|
|
decodedValue = (DateTime)dtutc;
|
|
}
|
|
if (decodedValue is not DateTime dateTimeValue ||
|
|
dateTimeValue == DateTime.MinValue)
|
|
{
|
|
return MakeReturnType("global::Opc.Ua.DateTimeUtc.MinValue");
|
|
}
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"global::Opc.Ua.DateTimeUtc.From(\"{0:yyyy-MM-dd HH:mm:ss}\")",
|
|
dateTimeValue));
|
|
case BasicDataType.Guid:
|
|
if (decodedValue is Uuid uuid)
|
|
{
|
|
decodedValue = uuid.Guid;
|
|
}
|
|
if (decodedValue is not Guid guidValue ||
|
|
guidValue == Guid.Empty)
|
|
{
|
|
return MakeReturnType("global::Opc.Ua.Uuid.Empty");
|
|
}
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"global::Opc.Ua.Uuid.Parse(\"{0}\")",
|
|
guidValue));
|
|
case BasicDataType.ByteString:
|
|
if (decodedValue is ByteString byteStringValue)
|
|
{
|
|
decodedValue = byteStringValue.ToArray();
|
|
}
|
|
if (decodedValue is not byte[] byteArray)
|
|
{
|
|
return MakeReturnType("global::Opc.Ua.ByteString.Empty");
|
|
}
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"global::Opc.Ua.ByteString.FromHexString(\"{0}\")",
|
|
CoreUtils.ToHexString(byteArray)));
|
|
case BasicDataType.NodeId:
|
|
if (decodedValue is not NodeId nodeId ||
|
|
nodeId.IsNull)
|
|
{
|
|
return MakeReturnType("global::Opc.Ua.NodeId.Null");
|
|
}
|
|
// The namespace index is one of the table the value was
|
|
// decoded with, not of the server the code runs in: resolve
|
|
// it to its URI and look that up at run time. Without a
|
|
// namespace table in scope (a structure field initializer)
|
|
// or for an index nothing declares, the literal id is all
|
|
// that can be emitted.
|
|
string? nodeIdNamespaceUri = nodeId.NamespaceIndex == 0
|
|
? null
|
|
: ResolveDecodedNamespaceUri(
|
|
nodeId.NamespaceIndex,
|
|
decodedValueNamespaceUris,
|
|
namespaces);
|
|
if (nodeIdNamespaceUri == null || namespaceTableVariable == null)
|
|
{
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"global::Opc.Ua.NodeId.Parse({0})",
|
|
nodeId.ToString().AsStringLiteral()));
|
|
}
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"global::Opc.Ua.NodeId.Parse({0}).WithNamespaceIndex({1}.GetIndexOrAppend({2}))",
|
|
nodeId.WithNamespaceIndex(0).ToString().AsStringLiteral(),
|
|
namespaceTableVariable,
|
|
nodeIdNamespaceUri.AsStringLiteral()));
|
|
case BasicDataType.ExpandedNodeId:
|
|
if (decodedValue is not ExpandedNodeId expandedNodeId ||
|
|
expandedNodeId.IsNull)
|
|
{
|
|
return MakeReturnType("global::Opc.Ua.ExpandedNodeId.Null");
|
|
}
|
|
// An ExpandedNodeId can carry the namespace URI itself, so an
|
|
// index-only value is emitted in its absolute "nsu=" form -
|
|
// the index would otherwise be read against the server's
|
|
// namespace table.
|
|
if (string.IsNullOrEmpty(expandedNodeId.NamespaceUri) &&
|
|
expandedNodeId.NamespaceIndex != 0)
|
|
{
|
|
string? expandedNamespaceUri = ResolveDecodedNamespaceUri(
|
|
expandedNodeId.NamespaceIndex,
|
|
decodedValueNamespaceUris,
|
|
namespaces);
|
|
if (expandedNamespaceUri != null)
|
|
{
|
|
expandedNodeId = new ExpandedNodeId(
|
|
expandedNodeId.InnerNodeId,
|
|
expandedNamespaceUri,
|
|
expandedNodeId.ServerIndex);
|
|
}
|
|
}
|
|
// A server index is one of the NodeSet's ServerUris table
|
|
// (OPC 10000-6 F.2, F.14), not of the server the code runs
|
|
// in, and the ExpandedNodeId text has no server URI form:
|
|
// map the URI through the context's ServerUris at run time
|
|
// (the literal index stays the fallback without a table).
|
|
string? serverUri = expandedNodeId.ServerIndex == 0 ||
|
|
decodedValueNamespaceUris is not DecodedValueNamespaceTable decodedTable
|
|
? null
|
|
: decodedTable.ServerUris?.GetString(expandedNodeId.ServerIndex);
|
|
string? serverTableVariable = GetServerTableVariable(namespaceTableVariable);
|
|
if (serverUri != null && serverTableVariable != null)
|
|
{
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"global::Opc.Ua.ExpandedNodeId.Parse({0}).WithServerIndex({1}?.GetIndexOrAppend({2}) ?? {3}u)",
|
|
expandedNodeId.WithServerIndex(0).ToString().AsStringLiteral(),
|
|
serverTableVariable,
|
|
serverUri.AsStringLiteral(),
|
|
expandedNodeId.ServerIndex));
|
|
}
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"global::Opc.Ua.ExpandedNodeId.Parse({0})",
|
|
expandedNodeId.ToString().AsStringLiteral()));
|
|
case BasicDataType.QualifiedName:
|
|
if (decodedValue is not QualifiedName qualifiedName ||
|
|
qualifiedName.IsNull)
|
|
{
|
|
return MakeReturnType("global::Opc.Ua.QualifiedName.Null");
|
|
}
|
|
string? qualifiedNameNamespaceUri = qualifiedName.NamespaceIndex == 0
|
|
? null
|
|
: ResolveDecodedNamespaceUri(
|
|
qualifiedName.NamespaceIndex,
|
|
decodedValueNamespaceUris,
|
|
namespaces);
|
|
if (qualifiedNameNamespaceUri == null || namespaceTableVariable == null)
|
|
{
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"global::Opc.Ua.QualifiedName.Parse({0})",
|
|
qualifiedName.ToString().AsStringLiteral()));
|
|
}
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"new global::Opc.Ua.QualifiedName({0}, {1}.GetIndexOrAppend({2}))",
|
|
qualifiedName.Name.AsStringLiteral(),
|
|
namespaceTableVariable,
|
|
qualifiedNameNamespaceUri.AsStringLiteral()));
|
|
case BasicDataType.LocalizedText:
|
|
if (decodedValue is not Ua.LocalizedText localizedText ||
|
|
localizedText.IsNullOrEmpty)
|
|
{
|
|
return MakeReturnType("global::Opc.Ua.LocalizedText.Null");
|
|
}
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"new global::Opc.Ua.LocalizedText({0}, {1})",
|
|
localizedText.Locale.AsStringLiteral(),
|
|
localizedText.Text.AsStringLiteral()));
|
|
case BasicDataType.StatusCode:
|
|
if (decodedValue is not StatusCode statusCode ||
|
|
statusCode.Code == 0)
|
|
{
|
|
return MakeReturnType("default(global::Opc.Ua.StatusCode)");
|
|
}
|
|
// The numeric code, not StatusCode.ToString(): that yields
|
|
// "BadNodeIdUnknown [0x80340000]", which is no C# expression.
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"new global::Opc.Ua.StatusCode(0x{0:X8}u)",
|
|
statusCode.Code));
|
|
case BasicDataType.Enumeration:
|
|
if (dataType.BaseTypeNode?.SymbolicId ==
|
|
new XmlQualifiedName("OptionSet", Namespaces.OpcUa))
|
|
{
|
|
// OptionSet subtypes inherit from Opc.Ua.OptionSet (a
|
|
// class implementing IEncodeable), so Variant.From<T>
|
|
// (where T : struct, Enum) does not apply. Route them
|
|
// through Variant.FromStructure<T> (where T : IEncodeable).
|
|
return MakeReturnType(
|
|
CoreUtils.Format("new {0}()",
|
|
dataType.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces)),
|
|
"Structure");
|
|
}
|
|
if (!dataType.IsOptionSet && dataType.Fields?.Length > 0)
|
|
{
|
|
if (decodedValue is int enumValue)
|
|
{
|
|
return MakeReturnType(CoreUtils.Format("({0})({1})",
|
|
dataType.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces),
|
|
enumValue));
|
|
}
|
|
if (decodedValue is null)
|
|
{
|
|
// Enum type declared - use first field as default
|
|
return MakeReturnType(CoreUtils.Format("{0}.{1}",
|
|
dataType.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces),
|
|
dataType.Fields[0].Name));
|
|
}
|
|
// TODO: change to initialize enum with the value
|
|
return onUnknownElement?.Invoke() ??
|
|
MakeReturnType(CoreUtils.Format("default({0})",
|
|
dataType.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces)));
|
|
}
|
|
// Default to enum with value 0 which should be "none" for option set
|
|
return onUnknownElement?.Invoke();
|
|
case BasicDataType.DataValue:
|
|
return MakeReturnType("new global::Opc.Ua.DataValue()");
|
|
case BasicDataType.UserDefined:
|
|
if (defaultValue == null)
|
|
{
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"new {0}()",
|
|
GetDotNetTypeName(
|
|
dataType,
|
|
ValueRank.Scalar,
|
|
targetNamespace,
|
|
namespaces,
|
|
nullable: NullableAnnotation.NonNullable)),
|
|
"Structure");
|
|
}
|
|
return onUnknownElement?.Invoke();
|
|
default:
|
|
return onUnknownElement?.Invoke();
|
|
}
|
|
|
|
string MakeReturnType(string value, string? prefix = null)
|
|
{
|
|
if (valueAsVariant)
|
|
{
|
|
return CoreUtils.Format(
|
|
"global::Opc.Ua.Variant.From{0}({1})",
|
|
prefix ?? string.Empty,
|
|
value);
|
|
}
|
|
return value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The run-time server URI table that sits next to a namespace table
|
|
/// expression: "context.NamespaceUris" pairs with "context.ServerUris".
|
|
/// Null when the expression is not a member access on a context.
|
|
/// </summary>
|
|
private static string? GetServerTableVariable(string? namespaceTableVariable)
|
|
{
|
|
const string suffix = ".NamespaceUris";
|
|
if (namespaceTableVariable == null ||
|
|
!namespaceTableVariable.EndsWith(suffix, StringComparison.Ordinal))
|
|
{
|
|
return null;
|
|
}
|
|
return namespaceTableVariable[..(namespaceTableVariable.Length - suffix.Length)] +
|
|
".ServerUris";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves a namespace index found inside a decoded default value to
|
|
/// its namespace URI. A value read from a NodeSet carries the table of
|
|
/// that NodeSet; a value authored in a ModelDesign numbers OPC UA 0 and
|
|
/// then the design's namespaces in declaration order. Returns null when
|
|
/// the index is not in the table.
|
|
/// </summary>
|
|
internal static string? ResolveDecodedNamespaceUri(
|
|
ushort namespaceIndex,
|
|
NamespaceTable? decodedValueNamespaceUris,
|
|
Namespace[] namespaces)
|
|
{
|
|
if (namespaceIndex == 0)
|
|
{
|
|
return Namespaces.OpcUa;
|
|
}
|
|
NamespaceTable table = decodedValueNamespaceUris ??
|
|
CreateDesignNamespaceTable(namespaces);
|
|
return table.GetString(namespaceIndex);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The table the namespace indexes of a value authored in a ModelDesign
|
|
/// refer to: OPC UA 0 and then the design's namespaces in declaration
|
|
/// order.
|
|
/// </summary>
|
|
internal static NamespaceTable CreateDesignNamespaceTable(Namespace[] namespaces)
|
|
{
|
|
var table = new NamespaceTable();
|
|
foreach (Namespace ns in namespaces ?? [])
|
|
{
|
|
if (!string.IsNullOrEmpty(ns?.Value))
|
|
{
|
|
table.GetIndexOrAppend(ns.Value);
|
|
}
|
|
}
|
|
return table;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Formats a double as the digits of a C# literal that evaluates to
|
|
/// exactly the same value. The default "G" format depends on the
|
|
/// runtime the generator runs in: 15 significant digits on .NET
|
|
/// Framework (Visual Studio), the shortest round-trip form on .NET -
|
|
/// so the generated code differed between the two, lost precision on
|
|
/// .NET Framework and turned double.MaxValue into an out-of-range
|
|
/// literal (CS0594). The shortest of G15..G17 that parses back to the
|
|
/// value is the same on every runtime.
|
|
/// </summary>
|
|
internal static string FormatDoubleLiteral(double value)
|
|
{
|
|
if (value == 0)
|
|
{
|
|
// "(double)-0" would be integer zero cast to +0.
|
|
return BitConverter.DoubleToInt64Bits(value) < 0 ? "-0.0" : "0";
|
|
}
|
|
for (int precision = 15; precision < 17; precision++)
|
|
{
|
|
string text = value.ToString(
|
|
"G" + precision.ToString(CultureInfo.InvariantCulture),
|
|
CultureInfo.InvariantCulture);
|
|
if (double.TryParse(
|
|
text,
|
|
NumberStyles.Float,
|
|
CultureInfo.InvariantCulture,
|
|
out double parsed) &&
|
|
parsed.Equals(value))
|
|
{
|
|
return text;
|
|
}
|
|
}
|
|
return value.ToString("G17", CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Formats a float as the digits of a literal that, cast to float,
|
|
/// evaluates to exactly the same value. The generated expression is
|
|
/// <c>(float)digits</c>, i.e. the digits are parsed as a double and then
|
|
/// converted, so the round trip is checked the same way.
|
|
/// See <see cref="FormatDoubleLiteral"/> for why "G" is not used.
|
|
/// </summary>
|
|
internal static string FormatFloatLiteral(float value)
|
|
{
|
|
if (value == 0)
|
|
{
|
|
return BitConverter.DoubleToInt64Bits(value) < 0 ? "-0.0" : "0";
|
|
}
|
|
for (int precision = 6; precision < 9; precision++)
|
|
{
|
|
string text = value.ToString(
|
|
"G" + precision.ToString(CultureInfo.InvariantCulture),
|
|
CultureInfo.InvariantCulture);
|
|
if (double.TryParse(
|
|
text,
|
|
NumberStyles.Float,
|
|
CultureInfo.InvariantCulture,
|
|
out double parsed) &&
|
|
((float)parsed).Equals(value))
|
|
{
|
|
return text;
|
|
}
|
|
}
|
|
return value.ToString("G9", CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get default for a array type
|
|
/// </summary>
|
|
internal static string? GetArrayValueAsCode(
|
|
this DataTypeDesign dataType,
|
|
System.Xml.XmlElement? defaultValue,
|
|
object? decodedValue,
|
|
bool valueAsVariant,
|
|
string targetNamespace,
|
|
Namespace[] namespaces,
|
|
Func<string>? onUnknownElement = null)
|
|
{
|
|
if ((decodedValue is not null &&
|
|
(decodedValue is not IList l || l.Count != 0) &&
|
|
(decodedValue is not Array a || a.Length != 0)) ||
|
|
(decodedValue is null && defaultValue is not null))
|
|
{
|
|
// TODO: change to initialize simple arrays with values
|
|
return onUnknownElement?.Invoke();
|
|
}
|
|
// Emit Array.Empty<T>() for empty arrays
|
|
switch (dataType.BasicDataType)
|
|
{
|
|
case BasicDataType.Boolean:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<bool>()");
|
|
case BasicDataType.SByte:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<sbyte>()");
|
|
case BasicDataType.Byte:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<byte>()");
|
|
case BasicDataType.Int16:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<short>()");
|
|
case BasicDataType.UInt16:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<ushort>()");
|
|
case BasicDataType.Int32:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<int>()");
|
|
case BasicDataType.UInt32:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<uint>()");
|
|
case BasicDataType.Int64:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<long>()");
|
|
case BasicDataType.UInt64:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<ulong>()");
|
|
case BasicDataType.Float:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<float>()");
|
|
case BasicDataType.Double:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<double>()");
|
|
case BasicDataType.String:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<string>()");
|
|
case BasicDataType.Guid:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.Uuid>()");
|
|
case BasicDataType.DateTime:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.DateTimeUtc>()");
|
|
case BasicDataType.ByteString:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.ByteString>()");
|
|
case BasicDataType.XmlElement:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.XmlElement>()");
|
|
case BasicDataType.NodeId:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.NodeId>()");
|
|
case BasicDataType.ExpandedNodeId:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.ExpandedNodeId>()");
|
|
case BasicDataType.QualifiedName:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.QualifiedName>()");
|
|
case BasicDataType.LocalizedText:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.LocalizedText>()");
|
|
case BasicDataType.StatusCode:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.StatusCode>()");
|
|
case BasicDataType.DataValue:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.DataValue>()");
|
|
case BasicDataType.Structure:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.ExtensionObject>()");
|
|
case BasicDataType.Enumeration:
|
|
case BasicDataType.UserDefined:
|
|
return MakeReturnType(
|
|
CoreUtils.Format(
|
|
"global::Opc.Ua.ArrayOf.Empty<{0}>()",
|
|
GetDotNetTypeName(
|
|
dataType,
|
|
ValueRank.Scalar,
|
|
targetNamespace,
|
|
namespaces,
|
|
nullable: NullableAnnotation.NonNullable)),
|
|
dataType.BasicDataType == BasicDataType.UserDefined ? "Structure" : null);
|
|
case BasicDataType.DiagnosticInfo:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.DiagnosticInfo>()");
|
|
case BasicDataType.Number:
|
|
case BasicDataType.Integer:
|
|
case BasicDataType.UInteger:
|
|
case BasicDataType.BaseDataType:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.Variant>()");
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
string MakeReturnType(string value, string? prefix = null)
|
|
{
|
|
if (valueAsVariant)
|
|
{
|
|
return CoreUtils.Format(
|
|
"global::Opc.Ua.Variant.From{0}({1})",
|
|
prefix ?? string.Empty,
|
|
value);
|
|
}
|
|
return value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get code string to load a variant from XML resource stream.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public static string GetVariantValueFromXmlAsCode(
|
|
this DataTypeDesign dataType,
|
|
ValueRank valueRank,
|
|
string targetNamespace,
|
|
Namespace[] namespaces,
|
|
string xmlStreamResource,
|
|
string? contextVariable = null)
|
|
{
|
|
contextVariable ??= "context";
|
|
switch (dataType.BasicDataType)
|
|
{
|
|
case BasicDataType.BaseDataType:
|
|
return CoreUtils.Format(
|
|
"global::Opc.Ua.Variant.FromXml({0}AsStream, {1})",
|
|
xmlStreamResource,
|
|
contextVariable);
|
|
case BasicDataType.UserDefined:
|
|
case BasicDataType.Structure:
|
|
case BasicDataType.Enumeration:
|
|
string dataTypeString = dataType.GetDotNetTypeName(
|
|
targetNamespace,
|
|
namespaces,
|
|
NullableAnnotation.NonNullable,
|
|
false);
|
|
return CoreUtils.Format(
|
|
"global::Opc.Ua.Variant.FromXml({0}AsStream, {1}).Get{2}{3}<{4}>()",
|
|
xmlStreamResource,
|
|
contextVariable,
|
|
dataType.BasicDataType == BasicDataType.Enumeration ? "Enumeration" : "Structure",
|
|
valueRank == ValueRank.Array ? "Array" : string.Empty,
|
|
dataTypeString);
|
|
default:
|
|
return CoreUtils.Format(
|
|
"global::Opc.Ua.Variant.FromXml({0}AsStream, {1}).Get{2}{3}()",
|
|
xmlStreamResource,
|
|
contextVariable,
|
|
dataType.BasicDataType.ToString(),
|
|
valueRank == ValueRank.Array ? "Array" : string.Empty);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the data type for a method argument.
|
|
/// </summary>
|
|
public static string GetMethodArgumentTypeAsCode(
|
|
this DataTypeDesign datatype,
|
|
ValueRank valueRank,
|
|
string targetNamespace,
|
|
Namespace[] namespaces,
|
|
bool isOptional)
|
|
{
|
|
string typeName = GetDotNetTypeName(
|
|
datatype,
|
|
targetNamespace,
|
|
namespaces,
|
|
// Make scalar optional nullable, but array/matrix elements non nullable
|
|
// TODO: Revisit this and make input non nullable but output parameter T
|
|
// nullable when we get rid of collections
|
|
nullable: isOptional && valueRank == ValueRank.Scalar ?
|
|
NullableAnnotation.Nullable : NullableAnnotation.NonNullable,
|
|
// Use raw arguments
|
|
false);
|
|
|
|
if (typeName is "global::Opc.Ua.IEncodeable" or "global::Opc.Ua.IEncodeable?")
|
|
|
|
{
|
|
typeName = "global::Opc.Ua.ExtensionObject";
|
|
}
|
|
if (typeName is "object" or "object?")
|
|
{
|
|
typeName = "global::Opc.Ua.Variant";
|
|
}
|
|
switch (valueRank)
|
|
{
|
|
case ValueRank.Scalar:
|
|
return typeName;
|
|
case ValueRank.Array:
|
|
return CoreUtils.Format("global::Opc.Ua.ArrayOf<{0}>", typeName);
|
|
case ValueRank.OneOrMoreDimensions:
|
|
return CoreUtils.Format("global::Opc.Ua.MatrixOf<{0}>", typeName);
|
|
default:
|
|
return "global::Opc.Ua.Variant";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the type is nullable
|
|
/// </summary>
|
|
/// <param name="type"></param>
|
|
/// <returns></returns>
|
|
public static bool IsXmlNillable(this BasicDataType type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case BasicDataType.Boolean:
|
|
case BasicDataType.SByte:
|
|
case BasicDataType.Byte:
|
|
case BasicDataType.Int16:
|
|
case BasicDataType.UInt16:
|
|
case BasicDataType.Int32:
|
|
case BasicDataType.UInt32:
|
|
case BasicDataType.Int64:
|
|
case BasicDataType.UInt64:
|
|
case BasicDataType.Float:
|
|
case BasicDataType.Double:
|
|
case BasicDataType.StatusCode:
|
|
case BasicDataType.Enumeration:
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns system type for a basic data type.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentException"></exception>
|
|
public static string GetDotNetTypeName(
|
|
this DataTypeDesign? datatype,
|
|
string targetNamespace,
|
|
Namespace[] namespaces,
|
|
NullableAnnotation nullable = NullableAnnotation.NonNullable,
|
|
bool useBuiltInWrapperTypes = false)
|
|
{
|
|
switch (datatype!.BasicDataType)
|
|
{
|
|
case BasicDataType.Boolean:
|
|
return "bool";
|
|
case BasicDataType.SByte:
|
|
return "sbyte";
|
|
case BasicDataType.Byte:
|
|
return "byte";
|
|
case BasicDataType.Int16:
|
|
return "short";
|
|
case BasicDataType.UInt16:
|
|
return "ushort";
|
|
case BasicDataType.Int32:
|
|
return "int";
|
|
case BasicDataType.UInt32:
|
|
return "uint";
|
|
case BasicDataType.Int64:
|
|
return "long";
|
|
case BasicDataType.UInt64:
|
|
return "ulong";
|
|
case BasicDataType.Float:
|
|
return "float";
|
|
case BasicDataType.Double:
|
|
return "double";
|
|
case BasicDataType.String:
|
|
return nullable == NullableAnnotation.NonNullable ?
|
|
"string" :
|
|
"string?";
|
|
case BasicDataType.DateTime:
|
|
return "global::Opc.Ua.DateTimeUtc";
|
|
case BasicDataType.Guid:
|
|
return "global::Opc.Ua.Uuid";
|
|
case BasicDataType.ByteString:
|
|
return "global::Opc.Ua.ByteString";
|
|
case BasicDataType.XmlElement:
|
|
return "global::Opc.Ua.XmlElement";
|
|
case BasicDataType.NodeId:
|
|
return "global::Opc.Ua.NodeId";
|
|
case BasicDataType.ExpandedNodeId:
|
|
return "global::Opc.Ua.ExpandedNodeId";
|
|
case BasicDataType.StatusCode:
|
|
return "global::Opc.Ua.StatusCode";
|
|
case BasicDataType.DiagnosticInfo:
|
|
return nullable == NullableAnnotation.NonNullable ?
|
|
"global::Opc.Ua.DiagnosticInfo" :
|
|
"global::Opc.Ua.DiagnosticInfo?";
|
|
case BasicDataType.QualifiedName:
|
|
return "global::Opc.Ua.QualifiedName";
|
|
case BasicDataType.LocalizedText:
|
|
return "global::Opc.Ua.LocalizedText";
|
|
case BasicDataType.DataValue:
|
|
return "global::Opc.Ua.DataValue";
|
|
case BasicDataType.Structure:
|
|
if (!useBuiltInWrapperTypes)
|
|
{
|
|
// return nullable == NullableAnnotation.NonNullable ?
|
|
// "global::Opc.Ua.IEncodeable" :
|
|
// "global::Opc.Ua.IEncodeable?";
|
|
}
|
|
return "global::Opc.Ua.ExtensionObject";
|
|
case BasicDataType.Enumeration:
|
|
if (datatype.SymbolicId ==
|
|
new XmlQualifiedName("Enumeration", Namespaces.OpcUa))
|
|
{
|
|
return "int";
|
|
}
|
|
|
|
if (datatype.IsOptionSet)
|
|
{
|
|
return GetDotNetTypeName(
|
|
(DataTypeDesign)datatype.BaseTypeNode!,
|
|
targetNamespace,
|
|
namespaces,
|
|
nullable, // Should it always be non nullable?
|
|
useBuiltInWrapperTypes);
|
|
}
|
|
return datatype.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
|
|
case BasicDataType.UserDefined:
|
|
string typeName = datatype.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
|
|
if (datatype.IsEnumeration)
|
|
{
|
|
return typeName;
|
|
}
|
|
// All of these are always set to default type in properties when null
|
|
// is passed therefore we want to maintain non nullability no matter
|
|
return nullable != NullableAnnotation.Nullable ? typeName : typeName + "?";
|
|
}
|
|
if (!useBuiltInWrapperTypes)
|
|
{
|
|
return nullable == NullableAnnotation.NonNullable ? "object" : "object?";
|
|
}
|
|
return "global::Opc.Ua.Variant";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the type name for the data type with the provided value rank.
|
|
/// </summary>
|
|
public static string GetDotNetTypeName(
|
|
this DataTypeDesign dataType,
|
|
ValueRank valueRank,
|
|
string targetNamespace,
|
|
Namespace[] namespaces,
|
|
NullableAnnotation nullable = NullableAnnotation.NonNullable,
|
|
bool useMatrixTypeInsteadOfVariant = false)
|
|
{
|
|
string typeName = dataType.GetDotNetTypeName(
|
|
targetNamespace,
|
|
namespaces,
|
|
// Make scalar optional nullable, but array/matrix elements non nullable
|
|
// TODO: Revisit this when we get rid of generated collections
|
|
valueRank == ValueRank.Scalar ? nullable : NullableAnnotation.NonNullable,
|
|
true);
|
|
if (typeName != null)
|
|
{
|
|
switch (valueRank)
|
|
{
|
|
case ValueRank.Scalar:
|
|
return typeName;
|
|
case ValueRank.Array:
|
|
return CoreUtils.Format("global::Opc.Ua.ArrayOf<{0}>", typeName);
|
|
case ValueRank.OneOrMoreDimensions when useMatrixTypeInsteadOfVariant:
|
|
return CoreUtils.Format("global::Opc.Ua.MatrixOf<{0}>", typeName);
|
|
}
|
|
}
|
|
return "global::Opc.Ua.Variant";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether a template parameter is required.
|
|
/// </summary>
|
|
public static bool IsTemplateParameterRequired(
|
|
this DataTypeDesign dataType,
|
|
ValueRank valueRank)
|
|
{
|
|
// DiagnosticInfo is not a valid Variant value (OPC UA Part 6) and
|
|
// therefore has no IVariantBuilder<DiagnosticInfo>. A typed
|
|
// State<DiagnosticInfo> (scalar or ArrayOf/MatrixOf) cannot be
|
|
// emitted, so fall back to the non-generic, Variant-backed state.
|
|
// Mirrors the DiagnosticInfo exclusion in SupportsMatrixOf.
|
|
if (dataType.BasicDataType == BasicDataType.DiagnosticInfo)
|
|
{
|
|
return false;
|
|
}
|
|
if (dataType.BasicDataType
|
|
is not BasicDataType.BaseDataType
|
|
and not BasicDataType.Number
|
|
and not BasicDataType.UInteger
|
|
and not BasicDataType.Integer &&
|
|
valueRank
|
|
is not ValueRank.OneOrMoreDimensions
|
|
and not ValueRank.ScalarOrOneDimension
|
|
and not ValueRank.ScalarOrArray)
|
|
{
|
|
return true;
|
|
}
|
|
if (valueRank == ValueRank.Array)
|
|
{
|
|
return true;
|
|
}
|
|
// Matrix-rank with a concrete data type that supports
|
|
// `MatrixOf<T>` is fully determinate (the generator produces
|
|
// `State<MatrixOf<T>>` rather than `State<T>`/`State<Variant>`),
|
|
// so the template parameter is in fact required to specialize
|
|
// the inherited State class.
|
|
if (valueRank == ValueRank.OneOrMoreDimensions &&
|
|
dataType.SupportsMatrixOf() &&
|
|
dataType.BasicDataType
|
|
is not BasicDataType.BaseDataType
|
|
and not BasicDataType.Number
|
|
and not BasicDataType.UInteger
|
|
and not BasicDataType.Integer)
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Is overridden instance.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
public static bool IsOverridden(this InstanceDesign instance)
|
|
{
|
|
if (instance is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(instance));
|
|
}
|
|
return instance.OveriddenNode != null &&
|
|
instance.ModellingRule != ModellingRule.None &&
|
|
instance.ModellingRule != ModellingRule.ExposesItsArray &&
|
|
instance.ModellingRule != ModellingRule.MandatoryPlaceholder &&
|
|
instance.ModellingRule != ModellingRule.OptionalPlaceholder;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether a placeholder redeclares one its base type already
|
|
/// declares.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// A placeholder is not overridden in the sense
|
|
/// <see cref="IsOverridden"/> means, because it names a slot rather than
|
|
/// a child, and merging it with the base declaration would be wrong. It
|
|
/// does however emit an Add method named after the slot, so a subtype
|
|
/// that redeclares the slot hides its base method and has to say so.
|
|
/// This method detects that redeclaration so the generated member can
|
|
/// be marked appropriately.
|
|
/// </remarks>
|
|
public static bool HidesBaseTypePlaceholder(this InstanceDesign instance)
|
|
{
|
|
if (instance is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(instance));
|
|
}
|
|
if (instance.ModellingRule != ModellingRule.MandatoryPlaceholder &&
|
|
instance.ModellingRule != ModellingRule.OptionalPlaceholder)
|
|
{
|
|
return false;
|
|
}
|
|
// OveriddenNode is not linked for a placeholder, so the base chain
|
|
// is walked for a slot of the same name.
|
|
if (instance.Parent is not TypeDesign declaringType)
|
|
{
|
|
return false;
|
|
}
|
|
for (TypeDesign? baseType = declaringType.BaseTypeNode;
|
|
baseType != null;
|
|
baseType = baseType.BaseTypeNode)
|
|
{
|
|
InstanceDesign[]? children = baseType.Children?.Items;
|
|
if (children == null)
|
|
{
|
|
continue;
|
|
}
|
|
foreach (InstanceDesign child in children)
|
|
{
|
|
if (child.SymbolicName == instance.SymbolicName)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the merged instance for an overriden node.
|
|
/// </summary>
|
|
public static InstanceDesign GetMergedInstance(this InstanceDesign instance)
|
|
{
|
|
for (NodeDesign? parent = instance.Parent; parent != null; parent = parent.Parent)
|
|
{
|
|
if (parent.Parent == null && parent.Hierarchy != null)
|
|
{
|
|
// The hierarchy is keyed by the path relative to the root, so
|
|
// the root's own symbolic name has to come off the front.
|
|
// Strip that exact prefix rather than everything up to the
|
|
// first '_', which mangles the path whenever the root type's
|
|
// name itself contains an underscore.
|
|
string relativePath = instance.SymbolicId.Name;
|
|
string rootPrefix = parent.SymbolicId?.Name + "_";
|
|
|
|
if (rootPrefix.Length > 1 &&
|
|
relativePath.StartsWith(rootPrefix, StringComparison.Ordinal))
|
|
{
|
|
relativePath = relativePath[rootPrefix.Length..];
|
|
}
|
|
else
|
|
{
|
|
int index = relativePath.IndexOf('_', StringComparison.Ordinal);
|
|
|
|
if (index != -1)
|
|
{
|
|
relativePath = relativePath[(index + 1)..];
|
|
}
|
|
}
|
|
if (parent.Hierarchy.Nodes.TryGetValue(relativePath,
|
|
out HierarchyNode? hierarchyNode) &&
|
|
hierarchyNode.Instance is InstanceDesign instanceDesign)
|
|
{
|
|
return instanceDesign;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return instance;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a name qualified with a namespace prefix.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
public static string GetPrefixedName(this XmlQualifiedName qname, List<string> namespaceUris)
|
|
{
|
|
if (qname.IsNull())
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
if (qname.Namespace == Namespaces.OpcUaBuiltInTypes)
|
|
{
|
|
return CoreUtils.Format("ua:{0}", qname.Name);
|
|
}
|
|
|
|
if (namespaceUris is null)
|
|
|
|
{
|
|
throw new ArgumentNullException(nameof(namespaceUris));
|
|
}
|
|
int index = namespaceUris.IndexOf(qname.Namespace);
|
|
|
|
if (index > 0)
|
|
{
|
|
return CoreUtils.Format("s{0}:{1}", index - 1, qname.Name);
|
|
}
|
|
|
|
return qname.Name;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the data type to use for the value of a variable or the argument of a method.
|
|
/// </summary>
|
|
public static string GetBinaryDataType(
|
|
this DataTypeDesign? dataType,
|
|
string targetNamespace,
|
|
Namespace[] namespaces)
|
|
{
|
|
switch (dataType!.BasicDataType)
|
|
{
|
|
case BasicDataType.Boolean:
|
|
return "opc:Boolean";
|
|
case BasicDataType.SByte:
|
|
return "opc:SByte";
|
|
case BasicDataType.Byte:
|
|
return "opc:Byte";
|
|
case BasicDataType.Int16:
|
|
return "opc:Int16";
|
|
case BasicDataType.UInt16:
|
|
return "opc:UInt16";
|
|
case BasicDataType.Int32:
|
|
return "opc:Int32";
|
|
case BasicDataType.UInt32:
|
|
return "opc:UInt32";
|
|
case BasicDataType.Int64:
|
|
return "opc:Int64";
|
|
case BasicDataType.UInt64:
|
|
return "opc:UInt64";
|
|
case BasicDataType.Float:
|
|
return "opc:Float";
|
|
case BasicDataType.Double:
|
|
return "opc:Double";
|
|
case BasicDataType.String:
|
|
return "opc:CharArray";
|
|
case BasicDataType.DateTime:
|
|
return "opc:DateTime";
|
|
case BasicDataType.Guid:
|
|
return "opc:Guid";
|
|
case BasicDataType.ByteString:
|
|
return "opc:ByteString";
|
|
case BasicDataType.XmlElement:
|
|
return "ua:XmlElement";
|
|
case BasicDataType.NodeId:
|
|
return "ua:NodeId";
|
|
case BasicDataType.ExpandedNodeId:
|
|
return "ua:ExpandedNodeId";
|
|
case BasicDataType.StatusCode:
|
|
return "ua:StatusCode";
|
|
case BasicDataType.DiagnosticInfo:
|
|
return "ua:DiagnosticInfo";
|
|
case BasicDataType.QualifiedName:
|
|
return "ua:QualifiedName";
|
|
case BasicDataType.LocalizedText:
|
|
return "ua:LocalizedText";
|
|
case BasicDataType.DataValue:
|
|
return "ua:DataValue";
|
|
case BasicDataType.Number:
|
|
case BasicDataType.Integer:
|
|
case BasicDataType.UInteger:
|
|
case BasicDataType.BaseDataType:
|
|
return "ua:Variant";
|
|
default:
|
|
if (dataType.SymbolicName ==
|
|
new XmlQualifiedName("Structure", Namespaces.OpcUa))
|
|
{
|
|
return CoreUtils.Format("ua:ExtensionObject");
|
|
}
|
|
|
|
if (dataType.SymbolicName ==
|
|
new XmlQualifiedName("Enumeration", Namespaces.OpcUa))
|
|
{
|
|
if (dataType.IsOptionSet)
|
|
{
|
|
return GetBinaryDataType(
|
|
(DataTypeDesign)dataType.BaseTypeNode!,
|
|
targetNamespace,
|
|
namespaces);
|
|
}
|
|
|
|
return CoreUtils.Format("opc:Int32");
|
|
}
|
|
|
|
string? prefix = "tns";
|
|
|
|
if (dataType.SymbolicName.Namespace != targetNamespace)
|
|
{
|
|
if (dataType.SymbolicName.Namespace == Namespaces.OpcUa)
|
|
{
|
|
prefix = "ua";
|
|
}
|
|
else
|
|
{
|
|
prefix = GetXmlNamespacePrefix(
|
|
namespaces,
|
|
dataType.SymbolicName.Namespace);
|
|
}
|
|
}
|
|
return CoreUtils.Format("{0}:{1}", prefix, dataType.SymbolicName.Name);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the data type to use for the value of a variable or the argument of a method.
|
|
/// </summary>
|
|
public static string GetXmlDataType(
|
|
this DataTypeDesign? dataType,
|
|
ValueRank valueRank,
|
|
string targetNamespace,
|
|
Namespace[] namespaces)
|
|
{
|
|
if (valueRank != ValueRank.Scalar)
|
|
{
|
|
switch (dataType!.BasicDataType)
|
|
{
|
|
case BasicDataType.Boolean:
|
|
return "ua:ListOfBoolean";
|
|
case BasicDataType.SByte:
|
|
return "ua:ListOfSByte";
|
|
case BasicDataType.Byte:
|
|
return "ua:ListOfByte";
|
|
case BasicDataType.Int16:
|
|
return "ua:ListOfInt16";
|
|
case BasicDataType.UInt16:
|
|
return "ua:ListOfUInt16";
|
|
case BasicDataType.Int32:
|
|
return "ua:ListOfInt32";
|
|
case BasicDataType.UInt32:
|
|
return "ua:ListOfUInt32";
|
|
case BasicDataType.Int64:
|
|
return "ua:ListOfInt64";
|
|
case BasicDataType.UInt64:
|
|
return "ua:ListOfUInt64";
|
|
case BasicDataType.Float:
|
|
return "ua:ListOfFloat";
|
|
case BasicDataType.Double:
|
|
return "ua:ListOfDouble";
|
|
case BasicDataType.String:
|
|
return "ua:ListOfString";
|
|
case BasicDataType.DateTime:
|
|
return "ua:ListOfDateTime";
|
|
case BasicDataType.Guid:
|
|
return "ua:ListOfGuid";
|
|
case BasicDataType.ByteString:
|
|
return "ua:ListOfByteString";
|
|
case BasicDataType.XmlElement:
|
|
return "ua:ListOfXmlElement";
|
|
case BasicDataType.NodeId:
|
|
return "ua:ListOfNodeId";
|
|
case BasicDataType.ExpandedNodeId:
|
|
return "ua:ListOfExpandedNodeId";
|
|
case BasicDataType.StatusCode:
|
|
return "ua:ListOfStatusCode";
|
|
case BasicDataType.DiagnosticInfo:
|
|
return "ua:ListOfDiagnosticInfo";
|
|
case BasicDataType.QualifiedName:
|
|
return "ua:ListOfQualifiedName";
|
|
case BasicDataType.LocalizedText:
|
|
return "ua:ListOfLocalizedText";
|
|
case BasicDataType.DataValue:
|
|
return "ua:ListOfDataValue";
|
|
case BasicDataType.Number:
|
|
case BasicDataType.Integer:
|
|
case BasicDataType.UInteger:
|
|
case BasicDataType.BaseDataType:
|
|
return "ua:ListOfVariant";
|
|
default:
|
|
if (dataType.SymbolicName ==
|
|
new XmlQualifiedName("Structure", Namespaces.OpcUa))
|
|
{
|
|
return CoreUtils.Format("ua:ListOfExtensionObject");
|
|
}
|
|
|
|
if (dataType.SymbolicName ==
|
|
new XmlQualifiedName("Enumeration", Namespaces.OpcUa))
|
|
{
|
|
if (dataType.IsOptionSet)
|
|
{
|
|
return GetXmlDataType(
|
|
(DataTypeDesign)dataType.BaseTypeNode!,
|
|
valueRank,
|
|
targetNamespace,
|
|
namespaces);
|
|
}
|
|
|
|
return CoreUtils.Format("ua:ListOfInt32");
|
|
}
|
|
|
|
string? prefix = "tns";
|
|
|
|
if (dataType.SymbolicName.Namespace != targetNamespace)
|
|
{
|
|
if (dataType.SymbolicName.Namespace == Namespaces.OpcUa)
|
|
{
|
|
if (dataType.SymbolicName.Name == "Enumeration")
|
|
{
|
|
if (dataType.IsOptionSet)
|
|
{
|
|
return GetXmlDataType(
|
|
(DataTypeDesign)dataType.BaseTypeNode!,
|
|
valueRank,
|
|
targetNamespace,
|
|
namespaces);
|
|
}
|
|
|
|
return CoreUtils.Format("ua:ListOfInt32");
|
|
}
|
|
|
|
prefix = "ua";
|
|
}
|
|
else
|
|
{
|
|
prefix = GetXmlNamespacePrefix(namespaces, dataType.SymbolicName.Namespace);
|
|
}
|
|
}
|
|
|
|
return CoreUtils.Format("{0}:ListOf{1}", prefix, dataType.SymbolicName.Name);
|
|
}
|
|
}
|
|
|
|
switch (dataType!.BasicDataType)
|
|
{
|
|
case BasicDataType.Boolean:
|
|
return "xs:boolean";
|
|
case BasicDataType.SByte:
|
|
return "xs:byte";
|
|
case BasicDataType.Byte:
|
|
return "xs:unsignedByte";
|
|
case BasicDataType.Int16:
|
|
return "xs:short";
|
|
case BasicDataType.UInt16:
|
|
return "xs:unsignedShort";
|
|
case BasicDataType.Int32:
|
|
return "xs:int";
|
|
case BasicDataType.UInt32:
|
|
return "xs:unsignedInt";
|
|
case BasicDataType.Int64:
|
|
return "xs:long";
|
|
case BasicDataType.UInt64:
|
|
return "xs:unsignedLong";
|
|
case BasicDataType.Float:
|
|
return "xs:float";
|
|
case BasicDataType.Double:
|
|
return "xs:double";
|
|
case BasicDataType.String:
|
|
return "xs:string";
|
|
case BasicDataType.DateTime:
|
|
return "xs:dateTime";
|
|
case BasicDataType.Guid:
|
|
return "ua:Guid";
|
|
case BasicDataType.ByteString:
|
|
return "xs:base64Binary";
|
|
case BasicDataType.XmlElement:
|
|
return "ua:XmlElement";
|
|
case BasicDataType.NodeId:
|
|
return "ua:NodeId";
|
|
case BasicDataType.ExpandedNodeId:
|
|
return "ua:ExpandedNodeId";
|
|
case BasicDataType.StatusCode:
|
|
return "ua:StatusCode";
|
|
case BasicDataType.DiagnosticInfo:
|
|
return "ua:DiagnosticInfo";
|
|
case BasicDataType.QualifiedName:
|
|
return "ua:QualifiedName";
|
|
case BasicDataType.LocalizedText:
|
|
return "ua:LocalizedText";
|
|
case BasicDataType.DataValue:
|
|
return "ua:DataValue";
|
|
case BasicDataType.Number:
|
|
case BasicDataType.Integer:
|
|
case BasicDataType.UInteger:
|
|
case BasicDataType.BaseDataType:
|
|
return "ua:Variant";
|
|
default:
|
|
if (dataType.SymbolicName ==
|
|
new XmlQualifiedName("Structure", Namespaces.OpcUa))
|
|
{
|
|
return CoreUtils.Format("ua:ExtensionObject");
|
|
}
|
|
|
|
if (dataType.SymbolicName ==
|
|
new XmlQualifiedName("Enumeration", Namespaces.OpcUa))
|
|
{
|
|
if (dataType.IsOptionSet)
|
|
{
|
|
return GetXmlDataType(
|
|
(DataTypeDesign)dataType.BaseTypeNode!,
|
|
valueRank,
|
|
targetNamespace,
|
|
namespaces);
|
|
}
|
|
|
|
return CoreUtils.Format("xs:int");
|
|
}
|
|
|
|
string? prefix = null;
|
|
|
|
if (dataType.SymbolicName.Namespace != targetNamespace)
|
|
{
|
|
if (dataType.SymbolicName.Namespace == Namespaces.OpcUa)
|
|
{
|
|
if (dataType.SymbolicName.Name == "Enumeration")
|
|
{
|
|
if (dataType.IsOptionSet)
|
|
{
|
|
return GetXmlDataType(
|
|
(DataTypeDesign)dataType.BaseTypeNode!,
|
|
valueRank,
|
|
targetNamespace,
|
|
namespaces);
|
|
}
|
|
|
|
return CoreUtils.Format("xs:int");
|
|
}
|
|
|
|
prefix = "ua";
|
|
}
|
|
else
|
|
{
|
|
prefix = GetXmlNamespacePrefix(namespaces, dataType.SymbolicName.Namespace);
|
|
}
|
|
}
|
|
|
|
return CoreUtils.Format("{0}:{1}", prefix ?? "tns", dataType.SymbolicName.Name);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a constant for the namespace uri.
|
|
/// </summary>
|
|
public static Namespace? GetNamespace(this Namespace[] namespaces, string namespaceUri)
|
|
{
|
|
if (namespaces != null)
|
|
{
|
|
foreach (Namespace ns in namespaces)
|
|
{
|
|
if (ns.Value == namespaceUri)
|
|
{
|
|
return ns;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a constant for the namespace uri.
|
|
/// </summary>
|
|
public static string? GetConstantForXmlNamespace(this Namespace[] namespaces, string namespaceUri)
|
|
{
|
|
Namespace? ns = GetNamespace(namespaces, namespaceUri);
|
|
if (ns != null)
|
|
{
|
|
// The "...Xsd" companion constant only exists when the XML
|
|
// namespace differs from the namespace URI - ConstantsGenerator
|
|
// emits it under exactly that condition. Referencing it whenever
|
|
// XmlNamespace is merely non-empty names a constant that was
|
|
// never declared (CS0117 in the generated sources).
|
|
if (!string.IsNullOrEmpty(ns.XmlNamespace) &&
|
|
!string.Equals(ns.XmlNamespace, ns.Value, StringComparison.Ordinal))
|
|
{
|
|
return CoreUtils.Format("{1}.Namespaces.{0}Xsd", ns.Name, ns.Prefix);
|
|
}
|
|
|
|
return CoreUtils.Format("{1}.Namespaces.{0}", ns.Name, ns.Prefix);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the XML prefix for the specified namespace.
|
|
/// </summary>
|
|
public static string? GetXmlNamespacePrefix(this Namespace[] namespaces, string namespaceUri)
|
|
{
|
|
if (namespaceUri == null)
|
|
{
|
|
return null;
|
|
}
|
|
if (namespaces != null)
|
|
{
|
|
for (int ii = 0; ii < namespaces.Length; ii++)
|
|
{
|
|
if (namespaces[ii].Value == namespaceUri)
|
|
{
|
|
if (string.IsNullOrEmpty(namespaces[ii].XmlPrefix))
|
|
{
|
|
return CoreUtils.Format("s{0}", ii);
|
|
}
|
|
|
|
return namespaces[ii].XmlPrefix;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public static bool IsMethodTypeDesign([NotNullWhen(true)] this NodeDesign? node)
|
|
{
|
|
if (node?.SymbolicId is null)
|
|
{
|
|
return false;
|
|
}
|
|
if (node is not MethodDesign)
|
|
|
|
{
|
|
return false;
|
|
}
|
|
string symbol = node.SymbolicId.Name;
|
|
|
|
int index = symbol.IndexOf('_', StringComparison.Ordinal);
|
|
|
|
if (index > 0)
|
|
|
|
{
|
|
symbol = symbol[..index];
|
|
}
|
|
return symbol.EndsWith("MethodType", StringComparison.Ordinal);
|
|
}
|
|
|
|
public static bool IsPartOfOpcUaTypesLibrary(this DataTypeDesign dataType)
|
|
{
|
|
if (dataType != null &&
|
|
dataType.SymbolicId.Namespace == Namespaces.OpcUa)
|
|
{
|
|
switch (dataType.NumericId)
|
|
{
|
|
case DataTypes.AccessRestrictionType:
|
|
case DataTypes.ReferenceDescription:
|
|
case DataTypes.AttributeWriteMask:
|
|
case DataTypes.Argument:
|
|
case DataTypes.IdType:
|
|
case DataTypes.RolePermissionType:
|
|
case DataTypes.PermissionType:
|
|
case DataTypes.ViewDescription:
|
|
case DataTypes.BrowseDescription:
|
|
case DataTypes.StructureDefinition:
|
|
case DataTypes.StructureType:
|
|
case DataTypes.StructureField:
|
|
case DataTypes.InstanceNode:
|
|
case DataTypes.ReferenceTypeNode:
|
|
case DataTypes.ReferenceNode:
|
|
case DataTypes.DataTypeDefinition:
|
|
case DataTypes.EnumDefinition:
|
|
case DataTypes.EnumField:
|
|
case DataTypes.EnumValueType:
|
|
case DataTypes.RelativePath:
|
|
case DataTypes.BrowseDirection:
|
|
case DataTypes.RelativePathElement:
|
|
case DataTypes.NodeClass:
|
|
case DataTypes.Node:
|
|
case DataTypes.ViewNode:
|
|
case DataTypes.ObjectNode:
|
|
case DataTypes.MethodNode:
|
|
case DataTypes.TypeNode:
|
|
case DataTypes.ObjectTypeNode:
|
|
case DataTypes.DataTypeNode:
|
|
case DataTypes.VariableTypeNode:
|
|
case DataTypes.VariableNode:
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public static Export.ReleaseStatus ToNodeSetReleaseStatus(
|
|
this ReleaseStatus releaseStatus)
|
|
{
|
|
switch (releaseStatus)
|
|
{
|
|
case ReleaseStatus.Deprecated:
|
|
return Export.ReleaseStatus.Deprecated;
|
|
case ReleaseStatus.RC:
|
|
case ReleaseStatus.Draft:
|
|
return Export.ReleaseStatus.Draft;
|
|
default:
|
|
return Export.ReleaseStatus.Released;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps the access level enumeration onto a code string.
|
|
/// </summary>
|
|
public static string GetAccessLevelAsCode(this AccessLevel accessLevel)
|
|
{
|
|
return accessLevel switch
|
|
{
|
|
AccessLevel.Read => "global::Opc.Ua.AccessLevels.CurrentRead",
|
|
AccessLevel.Write => "global::Opc.Ua.AccessLevels.CurrentWrite",
|
|
AccessLevel.ReadWrite => "global::Opc.Ua.AccessLevels.CurrentReadOrWrite",
|
|
AccessLevel.HistoryRead => "global::Opc.Ua.AccessLevels.HistoryRead",
|
|
AccessLevel.HistoryWrite => "global::Opc.Ua.AccessLevels.HistoryWrite",
|
|
AccessLevel.HistoryReadWrite => "global::Opc.Ua.AccessLevels.HistoryReadOrWrite",
|
|
_ => "global::Opc.Ua.AccessLevels.None"
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps the AccessLevel of a variable onto code. NodeSet2-sourced
|
|
/// designs carry the verbatim bitmask, which is preferred because
|
|
/// the ModelDesign <see cref="AccessLevel"/> enumeration cannot
|
|
/// represent combinations such as <c>CurrentRead | HistoryRead</c>.
|
|
/// </summary>
|
|
public static string GetAccessLevelAsCode(this VariableDesign? variable)
|
|
{
|
|
uint? rawAccessLevel = variable?.RawAccessLevel;
|
|
return rawAccessLevel != null
|
|
? GetAccessLevelBitsAsCode(rawAccessLevel.Value)
|
|
: GetAccessLevelAsCode(variable?.AccessLevel ?? AccessLevel.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps the UserAccessLevel of a variable onto code. NodeSet2-sourced
|
|
/// designs carry the verbatim UserAccessLevel bitmask when the
|
|
/// attribute is explicitly present and it is preferred; otherwise the
|
|
/// UserAccessLevel is derived from the AccessLevel, matching the
|
|
/// runtime NodeSet2 importer in <c>UANodeSetHelpers</c>.
|
|
/// </summary>
|
|
public static string GetUserAccessLevelAsCode(this VariableDesign variable)
|
|
{
|
|
uint? rawUserAccessLevel = variable?.RawUserAccessLevel;
|
|
return rawUserAccessLevel != null
|
|
? GetAccessLevelBitsAsCode(rawUserAccessLevel.Value)
|
|
: GetAccessLevelAsCode(variable);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps a verbatim OPC UA AccessLevel bitmask onto code, emitting
|
|
/// the named <c>AccessLevels</c> constants for every set bit.
|
|
/// </summary>
|
|
private static string GetAccessLevelBitsAsCode(uint accessLevel)
|
|
{
|
|
byte bits = (byte)accessLevel;
|
|
if (bits == 0)
|
|
{
|
|
return "global::Opc.Ua.AccessLevels.None";
|
|
}
|
|
|
|
var names = new List<string>();
|
|
AppendBit(names, bits, 0x1, "CurrentRead");
|
|
AppendBit(names, bits, 0x2, "CurrentWrite");
|
|
AppendBit(names, bits, 0x4, "HistoryRead");
|
|
AppendBit(names, bits, 0x8, "HistoryWrite");
|
|
AppendBit(names, bits, 0x10, "SemanticChange");
|
|
AppendBit(names, bits, 0x20, "StatusWrite");
|
|
AppendBit(names, bits, 0x40, "TimestampWrite");
|
|
|
|
if (names.Count == 0)
|
|
{
|
|
return CoreUtils.Format("(byte)0x{0:X2}", bits);
|
|
}
|
|
if (names.Count == 1)
|
|
{
|
|
return names[0];
|
|
}
|
|
return CoreUtils.Format("(byte)({0})", string.Join(" | ", names));
|
|
}
|
|
|
|
private static void AppendBit(List<string> names, byte bits, byte mask, string name)
|
|
{
|
|
if ((bits & mask) != 0)
|
|
{
|
|
names.Add("global::Opc.Ua.AccessLevels." + name);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps the WriteAccess (WriteMask) of a node onto code. The verbatim
|
|
/// OPC UA WriteMask bitmask is emitted so that attributes advertised as
|
|
/// writable in the NodeSet (for example DisplayName and Description)
|
|
/// stay writable at runtime instead of defaulting to None.
|
|
/// </summary>
|
|
public static string GetWriteMaskAsCode(this NodeDesign node)
|
|
{
|
|
uint writeMask = node?.WriteAccess ?? 0;
|
|
if (writeMask == 0)
|
|
{
|
|
return "global::Opc.Ua.AttributeWriteMask.None";
|
|
}
|
|
return CoreUtils.Format(
|
|
"(global::Opc.Ua.AttributeWriteMask){0}",
|
|
writeMask);
|
|
}
|
|
|
|
public static string GetLocalizedTextAsCode(this string localizedText)
|
|
{
|
|
return GetLocalizedTextAsCode(new LocalizedText
|
|
{
|
|
Value = localizedText
|
|
}, noAutoGen: false);
|
|
}
|
|
|
|
public static string GetLocalizedTextAsCode(this LocalizedText? localizedText, bool noAutoGen = false)
|
|
{
|
|
if (localizedText?.Value != null && (!noAutoGen || !localizedText.IsAutogenerated))
|
|
{
|
|
return CoreUtils.Format(
|
|
"new global::Opc.Ua.LocalizedText({0}, string.Empty, {1})",
|
|
localizedText.Key.AsStringLiteral(),
|
|
localizedText.Value.Trim().AsStringLiteral());
|
|
}
|
|
return "global::Opc.Ua.LocalizedText.Null";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps the array dimensions onto code that creates the array.
|
|
/// </summary>
|
|
public static string? GetArrayDimensionsAsCode(this ValueRank valueRank, string arrayDimensions)
|
|
{
|
|
// Part 3 5.6.2: ArrayDimensions is null unless the ValueRank names an
|
|
// array of a specific dimension. ModelDesign's ValueRank is the XSD
|
|
// enum (Scalar = 0 ... Any = 5), not the numeric OPC UA rank, so a
|
|
// "< 0" test never fired and a scalar declared with
|
|
// ArrayDimensions="0" (as NodeSet exporters write it) got them.
|
|
if (valueRank is ValueRank.Scalar or
|
|
ValueRank.ScalarOrArray or
|
|
ValueRank.ScalarOrOneDimension or
|
|
ValueRank.Any)
|
|
{
|
|
return null;
|
|
}
|
|
if (string.IsNullOrEmpty(arrayDimensions))
|
|
{
|
|
if (valueRank == ValueRank.Array)
|
|
{
|
|
return "new uint[] { 0 }";
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
var tokens = arrayDimensions
|
|
.Split([','], StringSplitOptions.RemoveEmptyEntries)
|
|
.Select(t => t.Trim())
|
|
.Where(t => t.Length > 0)
|
|
.ToList();
|
|
|
|
if (tokens.Count == 0)
|
|
|
|
{
|
|
return null;
|
|
}
|
|
return CoreUtils.Format(
|
|
"new uint[] {{ {0} }}",
|
|
string.Join(", ", tokens.Select(t =>
|
|
{
|
|
if (uint.TryParse(t, out uint val))
|
|
{
|
|
return val.ToString(CultureInfo.InvariantCulture);
|
|
}
|
|
return "0";
|
|
})));
|
|
}
|
|
|
|
public static string? GetAccessRestrictionsAsCode(
|
|
this AccessRestrictions restrictions,
|
|
bool restrictionsSpecified)
|
|
{
|
|
if (!restrictionsSpecified)
|
|
{
|
|
return null;
|
|
}
|
|
return restrictions switch
|
|
{
|
|
AccessRestrictions.SigningRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.SigningRequired",
|
|
AccessRestrictions.EncryptionRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.EncryptionRequired",
|
|
AccessRestrictions.SessionRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.SessionRequired",
|
|
AccessRestrictions.SessionWithSigningRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.SigningRequired | " +
|
|
"global::Opc.Ua.AccessRestrictionType.SessionRequired",
|
|
AccessRestrictions.SessionWithEncryptionRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.EncryptionRequired | " +
|
|
"global::Opc.Ua.AccessRestrictionType.SessionRequired",
|
|
AccessRestrictions.SigningAndApplyToBrowseRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.SigningRequired | " +
|
|
"global::Opc.Ua.AccessRestrictionType.ApplyRestrictionsToBrowse",
|
|
AccessRestrictions.EncryptionAndApplyToBrowseRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.EncryptionRequired | " +
|
|
"global::Opc.Ua.AccessRestrictionType.ApplyRestrictionsToBrowse",
|
|
AccessRestrictions.SessionAndApplyToBrowseRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.SessionRequired | " +
|
|
"global::Opc.Ua.AccessRestrictionType.ApplyRestrictionsToBrowse",
|
|
AccessRestrictions.SessionWithSigningAndApplyToBrowseRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.SigningRequired | " +
|
|
"global::Opc.Ua.AccessRestrictionType.SessionRequired | " +
|
|
"global::Opc.Ua.AccessRestrictionType.ApplyRestrictionsToBrowse",
|
|
AccessRestrictions.SessionWithEncryptionAndApplyToBrowseRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.EncryptionRequired | " +
|
|
"global::Opc.Ua.AccessRestrictionType.SessionRequired | " +
|
|
"global::Opc.Ua.AccessRestrictionType.ApplyRestrictionsToBrowse",
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
public static string GetPermissionTypeAsCode(this Permissions[] permissions)
|
|
{
|
|
if (permissions == null || permissions.Length == 0)
|
|
{
|
|
return "global::Opc.Ua.PermissionType.None";
|
|
}
|
|
var parts = new HashSet<string>();
|
|
foreach (Permissions p in permissions)
|
|
{
|
|
switch (p)
|
|
{
|
|
case Permissions.All:
|
|
AddPermissionStrings(Permissions.WriteAttribute, parts);
|
|
AddPermissionStrings(Permissions.WriteRolePermissions, parts);
|
|
AddPermissionStrings(Permissions.WriteHistorizing, parts);
|
|
AddPermissionStrings(Permissions.Write, parts);
|
|
AddPermissionStrings(Permissions.InsertHistory, parts);
|
|
AddPermissionStrings(Permissions.ModifyHistory, parts);
|
|
AddPermissionStrings(Permissions.DeleteHistory, parts);
|
|
AddPermissionStrings(Permissions.Call, parts);
|
|
AddPermissionStrings(Permissions.AddReference, parts);
|
|
AddPermissionStrings(Permissions.RemoveReference, parts);
|
|
AddPermissionStrings(Permissions.DeleteNode, parts);
|
|
AddPermissionStrings(Permissions.AddNode, parts);
|
|
goto case Permissions.AllRead;
|
|
case Permissions.AllRead:
|
|
AddPermissionStrings(Permissions.Browse, parts);
|
|
AddPermissionStrings(Permissions.Read, parts);
|
|
AddPermissionStrings(Permissions.ReadHistory, parts);
|
|
AddPermissionStrings(Permissions.ReceiveEvents, parts);
|
|
AddPermissionStrings(Permissions.ReadRolePermissions, parts);
|
|
break;
|
|
default:
|
|
AddPermissionStrings(p, parts);
|
|
break;
|
|
}
|
|
}
|
|
return parts.Count > 0 ?
|
|
string.Join(" | ", parts) :
|
|
"global::Opc.Ua.PermissionType.None";
|
|
|
|
static void AddPermissionStrings(Permissions p, HashSet<string> parts)
|
|
{
|
|
string? part = p switch
|
|
{
|
|
Permissions.Browse =>
|
|
"global::Opc.Ua.PermissionType.Browse",
|
|
Permissions.Read =>
|
|
"global::Opc.Ua.PermissionType.Read",
|
|
Permissions.Write =>
|
|
"global::Opc.Ua.PermissionType.Write",
|
|
Permissions.Call =>
|
|
"global::Opc.Ua.PermissionType.Call",
|
|
Permissions.ReadHistory =>
|
|
"global::Opc.Ua.PermissionType.ReadHistory",
|
|
Permissions.InsertHistory =>
|
|
"global::Opc.Ua.PermissionType.InsertHistory",
|
|
Permissions.ModifyHistory =>
|
|
"global::Opc.Ua.PermissionType.ModifyHistory",
|
|
Permissions.DeleteHistory =>
|
|
"global::Opc.Ua.PermissionType.DeleteHistory",
|
|
Permissions.ReceiveEvents =>
|
|
"global::Opc.Ua.PermissionType.ReceiveEvents",
|
|
Permissions.AddNode =>
|
|
"global::Opc.Ua.PermissionType.AddNode",
|
|
Permissions.DeleteNode =>
|
|
"global::Opc.Ua.PermissionType.DeleteNode",
|
|
Permissions.AddReference =>
|
|
"global::Opc.Ua.PermissionType.AddReference",
|
|
Permissions.RemoveReference =>
|
|
"global::Opc.Ua.PermissionType.RemoveReference",
|
|
Permissions.ReadRolePermissions =>
|
|
"global::Opc.Ua.PermissionType.ReadRolePermissions",
|
|
Permissions.WriteRolePermissions =>
|
|
"global::Opc.Ua.PermissionType.WriteRolePermissions",
|
|
Permissions.WriteAttribute =>
|
|
"global::Opc.Ua.PermissionType.WriteAttribute",
|
|
Permissions.WriteHistorizing =>
|
|
"global::Opc.Ua.PermissionType.WriteHistorizing",
|
|
_ => null
|
|
};
|
|
if (part != null)
|
|
{
|
|
parts.Add(part);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the identifier assigned to the node as one of the four
|
|
/// identifier types defined by OPC 10000-3 5.2.2, or <c>null</c> when
|
|
/// the node does not carry an explicit identifier.
|
|
/// </summary>
|
|
public static object? GetIdentifier(this NodeDesign? node)
|
|
{
|
|
if (node == null)
|
|
{
|
|
return null;
|
|
}
|
|
if (node.NumericIdSpecified)
|
|
{
|
|
return node.NumericId;
|
|
}
|
|
if (node.StringId != null)
|
|
{
|
|
return node.StringId;
|
|
}
|
|
if (node.GuidIdSpecified)
|
|
{
|
|
return node.GuidId;
|
|
}
|
|
if (node.OpaqueId != null)
|
|
{
|
|
return ByteString.From(node.OpaqueId);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Assigns the identifier of the node, clearing the identifiers of all
|
|
/// other identifier types. Passing <c>null</c> clears the identifier.
|
|
/// </summary>
|
|
public static void SetIdentifier(this NodeDesign? node, object? identifier)
|
|
{
|
|
if (node == null)
|
|
{
|
|
return;
|
|
}
|
|
node.NumericId = 0;
|
|
node.NumericIdSpecified = false;
|
|
node.StringId = null;
|
|
node.GuidId = Guid.Empty;
|
|
node.GuidIdSpecified = false;
|
|
node.OpaqueId = null;
|
|
|
|
switch (identifier)
|
|
{
|
|
case uint numericId:
|
|
node.NumericId = numericId;
|
|
node.NumericIdSpecified = true;
|
|
break;
|
|
case string stringId:
|
|
node.StringId = stringId;
|
|
break;
|
|
case Guid guidId:
|
|
node.GuidId = guidId;
|
|
node.GuidIdSpecified = true;
|
|
break;
|
|
case ByteString opaqueId:
|
|
node.OpaqueId = opaqueId.ToArray();
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true when the node carries an explicit identifier of any of
|
|
/// the four identifier types defined by OPC 10000-3 5.2.2.
|
|
/// </summary>
|
|
public static bool HasIdentifier(this NodeDesign node)
|
|
{
|
|
return node.GetIdentifier() != null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true when the node carries a Guid or Opaque identifier.
|
|
/// These cannot be emitted as a C# <c>const</c> and are not part of the
|
|
/// generated identifier reflection tables.
|
|
/// </summary>
|
|
public static bool HasNonConstantIdentifier(this NodeDesign node)
|
|
{
|
|
return node != null &&
|
|
!node.NumericIdSpecified &&
|
|
string.IsNullOrEmpty(node.StringId) &&
|
|
(node.GuidIdSpecified || node.OpaqueId != null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the C# literal for an identifier together with the C# type
|
|
/// the literal has.
|
|
/// </summary>
|
|
public static string? GetIdentifierAsCode(object identifier, out string? type)
|
|
{
|
|
switch (identifier)
|
|
{
|
|
case uint numericId:
|
|
type = "uint";
|
|
return CoreUtils.Format("{0}u", numericId);
|
|
case string stringId:
|
|
type = "string";
|
|
return stringId.AsStringLiteral();
|
|
case Guid guidId:
|
|
type = "global::System.Guid";
|
|
return CoreUtils.Format(
|
|
"new global::System.Guid({0})",
|
|
guidId.ToString("D", CultureInfo.InvariantCulture).AsStringLiteral());
|
|
case ByteString opaqueId:
|
|
type = "global::Opc.Ua.ByteString";
|
|
return CoreUtils.Format(
|
|
"global::Opc.Ua.ByteString.FromBase64({0})",
|
|
opaqueId.ToBase64().AsStringLiteral());
|
|
default:
|
|
type = null;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public static string GetNodeIdAsCode(
|
|
this NodeDesign? node,
|
|
Namespace[] namespaces,
|
|
string namespaceTableVariable)
|
|
{
|
|
string? identifier;
|
|
if (node == null)
|
|
{
|
|
return "global::Opc.Ua.NodeId.Null";
|
|
}
|
|
if (node.NumericIdSpecified)
|
|
{
|
|
identifier = CoreUtils.Format("{0}u", node.NumericId);
|
|
}
|
|
else if (!string.IsNullOrEmpty(node.StringId))
|
|
{
|
|
identifier = node.StringId.AsStringLiteral();
|
|
}
|
|
else if (node.HasNonConstantIdentifier())
|
|
{
|
|
identifier = GetIdentifierAsCode(node.GetIdentifier()!, out _);
|
|
}
|
|
else
|
|
{
|
|
uint? numericId = FindNumericIdentifier(node);
|
|
if (numericId.HasValue)
|
|
{
|
|
identifier = CoreUtils.Format("{0}u", numericId.Value);
|
|
}
|
|
else
|
|
{
|
|
if (string.IsNullOrEmpty(node.SymbolicId.Name))
|
|
{
|
|
return "global::Opc.Ua.NodeId.Null";
|
|
}
|
|
identifier = node.SymbolicId.Name.AsStringLiteral();
|
|
}
|
|
}
|
|
return CoreUtils.Format(
|
|
"global::Opc.Ua.NodeId.Create({0}, {1}, {2})",
|
|
identifier,
|
|
namespaces.GetNamespaceUriAsCode(
|
|
node.SymbolicId.Namespace),
|
|
namespaceTableVariable);
|
|
}
|
|
|
|
public static string GetExpandedNodeIdAsCode(
|
|
this NodeDesign node,
|
|
Namespace[] namespaces)
|
|
{
|
|
string? identifier;
|
|
if (node == null)
|
|
{
|
|
return "global::Opc.Ua.ExpandedNodeId.Null";
|
|
}
|
|
if (node.NumericIdSpecified)
|
|
{
|
|
identifier = CoreUtils.Format("{0}u", node.NumericId);
|
|
}
|
|
else if (!string.IsNullOrEmpty(node.StringId))
|
|
{
|
|
identifier = node.StringId.AsStringLiteral();
|
|
}
|
|
else if (node.HasNonConstantIdentifier())
|
|
{
|
|
identifier = GetIdentifierAsCode(node.GetIdentifier()!, out _);
|
|
}
|
|
else
|
|
{
|
|
uint? numericId = FindNumericIdentifier(node);
|
|
if (numericId.HasValue)
|
|
{
|
|
identifier = CoreUtils.Format("{0}u", numericId.Value);
|
|
}
|
|
else
|
|
{
|
|
if (string.IsNullOrEmpty(node.SymbolicId.Name))
|
|
{
|
|
return "global::Opc.Ua.ExpandedNodeId.Null";
|
|
}
|
|
identifier = node.SymbolicId.Name.AsStringLiteral();
|
|
}
|
|
}
|
|
return CoreUtils.Format(
|
|
"new global::Opc.Ua.ExpandedNodeId({0}, {1})",
|
|
identifier,
|
|
namespaces.GetNamespaceUriAsCode(
|
|
node.SymbolicId.Namespace));
|
|
}
|
|
|
|
public static uint? FindNumericIdentifier(this NodeDesign node)
|
|
{
|
|
uint? numericId = node.NumericIdSpecified ? node.NumericId : null;
|
|
for (NodeDesign? parent = node.Parent;
|
|
!numericId.HasValue && parent != null;
|
|
parent = parent.Parent)
|
|
{
|
|
if (parent.Hierarchy == null)
|
|
{
|
|
continue;
|
|
}
|
|
string browsePath = node.SymbolicId.Name;
|
|
string parentPath = parent.SymbolicId.Name;
|
|
if (browsePath.StartsWith(parentPath, StringComparison.InvariantCulture) &&
|
|
browsePath[parentPath.Length] == NodeDesign.PathChar)
|
|
{
|
|
browsePath = browsePath[(parentPath.Length + 1)..];
|
|
}
|
|
|
|
if (parent.Hierarchy.Nodes.TryGetValue(browsePath, out HierarchyNode? instance) &&
|
|
instance.Instance!.NumericId != 0)
|
|
{
|
|
numericId = instance.Instance.NumericId;
|
|
}
|
|
}
|
|
return numericId;
|
|
}
|
|
|
|
public static string GetNodeIdConstant(
|
|
this IModelDesign design,
|
|
XmlQualifiedName? symbolidId,
|
|
string type,
|
|
string namespaceTableVariable)
|
|
{
|
|
if (symbolidId == null)
|
|
{
|
|
return "global::Opc.Ua.NodeId.Null";
|
|
}
|
|
NodeDesign node = design.FindNode(
|
|
symbolidId,
|
|
symbolidId.Name,
|
|
type);
|
|
if (node == null)
|
|
{
|
|
return "global::Opc.Ua.NodeId.Null";
|
|
}
|
|
return node.GetNodeIdAsCode(design.Namespaces, namespaceTableVariable);
|
|
}
|
|
|
|
public static void SetDefaultValue(
|
|
this VariableTypeDesign node,
|
|
object value,
|
|
IServiceMessageContext context)
|
|
{
|
|
if (value != null)
|
|
{
|
|
node.DefaultValue = AsXmlElement(value, context);
|
|
node.DecodedValue = value;
|
|
node.DecodedValueNamespaceUris = null;
|
|
}
|
|
else
|
|
{
|
|
node.DefaultValue = null;
|
|
node.DecodedValue = null;
|
|
node.DecodedValueNamespaceUris = null;
|
|
}
|
|
}
|
|
|
|
public static void SetDefaultValue(
|
|
this VariableDesign node,
|
|
object? value,
|
|
IServiceMessageContext context)
|
|
{
|
|
if (value != null)
|
|
{
|
|
node.DefaultValue = AsXmlElement(value, context);
|
|
node.DecodedValue = value;
|
|
node.DecodedValueNamespaceUris = null;
|
|
}
|
|
else
|
|
{
|
|
node.DefaultValue = null;
|
|
node.DecodedValue = null;
|
|
node.DecodedValueNamespaceUris = null;
|
|
}
|
|
}
|
|
|
|
private static System.Xml.XmlElement? AsXmlElement(
|
|
object value,
|
|
IServiceMessageContext context)
|
|
{
|
|
if (VariantHelper.TryCastFromWithReflectionFallback(value, out Variant variant) &&
|
|
!variant.IsNull)
|
|
{
|
|
using var encoder = new XmlEncoder(context);
|
|
encoder.WriteVariantValue(null, variant);
|
|
var xmlDoc = new XmlDocument();
|
|
xmlDoc.LoadInnerXml(encoder.CloseAndReturnText()!);
|
|
return xmlDoc.DocumentElement;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static bool IsBasicDataType(
|
|
this DataTypeDesign dataType,
|
|
out BasicDataType basicDataType)
|
|
{
|
|
if (dataType.SymbolicName.Namespace == "http://opcfoundation.org/UA/")
|
|
{
|
|
#if NET8_0_OR_GREATER
|
|
foreach (string name in Enum.GetNames<BasicDataType>())
|
|
#else
|
|
foreach (string name in Enum.GetNames(typeof(BasicDataType)))
|
|
#endif
|
|
{
|
|
if (name == dataType.SymbolicName.Name)
|
|
{
|
|
#if NET8_0_OR_GREATER
|
|
basicDataType = Enum.Parse<BasicDataType>(
|
|
dataType.SymbolicName.Name);
|
|
#else
|
|
basicDataType = (BasicDataType)Enum.Parse(
|
|
typeof(BasicDataType),
|
|
dataType.SymbolicName.Name);
|
|
#endif
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
basicDataType = default;
|
|
return false;
|
|
}
|
|
}
|
|
}
|