// // ContractBuilder.cs // // Copyright (c) František Boháček. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Collections.Generic; using System.ComponentModel; using System.Threading; using System.Threading.Tasks; using Remora.Results; namespace NosSmooth.Core.Contracts; /// /// Builds with given states /// and errors. /// /// The data type. /// The states. /// The errors that may be returned. public class ContractBuilder where TState : struct, IComparable where TError : struct where TData : notnull { private readonly Contractor _contractor; private readonly TState _defaultState; private readonly Dictionary.StateActionAsync> _actions; private readonly Dictionary _timeouts; private TState? _fillAtState; private DefaultContract.FillDataAsync? _fillData; /// /// Initializes a new instance of the class. /// /// The contractor. /// The default state of the contract. public ContractBuilder(Contractor contractor, TState defaultState) { _contractor = contractor; _defaultState = defaultState; _actions = new Dictionary.StateActionAsync>(); _timeouts = new Dictionary(); } /// /// Sets timeout of the given state. /// /// The state to set timeout for. /// The timeout span. /// The state to go to after timeout. /// The updated builder. public ContractBuilder SetTimeout(TState state, TimeSpan timeout, TState nextState) { _timeouts[state] = (timeout, nextState); return this; } /// /// Set up an action filter that works for the given /// If the filter matches, moves to the given state from . /// /// The state to apply filter action to. /// The filter to match. /// The state to move to. /// The type of the filter data. /// The updated builder. public ContractBuilder SetMoveFilter (TState state, Func filter, TState nextState) { _actions[state] = (data, ct) => { if (data is TAny matched) { if (filter(matched)) { return Task.FromResult (Result<(TError?, TState?)>.FromSuccess((null, nextState))); } } return Task.FromResult (Result<(TError?, TState?)>.FromSuccess((null, null))); }; return this; } /// /// Set up an action filter that works for the given /// If the filter matches, moves to the given state from . /// /// The state to apply filter action to. /// The state to move to. /// The type of the filter data. /// The updated builder. public ContractBuilder SetMoveFilter(TState state, TState nextState) => SetMoveFilter(state, d => true, nextState); /// /// Sets that the given state will fill the data. /// /// The state that will fill the data. /// The function to fill the data. /// The type that is expected to fill the data. /// The updated builder. public ContractBuilder SetFillData(TState state, Func> fillData) { _fillAtState = state; _fillData = (data, ct) => { if (data is not TAny matched) { return Task.FromResult(Result.FromError(new GenericError("Fill data not matched."))); } return Task.FromResult(fillData(matched)); }; return this; } /// /// Sets that the given state should error on the given type. /// /// The state to accept error at. /// The error function. /// The type to match. /// The updated builder. public ContractBuilder SetError(TState state, Func errorFunction) { var last = _actions[state]; _actions[state] = async (data, ct) => { if (data is TAny matched) { var error = errorFunction(matched); if (error is not null) { return Result<(TError?, TState?)>.FromSuccess((error, null)); } } return await last(data, ct); }; return this; } /// /// Set up an action that works for the given /// If the given state is reached and data are updated, this function is called. /// /// The state to apply filter action to. /// The filter to filter the action. /// The state to move to. /// The type of the filter data. /// The updated builder. public ContractBuilder SetMoveAction (TState state, Func>> actionFilter, TState nextState) { _actions[state] = async (data, ct) => { if (data is TAny matched) { var filterResult = await actionFilter(matched); if (!filterResult.IsDefined(out var filter)) { return Result<(TError?, TState?)>.FromError(filterResult); } if (filter) { return Result<(TError?, TState?)>.FromSuccess((null, nextState)); } } return Result<(TError?, TState?)>.FromSuccess((null, null)); }; return this; } /// /// Set up an action that works for the given /// If the given state is reached and data are updated, this function is called. /// /// The state to apply filter action to. /// The filter to filter the action. /// The state to move to. /// The updated builder. public ContractBuilder SetMoveAction (TState state, Func>> actionFilter, TState nextState) { _actions[state] = async (data, ct) => { var filterResult = await actionFilter(data, ct); if (!filterResult.IsDefined(out var filter)) { return Result<(TError?, TState?)>.FromError(filterResult); } if (filter) { return Result<(TError?, TState?)>.FromSuccess((null, nextState)); } return Result<(TError?, TState?)>.FromSuccess((null, null)); }; return this; } /// /// Build the associate contract. /// /// The contract. /// Thrown in case FillAtState or FillData is null. public IContract Build() { if (_fillAtState is null) { throw new InvalidOperationException("FillAtState cannot be null."); } if (_fillData is null) { throw new InvalidOperationException("FillData cannot be null."); } return new DefaultContract ( _contractor, _defaultState, _fillAtState.Value, _fillData, _actions, _timeouts ); } }