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
22 changes: 19 additions & 3 deletions src/Trax.Effect/Services/ServiceTrain/ServiceTrain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -332,10 +332,26 @@ CancellationToken cancellationToken
}

/// <summary>
/// Abstract method that must be implemented by concrete train classes.
/// This method contains the core business logic.
/// The core implementation method that executes the train's logic.
/// Override this for full control over the railway pipeline (advanced).
/// If not overridden, the default implementation calls Junctions().
/// </summary>
protected abstract override Task<Either<Exception, TOut>> RunInternal(TIn input);
protected override Task<Either<Exception, TOut>> RunInternal(TIn input)
{
TrainMonad = new Monad<TIn, TOut>(this, ServiceProvider!, CancellationToken).Activate(
input
);

try
{
TOut result = Junctions();
return Task.FromResult<Either<Exception, TOut>>(result);
}
catch (Exception ex)
{
return Task.FromResult<Either<Exception, TOut>>(ex);
}
}

/// <summary>
/// Creates a composable Monad helper with ServiceProvider for junction DI.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
using FluentAssertions;
using LanguageExt;
using Microsoft.Extensions.DependencyInjection;
using Trax.Core.Exceptions;
using Trax.Core.Junction;
using Trax.Effect.Data.Services.DataContext;
using Trax.Effect.Enums;
using Trax.Effect.Extensions;
using Trax.Effect.Models.Metadata.DTOs;
using Trax.Effect.Services.ServiceTrain;
using Trax.Effect.Tests.Data.InMemory.Integration.Fixtures;
using Metadata = Trax.Effect.Models.Metadata.Metadata;

namespace Trax.Effect.Tests.Data.InMemory.Integration.IntegrationTests;

public class JunctionsApiTests : TestSetup
{
public override ServiceProvider ConfigureServices(IServiceCollection services) =>
services
.AddScopedTraxRoute<IJunctionsTrain, JunctionsTrain>()
.AddScopedTraxRoute<IJunctionsMultiTrain, JunctionsMultiTrain>()
.AddScopedTraxRoute<IJunctionsFailingTrain, JunctionsFailingTrain>()
.AddScopedTraxRoute<IRunInternalTrain, RunInternalTrain>()
.BuildServiceProvider();

#region Junctions API — happy path

[Test]
public async Task Junctions_SingleChain_ReturnsOutput()
{
var train = Scope.ServiceProvider.GetRequiredService<IJunctionsTrain>();

var result = await train.Run("hello");

result.Should().Be(5);
}

[Test]
public async Task Junctions_MultipleChains_ReturnsOutput()
{
var train = Scope.ServiceProvider.GetRequiredService<IJunctionsMultiTrain>();

var result = await train.Run("hello");

result.Should().Be("5");
}

[Test]
public async Task Junctions_MetadataTracked()
{
var train = Scope.ServiceProvider.GetRequiredService<IJunctionsTrain>();

await train.Run("hello");

var serviceTrain = (ServiceTrain<string, int>)train;
serviceTrain.Metadata.Should().NotBeNull();
serviceTrain.Metadata!.TrainState.Should().Be(TrainState.Completed);
}

#endregion

#region Junctions API — failure path

[Test]
public async Task Junctions_JunctionThrows_SetsFailedState()
{
var train = Scope.ServiceProvider.GetRequiredService<IJunctionsFailingTrain>();

var act = async () => await train.Run("hello");

await act.Should().ThrowAsync<Exception>();

var serviceTrain = (ServiceTrain<string, int>)train;
serviceTrain.Metadata.Should().NotBeNull();
serviceTrain.Metadata!.TrainState.Should().Be(TrainState.Failed);
}

#endregion

#region Backwards compatibility

[Test]
public async Task RunInternal_Override_StillWorks()
{
var train = Scope.ServiceProvider.GetRequiredService<IRunInternalTrain>();

var result = await train.Run("hello");

result.Should().Be(5);
}

[Test]
public async Task RunInternal_MetadataTracked()
{
var train = Scope.ServiceProvider.GetRequiredService<IRunInternalTrain>();

await train.Run("hello");

var serviceTrain = (ServiceTrain<string, int>)train;
serviceTrain.Metadata.Should().NotBeNull();
serviceTrain.Metadata!.TrainState.Should().Be(TrainState.Completed);
}

#endregion

#region Fakes

private class StringLengthJunction : Junction<string, int>
{
public override async Task<int> Run(string input) => input.Length;
}

private class IntToStringJunction : Junction<int, string>
{
public override async Task<string> Run(int input) => input.ToString();
}

private class ThrowingJunction : Junction<string, int>
{
public override Task<int> Run(string input) =>
throw new InvalidOperationException("junction failed");
}

private class JunctionsTrain : ServiceTrain<string, int>, IJunctionsTrain
{
protected override int Junctions() => Chain<StringLengthJunction>();
}

private interface IJunctionsTrain : IServiceTrain<string, int> { }

private class JunctionsMultiTrain : ServiceTrain<string, string>, IJunctionsMultiTrain
{
protected override string Junctions() =>
Chain<StringLengthJunction>().Chain<IntToStringJunction>();
}

private interface IJunctionsMultiTrain : IServiceTrain<string, string> { }

private class JunctionsFailingTrain : ServiceTrain<string, int>, IJunctionsFailingTrain
{
protected override int Junctions() => Chain<ThrowingJunction>();
}

private interface IJunctionsFailingTrain : IServiceTrain<string, int> { }

private class RunInternalTrain : ServiceTrain<string, int>, IRunInternalTrain
{
protected override async Task<Either<Exception, int>> RunInternal(string input) =>
Activate(input).Chain<StringLengthJunction>().Resolve();
}

private interface IRunInternalTrain : IServiceTrain<string, int> { }

#endregion
}
Loading