/* ======================================================================== * 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.Generic; using System.IO; using System.Linq; using Opc.Ua.Schema.Model; namespace Opc.Ua.SourceGeneration { /// /// Model options /// public sealed record class DesignFileOptions { /// /// -version [v104, v105] /// public string? Version { get; init; } /// /// -id [start id] /// public uint StartId { get; init; } /// /// -mv [model version] /// public string? ModelVersion { get; init; } /// /// -pd [publication date] /// public string? ModelPublicationDate { get; init; } /// /// -rc /// public bool ReleaseCandidate { get; init; } = true; /// /// When true, also emits a {Namespace}NodeManager /// (a partial class deriving from /// Opc.Ua.Server.AsyncCustomNodeManager) and a matching /// {Namespace}NodeManagerFactory implementing /// Opc.Ua.Server.INodeManagerFactory. /// /// The generated manager exposes a /// partial void Configure(INodeManagerBuilder builder) /// extensibility hook that user code can implement in a sibling /// partial to wire callbacks via the fluent /// Opc.Ua.Server.Fluent API. /// /// public bool GenerateNodeManager { get; init; } /// /// Optional override for the namespace of the generated /// NodeManager partial. When set, the generator emits /// partial class {NodeManagerClassName} in this namespace /// instead of using the design file Prefix. Used by the /// [NodeManager] attribute discovery path. /// public string? NodeManagerNamespace { get; init; } /// /// Optional override for the class name of the generated /// NodeManager partial. Defaults to /// {Prefix}NodeManager. Used by the [NodeManager] /// attribute discovery path. /// public string? NodeManagerClassName { get; init; } /// /// Whether to also emit the {ClassName}Factory. Defaults /// to true. Set to false when the consumer wants /// to author the factory by hand. /// public bool EmitNodeManagerFactory { get; init; } = true; /// /// Whether to emit the public /// (IServerInternal, ApplicationConfiguration) constructor /// on the generated NodeManager. Defaults to true. /// Set to false for managers that need collaborators /// beyond those two arguments; the protected constructor /// taking the namespace URI array is emitted either way. Used by /// the [NodeManager] attribute discovery path /// (GenerateDefaultConstructor named argument). /// public bool EmitNodeManagerDefaultConstructor { get; init; } = true; /// /// Additional namespace URIs (beyond the model namespace) that /// the generated NodeManager constructor reports to the /// base node manager and the generated factory advertises via /// NamespacesUris. Used by the [NodeManager] /// attribute discovery path /// (AdditionalNamespaceUris named argument). /// public IReadOnlyList? NodeManagerAdditionalNamespaceUris { get; init; } } /// /// Collection of design files and options /// public sealed record class DesignFileCollection { /// /// Design files to generate code for. Typically a single file. /// public IReadOnlyList Targets { get; init; } = []; /// /// Design files referenced by but not /// generated. They provide node resolution only (e.g. when a /// target imports types from another companion specification). /// public IReadOnlyList Dependencies { get; init; } = []; /// /// Optional identifier file if not same name and side /// by side with design files /// public string? IdentifierFilePath { get; init; } /// /// Design file options /// public DesignFileOptions? Options { get; init; } } /// /// Validate model design /// internal static class DesignFileExtensions { /// /// Gets design file groups for processing. Each target is emitted as an /// independent model and is paired with an adjacent identifier file that /// has the same base name. Every other target remains available as a /// dependency so cross-model references resolve in either direction. /// /// /// is null. public static IEnumerable Group( this DesignFileCollection collection, List? identifierFiles = null) { if (collection is null) { throw new ArgumentNullException(nameof(collection)); } IReadOnlyList idFiles = identifierFiles ?? []; IReadOnlyList dependencies = collection.Dependencies ?? []; IReadOnlyList targets = collection.Targets; return collection.Targets .Select(target => new DesignFileCollection { Targets = [target], Dependencies = [.. dependencies .Concat(targets) .Where(path => !string.Equals(path, target, StringComparison.Ordinal)) .Distinct(StringComparer.Ordinal)], IdentifierFilePath = ResolveIdentifierFile( target, idFiles, collection.IdentifierFilePath!, IsSoleTargetInDirectory(target, targets)), Options = collection.Options }); } private static string ResolveIdentifierFile( string target, IReadOnlyList identifierFiles, string fallback, bool useSoleAdjacentFallback) { string targetDirectory = Path.GetDirectoryName(target) ?? string.Empty; string targetName = Path.GetFileNameWithoutExtension(target); string? basenameMatch = identifierFiles.FirstOrDefault(path => string.Equals( Path.GetDirectoryName(path) ?? string.Empty, targetDirectory, StringComparison.Ordinal) && string.Equals( Path.GetFileNameWithoutExtension(path), targetName, StringComparison.Ordinal) && string.Equals( Path.GetExtension(path), ".csv", StringComparison.OrdinalIgnoreCase)); if (basenameMatch != null) { return basenameMatch; } if (useSoleAdjacentFallback) { string[] adjacentFiles = [.. identifierFiles.Where(path => string.Equals( Path.GetDirectoryName(path) ?? string.Empty, targetDirectory, StringComparison.Ordinal))]; if (adjacentFiles.Length == 1) { return adjacentFiles[0]; } } return fallback; } private static bool IsSoleTargetInDirectory( string target, IReadOnlyList targets) { string targetDirectory = Path.GetDirectoryName(target) ?? string.Empty; bool foundTarget = false; foreach (string path in targets) { if (!string.Equals( Path.GetDirectoryName(path) ?? string.Empty, targetDirectory, StringComparison.Ordinal)) { continue; } if (foundTarget) { return false; } foundTarget = true; } return foundTarget; } /// /// Validates the model design files /// public static IModelDesign OpenModelDesign( this IFileSystem fileSystem, DesignFileCollection designFiles, IReadOnlyList exclusions, ITelemetryContext telemetry, bool useAllowSubtypes = true) { return OpenModelDesign( fileSystem, designFiles, exclusions, telemetry, useAllowSubtypes, referencedDependencies: null); } /// /// Validates the model design files, optionally importing /// per-namespace model dependency payloads from referenced /// assemblies before validation. The payloads pre-populate the /// validator's node table so downstream models can resolve /// upstream types without an explicit AdditionalFiles /// entry for them. /// public static IModelDesign OpenModelDesign( this IFileSystem fileSystem, DesignFileCollection designFiles, IReadOnlyList exclusions, ITelemetryContext telemetry, bool useAllowSubtypes, IReadOnlyDictionary? referencedDependencies) { DesignFileOptions options = designFiles.Options ?? new DesignFileOptions(); var validator = new ModelDesignValidator( fileSystem, options.StartId, exclusions, telemetry, SpecificationVersion.V105) { UseAllowSubtypes = useAllowSubtypes, ReleaseCandidate = options.ReleaseCandidate, ModelVersion = options.ModelVersion, ModelPublicationDate = options.ModelPublicationDate }; if (referencedDependencies != null) { foreach (KeyValuePair entry in referencedDependencies) { if (entry.Value == null) { continue; } validator.ImportDependency(entry.Value, prefix: null, name: null); } } string? identifierFilePath = designFiles.IdentifierFilePath; validator.Validate( designFiles.Targets, designFiles.Dependencies ?? [], identifierFilePath!); return validator; } } }