diff --git a/YaNco.sln.DotSettings b/YaNco.sln.DotSettings
index 2de6434c..be89d3b3 100644
--- a/YaNco.sln.DotSettings
+++ b/YaNco.sln.DotSettings
@@ -1,3 +1,4 @@
True
- True
\ No newline at end of file
+ True
+ True
\ No newline at end of file
diff --git a/src/YaNco.Abstractions/IConnection.cs b/src/YaNco.Abstractions/IConnection.cs
index e7a7a6bb..4faa8a3e 100644
--- a/src/YaNco.Abstractions/IConnection.cs
+++ b/src/YaNco.Abstractions/IConnection.cs
@@ -16,6 +16,8 @@ public interface IConnection : IDisposable
EitherAsync CreateFunction(string name);
EitherAsync InvokeFunction(IFunction function);
EitherAsync InvokeFunction(IFunction function, CancellationToken cancellationToken);
+
+ [Obsolete("Use method WithStartProgramCallback of ConnectionBuilder instead. This method will be removed in next major release.")]
EitherAsync AllowStartOfPrograms(StartProgramDelegate callback);
EitherAsync Cancel();
diff --git a/src/YaNco.Abstractions/IDataContainer.cs b/src/YaNco.Abstractions/IDataContainer.cs
index 8678a78f..5d0740b5 100644
--- a/src/YaNco.Abstractions/IDataContainer.cs
+++ b/src/YaNco.Abstractions/IDataContainer.cs
@@ -12,5 +12,6 @@ public interface IDataContainer : IDisposable
Either GetFieldBytes(string name);
Either GetStructure(string name);
Either GetTable(string name);
+ Either GetTypeDescription();
}
}
\ No newline at end of file
diff --git a/src/YaNco.Abstractions/IFunctionBuilder.cs b/src/YaNco.Abstractions/IFunctionBuilder.cs
new file mode 100644
index 00000000..0d1099a7
--- /dev/null
+++ b/src/YaNco.Abstractions/IFunctionBuilder.cs
@@ -0,0 +1,18 @@
+using LanguageExt;
+
+namespace Dbosoft.YaNco
+{
+ public interface IFunctionBuilder
+ {
+ IFunctionBuilder AddParameter(RfcParameterDescription parameter);
+ IFunctionBuilder AddChar(string name, RfcDirection direction, uint length, bool optional = true, string defaultValue = null);
+ IFunctionBuilder AddInt(string name, RfcDirection direction, bool optional = true, int defaultValue = 0);
+ IFunctionBuilder AddLong(string name, RfcDirection direction, bool optional = true, long defaultValue = 0);
+ IFunctionBuilder AddString(string name, RfcDirection direction, bool optional = true, uint length = 0, string defaultValue = null);
+ IFunctionBuilder AddStructure(string name, RfcDirection direction, ITypeDescriptionHandle typeHandle, bool optional = true);
+ IFunctionBuilder AddStructure(string name, RfcDirection direction, IStructure structure, bool optional = true);
+ IFunctionBuilder AddTable(string name, RfcDirection direction, ITable table, bool optional = true);
+ IFunctionBuilder AddTable(string name, RfcDirection direction, ITypeDescriptionHandle typeHandle, bool optional = true);
+ Either Build();
+ }
+}
\ No newline at end of file
diff --git a/src/YaNco.Abstractions/IRfcHandle.cs b/src/YaNco.Abstractions/IRfcHandle.cs
new file mode 100644
index 00000000..f77b6674
--- /dev/null
+++ b/src/YaNco.Abstractions/IRfcHandle.cs
@@ -0,0 +1,6 @@
+namespace Dbosoft.YaNco
+{
+ public interface IRfcHandle
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/YaNco.Abstractions/IRfcRuntime.cs b/src/YaNco.Abstractions/IRfcRuntime.cs
index 78929a9e..be20607d 100644
--- a/src/YaNco.Abstractions/IRfcRuntime.cs
+++ b/src/YaNco.Abstractions/IRfcRuntime.cs
@@ -8,6 +8,7 @@ namespace Dbosoft.YaNco
{
public interface IRfcRuntime
{
+ [Obsolete("Use method AllowStartOfPrograms of ConnectionBuilder. This method will be removed in next major release.")]
Either AllowStartOfPrograms(IConnectionHandle connectionHandle, StartProgramDelegate callback);
Either AppendTableRow(ITableHandle tableHandle);
Either CreateFunction(IFunctionDescriptionHandle descriptionHandle);
@@ -16,8 +17,15 @@ public interface IRfcRuntime
Either GetFunctionDescription(IFunctionHandle functionHandle);
Either GetFunctionName(IFunctionDescriptionHandle descriptionHandle);
Either GetFunctionParameterCount(IFunctionDescriptionHandle descriptionHandle);
+ Either CreateFunctionDescription(string functionName);
+ Either AddFunctionParameter(IFunctionDescriptionHandle descriptionHandle, RfcParameterDescription parameterDescription);
Either GetFunctionParameterDescription(IFunctionDescriptionHandle descriptionHandle, int index);
Either GetFunctionParameterDescription(IFunctionDescriptionHandle descriptionHandle, string name);
+ Either AddFunctionHandler(string sysid,
+ string functionName, IFunction function, Func> handler);
+ Either AddFunctionHandler(string sysid, string functionName,
+ IFunctionDescriptionHandle descriptionHandle,
+ Func> handler);
Either GetStructure(IDataContainerHandle dataContainer, string name);
Either GetTable(IDataContainerHandle dataContainer, string name);
Either CloneTable(ITableHandle tableHandle);
@@ -58,6 +66,7 @@ public interface IRfcRuntime
Either GetFieldValue(IDataContainerHandle handle, Func> func);
RfcRuntimeOptions Options { get; }
+ bool IsFunctionHandlerRegistered(string sysId, string functionName);
}
diff --git a/src/YaNco.Core/CalledFunction.cs b/src/YaNco.Core/CalledFunction.cs
new file mode 100644
index 00000000..a879b669
--- /dev/null
+++ b/src/YaNco.Core/CalledFunction.cs
@@ -0,0 +1,63 @@
+using System;
+using LanguageExt;
+
+namespace Dbosoft.YaNco
+{
+ public readonly struct CalledFunction
+ {
+ public readonly IFunction Function;
+
+ internal CalledFunction(IFunction function)
+ {
+ Function = function;
+ }
+
+ public Either> Input(Func, Either> inputFunc)
+ {
+ var function = Function;
+ return inputFunc(Prelude.Right(function)).Map(input => new FunctionInput(input, function));
+ }
+
+
+ }
+
+ public readonly struct FunctionInput
+ {
+ public readonly TInput Input;
+ public readonly IFunction Function;
+
+ internal FunctionInput(TInput input, IFunction function)
+ {
+ Input = input;
+ Function = function;
+ }
+
+ public FunctionProcessed Process(Func processFunc)
+ {
+ return new FunctionProcessed(processFunc(Input), Function);
+ }
+
+ public void Deconstruct(out IFunction function, out TInput input)
+ {
+ function = Function;
+ input = Input;
+ }
+ }
+
+ public readonly struct FunctionProcessed
+ {
+ private readonly TOutput _output;
+ private readonly IFunction _function;
+
+ internal FunctionProcessed(TOutput output, IFunction function)
+ {
+ _output = output;
+ _function = function;
+ }
+
+ public Either Reply(Func, Either> replyFunc)
+ {
+ return replyFunc(_output, Prelude.Right(_function)).Map(_ => Unit.Default);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/YaNco.Core/Connection.cs b/src/YaNco.Core/Connection.cs
index 8659aa6a..b3d607f2 100644
--- a/src/YaNco.Core/Connection.cs
+++ b/src/YaNco.Core/Connection.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Dbosoft.Functional;
@@ -59,14 +60,6 @@ public Connection(
}
}
- case AllowStartOfProgramsMessage allowStartOfProgramsMessage:
- {
- var result = rfcRuntime.AllowStartOfPrograms(handle,
- allowStartOfProgramsMessage.Callback).Map(u => (object)u);
- return (handle, result) ;
-
- }
-
case DisposeMessage disposeMessage:
{
handle.Dispose();
@@ -165,8 +158,12 @@ public EitherAsync InvokeFunction(IFunction function)
public EitherAsync InvokeFunction(IFunction function, CancellationToken cancellationToken)
=> _stateAgent.Tell(new InvokeFunctionMessage(function, cancellationToken)).ToAsync().Map(_ => Unit.Default);
- public EitherAsync AllowStartOfPrograms(StartProgramDelegate callback) =>
- _stateAgent.Tell(new AllowStartOfProgramsMessage(callback)).ToAsync().Map(r => Unit.Default);
+ [Obsolete("Use method WithStartProgramCallback of ConnectionBuilder instead. This method will be removed in next major release.")]
+ [ExcludeFromCodeCoverage]
+ public EitherAsync AllowStartOfPrograms(StartProgramDelegate callback)
+ {
+ return RfcRuntime.AllowStartOfPrograms(_connectionHandle, callback).ToAsync();
+ }
public EitherAsync GetAttributes()
{
@@ -200,15 +197,6 @@ public InvokeFunctionMessage(IFunction function, CancellationToken cancellationT
}
}
- private class AllowStartOfProgramsMessage : AgentMessage
- {
- public readonly StartProgramDelegate Callback;
-
- public AllowStartOfProgramsMessage(StartProgramDelegate callback)
- {
- Callback = callback;
- }
- }
private class DisposeMessage : AgentMessage
{
diff --git a/src/YaNco.Core/ConnectionBuilder.cs b/src/YaNco.Core/ConnectionBuilder.cs
index d68ce30e..0f596dda 100644
--- a/src/YaNco.Core/ConnectionBuilder.cs
+++ b/src/YaNco.Core/ConnectionBuilder.cs
@@ -4,31 +4,114 @@
namespace Dbosoft.YaNco
{
+ ///
+ /// This class is used to build connections to a SAP ABAP backend.
+ ///
public class ConnectionBuilder
{
private readonly IDictionary _connectionParam;
- private StartProgramDelegate _startProgramDelegate;
- private Action _configureRuntime = (c) => {};
- private Func, IRfcRuntime, EitherAsync>
+ private Action _configureRuntime = (c) => { };
+
+ private Func, IRfcRuntime, EitherAsync>
_connectionFactory = Connection.Create;
+ readonly List<(string, Action,
+ Func>)> _functionHandlers
+ = new List<(string, Action, Func>)>();
+
+ ///
+ /// Creates a new connection builder.
+ ///
+ /// Dictionary of connection parameters
public ConnectionBuilder(IDictionary connectionParam)
{
_connectionParam = connectionParam;
}
+ ///
+ /// Registers a action to configure the
+ ///
+ /// action with
+ /// current instance for chaining.
+ ///
+ /// Multiple calls of this method will override the previous configuration action.
+ ///
public ConnectionBuilder ConfigureRuntime(Action configure)
{
_configureRuntime = configure;
return this;
}
+ ///
+ /// This method registers a callback of type
+ /// to handle backend requests to start local programs.
+ ///
+ ///
+ /// The SAP backend can call function RFC_START_PROGRAM on back destination to request
+ /// clients to start local programs. This is used a lot in KPRO applications to start saphttp and sapftp.
+ ///
+ /// Delegate to callback function implementation.
+ /// current instance for chaining
public ConnectionBuilder WithStartProgramCallback(StartProgramDelegate startProgramDelegate)
{
- _startProgramDelegate = startProgramDelegate;
+ return WithFunctionHandler("RFC_START_PROGRAM", builder => builder
+ .AddChar("COMMAND", RfcDirection.Import, 512),
+ cf => cf
+ .Input(f => f.GetField("COMMAND"))
+ .Process(cmd => startProgramDelegate(cmd))
+ .NoReply()
+ );
+ }
+
+ ///
+ /// This method registers a function handler from a SAP function name.
+ ///
+ /// Name of function
+ /// function handler
+ /// current instance for chaining
+ ///
+ /// The metadata of the function is retrieved from the backend. Therefore the function
+ /// must exists on the SAP backend system.
+ /// To register a generic function use the signature that builds from a .
+ /// Function handlers are registered process wide (in the SAP NW RFC Library).and mapped to backend system id.
+ /// Multiple registrations of same function and same backend id will therefore have no effect.
+ ///
+ public ConnectionBuilder WithFunctionHandler(string functionName,
+ Func> calledFunc)
+ {
+ _functionHandlers.Add((functionName, null, calledFunc));
+ return this;
+ }
+
+ ///
+ /// This method registers a function handler from a
+ ///
+ /// Name of function
+ /// action to configure function builder
+ /// function handler
+ /// current instance for chaining
+ ///
+ /// The metadata of the function is build in the . This allows to register
+ /// any kind of function.
+ /// To register a known function use the signature with function name
+ /// Function handlers are registered process wide (in the SAP NW RFC Library) and mapped to backend system id.
+ /// Multiple registrations of same function and same backend id will therefore have no effect.
+ ///
+ public ConnectionBuilder WithFunctionHandler(string functionName,
+ Action configureBuilder,
+ Func> calledFunc)
+ {
+ _functionHandlers.Add((functionName, configureBuilder, calledFunc));
return this;
}
+ ///
+ /// Use a alternative factory method to create connection.
+ ///
+ /// factory method
+ /// current instance for chaining.The default implementation call .
+ ///
public ConnectionBuilder UseFactory(
Func, IRfcRuntime, EitherAsync> factory)
{
@@ -36,19 +119,60 @@ public ConnectionBuilder UseFactory(
return this;
}
+ ///
+ /// This method Builds the connection function from the settings.
+ ///
+ /// current instance for chaining.
+ ///
+ /// The connection builder first creates RfcRuntime and calls any registered runtime configure action.
+ /// The result is a function that first calls the connection factory (defaults to
+ /// and afterwards registers function handlers.
+ ///
public Func> Build()
{
var runtimeConfigurer = new RfcRuntimeConfigurer();
_configureRuntime(runtimeConfigurer);
var runtime = runtimeConfigurer.Create();
-
- if(_startProgramDelegate == null)
- return () => _connectionFactory(_connectionParam, runtime);
+ return () => _connectionFactory(_connectionParam, runtime)
+ .Bind(RegisterFunctionHandlers);
+
+ }
+
+ private EitherAsync RegisterFunctionHandlers(IConnection connection)
+ {
+ return connection.GetAttributes().Bind(attributes =>
+ {
+ return _functionHandlers.Map(reg =>
+ {
+ var (functionName, configureBuilder, callBackFunction) = reg;
+
+ if (connection.RfcRuntime.IsFunctionHandlerRegistered(attributes.SystemId, functionName))
+ return Unit.Default;
+
+ if (configureBuilder != null)
+ {
+ var builder = new FunctionBuilder(connection.RfcRuntime, functionName);
+ configureBuilder(builder);
+ return builder.Build().ToAsync().Bind(descr =>
+ {
+ return connection.RfcRuntime.AddFunctionHandler(attributes.SystemId,
+ functionName,
+ descr,
+ f => callBackFunction(new CalledFunction(f))).ToAsync();
+ });
+
+ }
- return () => (from c in _connectionFactory(_connectionParam, runtime)
- from _ in c.AllowStartOfPrograms(_startProgramDelegate)
- select c);
+ return connection.CreateFunction(functionName).Bind(func =>
+ {
+ return connection.RfcRuntime.AddFunctionHandler(attributes.SystemId,
+ functionName,
+ func,
+ f => callBackFunction(new CalledFunction(f))).ToAsync();
+ });
+ }).Traverse(l => l).Map(eu => connection);
+ });
}
}
}
diff --git a/src/YaNco.Core/DataContainer.cs b/src/YaNco.Core/DataContainer.cs
index d9ed3cd2..aeb8b73b 100644
--- a/src/YaNco.Core/DataContainer.cs
+++ b/src/YaNco.Core/DataContainer.cs
@@ -47,7 +47,12 @@ public Either GetTable(string name)
{
return _rfcRuntime.GetTable(_handle, name).Map(handle => (ITable) new Table(handle, _rfcRuntime));
}
-
+
+ public Either GetTypeDescription()
+ {
+ return _rfcRuntime.GetTypeDescription(_handle);
+ }
+
protected virtual void Dispose(bool disposing)
{
if (disposing)
diff --git a/src/YaNco.Core/Delegates.cs b/src/YaNco.Core/Delegates.cs
new file mode 100644
index 00000000..d6938c17
--- /dev/null
+++ b/src/YaNco.Core/Delegates.cs
@@ -0,0 +1,4 @@
+using Dbosoft.YaNco;
+using LanguageExt;
+
+public delegate Either RfcFunctionDelegate(IRfcHandle rfcHandle, IFunctionHandle functionHandle);
\ No newline at end of file
diff --git a/src/YaNco.Core/FunctionBuilder.cs b/src/YaNco.Core/FunctionBuilder.cs
new file mode 100644
index 00000000..e0fcb3dc
--- /dev/null
+++ b/src/YaNco.Core/FunctionBuilder.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Dbosoft.YaNco.Internal;
+using LanguageExt;
+
+namespace Dbosoft.YaNco
+{
+ public class FunctionBuilder : IFunctionBuilder
+ {
+ private readonly string _functionName;
+ private readonly IDictionary _parameters = new Dictionary();
+ private readonly IRfcRuntime _runtime;
+
+
+ public FunctionBuilder(IRfcRuntime runtime, string functionName)
+ {
+ _runtime = runtime;
+ _functionName = functionName;
+ }
+
+ public IFunctionBuilder AddParameter(RfcParameterDescription parameter)
+ {
+ _parameters.Add(parameter.Name, parameter);
+ return this;
+ }
+
+ public IFunctionBuilder AddChar(string name, RfcDirection direction, uint length, bool optional = true, string defaultValue = null)
+ {
+ return AddParameter(new RfcParameterDescription(name, RfcType.CHAR, direction, length, length * 2, 0, optional, defaultValue));
+ }
+
+ public IFunctionBuilder AddInt(string name, RfcDirection direction, bool optional = true, int defaultValue = 0)
+ {
+ return AddParameter(new RfcParameterDescription(name, RfcType.INT, direction, 0, 0, 0, optional, defaultValue.ToString()));
+ }
+
+ public IFunctionBuilder AddLong(string name, RfcDirection direction, bool optional = true, long defaultValue = 0)
+ {
+ return AddParameter(new RfcParameterDescription(name, RfcType.INT8, direction, 0, 0, 0, optional, defaultValue.ToString()));
+ }
+
+ public IFunctionBuilder AddString(string name, RfcDirection direction, bool optional = true, uint length = 0, string defaultValue = null)
+ {
+ return AddParameter(new RfcParameterDescription(name, RfcType.STRING, direction, length, length * 2, 0, optional, defaultValue));
+ }
+
+ public IFunctionBuilder AddStructure(string name, RfcDirection direction, ITypeDescriptionHandle typeHandle, bool optional = true)
+ {
+ return AddTyped(name, RfcType.STRUCTURE, direction, typeHandle, optional);
+ }
+
+ public IFunctionBuilder AddStructure(string name, RfcDirection direction, IStructure structure, bool optional = true)
+ {
+ return structure.GetTypeDescription()
+ .Match(
+ Right: r => AddTyped(name, RfcType.STRUCTURE, direction, r, optional),
+ Left: l => throw new ArgumentException("Argument is not a valid type handle", nameof(structure)));
+ }
+
+ public IFunctionBuilder AddTable(string name, RfcDirection direction, ITable table, bool optional = true)
+ {
+ return table.GetTypeDescription()
+ .Match(
+ Right: r => AddTyped(name, RfcType.STRUCTURE, direction, r, optional),
+ Left: l => throw new ArgumentException("Argument is not a valid type handle", nameof(table)));
+ }
+
+ public IFunctionBuilder AddTable(string name, RfcDirection direction, ITypeDescriptionHandle typeHandle, bool optional = true)
+ {
+ return AddTyped(name, RfcType.TABLE, direction, typeHandle, optional);
+ }
+
+ private IFunctionBuilder AddTyped(string name, RfcType type, RfcDirection direction, ITypeDescriptionHandle typeHandle, bool optional = true)
+ {
+ if (!(typeHandle is TypeDescriptionHandle handle))
+ throw new ArgumentException("Argument has to be of type TypeDescriptionHandle", nameof(typeHandle));
+
+ var ptr = handle.Ptr;
+ return AddParameter(new RfcParameterDescription(name, type, direction, 0, 0, 0, optional, null) { TypeDescriptionHandle = ptr });
+ }
+
+ public Either Build()
+ {
+
+ return _runtime.CreateFunctionDescription(_functionName).Bind(functionHandle =>
+ {
+ return _parameters.Values.Map(parameter =>
+ _runtime.AddFunctionParameter(functionHandle, parameter))
+ .Traverse(l => l).Map(e => functionHandle);
+
+ });
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/YaNco.Core/FunctionalServerExtensions.cs b/src/YaNco.Core/FunctionalServerExtensions.cs
new file mode 100644
index 00000000..a04014ec
--- /dev/null
+++ b/src/YaNco.Core/FunctionalServerExtensions.cs
@@ -0,0 +1,42 @@
+using System;
+using LanguageExt;
+// ReSharper disable InconsistentNaming
+
+namespace Dbosoft.YaNco
+{
+ public static class FunctionalServerExtensions
+ {
+
+ public static Either> Process(
+ this Either> input,
+ Func processFunc)
+ {
+ return input.Map(i => i.Process(processFunc));
+ }
+
+ public static Either> Process(
+ this Either> input,
+ Action processAction)
+ {
+ return input.Map(i =>
+ {
+ var (function, input1) = i;
+ processAction(input1);
+ return new FunctionProcessed(Unit.Default, function);
+ });
+ }
+
+ public static Either Reply(this Either> self, Func, Either> replyFunc)
+ {
+ return self.Bind(p => p.Reply(replyFunc));
+ }
+
+ public static Either NoReply(this Either> self)
+ {
+ return self.Bind(p => p.Reply((o, f) => f));
+ }
+
+
+
+ }
+}
\ No newline at end of file
diff --git a/src/YaNco.Core/Internal/Api.cs b/src/YaNco.Core/Internal/Api.cs
index 800d43ae..dcdeeb42 100644
--- a/src/YaNco.Core/Internal/Api.cs
+++ b/src/YaNco.Core/Internal/Api.cs
@@ -1,8 +1,10 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using LanguageExt;
namespace Dbosoft.YaNco.Internal
{
@@ -42,6 +44,13 @@ public static RfcRc GetConnectionAttributes(ConnectionHandle connectionHandle, o
return rc;
}
+ public static FunctionDescriptionHandle CreateFunctionDescription(string functionName,
+ out RfcErrorInfo errorInfo)
+ {
+ var ptr = Interopt.RfcCreateFunctionDesc(functionName, out errorInfo);
+ return ptr == IntPtr.Zero ? null : new FunctionDescriptionHandle(ptr);
+ }
+
public static FunctionDescriptionHandle GetFunctionDescription(FunctionHandle functionHandle,
out RfcErrorInfo errorInfo)
{
@@ -105,6 +114,24 @@ public static FunctionHandle CreateFunction(FunctionDescriptionHandle descriptio
}
+ public static RfcRc AddFunctionParameter(FunctionDescriptionHandle descriptionHandle, RfcParameterDescription parameterDescription, out RfcErrorInfo errorInfo)
+ {
+ var parameterDesc = new Interopt.RFC_PARAMETER_DESC
+ {
+ Name = parameterDescription.Name,
+ Type = parameterDescription.Type,
+ Direction = parameterDescription.Direction,
+ Optional = parameterDescription.Optional ? 'X' : ' ',
+ Decimals = parameterDescription.Decimals,
+ NucLength = parameterDescription.NucLength,
+ UcLength = parameterDescription.UcLength,
+ TypeDescHandle = parameterDescription.TypeDescriptionHandle
+ };
+
+ return Interopt.RfcAddParameter(descriptionHandle.Ptr, ref parameterDesc, out errorInfo);
+ }
+
+
public static RfcRc GetFunctionParameterCount(FunctionDescriptionHandle descriptionHandle, out int count,
out RfcErrorInfo errorInfo)
{
@@ -171,64 +198,136 @@ public static TableHandle CloneTable(TableHandle tableHandle, out RfcErrorInfo e
return ptr == IntPtr.Zero ? null : new TableHandle(ptr, true);
}
- public static void AllowStartOfPrograms(ConnectionHandle connectionHandle, StartProgramDelegate callback, out
- RfcErrorInfo errorInfo)
+ public static RfcRc RegisterServerFunctionHandler(string sysId,
+ string functionName,
+ FunctionDescriptionHandle functionDescription,
+ RfcFunctionDelegate functionHandler, out RfcErrorInfo errorInfo)
{
- var descriptionHandle = new FunctionDescriptionHandle(Interopt.RfcCreateFunctionDesc("RFC_START_PROGRAM", out errorInfo));
- if (descriptionHandle.Ptr == IntPtr.Zero)
+ var registration = new FunctionRegistration(sysId, functionName);
+
+ if (_registeredFunctionNames.Contains(registration))
{
- return;
+ errorInfo = RfcErrorInfo.Ok();
+ return RfcRc.RFC_OK;
}
+ _registeredFunctionNames = _registeredFunctionNames.Add(registration);
- var paramDesc = new Interopt.RFC_PARAMETER_DESC { Name = "COMMAND", Type = RfcType.CHAR, Direction = RfcDirection.Import, NucLength = 512, UcLength = 1024 };
- var rc = Interopt.RfcAddParameter(descriptionHandle.Ptr, ref paramDesc, out errorInfo);
+ var rc = Interopt.RfcInstallServerFunction(sysId, functionDescription.Ptr, RFC_Function_Handler,
+ out errorInfo);
if (rc != RfcRc.RFC_OK)
{
- return;
+ _registeredFunctionNames = _registeredFunctionNames.Remove(registration);
+ return rc;
}
- rc = Interopt.RfcInstallServerFunction(null, descriptionHandle.Ptr, StartProgramHandler, out errorInfo);
- if (rc != RfcRc.RFC_OK)
+
+ RegisteredFunctions.AddOrUpdate(functionDescription.Ptr, functionHandler, (c, v) => v);
+ return rc;
+ }
+
+ private static readonly object AllowStartOfProgramsLock = new object();
+
+ [Obsolete("Use method AllowStartOfPrograms of ConnectionBuilder. This method will be removed in next major release.")]
+ public static void AllowStartOfPrograms(ConnectionHandle connectionHandle, StartProgramDelegate callback, out
+ RfcErrorInfo errorInfo)
+ {
+ lock (AllowStartOfProgramsLock)
{
- return;
+
+ GetConnectionAttributes(connectionHandle, out var attributes, out errorInfo);
+ if (errorInfo.Code != RfcRc.RFC_OK)
+ return;
+
+
+ RfcErrorInfo errorInfoLocal = default;
+ //function runtime is not available at this API level => as workaround create a new runtime -> in FunctionBuilder it is
+ //only used to wrap this API implementation.
+ new FunctionBuilder(new RfcRuntime(), "RFC_START_PROGRAM")
+ .AddChar("COMMAND", RfcDirection.Import, 512)
+ .Build()
+ .Match(funcDescriptionHandle =>
+ {
+ RegisterServerFunctionHandler(attributes.SystemId,
+ "RFC_START_PROGRAM",
+ funcDescriptionHandle as FunctionDescriptionHandle,
+ (_, funcHandle) =>
+ {
+ var functionHandle = funcHandle as FunctionHandle;
+ Debug.Assert(functionHandle != null, nameof(functionHandle) + " != null");
+
+ var commandBuffer = new char[513];
+ var rc = Interopt.RfcGetStringByIndex(functionHandle.Ptr, 0, commandBuffer,
+ (uint)commandBuffer.Length - 1, out var commandLength, out var error);
+
+ if (rc != RfcRc.RFC_OK)
+ return Unit.Default;
+
+ var command = new string(commandBuffer, 0, (int)commandLength);
+ error = callback(command);
+
+ if (error.Code == RfcRc.RFC_OK)
+ return Unit.Default;
+
+ return error;
+ }, out errorInfoLocal);
+ }, l => errorInfoLocal = l);
+
+ errorInfo = errorInfoLocal;
}
- RegisteredCallbacks.AddOrUpdate(connectionHandle.Ptr, callback, (c,v) => v );
-
}
- private static readonly ConcurrentDictionary RegisteredCallbacks
- = new ConcurrentDictionary();
+ private static readonly ConcurrentDictionary RegisteredFunctions
+ = new ConcurrentDictionary();
- private static readonly Interopt.RfcServerFunction StartProgramHandler = RFC_START_PROGRAM_Handler;
+ private static LanguageExt.HashSet _registeredFunctionNames;
- static RfcRc RFC_START_PROGRAM_Handler(IntPtr rfcHandle, IntPtr funcHandle, out RfcErrorInfo errorInfo)
+ public static bool IsFunctionHandlerRegistered(string sysId, string functionName)
{
- if (!RegisteredCallbacks.TryGetValue(rfcHandle, out var startProgramDelegate))
+ var registration = new FunctionRegistration(sysId, functionName);
+
+ return _registeredFunctionNames.Contains(registration);
+ }
+
+ private static RfcRc RFC_Function_Handler(IntPtr rfcHandle, IntPtr funcHandle, out RfcErrorInfo errorInfo)
+ {
+ var descriptionHandle = Interopt.RfcDescribeFunction(funcHandle, out errorInfo);
+ if (descriptionHandle == IntPtr.Zero)
+ return errorInfo.Code;
+
+
+ if (!RegisteredFunctions.TryGetValue(descriptionHandle, out var functionDelegate))
{
- errorInfo = new RfcErrorInfo(RfcRc.RFC_INVALID_HANDLE, RfcErrorGroup.EXTERNAL_APPLICATION_FAILURE, "",
- "no connection registered for this callback", "", "", "", "", "", "", "");
+ Interopt.RfcGetFunctionName(descriptionHandle, out var funcName, out _);
+ if (string.IsNullOrWhiteSpace(funcName))
+ funcName = "[unknown function]";
+
+ errorInfo = new RfcErrorInfo(RfcRc.RFC_INVALID_HANDLE, RfcErrorGroup.EXTERNAL_APPLICATION_FAILURE, "",
+ $"no function handler registered for function '{funcName}'", "", "", "", "", "", "", "");
return RfcRc.RFC_INVALID_HANDLE;
}
-
- var commandBuffer = new char[513];
- var rc = Interopt.RfcGetStringByIndex(funcHandle, 0, commandBuffer, (uint)commandBuffer.Length - 1, out var commandLength, out errorInfo);
+ RfcErrorInfo errorInfoLocal = default;
+ var rc = functionDelegate(new RfcHandle(rfcHandle), new FunctionHandle(funcHandle)).Match(
+ Right: r => RfcRc.RFC_OK,
+ l =>
+ {
+ errorInfoLocal = l;
+ return l.Code;
- if (rc != RfcRc.RFC_OK)
- return rc;
+ });
- var command = new string(commandBuffer, 0, (int)commandLength);
- errorInfo = startProgramDelegate(command);
+ errorInfo = errorInfoLocal;
+ return rc;
- return errorInfo.Code;
}
-
+ [Obsolete("Callback handlers are no longer bound to connection. This method will do nothing and will be removed in next major release.")]
+ // ReSharper disable once UnusedParameter.Global
public static void RemoveCallbackHandler(IntPtr connectionHandle)
{
- RegisteredCallbacks.TryRemove(connectionHandle, out var _);
+
}
public static RfcRc GetTableRowCount(TableHandle table, out int count, out RfcErrorInfo errorInfo)
@@ -379,5 +478,37 @@ public static RfcRc GetBytes(IDataContainerHandle containerHandle, string name,
return rc;
}
+
+
+ private struct FunctionRegistration: IEquatable
+ {
+ public readonly string SysId;
+ public readonly string Name;
+
+ public FunctionRegistration(string sysId, string name)
+ {
+ SysId = sysId;
+ Name = name;
+ }
+
+ public bool Equals(FunctionRegistration other)
+ {
+ return SysId == other.SysId && Name == other.Name;
+ }
+
+ public override bool Equals(object obj)
+ {
+ return obj is FunctionRegistration other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return (SysId.GetHashCode() * 397) ^ Name.GetHashCode();
+ }
+ }
+ }
}
+
}
\ No newline at end of file
diff --git a/src/YaNco.Core/Internal/RfcHandle.cs b/src/YaNco.Core/Internal/RfcHandle.cs
new file mode 100644
index 00000000..17e555e1
--- /dev/null
+++ b/src/YaNco.Core/Internal/RfcHandle.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Dbosoft.YaNco.Internal
+{
+ public class RfcHandle : IRfcHandle
+ {
+ internal IntPtr Ptr { get; set; }
+
+ internal RfcHandle(IntPtr ptr)
+ {
+ Ptr = ptr;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/YaNco.Core/RfcRuntime.cs b/src/YaNco.Core/RfcRuntime.cs
index 8850476b..ebdae25c 100644
--- a/src/YaNco.Core/RfcRuntime.cs
+++ b/src/YaNco.Core/RfcRuntime.cs
@@ -32,6 +32,12 @@ public static IFieldMapper CreateDefaultFieldMapper(IEnumerable fromRfcCon
public RfcRuntimeOptions Options { get; }
+
+ public bool IsFunctionHandlerRegistered(string sysId, string functionName)
+ {
+ return Api.IsFunctionHandlerRegistered(sysId, functionName);
+ }
+
private Either ResultOrError(TResult result, RfcErrorInfo errorInfo, bool logAsError = false)
{
if (result == null || errorInfo.Code != RfcRc.RFC_OK)
@@ -90,6 +96,19 @@ public Either OpenConnection(IDictionary CreateFunctionDescription(string functionName)
+ {
+ Logger.IfSome(l => l.LogTrace("creating function description without connection", functionName));
+ IFunctionDescriptionHandle handle = Api.CreateFunctionDescription(functionName, out var errorInfo);
+ return ResultOrError(handle, errorInfo);
+ }
+
+ public Either AddFunctionParameter(IFunctionDescriptionHandle descriptionHandle, RfcParameterDescription parameterDescription)
+ {
+ Logger.IfSome(l => l.LogTrace("adding parameter to function description", new { handle = descriptionHandle, parameter = parameterDescription }));
+ var rc = Api.AddFunctionParameter(descriptionHandle as FunctionDescriptionHandle, parameterDescription, out var errorInfo);
+ return ResultOrError(descriptionHandle, rc, errorInfo);
+ }
public Either GetFunctionDescription(IConnectionHandle connectionHandle,
string functionName)
@@ -184,6 +203,32 @@ public Either GetFunctionParameterDescription(
}
+ public Either AddFunctionHandler(string sysid,
+ string functionName,
+ IFunction function, Func> handler)
+ {
+ return GetFunctionDescription(function.Handle)
+ .Use(used => used.Bind(d => AddFunctionHandler(sysid,
+ functionName, d, handler)));
+ }
+
+ public Either AddFunctionHandler(string sysid,
+ string functionName,
+ IFunctionDescriptionHandle descriptionHandle, Func> handler)
+ {
+ Api.RegisterServerFunctionHandler(sysid,
+ functionName,
+ descriptionHandle as FunctionDescriptionHandle,
+ (rfcHandle, functionHandle) =>
+ {
+ var func = new Function(functionHandle, this);
+ return handler(func);
+ },
+ out var errorInfo);
+
+ return ResultOrError(Unit.Default, errorInfo);
+ }
+
public Either Invoke(IConnectionHandle connectionHandle, IFunctionHandle functionHandle)
{
Logger.IfSome(l => l.LogTrace("Invoking function", new { connectionHandle, functionHandle }));
@@ -238,6 +283,7 @@ public Either CloneTable(ITableHandle tableHandle)
}
+ [Obsolete("Use method WithStartProgramCallback of ConnectionBuilder. This method will be removed in next major release.")]
public Either AllowStartOfPrograms(IConnectionHandle connectionHandle,
StartProgramDelegate callback)
{
diff --git a/src/YaNco.Primitives/RfcParameterDescription.cs b/src/YaNco.Primitives/RfcParameterDescription.cs
new file mode 100644
index 00000000..5857c2aa
--- /dev/null
+++ b/src/YaNco.Primitives/RfcParameterDescription.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace Dbosoft.YaNco
+{
+ public class RfcParameterDescription : RfcParameterInfo
+ {
+ public IntPtr TypeDescriptionHandle { get; set; }
+
+ public RfcParameterDescription(string name, RfcType type, RfcDirection direction, uint nucLength, uint ucLength, uint decimals, bool optional, string defaultValue) : base(name, type, direction, nucLength, ucLength, decimals, defaultValue, null, optional)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/YaNco.Primitives/RfcParameterInfo.cs b/src/YaNco.Primitives/RfcParameterInfo.cs
index 17b9196a..d88b3ff5 100644
--- a/src/YaNco.Primitives/RfcParameterInfo.cs
+++ b/src/YaNco.Primitives/RfcParameterInfo.cs
@@ -42,5 +42,4 @@ public RfcFieldInfo(string name, RfcType type, uint nucLength, uint ucLength, ui
Decimals = decimals;
}
}
-
}
\ No newline at end of file
diff --git a/test/SAPSystemTests/Program.cs b/test/SAPSystemTests/Program.cs
index 484e23fb..ea8b0410 100644
--- a/test/SAPSystemTests/Program.cs
+++ b/test/SAPSystemTests/Program.cs
@@ -18,6 +18,9 @@ namespace SAPSystemTests
{
class Program
{
+
+ private static string CallbackCommand = null;
+
static async Task Main(string[] args)
{
var configurationBuilder =
@@ -53,35 +56,43 @@ static async Task Main(string[] args)
StartProgramDelegate callback = command =>
{
- var programParts = command.Split(' ');
- var arguments = command.Replace(programParts[0], "");
- var p = Process.Start(AppDomain.CurrentDomain.BaseDirectory + @"\" + programParts[0] + ".exe",
- arguments.TrimStart());
+ CallbackCommand = command;
return RfcErrorInfo.Ok();
};
+
var connectionBuilder = new ConnectionBuilder(settings)
.WithStartProgramCallback(callback)
.ConfigureRuntime(c =>
c.WithLogger(new SimpleConsoleLogger()));
- using (var context = new RfcContext(connectionBuilder.Build()))
+ var connectionFunc = connectionBuilder.Build();
+
+ using (var context = new RfcContext(connectionFunc))
{
await context.PingAsync();
await context.GetConnection().Bind(c => c.GetAttributes())
.IfRight(attributes =>
- {
+ {
Console.WriteLine("connection attributes:");
Console.WriteLine(JsonConvert.SerializeObject(attributes));
});
await RunIntegrationTests(context);
+ }
+
+ using (var context = new RfcContext(connectionFunc))
+ {
long totalTest1 = 0;
long totalTest2 = 0;
+ //second call back test (should still be called)
+ await RunCallbackTest(context);
+
+
for (var run = 0; run < repeats; run++)
{
Console.WriteLine($"starting Test Run {run + 1} of {repeats}\tTest 01");
@@ -108,6 +119,7 @@ private static async Task RunIntegrationTests(IRfcContext context)
await RunIntegrationTest01(context);
await RunIntegrationTest02(context);
await RunIntegrationTest03(context);
+ await RunCallbackTest(context);
Console.WriteLine("*** END OF Integration Tests ***");
}
@@ -414,7 +426,19 @@ from connection in context.GetConnection()
});
}
+ private static async Task RunCallbackTest(IRfcContext context)
+ {
+ Console.WriteLine("Integration Tests 04 (callback function test)");
+ var commandString = RandomString(40);
+ await context.CallFunctionOneWay("ZYANCO_IT_2", f => f.SetField("COMMAND", commandString)).ToEither();
+ if (CallbackCommand == commandString)
+ Console.WriteLine("Test succeed");
+ else
+ {
+ Console.WriteLine("Callback Test failed, command received: " + CallbackCommand);
+ }
+ }
private static async Task RunPerformanceTest01(IRfcContext context, int rows = 0)
{
diff --git a/test/SAPSystemTests/Properties/launchSettings.json b/test/SAPSystemTests/Properties/launchSettings.json
index 076b233c..ad122edf 100644
--- a/test/SAPSystemTests/Properties/launchSettings.json
+++ b/test/SAPSystemTests/Properties/launchSettings.json
@@ -2,7 +2,7 @@
"profiles": {
"SAPSystemTests": {
"commandName": "Project",
- "commandLineArgs": "/tests:repeats=100000 /tests:rows=0",
+ "commandLineArgs": "/tests:repeats=10 /tests:rows=100",
"nativeDebugging": true
}
}
diff --git a/test/SAPSystemTests/SimpleConsoleLogger.cs b/test/SAPSystemTests/SimpleConsoleLogger.cs
index 8aa60ac8..6953b597 100644
--- a/test/SAPSystemTests/SimpleConsoleLogger.cs
+++ b/test/SAPSystemTests/SimpleConsoleLogger.cs
@@ -20,7 +20,7 @@ public void LogException(Exception exception)
public void LogTrace(string message, object data)
{
- //Console.WriteLine($"TRACE\t{message}{ObjectToString(data)}");
+ // Console.WriteLine($"TRACE\t{message}{ObjectToString(data)}");
}
public void LogError(string message, object data)
@@ -30,6 +30,9 @@ public void LogError(string message, object data)
public void LogDebug(string message, object data)
{
+ if (data is RfcErrorInfo { Key: "RFC_TABLE_MOVE_EOF" })
+ return;
+
Console.WriteLine($"DEBUG\t{message}{ObjectToString(data)}");
}
diff --git a/test/YaNco.Core.Tests/ConnectionTests.cs b/test/YaNco.Core.Tests/ConnectionTests.cs
index dca3ae6a..a376759f 100644
--- a/test/YaNco.Core.Tests/ConnectionTests.cs
+++ b/test/YaNco.Core.Tests/ConnectionTests.cs
@@ -136,24 +136,5 @@ from __ in c.Cancel()
rfcRuntimeMock.VerifyAll();
}
-
- [Fact]
- public async Task AllowStartOfPrograms_is_cancelled()
- {
- var rfcRuntimeMock = new Mock()
- .SetupOpenConnection(out var connHandle);
-
- StartProgramDelegate callback = (c) => RfcErrorInfo.Ok();
-
- rfcRuntimeMock.Setup(r => r
- .AllowStartOfPrograms(connHandle.Object,callback))
- .Returns(Unit.Default);
-
- var conn = await rfcRuntimeMock.CreateConnection()
- .Map(c => c.AllowStartOfPrograms(callback));
-
- rfcRuntimeMock.VerifyAll();
-
- }
}
}
\ No newline at end of file
diff --git a/test/YaNco.Core.Tests/YaNco.Core.Tests.csproj b/test/YaNco.Core.Tests/YaNco.Core.Tests.csproj
index d57fa147..5956d808 100644
--- a/test/YaNco.Core.Tests/YaNco.Core.Tests.csproj
+++ b/test/YaNco.Core.Tests/YaNco.Core.Tests.csproj
@@ -1,7 +1,7 @@
- netcoreapp3.1;net471;net5.0
+ netcoreapp3.1;net471;net5.0;net6.0
false