RPS Engine#

RPSEngine class#

Facade for direct communication with RPSEngine through the IRPSEngineProvider interface. It contains all steps required to transform the request contexts or simple list of IRPSValue into the Request model and Response model back.

public class RPSEngine
{
    private readonly IRPSEngineProvider _provider;
    private readonly RPSEngineConverter _converter;
    private readonly RPSEngineContextResolver _engineContextResolver;

    public RPSEngine(IRPSEngineProvider engineProvider,
        RPSEngineConverter converter,
        RPSEngineContextResolver contextResolver = null)
    {
        _provider = engineProvider;
        _converter = converter;
        _engineContextResolver = contextResolver;
    }

    public RequestContext CreateContext() => new RequestContext(this, _engineContextResolver);

    public void Transform(IEnumerable<IRPSValue> rpsValues,
        string rightsContextName,
        string processingContextName,
        Context loggingContext = null) =>
        TransformAsync(rpsValues,
            rightsContextName,
            processingContextName,
            loggingContext: loggingContext).GetAwaitedResult();

    public void Transform(IEnumerable<IRPSValue> rpsValues,
        Context rightsContext,
        ProcessingContext processingContext,
        Context loggingContext = null) =>
        TransformAsync(rpsValues,
            rightsContext,
            processingContext,
            loggingContext: loggingContext).GetAwaitedResult();

    public void Transform(RequestContext requestContext) => TransformAsync(requestContext).GetAwaitedResult();

    public async Task TransformAsync(IEnumerable<IRPSValue> rpsValues,
        string rightsContextName,
        string processingContextName,
        Context loggingContext = null)
    {
        if (_engineContextResolver == null)
            throw new InvalidOperationException("Context resolver not found");

        (Context RightsContext, ProcessingContext ProcessingContext) contexts =
            _engineContextResolver.Resolve(rightsContextName, processingContextName);

        await TransformAsync(rpsValues,
            contexts.RightsContext,
            contexts.ProcessingContext,
            loggingContext: loggingContext);
    }

    public async Task TransformAsync(IEnumerable<IRPSValue> rpsValues,
        Context rightsContext,
        ProcessingContext processingContext,
        Context loggingContext = null)
        => await TransformAsync(CreateContext().WithRequest(rpsValues,
            rightsContext,
            processingContext,
            loggingContext: loggingContext));

    public virtual async Task TransformAsync(RequestContext requestContext)
    {
        RequestBody requestBody = _converter.ToRequestBody(requestContext);

        try
        {
            ResponseBody responseBody = await _provider.TransformAsync(requestBody);
            _converter.FromResponseBody(responseBody, requestContext);
        }
        catch
        {
            _converter.AssignNullValues(requestContext);
            throw;
        }
    }
}

IRPSEngineProvider interface#

Interface to communicate with RPSEngine. Send RequestBody and return ResponseBody. Implementation should contains call RPSEngine API.

public interface IRPSEngineProvider
{
    Task<ResponseBody> TransformAsync(RequestBody requestBody);
}

EngineRestApiClient class#

Example of implementation for IRPSEngineProvider interface, based on MPackRestApiClient and Flurl NuGet package.

public class EngineJsonRestApiClient : IRPSEngineProvider
{
    public EngineJsonRestApiClient(
        EngineClientOptions clientOptions,
        ITokenProvider tokenProvider)
    {
        HostName = clientOptions.EngineHostName;
        Timeout = clientOptions.Timeout;
        TokenProvider = tokenProvider;
    }

    public TimeSpan? Timeout { get; }

    public string HostName { get; }

    public Url Url => HostName.AppendPathSegment(ControllerPath);

    protected ITokenProvider TokenProvider { get; }

    protected string ControllerPath => "api";

    public async Task<ResponseBody> TransformAsync(RequestBody requestBody)
        => await Url.SendAsync(
            r => r.AppendPathSegment("transform")
                .ConfigureRequest(settings => settings.Timeout = Timeout ?? settings.Timeout)
                .AllowHttpStatus(HttpStatusCode.BadRequest)
                .PostJsonAsync(requestBody)
                .ReceiveJson<ResponseBody>(), TokenProvider);
}

RPSEngineConverter class#

Contains methods for convert RequestContext into RequestBody model and ResponseBody model into RequestContext back.

public class RPSEngineConverter
{
    public RequestBody ToRequestBody(RequestContext requestContext)
    {
        var requestBody = new RequestBody();

        if (!requestContext.Requests.Any())
            return requestBody;

        var rightsContextsByGuid = new Dictionary<Model.Api.Context.Context, Guid>();
        var processingContextsByGuid = new Dictionary<Model.Api.Context.Context, Guid>();

        foreach (Request request in requestContext.Requests)
        {
            if (!request.Values.Any())
                continue;

            Model.Api.Context.Context rightsContext = ToModel<Model.Api.Context.Context>(request.RightsContext);
            if (!rightsContextsByGuid.TryGetValue(rightsContext, out Guid rightsContextGuid))
            {
                rightsContextGuid = Guid.NewGuid();
                rightsContext.Guid = rightsContextGuid;

                rightsContextsByGuid[rightsContext] = rightsContextGuid;

                requestBody.RightsContexts.Add(rightsContext);
            }

            Model.Api.Context.Context processingContext = ToModel<Model.Api.Context.Context>(request.ProcessingContext);
            Model.Api.Context.Context loggingContext = ToModel<Model.Api.Context.Context>(request.LoggingContext);

            if (processingContext == null)
            {
                requestBody.Requests.Add(ToRequest(request.Values,
                    request.Guid,
                    rightsContextGuid,
                    null,
                    loggingContext: loggingContext));
                continue;
            }

            if (!processingContextsByGuid.TryGetValue(processingContext, out Guid processingContextGuid))
            {
                processingContextGuid = Guid.NewGuid();
                processingContext.Guid = processingContextGuid;

                processingContextsByGuid[processingContext] = processingContextGuid;

                requestBody.ProcessingContexts.Add(processingContext);
            }

            requestBody.Requests.Add(ToRequest(request.Values,
                request.Guid,
                rightsContextGuid,
                processingContextGuid,
                loggingContext: loggingContext));
        }

        return requestBody;
    }

    public void FromResponseBody(ResponseBody responseBody, RequestContext requestContext)
    {
        if (responseBody.Error != null)
            throw new RPSEngineException("Error received from RPS Engine API response. " +
                                         $"Code: '{responseBody.Error.Code}'. " +
                                         $"Message: '{responseBody.Error.Message}'.", responseBody.Error);

        foreach (Response response in responseBody.Responses)
        {
            if (!requestContext.TryGetRequest(response.Request, out Request request))
                continue;

            for (int i = 0; i < response.Instances.Count; i++)
            {
                Model.Api.Instance responseInstance = response.Instances[i];
                request.Values[i].Value = responseInstance.Value;

                if (responseInstance.Error != null)
                    request.Values[i].Error = new ValueError(
                        responseInstance.Error.Code,
                        responseInstance.Error.Message);
            }
        }
    }

    public void AssignNullValues(RequestContext requestContext)
    {
        foreach (Request request in requestContext.Requests)
        {
            foreach (IRPSValue rpsValue in request.Values)
            {
                rpsValue.Value = null;
            }
        }
    }

    private static Model.Api.Request.Request ToRequest(
        IEnumerable<IRPSValue> rpsValues,
        Guid requestGuid,
        Guid rightsContextGuid,
        Guid? processingContextGuid,
        Model.Api.Context.Context loggingContext = null)
    {
        var request = new Model.Api.Request.Request
        {
            Guid = requestGuid,
            RightsContext = rightsContextGuid,
            ProcessingContext = processingContextGuid,
            LoggingContext = loggingContext
        };

        foreach (var rpsValue in rpsValues)
        {
            var instance = new Model.Api.Instance {Value = rpsValue.Value};
            request.Instances.Add(instance);

            if (rpsValue.Instance != null)
            {
                instance.ClassName = rpsValue.Instance.ClassName;
                instance.PropertyName = rpsValue.Instance.PropertyName;
            }

            if (rpsValue.Dependencies.Any())
                instance.DependencyContext = rpsValue.Dependencies.ToContext();
        }

        return request;
    }

    private static Model.Api.Context.Context ToModel<T>(Context context, Func<T, T> modifyContext = null)
        where T : Model.Api.Context.Context, new()
    {
        if (context == null)
            return default;

        var modelContext = new T
        {
            Evidences = new List<Model.Api.Context.Evidence>(
                context.Evidences.Select(ToModel))
        };

        return modifyContext?.Invoke(modelContext) ?? modelContext;
    }

    private static Model.Api.Context.Evidence ToModel(Evidence evidence)
        => new Model.Api.Context.Evidence
        {
            Name = evidence.Name,
            Value = evidence.Value
        };
}

RPSEngineException class#

Represent class for an exception which happens in RPSEngine or RPSEngineConverter.

public class RPSEngineException : Exception
{
    public RPSEngineException()
    {
    }

    public RPSEngineException(string message)
        : base(message)
    {
    }

    public RPSEngineException(string message, Exception innerException)
        : base(message, innerException)
    {
    }

    public RPSEngineException(string message, Error error)
        : base(message)
        => Error = error;

    public Error Error { get; set; }
}