/* ======================================================================== * 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 { /// /// Generates client API code for the services defined in the model. /// internal sealed class ClientApiGenerator : IGenerator { /// /// Create client api generator. /// public ClientApiGenerator(IGeneratorContext context) { m_context = context ?? throw new ArgumentNullException(nameof(context)); } /// /// Returns the service sets available. /// private static List ServiceSets => [ new ServiceSet("AttributeServiceSet", [], EmitClass: false, ServiceCategory.Attribute), new ServiceSet("ViewServiceSet", [], EmitClass: false, ServiceCategory.View), new ServiceSet("MethodServiceSet", [], EmitClass: false, ServiceCategory.Method), new ServiceSet("MonitoredItemServiceSet", [], EmitClass: false, ServiceCategory.MonitoredItem), new ServiceSet("SubscriptionServiceSet", [], EmitClass: false, ServiceCategory.Subscription), new ServiceSet("NodeManagementServiceSet", [], EmitClass: false, ServiceCategory.NodeManagement), new ServiceSet("QueryServiceSet", [], EmitClass: false, ServiceCategory.Query), new ServiceSet("Session", [ "IAttributeServiceSetClientMethods", "IViewServiceSetClientMethods", "IMethodServiceSetClientMethods", "IMonitoredItemServiceSetClientMethods", "ISubscriptionServiceSetClientMethods", "INodeManagementServiceSetClientMethods", "IQueryServiceSetClientMethods" ], [ServiceCategory.Session, ServiceCategory.Test], [ ServiceCategory.Session, ServiceCategory.Attribute, ServiceCategory.View, ServiceCategory.Method, ServiceCategory.MonitoredItem, ServiceCategory.Subscription, ServiceCategory.NodeManagement, ServiceCategory.Query, ServiceCategory.Test ]), new ServiceSet("Discovery", [], ServiceCategory.Discovery), new ServiceSet("Registration", [], ServiceCategory.Registration) ]; /// public IEnumerable Emit() { List serviceSets = ServiceSets; string fileName = Path.Combine( m_context.OutputFolder, CoreUtils.Format("{0}.Client.g.cs", Constants.CoreNamespacePrefix)); using TextWriter writer = m_context.FileSystem.CreateTextWriter(fileName); using var templateWriter = new TemplateWriter(writer); var template = new Template(templateWriter, ClientApiTemplates.File); template.AddReplacement(Tokens.Prefix, Constants.CoreNamespacePrefix); template.AddReplacement(Tokens.Namespace, Constants.CoreNamespace); template.AddReplacement( Tokens.ServiceSets, ClientApiTemplates.ServiceSet, serviceSets, onLoad: SelectServiceSetTemplate, onWrite: WriteTemplate_ClientApiServiceSet); template.Render(); return [fileName.AsTextFileResource()]; } /// /// Selects the appropriate template based on whether /// the service set should emit a class or only an interface. /// private static TemplateString SelectServiceSetTemplate(ILoadContext context) { if (context.Target is ServiceSet serviceSet && !serviceSet.EmitClass) { return ClientApiTemplates.ServiceSetInterfaceOnly; } return context.TemplateString; } /// /// Writes the client API service set. /// private bool WriteTemplate_ClientApiServiceSet(IWriteContext context) { if (context.Target is not ServiceSet serviceSet) { return false; } // get service types for interface (may be a subset). Service[] interfaceServiceTypes = m_context.ModelDesign.GetListOfServices(serviceSet.InterfaceCategories); if (interfaceServiceTypes.Length == 0 && serviceSet.BaseInterfaces.Length == 0) { return false; } context.Template.AddReplacement(Tokens.ServiceSet, serviceSet.Name); string baseInterfaces = serviceSet.BaseInterfaces.Length > 0 ? " :\n " + string.Join(",\n ", serviceSet.BaseInterfaces.Select( b => CoreUtils.Format("global::Opc.Ua.{0}", b))) : string.Empty; context.Template.AddReplacement(Tokens.BaseInterfaces, baseInterfaces); context.Template.AddReplacement( Tokens.ClientMethod, ClientApiTemplates.InterfaceMethods, interfaceServiceTypes, context => WriteTemplate_ClientApiMethod( context, isInterface: true)); if (serviceSet.EmitClass) { // get service types for class (may include inherited categories). Service[] classServiceTypes = m_context.ModelDesign.GetListOfServices(serviceSet.ClassCategories); if (classServiceTypes.Length == 0) { return false; } context.Template.AddReplacement( Tokens.ClientApi, ClientApiTemplates.Methods, classServiceTypes, context => WriteTemplate_ClientApiMethod( context, isInterface: false)); } return context.Template.Render(); } /// /// Writes a client API method. /// private bool WriteTemplate_ClientApiMethod(IWriteContext context, bool isInterface) { if (context.Target is not Service serviceType) { return false; } context.Template.AddReplacement(Tokens.Name, serviceType.Name); context.Template.AddReplacement(Tokens.Namespace, Constants.CoreNamespace); context.Template.AddReplacement( Tokens.ClientMethodSync, [serviceType], context => LoadTemplate_SyncParameters( context, isInterface)); context.Template.AddReplacement( Tokens.ClientMethodAsync, [serviceType], context => LoadTemplate_AsyncParameters( context, isInterface)); context.Template.AddReplacement( Tokens.ClientMethodBegin, [serviceType], context => LoadTemplate_BeginAsyncParameters( context, isInterface)); context.Template.AddReplacement( Tokens.ClientMethodEnd, [serviceType], context => LoadTemplate_EndAsyncParameters( context, isInterface)); context.Template.AddReplacement( Tokens.RequestParameters, [serviceType], LoadTemplate_RequestParameters); context.Template.AddReplacement( Tokens.ResponseParameters, [serviceType], LoadTemplate_ResponseParameters); return context.Template.Render(); } /// /// Writes a synchronous method declaration. /// private TemplateString LoadTemplate_SyncParameters( ILoadContext context, bool isInterface) { if (context.Target is not Service serviceType) { return null; } List types = []; List names = []; CollectParameters(serviceType.Request, false, types, names); CollectParameters(serviceType.Response, true, types, names); // write method type if not writing an interface declaration. if (!isInterface) { context.Out.Write("public virtual "); } context.Out.WriteLine("{0} {1}(", GetReturnType(serviceType), serviceType.Name); WriteParameters(context, types, names); // write closing semicolon for interface. if (isInterface) { context.Out.WriteLine(";"); } return null; } /// /// Writes an asynchronous method declaration. /// private TemplateString LoadTemplate_AsyncParameters( ILoadContext context, bool isInterface) { if (context.Target is not Service serviceType) { return null; } List types = []; List names = []; CollectParameters(serviceType.Request, false, types, names); types.Add("global::System.Threading.CancellationToken"); names.Add("ct"); // write method type if not writing an interface declaration. if (!isInterface) { context.Out.Write("public virtual async "); } context.Out.WriteLine( "global::System.Threading.Tasks.ValueTask<{0}Response> {1}Async(", serviceType.Name, serviceType.Name); WriteParameters(context, types, names); // write closing semicolon for interface. if (isInterface) { context.Out.WriteLine(";"); } return null; } /// /// Writes a begin asynchronous method declaration. /// private TemplateString LoadTemplate_BeginAsyncParameters( ILoadContext context, bool isInterface) { if (context.Target is not Service serviceType) { return null; } List types = []; List names = []; CollectParameters(serviceType.Request, false, types, names); types.Add("AsyncCallback"); names.Add("callback"); types.Add("object"); names.Add("asyncState"); if (!isInterface) { context.Out.Write("public virtual "); } context.Out.WriteLine("IAsyncResult Begin{0}(", serviceType.Name); WriteParameters(context, types, names); // write closing semicolon for interface. if (isInterface) { context.Out.WriteLine(";"); } return null; } /// /// Writes an end asynchronous method declaration. /// private TemplateString LoadTemplate_EndAsyncParameters( ILoadContext context, bool isInterface) { if (context.Target is not Service serviceType) { return null; } List types = []; List names = []; types.Add("IAsyncResult"); names.Add("result"); CollectParameters(serviceType.Response, true, types, names); if (!isInterface) { context.Out.Write("public virtual "); } context.Out.WriteLine("{0} End{1}(", GetReturnType(serviceType), serviceType.Name); WriteParameters(context, types, names); // write closing semicolon for interface. if (isInterface) { context.Out.WriteLine(";"); } return null; } /// /// Copies the request paramaters into the request object. /// private TemplateString LoadTemplate_RequestParameters(ILoadContext context) { if (context.Target is not Service serviceType) { return null; } if (serviceType.Request?.Fields != null) { foreach (Parameter field in serviceType.Request.Fields) { context.Out.Write("request."); context.Out.Write(field.Name); context.Out.Write(" = "); context.Out.Write(field.Name.ToLowerCamelCase()); context.Out.WriteLine(";"); } } return null; } /// /// Copies the response paramaters into the request object. /// private TemplateString LoadTemplate_ResponseParameters(ILoadContext context) { if (context.Target is not Service serviceType) { return null; } if (serviceType.Response?.Fields != null) { bool first = true; foreach (Parameter field in serviceType.Response.Fields) { if (first) { first = false; continue; } context.Out.Write(field.Name.ToLowerCamelCase()); context.Out.Write(" = response."); context.Out.Write(field.Name); context.Out.WriteLine(";"); } } return null; } /// /// Collects the parameters to write. /// private void CollectParameters( DataTypeDesign dataType, bool output, List types, List names) { Parameter[] fields = dataType?.Fields; if (fields != null) { bool first = true; foreach (Parameter field in fields) { // first parameter is the return parameter. if (first && output) { first = false; continue; } DataTypeDesign datatype = field.DataTypeNode; string typeName = datatype.GetDotNetTypeName( field.ValueRank, m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces, nullable: NullableAnnotation.Nullable); // prefix out parameters. if (output) { typeName = "out " + typeName; } types.Add(typeName); names.Add(field.Name.ToLowerCamelCase()); } } } /// /// Writes a set of method parameters. /// private static void WriteParameters( ILoadContext context, List types, List names) { for (int ii = 0; ii < types.Count; ii++) { string typeName = types[ii]; context.Out.Write(" {0} {1}", typeName, names[ii]); if (ii < types.Count - 1) { context.Out.WriteLine(","); } else { context.Out.Write(")"); } } } /// /// Gets the return type for the service. /// private string GetReturnType(Service serviceType) { string returnType = "void"; if (serviceType.Response?.Fields != null && serviceType.Response.Fields.Length > 0) { DataTypeDesign datatype = serviceType.Response.Fields[0].DataTypeNode; if (datatype != null) { returnType = datatype.GetDotNetTypeName( m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces, nullable: NullableAnnotation.Nullable, false); // Use raw types as return type. } } return returnType; } /// /// A set of services that are grouped into a single interface. /// private sealed record class ServiceSet( string Name, string[] BaseInterfaces, ServiceCategory[] InterfaceCategories, ServiceCategory[] ClassCategories, bool EmitClass = true) { public ServiceSet(string Name, string[] BaseInterfaces, params ServiceCategory[] Categories) : this(Name, BaseInterfaces, Categories, Categories, EmitClass: true) { } public ServiceSet(string Name, string[] BaseInterfaces, bool EmitClass, params ServiceCategory[] Categories) : this(Name, BaseInterfaces, Categories, Categories, EmitClass) { } } private readonly IGeneratorContext m_context; } }