/* ======================================================================== * 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/ * ======================================================================*/ namespace Opc.Ua.SourceGeneration { /// /// Templates for the . Emits one /// partial record {Type}Record : {Parent}Record per OPC UA /// ObjectType that derives from BaseEventType. Each /// record declares only the fields introduced by its type; /// inherited fields come from the parent record. The /// generated chain mirrors the OPC UA event-type hierarchy. /// /// /// In addition to the record class, each emitted record contains /// a nested public static class Decoder with a positional /// StandardFields browse-path table and a /// Decode(IReadOnlyList<Variant>) method that /// populates every property (own + inherited) from positional /// variant reads. A per-file /// Register{ModelPrefix}Decoders(this EventRecordDecoderRegistry) /// extension method registers every emitted decoder against a /// caller-supplied Opc.Ua.EventRecordDecoderRegistry. /// internal static class EventRecordTemplates { /// /// Single output file template. /// public static readonly TemplateString File = TemplateString.Parse( $$""" {{Tokens.CodeHeader}} #nullable enable annotations #nullable disable warnings namespace {{Tokens.Namespace}} { {{Tokens.ListOfTypes}} {{Tokens.ListOfActivatorRegistrations}} } """); /// /// One event record per ObjectType. Generated as a /// partial record so consumers can add convenience /// properties without touching the generated source. /// public static readonly TemplateString RecordClass = TemplateString.Parse( $$""" /// /// Decoded event record for OPC UA event type /// {{Tokens.SymbolicName}}. Add convenience members in /// a hand-written partial declaration of this same record. /// [global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{Tokens.Tool}}", "{{Tokens.Version}}")] public partial record {{Tokens.ClassName}} : {{Tokens.BaseClassName}} { {{Tokens.ListOfProperties}} /// /// Source-generated positional decoder for /// . Reads variant fields /// at indices fixed by and /// populates every own + inherited init-only property. /// public {{Tokens.AccessModifier}}static class Decoder { {{Tokens.FieldTable}} /// /// Decodes into a /// . /// /// Positional variant array /// matching . /// null when /// is null or empty; /// a fully populated record otherwise. public static {{Tokens.ClassName}}? Decode( global::System.Collections.Generic.IReadOnlyList fields) { if (fields == null || fields.Count == 0) { return null; } return new {{Tokens.ClassName}} { {{Tokens.ListOfDecodedFields}} }; } } /// /// Source-generated event-filter factory for /// . Produces an /// whose /// where clause restricts events to /// {{Tokens.SymbolicName}} and whose select /// clauses come from a supplied /// /// (or its Default). /// public {{Tokens.AccessModifier}}static class EventFilters { /// /// Builds the event filter. /// /// Decoder registry whose /// composed StandardFields form the select /// clauses. When null, /// /// is used. {{Tokens.EventFilterParameterDocumentation}} public static global::Opc.Ua.EventFilter Build( {{Tokens.EventFilterParameters}} global::Opc.Ua.EventRecordDecoderRegistry? registry = null) { return global::Opc.Ua.EventFilterFactory.Create( {{Tokens.EventTypeId}}, registry); } } } """); /// /// Positional browse path table of a record of the standard /// model: every browse name is in namespace 0. /// public static readonly TemplateString StaticFieldTable = TemplateString.Parse( $$""" /// /// Browse paths in positional order. The composed /// registry layout (and any filter built from it) /// uses the same positional convention; the /// runtime remaps from the composed positions /// before calling . /// public static readonly global::Opc.Ua.QualifiedName[][] StandardFields = new global::Opc.Ua.QualifiedName[][] { {{Tokens.ListOfFields}} }; """); /// /// Positional browse path table of a record of a companion model: /// the browse names declared by the model are in the model's /// namespace, whose index is only known for a session. /// public static readonly TemplateString NamespaceFieldTable = TemplateString.Parse( $$""" /// /// Browse paths in positional order with every browse name in /// namespace index 0: a positional layout only. Positions match /// , which resolves the /// namespace of the browse names declared by the model and /// is what select clauses have to use. /// [global::System.Obsolete( "The browse names of a companion model are not in namespace 0, so a select " + "clause built from this table does not match them (OPC 10000-4 7.7.4.5). " + "Use GetStandardFields(NamespaceTable) with the session namespace table.")] public static readonly global::Opc.Ua.QualifiedName[][] StandardFields = GetStandardFields(null); /// /// Browse paths in positional order with the browse names of /// the model in their namespace index of /// . The composed registry /// layout (and any filter built from it) uses the same /// positional convention; the runtime remaps from the /// composed positions before calling . /// A browse name whose namespace is not in /// gets an index no server /// table holds, so the server returns null for that field /// (OPC 10000-4 7.7.4.5) instead of matching a namesake in /// namespace 0. Without a table every browse name is in /// namespace 0 (a positional layout only). /// /// The session namespace table. public static global::Opc.Ua.QualifiedName[][] GetStandardFields( global::Opc.Ua.NamespaceTable? namespaceUris) { return new global::Opc.Ua.QualifiedName[][] { {{Tokens.ListOfFields}} }; } private static ushort GetNamespaceIndex( global::Opc.Ua.NamespaceTable? namespaceUris, string namespaceUri) { if (namespaceUris == null) { return 0; } int index = namespaceUris.GetIndex(namespaceUri); return index > 0 ? (ushort)index : ushort.MaxValue; } """); /// /// One init-only property per directly-declared field on the type. /// public static readonly TemplateString FieldProperty = TemplateString.Parse( $$""" /// /// {{Tokens.Description}} /// public {{Tokens.DataType}} {{Tokens.PropertyName}} { get; init; } """); /// /// One QualifiedName[] entry per StandardFields row. /// For TwoStateVariable children the path has two segments /// (the variable + its Id property). /// public static readonly TemplateString StandardFieldEntry = TemplateString.Parse( $$""" {{Tokens.ChildPath}}, """); /// /// One property assignment per StandardFields position. /// public static readonly TemplateString DecodedField = TemplateString.Parse( $$""" {{Tokens.PropertyName}} = global::Opc.Ua.EventRecordFieldReaders.{{Tokens.ClientMethod}}( fields, {{Tokens.FieldIndex}}), """); /// /// Per-file static class that registers every emitted decoder /// onto a caller-supplied Opc.Ua.EventRecordDecoderRegistry. /// public static readonly TemplateString RegistrationExtension = TemplateString.Parse( $$""" /// /// Registration extensions for the source-generated event /// record decoders in this file. The /// {{Tokens.ClientMethod}} extension method is idempotent /// via EventRecordDecoderRegistry.TryRegister so /// duplicate calls are safe. /// [global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{Tokens.Tool}}", "{{Tokens.Version}}")] public static class {{Tokens.ClassName}} { /// /// Registers every event-record decoder in this model /// with . Returns the /// registry for chaining. /// /// The decoder registry to update. {{Tokens.EventFilterParameterDocumentation}} public static global::Opc.Ua.EventRecordDecoderRegistry {{Tokens.ClientMethod}}( this global::Opc.Ua.EventRecordDecoderRegistry registry{{Tokens.EventFilterParameters}}) { {{Tokens.ListOfActivatorRegistrations}} return registry; } } """); /// /// One TryRegister call per emitted record decoder. /// public static readonly TemplateString DecoderRegistration = TemplateString.Parse( $$""" registry.TryRegister( {{Tokens.EventTypeId}}, {{Tokens.ClassName}}.Decoder.{{Tokens.FieldTable}}, {{Tokens.ClassName}}.Decoder.Decode); """); } }