Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 81 additions & 20 deletions src/Trax.Core/Extensions/MonadExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,10 @@ this Monad<TInput, TReturn> monad
junctionType,
type =>
{
if (!type.IsClass)
if (JunctionConstructorProblem(type) is not null)
return null;

var constructors = type.GetConstructors();
if (constructors.Length != 1)
return null;

var parameterTypes = constructors[0]
.GetParameters()
Expand All @@ -56,21 +54,21 @@ this Monad<TInput, TReturn> monad

if (cached is null)
{
if (!junctionType.IsClass)
monad.Exception ??= new TrainException(
$"Junction ({junctionType}) must be a class."
);
else
monad.Exception ??= new TrainException(
$"Junction classes can only have a single constructor ({junctionType})."
);
monad.Exception ??= new TrainException(
JunctionConstructorProblem(junctionType, monad.Train.GetType())!
);
return null;
}

var (constructor, constructorArguments) = cached.Value;

// Extract the constructor parameters from Memory
var constructorParameters = monad.ExtractTypesFromMemory(constructorArguments);
// Extract the constructor parameters from Memory, naming the junction when one is missing:
// the type alone does not say which junction asked for it, or whether it was expected
// from an earlier junction or from the container.
var constructorParameters = monad.ExtractTypesFromMemory(
constructorArguments,
missing => MissingConstructorArgumentMessage(junctionType, missing, monad.Train)
);

if (monad.Exception is not null)
return null;
Expand Down Expand Up @@ -110,22 +108,26 @@ this Monad<TInput, TReturn> monad
/// </summary>
public static dynamic?[] ExtractTypesFromMemory<TInput, TReturn>(
this Monad<TInput, TReturn> monad,
IEnumerable<Type> types
IEnumerable<Type> types,
Func<Type, string>? missingMessage = null
)
{
var typeArray = types as Type[] ?? types.ToArray();
var result = new dynamic?[typeArray.Length];
for (var i = 0; i < typeArray.Length; i++)
result[i] = monad.ExtractTypeFromMemory(typeArray[i]);
result[i] = monad.ExtractTypeFromMemory(typeArray[i], missingMessage);
return result;
}

/// <summary>
/// Extracts a value of type T from Memory.
/// </summary>
public static T? ExtractTypeFromMemory<T, TInput, TReturn>(this Monad<TInput, TReturn> monad)
public static T? ExtractTypeFromMemory<T, TInput, TReturn>(
this Monad<TInput, TReturn> monad,
Func<Type, string>? missingMessage = null
)
{
var type = monad.ExtractTypeFromMemory(typeof(T));
var type = monad.ExtractTypeFromMemory(typeof(T), missingMessage);

return type is null ? default : (T)type;
}
Expand Down Expand Up @@ -176,11 +178,14 @@ is IServiceProvider serviceProvider
}

/// <summary>
/// Extracts a value from Memory by its type.
/// Extracts a value from Memory by its type, falling back to the container and then to the
/// logger factory. When nothing supplies it, the chain fails with
/// <paramref name="missingMessage"/>'s text, which should say who needed the value.
/// </summary>
public static dynamic? ExtractTypeFromMemory<TInput, TReturn>(
this Monad<TInput, TReturn> monad,
Type tIn
Type tIn,
Func<Type, string>? missingMessage = null
)
{
try
Expand All @@ -192,7 +197,9 @@ Type tIn
?? monad.ExtractLoggerFromLoggerFactory(tIn);

if (input is null)
throw new TrainException($"Could not find type: ({tIn}).");
throw new TrainException(
missingMessage?.Invoke(tIn) ?? MissingValueMessage(tIn, monad.Train)
);

return input;
}
Expand All @@ -203,6 +210,60 @@ Type tIn
}
}

/// <summary>
/// Why Trax cannot build <paramref name="junctionType"/> from its constructor, or null when
/// it can. Trax builds a junction through its one public constructor, so anything else fails
/// every run; the chain recorder asks the same question so the host refuses it at startup.
/// </summary>
internal static string? JunctionConstructorProblem(Type junctionType, Type? trainType = null)
{
var train = trainType is null ? "" : $" (train '{trainType.ReadableName()}')";

if (!junctionType.IsClass)
return $"Junction '{junctionType.ReadableName()}'{train} must be a class; Trax builds "
+ "a junction from its class. Chain the class that implements it, or use IChain "
+ "to resolve it by its interface.";

if (junctionType.IsAbstract)
return $"Junction '{junctionType.ReadableName()}'{train} is abstract, so Trax cannot "
+ "build it. Chain a concrete class.";

var count = junctionType.GetConstructors().Length;

if (count == 1)
return null;

return $"Junction '{junctionType.ReadableName()}'{train} has {count} public constructors; "
+ "Trax builds a junction through its single public constructor. Give it exactly one.";
}

internal static string MissingConstructorArgumentMessage<TInput, TReturn>(
Type junctionType,
Type missing,
Train.Train<TInput, TReturn> train
) =>
$"Junction '{junctionType.ReadableName()}' (train '{train.GetType().ReadableName()}') "
+ $"needs '{missing.ReadableName()}' as a constructor argument, but nothing earlier in "
+ "the chain produced one and it is not registered in the container. Register it or "
+ "chain a junction that outputs it first.";

internal static string MissingJunctionInputMessage<TInput, TReturn>(
Type junctionType,
Type missing,
Train.Train<TInput, TReturn> train
) =>
$"Junction '{junctionType.ReadableName()}' (train '{train.GetType().ReadableName()}') "
+ $"needs '{missing.ReadableName()}' as its input, but nothing earlier in the chain "
+ "produced one and it is not registered in the container. Chain a junction that "
+ "outputs it first, or register it.";

private static string MissingValueMessage<TInput, TReturn>(
Type missing,
Train.Train<TInput, TReturn> train
) =>
$"Train '{train.GetType().ReadableName()}' needs '{missing.ReadableName()}', but nothing "
+ "earlier in the chain produced one and it is not registered in the container.";

/// <summary>
/// Extracts a tuple from Memory.
/// </summary>
Expand Down
44 changes: 32 additions & 12 deletions src/Trax.Core/Monad/Monad.AddServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ public partial class Monad<TInput, TReturn>
/// object may be passed under several interfaces. A Moq mock is stored under the type it mocks.
/// </summary>
/// <remarks>
/// A null service throws immediately rather than failing the chain. While the chain is read at
/// startup, null, a struct or a class type argument is recorded as a refusal instead, so the
/// argument must already be assigned when <c>Junctions()</c> runs.
/// A null service fails the chain with a <see cref="Exceptions.TrainException"/> naming its
/// type argument and position, like every other invalid argument, and nothing from that call is
/// stored. While the chain is read at startup, null, a struct or a class type argument is
/// recorded as a refusal instead, so the argument must already be assigned when
/// <c>Junctions()</c> runs.
/// </remarks>
/// <param name="service">The service to store under <typeparamref name="T1"/>.</param>
public Monad<TInput, TReturn> AddServices<T1>(T1 service)
Expand Down Expand Up @@ -169,7 +171,7 @@ T7 service7
/// Internal method that adds services to the chain's memory.
/// </summary>
/// <remarks>
/// A null service is refused while a chain is recorded and throws when it runs. Either way
/// A null service is refused while a chain is recorded and fails the chain when it runs. Either way
/// the argument has to exist when <c>Junctions()</c> runs: a field assigned later, in a
/// lifecycle hook say, is still null when the chain is read at startup.
/// </remarks>
Expand All @@ -184,7 +186,7 @@ internal Monad<TInput, TReturn> AddServices(object?[] services, Type[] typeArray
var serviceType = typeArray[i];

if (services[i] is null)
Recorder.Refuse(NullServiceMessage(serviceType, whileRecording: true));
Recorder.Refuse(NullServiceMessage(i, typeArray, whileRecording: true));
else if (services[i]!.GetType().IsValueType)
// A struct passed as an interface is boxed, and the runtime refuses it.
Recorder.Refuse(
Expand All @@ -206,9 +208,17 @@ internal Monad<TInput, TReturn> AddServices(object?[] services, Type[] typeArray
return this;
}

// Checked before anything is stored, so a failed call leaves Memory as it found it.
for (var i = 0; i < typeArray.Length; i++)
{
if (services[i] is null)
throw new Exception(NullServiceMessage(typeArray[i], whileRecording: false));
{
Exception ??= new TrainException(
NullServiceMessage(i, typeArray, whileRecording: false)
);
return this;
}
}

// Each service goes under the type argument it was passed as, in the same position:
// one object may be passed under several interfaces, and each is a separate slot the
Expand Down Expand Up @@ -252,10 +262,20 @@ internal Monad<TInput, TReturn> AddServices(object?[] services, Type[] typeArray
return this;
}

private static string NullServiceMessage(Type serviceType, bool whileRecording) =>
whileRecording
? $"AddServices<{serviceType.Name}> received null while the chain was being recorded. "
+ "A service cannot be null, and the argument must be available when Junctions() "
+ "runs; assigning it later, in OnStarted say, is not supported."
: $"AddServices<{serviceType.Name}> received null. A service cannot be null.";
private static string NullServiceMessage(int position, Type[] typeArray, bool whileRecording)
{
var received =
$"AddServices<{typeArray[position].Name}> received null"
+ (
typeArray.Length > 1
? $" for the service at position {position + 1} of {typeArray.Length}"
: ""
);

return whileRecording
? $"{received} while the chain was being recorded. A service cannot be null, and the "
+ "argument must be available when Junctions() runs; assigning it later, in "
+ "OnStarted say, is not supported."
: $"{received}. A service cannot be null.";
}
}
26 changes: 23 additions & 3 deletions src/Trax.Core/Monad/Monad.Chain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ TJunction junction
if (Exception is not null)
return this;

var input = this.ExtractTypeFromMemory<TIn, TInput, TReturn>();
var input = this.ExtractTypeFromMemory<TIn, TInput, TReturn>(missing =>
MonadExtensions.MissingJunctionInputMessage(typeof(TJunction), missing, Train)
);

if (input is null)
return this;
Expand Down Expand Up @@ -114,7 +116,12 @@ private Task<Monad<TInput, TReturn>> IChainAsync<TJunction>()
if (Exception is not null)
return Task.FromResult(this);

var junctionService = this.ExtractTypeFromMemory<TJunction, TInput, TReturn>();
var junctionService = this.ExtractTypeFromMemory<TJunction, TInput, TReturn>(missing =>
$"IChain<{missing.ReadableName()}> (train '{Train.GetType().ReadableName()}') found "
+ $"no junction implementing '{missing.ReadableName()}': none was passed to "
+ "AddServices and it is not registered in the container. Pass one with AddServices "
+ "or register it."
);

if (junctionService is null)
return Task.FromResult(this);
Expand All @@ -128,7 +135,7 @@ private Task<Monad<TInput, TReturn>> IChainAsync<TJunction>()
public MonadTask<TInput, TReturn> Chain<TJunction>()
where TJunction : class =>
Recorder is not null
? RecordStep<TJunction>(ChainStepKind.Chain)
? RecordBuiltStep<TJunction>(ChainStepKind.Chain)
: new(ChainAsync<TJunction>());

private Task<Monad<TInput, TReturn>> ChainAsync<TJunction>()
Expand Down Expand Up @@ -245,6 +252,19 @@ private MonadTask<TInput, TReturn> RecordStep<TJunction>(ChainStepKind kind)
return new MonadTask<TInput, TReturn>(Task.FromResult(this));
}

/// <summary>
/// Records a step whose junction Trax builds from its constructor, refusing a junction it
/// cannot build. Only the constructor count is decided here: the arguments come from Memory
/// and the container as the chain runs.
/// </summary>
private MonadTask<TInput, TReturn> RecordBuiltStep<TJunction>(ChainStepKind kind)
{
if (MonadExtensions.JunctionConstructorProblem(typeof(TJunction)) is { } problem)
Recorder!.Refuse($"{kind}<{typeof(TJunction).ReadableName()}>: {problem}");

return RecordStep<TJunction>(kind);
}

/// <summary>
/// Records a step whose input and output types the caller stated explicitly, rather than
/// ones inferred from the junction's interface.
Expand Down
8 changes: 7 additions & 1 deletion src/Trax.Core/Monad/Monad.Extract.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Reflection;
using Trax.Core.Exceptions;
using Trax.Core.Utils;

namespace Trax.Core.Monad;

Expand All @@ -19,7 +20,12 @@ public Monad<TInput, TReturn> Extract<TIn, TOut>()
// Try to get the source object from Memory
if (!Memory.TryGetValue(typeof(TIn), out var stored) || stored is not TIn typeFromMemory)
{
Exception ??= new TrainException($"Could not find type: ({typeof(TIn)}).");
Exception ??= new TrainException(
$"Extract<{typeof(TIn).ReadableName()}, {typeof(TOut).ReadableName()}> (train "
+ $"'{Train.GetType().ReadableName()}') found no '{typeof(TIn).ReadableName()}' "
+ $"in Memory. Chain a junction that outputs '{typeof(TIn).ReadableName()}' "
+ "before the Extract."
);

return this;
}
Expand Down
14 changes: 12 additions & 2 deletions src/Trax.Core/Monad/Monad.Resolve.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using LanguageExt;
using Trax.Core.Exceptions;
using Trax.Core.Extensions;
using Trax.Core.Utils;

namespace Trax.Core.Monad;

Expand Down Expand Up @@ -55,10 +56,19 @@ public Either<Exception, TReturn> Resolve()
if (ShortCircuitValueSet)
return ShortCircuitValue;

var result = this.ExtractTypeFromMemory<TReturn, TInput, TReturn>();
var result = this.ExtractTypeFromMemory<TReturn, TInput, TReturn>(missing =>
$"Train '{Train.GetType().ReadableName()}' has no '{missing.ReadableName()}' to "
+ "resolve: nothing in the chain produced one and it is not registered in the "
+ $"container. Chain a junction that outputs '{missing.ReadableName()}' before "
+ "Resolve()."
);

// Checked before the value: a missing value type comes back as its default, not null.
if (Exception is not null)
return Exception;

if (result is null)
return new TrainException($"Could not find type: ({typeof(TReturn)}).");
return new TrainException($"'{typeof(TReturn).ReadableName()}' is null.");

return (TReturn)result;
}
Expand Down
12 changes: 7 additions & 5 deletions src/Trax.Core/Monad/Monad.ShortCircuit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Either<Exception, TOut> Result
public MonadTask<TInput, TReturn> ShortCircuit<TJunction>()
where TJunction : class =>
Recorder is not null
? RecordStep<TJunction>(ChainStepKind.ShortCircuit)
? RecordBuiltStep<TJunction>(ChainStepKind.ShortCircuit)
: new(ShortCircuitAsync<TJunction>());

private Task<Monad<TInput, TReturn>> ShortCircuitAsync<TJunction>()
Expand Down Expand Up @@ -101,13 +101,15 @@ TJunction junctionInstance
TReturn
>(this, tIn, tOut, 2);

var input = MonadExtensions.ExtractTypeFromMemory(this, tIn);
var input = MonadExtensions.ExtractTypeFromMemory(
this,
tIn,
missing =>
MonadExtensions.MissingJunctionInputMessage(typeof(TJunction), missing, Train)
);

if (input is null)
{
Exception ??= new TrainException($"Could not find ({tIn}) in Memory.");
return this;
}

// Invoke the generic ShortCircuitJunction — returns Task<(Monad, Either<Exception, TOut>)>
var taskObj = chainMethod.Invoke(this, [junctionInstance, input])!;
Expand Down
Loading
Loading