Compare commits

...

13 commits

Author SHA1 Message Date
D. Moonfire 5a993e85cb fix(html): added validatiors to Autofac module
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-01-21 21:49:41 -06:00
D. Moonfire aac4b4373d feat(html): added identify operations
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-01-21 21:14:36 -06:00
D. Moonfire 22ddae11f8 fix(temporal): added CurrentInstant and CurrentDateTime to TimeService
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-01-21 17:48:39 -06:00
D. Moonfire b32ca7582c fix(schedules): protect against null schedules 2023-01-21 12:13:09 -06:00
D. Moonfire 07eb12414a refactor(temporal)!: renamed Timekeeper to TimeService
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-01-21 01:52:52 -06:00
D. Moonfire 82e1bc3c28 feat(schedules)!: reworked schedule names and added a new style
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-01-20 23:28:41 -06:00
D. Moonfire 070cf2bfb8 fix(scheduler): added better error messages and regular expression for numerical paths
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-01-18 23:51:37 -06:00
D. Moonfire 185980b5c4 fix(schedules): corrected serialization of pathRegex
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-01-18 22:32:54 -06:00
D. Moonfire 2e93ebdb7c ci: add manual to the release process
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-01-18 15:52:39 -06:00
D. Moonfire bca501d4e5 ci: updating to not create 1.0.0 packages
Some checks failed
ci/woodpecker/manual/woodpecker Pipeline failed
2023-01-18 15:49:51 -06:00
D. Moonfire 189273692c feat(schedules): switched how periods were parsed to allow for "2 weeks"
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-01-18 14:29:11 -06:00
D. Moonfire 2892ec3445 feat!: added cancellation token support to pipelines and operations
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-01-17 19:24:09 -06:00
D. Moonfire 08aafb144c feat(schedules): changed ApplySchedules.Schedules to a getter GetSchedules 2023-01-17 18:42:31 -06:00
87 changed files with 1439 additions and 511 deletions

View file

@ -10,20 +10,14 @@ pipeline:
commands:
- nix develop --command scripts/build.sh
when:
event: [push, pull_request, tag]
tag: v*
event: [push, pull_request, manual]
test:
image: registry.gitlab.com/dmoonfire/nix-flake-docker:latest
commands:
- nix develop --command scripts/test.sh
when:
event: [push, pull_request]
#paths:
# - ./**/*test-result.xml
# - ./coverage/Cobertura.xml
# - ./coverage/Summary.*
# - ./**/*.nupkg
event: [push, pull_request, manual]
release-main:
image: registry.gitlab.com/dmoonfire/nix-flake-docker:latest
@ -32,5 +26,5 @@ pipeline:
secrets:
- gitea_token
when:
event: push
event: [push, manual]
branch: main

View file

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using MfGames.Gallium;
using MfGames.Nitride;
@ -43,7 +44,9 @@ public class CopyFilesPipeline : PipelineBase
}
/// <inheritdoc />
public override IAsyncEnumerable<Entity> RunAsync(IEnumerable<Entity> _)
public override IAsyncEnumerable<Entity> RunAsync(
IEnumerable<Entity> _,
CancellationToken cancellationToken = default)
{
// We don't care about the incoming entities which means we can
// ignore them and use the entities from the ReadFiles operation
@ -73,8 +76,9 @@ public class CopyFilesPipeline : PipelineBase
// read. Coming out of this, we will have one entity that fulfills:
//
// entity.Get<UPath> == "/output/a.txt"
entities = entities.Run(this.removePathPrefix)
.Run(this.addPathPrefix);
entities = entities
.Run(this.removePathPrefix, cancellationToken)
.Run(this.addPathPrefix, cancellationToken);
// Then we write out the files to the output. First we make sure we
// clear out the output. This operation performs an action when it
@ -95,8 +99,9 @@ public class CopyFilesPipeline : PipelineBase
// The third way is to use an extension on entities which lets us
// chain calls, ala Gulp's pipelines. The below code does this along
// with writing the files to the output.
entities = entities.Run(this.clearDirectory)
.Run(this.writeFiles);
entities = entities
.Run(this.clearDirectory, cancellationToken)
.Run(this.writeFiles, cancellationToken);
// If we are chaining this pipeline into another, we return the
// entities. Otherwise, we can just return an empty list. The

View file

@ -27,9 +27,10 @@ public class CopyFilesTest : NitrideTestBase
public async Task Run()
{
// Figure out the paths for this test.
DirectoryInfo rootDir =
typeof(CopyFilesProgram).GetDirectory()!.FindGitRoot()!
.GetDirectory("examples/CopyFiles");
DirectoryInfo rootDir = typeof(CopyFilesProgram)
.GetDirectory()
!.FindGitRoot()!
.GetDirectory("examples/CopyFiles");
DirectoryInfo outputDir = rootDir.GetDirectory("output");
FileInfo projectFile = rootDir.GetFile("CopyFiles.csproj");

View file

@ -13,6 +13,7 @@ fi
# Clean up everything from the previous runs.
echo "$(basename $0): cleaning project"
dotnet clean
rm -f src/*/bin/Debug/*.nupkg
# Version the file based on the Git repository.
echo "$(basename $0): setting project version"
@ -35,8 +36,8 @@ dotnet pack -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg || exit 1
echo "$(basename $0): publishing NuGet package"
dotnet nuget remove source mfgames.com >& /dev/null
dotnet nuget add source --name mfgames.com --username dmoonfire --password $GITEA_TOKEN https://src.mfgames.com/api/packages/mfgames-cil/nuget/index.json --store-password-in-clear-text || exit 1
dotnet nuget push --skip-duplicate --source mfgames.com src/*/bin/Debug/*.nupkg || exit 1
dotnet nuget add source --name mfgames.com https://src.mfgames.com/api/packages/mfgames-cil/nuget/index.json || exit 1
dotnet nuget push --api-key $GITEA_TOKEN --skip-duplicate --source mfgames.com src/*/bin/Debug/*.nupkg || exit 1
# Tag and push, but only if we don't have a tag.
if ! git tag | grep $SEMVER >& /dev/null

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using FluentValidation;
@ -27,13 +28,13 @@ namespace MfGames.Nitride.Calendar;
[WithProperties]
public partial class CreateCalender : OperationBase
{
private readonly Timekeeper clock;
private readonly TimeService clock;
private readonly IValidator<CreateCalender> validator;
public CreateCalender(
IValidator<CreateCalender> validator,
Timekeeper clock)
TimeService clock)
{
this.validator = validator;
this.clock = clock;
@ -57,7 +58,9 @@ public partial class CreateCalender : OperationBase
public UPath? Path { get; set; }
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);

View file

@ -10,16 +10,16 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MfGames.Nitride.IO\MfGames.Nitride.IO.csproj" />
<ProjectReference Include="..\MfGames.Nitride.Temporal\MfGames.Nitride.Temporal.csproj" />
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj" />
<ProjectReference Include="..\MfGames.Nitride.IO\MfGames.Nitride.IO.csproj"/>
<ProjectReference Include="..\MfGames.Nitride.Temporal\MfGames.Nitride.Temporal.csproj"/>
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="MfGames.Gallium" Version="0.4.0" />
<PackageReference Include="Ical.Net" Version="4.2.0" />
<PackageReference Include="NodaTime" Version="3.1.2" />
<PackageReference Include="Zio" Version="0.15.0" />
<PackageReference Include="MfGames.Gallium" Version="0.4.0"/>
<PackageReference Include="Ical.Net" Version="4.2.0"/>
<PackageReference Include="NodaTime" Version="3.1.2"/>
<PackageReference Include="Zio" Version="0.15.0"/>
</ItemGroup>
<!-- Include the source generator -->

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using FluentValidation;
@ -93,7 +94,9 @@ public partial class CreateAtomFeed : OperationBase
public Func<Entity, Uri>? GetUrl { get; set; }
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);

View file

@ -10,15 +10,15 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MfGames.Nitride.IO\MfGames.Nitride.IO.csproj" />
<ProjectReference Include="..\MfGames.Nitride.Temporal\MfGames.Nitride.Temporal.csproj" />
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj" />
<ProjectReference Include="..\MfGames.Nitride.IO\MfGames.Nitride.IO.csproj"/>
<ProjectReference Include="..\MfGames.Nitride.Temporal\MfGames.Nitride.Temporal.csproj"/>
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="MfGames.Gallium" Version="0.4.0" />
<PackageReference Include="NodaTime" Version="3.1.2" />
<PackageReference Include="Zio" Version="0.15.0" />
<PackageReference Include="MfGames.Gallium" Version="0.4.0"/>
<PackageReference Include="NodaTime" Version="3.1.2"/>
<PackageReference Include="Zio" Version="0.15.0"/>
</ItemGroup>
<!-- Include the source generator -->

View file

@ -10,10 +10,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MfGames.Gallium" Version="0.4.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.3.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.3.1" />
<PackageReference Include="MfGames.Gallium" Version="0.4.0"/>
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.3.1"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.3.1"/>
</ItemGroup>
</Project>

View file

@ -42,46 +42,47 @@ public class SingletonComponentSourceGenerator
// Create the namespace.
SyntaxToken cls = cds.Identifier;
buffer.AppendLine(string.Join(
"\n",
$"using MfGames.Gallium;",
$"",
$"namespace {unit.Namespace}",
$"{{",
$" public partial class {cls}",
$" {{",
$" static {cls}()",
$" {{",
$" Instance = new {cls}();",
$" }}",
$"",
$" private {cls}()",
$" {{",
$" }}",
$"",
$" public static {cls} Instance {{ get; }}",
$" }}",
$"",
$" public static class {cls}Extensions",
$" {{",
$" public static bool Has{cls}(this Entity entity)",
$" {{",
$" return entity.Has<{cls}>();",
$" }}",
$"",
$" public static Entity Remove{cls}(this Entity entity)",
$" {{",
$" return entity.Remove<{cls}>();",
$" }}",
$"",
$" public static Entity Set{cls}(this Entity entity)",
$" {{",
$" return entity.Set({cls}.Instance);",
$" }}",
$" }}",
$"}}",
""));
buffer.AppendLine(
string.Join(
"\n",
$"using MfGames.Gallium;",
$"",
$"namespace {unit.Namespace}",
$"{{",
$" public partial class {cls}",
$" {{",
$" static {cls}()",
$" {{",
$" Instance = new {cls}();",
$" }}",
$"",
$" private {cls}()",
$" {{",
$" }}",
$"",
$" public static {cls} Instance {{ get; }}",
$" }}",
$"",
$" public static class {cls}Extensions",
$" {{",
$" public static bool Has{cls}(this Entity entity)",
$" {{",
$" return entity.Has<{cls}>();",
$" }}",
$"",
$" public static Entity Remove{cls}(this Entity entity)",
$" {{",
$" return entity.Remove<{cls}>();",
$" }}",
$"",
$" public static Entity Set{cls}(this Entity entity)",
$" {{",
$" return entity.Set({cls}.Instance);",
$" }}",
$" }}",
$"}}",
""));
// Create the source text and write out the file.
var sourceText = SourceText.From(buffer.ToString(), Encoding.UTF8);

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using FluentValidation;
@ -46,7 +47,9 @@ public partial class ApplyStyleTemplate : OperationBase
public IHandlebars? Handlebars { get; set; }
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
// Make sure we have sane data.
this.validator.ValidateAndThrow(this);

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using FluentValidation;
@ -27,7 +28,9 @@ public partial class IdentifyHandlebarsFromComponent : IOperation
public Func<Entity, bool> HasHandlebarsTest { get; set; } = null!;
/// <inheritdoc />
public IEnumerable<Entity> Run(IEnumerable<Entity> input)
public IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Threading;
using MfGames.Gallium;
using MfGames.Nitride.Contents;
@ -12,7 +13,9 @@ namespace MfGames.Nitride.Handlebars;
public class IdentifyHandlebarsFromContent : IOperation
{
/// <inheritdoc />
public IEnumerable<Entity> Run(IEnumerable<Entity> input)
public IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
return input.SelectEntity<ITextContent>(this.ScanContent);
}

View file

@ -10,15 +10,15 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="6.4.0" />
<PackageReference Include="MfGames.Gallium" Version="0.4.0" />
<PackageReference Include="Handlebars.Net" Version="2.1.2" />
<PackageReference Include="NodaTime.Testing" Version="3.1.2" />
<PackageReference Include="Open.Threading" Version="2.2.1" />
<PackageReference Include="Autofac" Version="6.4.0"/>
<PackageReference Include="MfGames.Gallium" Version="0.4.0"/>
<PackageReference Include="Handlebars.Net" Version="2.1.2"/>
<PackageReference Include="NodaTime.Testing" Version="3.1.2"/>
<PackageReference Include="Open.Threading" Version="2.2.1"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj" />
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj"/>
</ItemGroup>
<!-- Include the source generator -->

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using FluentValidation;
@ -39,7 +40,9 @@ public partial class RenderContentTemplate : OperationBase
public Func<Entity, object>? CreateModelCallback { get; set; }
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);

View file

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Net;
using System.Threading;
using MfGames.Gallium;
using MfGames.Nitride.Contents;
@ -13,7 +14,9 @@ namespace MfGames.Nitride.Html;
public class ConvertHtmlEntitiesToUnicode : OperationBase
{
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
return input.SelectEntity<ITextContent>(this.ResolveHtmlEntities);
}

View file

@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Threading;
using FluentValidation;
using MfGames.Gallium;
using MfGames.Nitride.Contents;
using MfGames.Nitride.Generators;
using Zio;
namespace MfGames.Nitride.Html;
/// <summary>
/// An operation that identifies Markdown files by their common extensions
/// and converts them to text input while also adding the IsMarkdown
/// component to identify them.
/// </summary>
[WithProperties]
public partial class IdentifyHtml : IOperation
{
private readonly IValidator<IdentifyHtml> validator;
public IdentifyHtml(IValidator<IdentifyHtml> validator)
{
this.validator = validator;
}
public Func<Entity, UPath, bool> IsHtmlTest { get; set; } = null!;
/// <inheritdoc />
public IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);
return input.SelectEntity<UPath, ITextContent>(this.MarkTextEntities)
.SelectEntity<UPath, IBinaryContent>(this.MarkBinaryEntities);
}
private Entity MarkBinaryEntities(
Entity entity,
UPath path,
IBinaryContent binary)
{
// If we aren't a Markdown file, then there is nothing we can do about that.
if (!this.IsHtmlTest(entity, path))
{
return entity;
}
// Convert the file as a binary.
if (binary is ITextContentConvertable textConvertable)
{
entity = entity.SetTextContent(textConvertable.ToTextContent())
.SetIsHtml();
}
else
{
throw new InvalidOperationException(
"Cannot convert a binary content to a text without ITextContentConvertable.");
}
return entity;
}
private Entity MarkTextEntities(
Entity entity,
UPath path,
ITextContent _)
{
return this.IsHtmlTest(entity, path)
? entity.SetIsHtml()
: entity;
}
}

View file

@ -0,0 +1,29 @@
using FluentValidation;
using MfGames.Gallium;
using Zio;
namespace MfGames.Nitride.Html;
public class IdentifyHtmlFromPath : IdentifyHtml
{
public IdentifyHtmlFromPath(IValidator<IdentifyHtml> validator)
: base(validator)
{
this.IsHtmlTest = DefaultIsHtml;
}
private static bool DefaultIsHtml(
Entity entity,
UPath path)
{
return (path.GetExtensionWithDot() ?? string.Empty).ToLowerInvariant()
switch
{
".htm" => true,
".html" => true,
_ => false,
};
}
}

View file

@ -8,5 +8,6 @@ public class NitrideHtmlModule : Module
protected override void Load(ContainerBuilder builder)
{
builder.RegisterOperators(this);
builder.RegisterValidators(this);
}
}

View file

@ -0,0 +1,12 @@
using FluentValidation;
namespace MfGames.Nitride.Html.Validators;
public class IdentifyHtmlValidator : AbstractValidator<IdentifyHtml>
{
public IdentifyHtmlValidator()
{
this.RuleFor(x => x.IsHtmlTest)
.NotNull();
}
}

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using DotNet.Globbing;
@ -43,9 +44,10 @@ public partial class ReadFiles : FileSystemOperationBase
/// minimatch pattern (as defined by DotNet.Blob).
/// </summary>
/// <returns>A populated collection of entities.</returns>
public IEnumerable<Entity> Run()
public IEnumerable<Entity> Run(
CancellationToken cancellationToken = default)
{
return this.Run(Array.Empty<Entity>());
return this.Run(Array.Empty<Entity>(), cancellationToken);
}
/// <summary>
@ -53,7 +55,9 @@ public partial class ReadFiles : FileSystemOperationBase
/// minimatch pattern (as defined by DotNet.Blob).
/// </summary>
/// <returns>A populated collection of entities.</returns>
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using FluentValidation;
@ -63,8 +64,11 @@ public partial class WriteFiles : FileSystemOperationBase, IOperation
/// a path and a registered writer will be written.
/// </summary>
/// <param name="entities">The entities to parse.</param>
/// <param name="cancellationToken"></param>
/// <returns>The same list of entities without changes.</returns>
public override IEnumerable<Entity> Run(IEnumerable<Entity> entities)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> entities,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using FluentValidation;
@ -40,13 +41,16 @@ public partial class ClearDirectory : FileSystemOperationBase, IOperation
/// </summary>
public UPath? Path { get; set; }
public IEnumerable<Entity> Run()
public IEnumerable<Entity> Run(
CancellationToken cancellationToken = default)
{
return this.Run(new List<Entity>());
return this.Run(new List<Entity>(), cancellationToken);
}
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Threading;
using MfGames.Gallium;
@ -16,5 +17,7 @@ public abstract class FileSystemOperationBase : IOperation
public IFileSystem FileSystem { get; set; }
/// <inheritdoc />
public abstract IEnumerable<Entity> Run(IEnumerable<Entity> input);
public abstract IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default);
}

View file

@ -8,17 +8,17 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="6.4.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
<PackageReference Include="FluentValidation" Version="11.2.1" />
<PackageReference Include="MfGames.Gallium" Version="0.4.0" />
<PackageReference Include="MAB.DotIgnore" Version="3.0.2" />
<PackageReference Include="Serilog" Version="2.11.0" />
<PackageReference Include="Zio" Version="0.15.0" />
<PackageReference Include="Autofac" Version="6.4.0"/>
<PackageReference Include="DotNet.Glob" Version="3.1.3"/>
<PackageReference Include="FluentValidation" Version="11.2.1"/>
<PackageReference Include="MfGames.Gallium" Version="0.4.0"/>
<PackageReference Include="MAB.DotIgnore" Version="3.0.2"/>
<PackageReference Include="Serilog" Version="2.11.0"/>
<PackageReference Include="Zio" Version="0.15.0"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj" />
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj"/>
</ItemGroup>
<!-- Include the source generator -->

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Threading;
using FluentValidation;
@ -29,7 +30,9 @@ public partial class AddPathPrefix : OperationBase
/// </summary>
public UPath? PathPrefix { get; set; }
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Threading;
using FluentValidation;
@ -32,7 +33,9 @@ public partial class ChangePathExtension : IOperation
/// </summary>
public string? Extension { get; set; }
public IEnumerable<Entity> Run(IEnumerable<Entity> input)
public IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);

View file

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using FluentValidation;
@ -29,7 +30,9 @@ public partial class LinkDirectChildren : CreateOrUpdateIndex
}
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
if (this.Scanner != null!)
{

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using FluentValidation;
@ -53,7 +54,9 @@ public partial class MoveToIndexPath : OperationBase
};
}
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Threading;
using FluentValidation;
@ -32,7 +33,9 @@ public partial class RemovePathPrefix : IOperation
/// </summary>
public UPath PathPrefix { get; set; }
public IEnumerable<Entity> Run(IEnumerable<Entity> input)
public IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using FluentValidation;
@ -36,8 +37,11 @@ public partial class ReplacePath : IOperation
/// will be updated, the others will be passed on as-is.
/// </summary>
/// <param name="input">The list of input entities.</param>
/// <param name="cancellationToken"></param>
/// <returns>The output entities.</returns>
public IEnumerable<Entity> Run(IEnumerable<Entity> input)
public IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);

View file

@ -10,12 +10,12 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj" />
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="MfGames.Gallium" Version="0.4.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="MfGames.Gallium" Version="0.4.0"/>
<PackageReference Include="Newtonsoft.Json" Version="13.0.2"/>
</ItemGroup>
<!-- Include the source generator -->

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using FluentValidation;
@ -28,7 +29,9 @@ public abstract partial class ConvertMarkdownToBase : IOperation
public Action<MarkdownPipelineBuilder>? ConfigureMarkdown { get; set; }
/// <inheritdoc />
public IEnumerable<Entity> Run(IEnumerable<Entity> input)
public IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
// Validate the inputs.
this.validator.ValidateAndThrow(this);

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using FluentValidation;
@ -29,7 +30,9 @@ public partial class IdentifyMarkdown : IOperation
public Func<Entity, UPath, bool> IsMarkdownTest { get; set; } = null!;
/// <inheritdoc />
public IEnumerable<Entity> Run(IEnumerable<Entity> input)
public IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);
@ -68,16 +71,8 @@ public partial class IdentifyMarkdown : IOperation
UPath path,
ITextContent _)
{
// If we aren't a Markdown file, then there is nothing
// we can do about that.
if (!this.IsMarkdownTest(entity, path))
{
return entity;
}
// We are already text, so just mark it as Markdown.
entity = entity.Set(IsMarkdown.Instance);
return entity;
return this.IsMarkdownTest(entity, path)
? entity.SetIsMarkdown()
: entity;
}
}

View file

@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using MfGames.Gallium;
using MfGames.Nitride.Contents;
@ -23,7 +24,9 @@ public class MakeSingleLinkListItems : IOperation
}
/// <inheritdoc />
public IEnumerable<Entity> Run(IEnumerable<Entity> input)
public IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
return input
.SelectManyEntity<IsMarkdown>(

View file

@ -1,6 +1,6 @@
using FluentValidation;
namespace MfGames.Nitride.Markdown;
namespace MfGames.Nitride.Markdown.Validators;
public class ConvertMarkdownToBaseValidator
: AbstractValidator<ConvertMarkdownToBase>

View file

@ -1,6 +1,6 @@
using FluentValidation;
namespace MfGames.Nitride.Markdown;
namespace MfGames.Nitride.Markdown.Validators;
public class IdentifyMarkdownValidator : AbstractValidator<IdentifyMarkdown>
{

View file

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using FluentValidation;
@ -8,24 +10,6 @@ using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Temporal.Schedules;
public enum SchedulePeriod
{
/// <summary>
/// Indicates that all the matching schedule periods are instant (zero time).
/// </summary>
Instant,
/// <summary>
/// Indicates that the entities will be scheduled a day apart.
/// </summary>
Day,
/// <summary>
/// Indicates that the entities will be scheduled seven days apart.
/// </summary>
Week,
}
/// <summary>
/// Applies schedules against the list of entities.
/// </summary>
@ -36,48 +20,75 @@ public partial class ApplySchedules : OperationBase
public ApplySchedules(
IValidator<ApplySchedules> validator,
Timekeeper timekeeper)
TimeService timeService)
{
this.Timekeeper = timekeeper;
this.TimeService = timeService;
this.validator = validator;
this.Schedules = new List<ISchedule>();
}
/// <summary>
/// Gets or sets the ordered list of schedules to apply to the entities.
/// Gets or sets the callback to get the schedules for the entity. This is
/// used to allow for per-entity schedules or generic schedules across
/// the entire system. If this returns null, then no schedule will be
/// applied.
/// </summary>
public IList<ISchedule> Schedules { get; set; }
public Func<Entity, IList<ISchedule>?>? GetSchedules { get; set; }
/// <summary>
/// Gets or sets the timekeeper associated with this operation.
/// Gets or sets the time service associated with this operation.
/// </summary>
public Timekeeper Timekeeper { get; set; }
public TimeService TimeService { get; set; }
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);
return input.Select(this.Apply);
}
public ApplySchedules WithSchedules<TItem>(IEnumerable<TItem> items)
where TItem : ISchedule
/// <summary>
/// Adds a single schedule into the apply schedules and returns the ApplySchedule
/// class.
/// </summary>
public ApplySchedules WithGetSchedules(Func<Entity, ISchedule> schedule)
{
this.Schedules = items.OfType<ISchedule>().ToList();
return this.WithGetSchedules(entity => new[] { schedule(entity) });
}
public ApplySchedules WithGetSchedules<TType>(
Func<Entity, IEnumerable<TType>?> value)
where TType : ISchedule
{
this.GetSchedules = entity => value?
.Invoke(entity)
?.Cast<ISchedule>()
.ToList();
return this;
}
private Entity Apply(Entity entity)
{
foreach (ISchedule schedule in this.Schedules)
// Get the schedule for this entity.
IList<ISchedule>? schedules = this.GetSchedules?.Invoke(entity);
if (schedules == null || schedules.Count == 0)
{
if (!schedule.CanApply(entity))
return entity;
}
// Otherwise, apply the schedules to this entity.
foreach (ISchedule schedule in schedules)
{
if (schedule == null! || !schedule.CanApply(entity))
{
continue;
}
entity = schedule.Apply(entity, this.Timekeeper);
entity = schedule.Apply(entity, this.TimeService);
}
return entity;

View file

@ -12,14 +12,14 @@ public interface ISchedule
/// Applies the schedule changes to the entity and returns the results.
/// </summary>
/// <param name="entity">The entity to update.</param>
/// <param name="timekeeper">The timekeeper for apply.</param>
/// <param name="timeService">The service to use for time.</param>
/// <returns>
/// The modified entity, if the changes can be applied, otherwise the same
/// entity.
/// </returns>
Entity Apply(
Entity entity,
Timekeeper timekeeper);
TimeService timeService);
/// <summary>
/// Determines if a schedule applies to a given entity.

View file

@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Linq;
using MfGames.Gallium;
namespace MfGames.Nitride.Temporal.Schedules;
/// <summary>
/// An indexed-based schedule that uses the regular expression to get a
/// numerical value and then uses that to determine the schedule.
/// </summary>
public partial class IndexedPathRegexSchedule<TSchedule> : PathRegexScheduleBase
where TSchedule : IndexedSchedule
{
public IndexedPathRegexSchedule()
{
this.Indexes = new Dictionary<int, TSchedule>();
}
/// <summary>
/// Gets or sets the dictionary that indexes the various numerical indexes
/// to provide the scheduling.
/// </summary>
public Dictionary<int, TSchedule> Indexes { get; set; }
public IndexedPathRegexSchedule<TSchedule> WithIndexes(
Dictionary<int, TSchedule> value)
{
this.Indexes = value;
return this;
}
/// <inheritdoc />
protected override Entity Apply(
Entity entity,
int number,
TimeService timeService)
{
// Figure out the entry in the index.
var applicableKeys = this.Indexes.Keys
.Where(a => a <= number)
.ToList();
if (applicableKeys.Count == 0)
{
return entity;
}
int startIndex = applicableKeys.Max(a => a);
TSchedule schedule = this.Indexes[startIndex];
// Pass everything into the schedule to perform the applying.
int startOffset = number - startIndex;
return schedule.Apply(entity, startOffset, timeService);
}
}

View file

@ -0,0 +1,79 @@
using System;
using MfGames.Gallium;
using NodaTime;
namespace MfGames.Nitride.Temporal.Schedules;
/// <summary>
/// Describes a simplified schedule object that contains a start and period
/// for the schedule and is designed to work with the numbers provided by
/// the <see cref="IndexedPathRegexSchedule" />.
/// </summary>
public partial class IndexedSchedule
{
/// <summary>
/// Gets or sets the period between each matching entity. The period is
/// amount of time between each item based on the number. So the first will
/// be on the ScheduleDate, the second SchedulePeriod later, the third
/// SchedulePeriod after the second, etc. More precisely, the date for
/// any item TimeSpan * ((int)CaptureGroup + (int)CaptureOffset).
/// </summary>
public string? SchedulePeriod { get; set; }
/// <summary>
/// Gets or sets the schedule period as a TimeSpan object. This is converted
/// from SchedulePeriod using https://github.com/pengowray/TimeSpanParser.
/// If the value is null or blank or "immediate", then this will be instant
/// (TimeSpan.Zero).
/// </summary>
public virtual TimeSpan? SchedulePeriodTimeSpan
{
get => TimeSpanHelper.Parse(this.SchedulePeriod);
set => this.SchedulePeriod = value?.ToString();
}
/// <summary>
/// Gets or sets when the first item is scheduled.
/// </summary>
public DateTime? ScheduleStart { get; set; }
public virtual Entity Apply(
Entity entity,
int number,
TimeService timeService)
{
// If we have a "never", then we skip it.
TimeSpan? period = this.SchedulePeriodTimeSpan;
DateTime? start = this.ScheduleStart;
if (period == null || start == null)
{
return entity;
}
// Figure out the time from the start.
DateTime when = start.Value + period.Value * number;
Instant instant = timeService.CreateInstant(when);
// If the time hasn't past, then we don't apply it.
Instant now = timeService.Clock.GetCurrentInstant();
return instant > now
? entity
: this.Apply(entity, number, instant);
}
/// <summary>
/// Applies the schedule to the entity based on the number and instant
/// given.
/// </summary>
protected virtual Entity Apply(
Entity entity,
int number,
Instant instant)
{
return entity.Set(instant);
}
}

View file

@ -14,6 +14,8 @@
<PackageReference Include="MfGames.Gallium" Version="0.4.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
<PackageReference Include="Serilog" Version="2.11.0" />
<PackageReference Include="TimeSpanParserUtil" Version="1.2.0" />
<PackageReference Include="YamlDotNet" Version="12.0.0" />
</ItemGroup>
<ItemGroup>

View file

@ -1,124 +0,0 @@
using System;
using System.Text.RegularExpressions;
using MfGames.Gallium;
using MfGames.Nitride.Generators;
using NodaTime;
using Zio;
namespace MfGames.Nitride.Temporal.Schedules;
/// <summary>
/// A schedule that uses the `UPath` of the entity to determine an offset from
/// a starting point and calculates the information from there.
/// </summary>
[WithProperties]
public partial class NumericalPathSchedule : ISchedule
{
public NumericalPathSchedule()
{
this.PathRegex = new Regex(@"^.*(\d+)");
this.GetPath = (entity) => entity.Get<UPath>().ToString();
this.CaptureGroup = 1;
this.CaptureOffset = -1;
}
/// <summary>
/// Gets or sets the group number of the capture group with 1 being being the
/// first group in PathRegex.
/// </summary>
public int CaptureGroup { get; set; }
/// <summary>
/// Gets or sets the offset to make the resulting capture group a zero-based
/// number.
/// </summary>
public int CaptureOffset { get; set; }
/// <summary>
/// Gets or sets the method for retrieving the path for the entity.
/// </summary>
public Func<Entity, string> GetPath { get; set; }
/// <summary>
/// Gets or sets the regular expression to identify a matching path.
/// </summary>
public Regex? PathRegex { get; set; }
/// <summary>
/// Gets or sets the period between each matching entity. More precisely,
/// the schedule will be TimeSpan * (CaptureGroup + CaptureOffset).
/// </summary>
public SchedulePeriod SchedulePeriod { get; set; }
/// <summary>
/// Gets or sets when the first item is scheduled.
/// </summary>
public DateTime? ScheduleStart { get; set; }
/// <inheritdoc />
public virtual Entity Apply(
Entity entity,
Timekeeper timekeeper)
{
// Get the path and match it.
string? path = this.GetPath(entity);
Match? match = this.PathRegex?.Match(path)
?? throw new NullReferenceException(
"PathRegex was not configured for the "
+ this.GetType().Name
+ ".");
if (!match.Success)
{
return entity;
}
// Figure out the index/number of this entry.
int number = int.Parse(match.Groups[this.CaptureGroup].Value)
+ this.CaptureOffset;
// Figure out the time from the start.
TimeSpan span = this.SchedulePeriod switch
{
SchedulePeriod.Instant => TimeSpan.Zero,
SchedulePeriod.Day => TimeSpan.FromDays(1),
SchedulePeriod.Week => TimeSpan.FromDays(7),
_ => throw new InvalidOperationException(
"Cannot parse schedule period from "
+ this.SchedulePeriod
+ "."),
};
DateTime start = this.ScheduleStart
?? throw new NullReferenceException(
"Cannot use a schedule without a start date.");
DateTime when = start + span * number;
Instant instant = timekeeper.CreateInstant(when);
// If the time hasn't past, then we don't apply it.
Instant now = timekeeper.Clock.GetCurrentInstant();
return instant > now
? entity
: this.Apply(entity, number, instant);
}
/// <inheritdoc />
public virtual bool CanApply(Entity entity)
{
string? path = this.GetPath(entity);
bool match = this.PathRegex?.IsMatch(path) ?? false;
return match;
}
protected virtual Entity Apply(
Entity entity,
int number,
Instant instant)
{
return entity.Set(instant);
}
}

View file

@ -0,0 +1,118 @@
using System;
using System.Text.RegularExpressions;
using MfGames.Gallium;
using MfGames.Nitride.Generators;
using Zio;
namespace MfGames.Nitride.Temporal.Schedules;
/// <summary>
/// A common base for a schedule that is built off the path associated with
/// the entity.
/// </summary>
[WithProperties]
public abstract partial class PathRegexScheduleBase : ISchedule
{
protected PathRegexScheduleBase()
{
this.PathRegex = @"^.*?(\d+)[^\d]*$";
this.GetPath = entity => entity.Get<UPath>().ToString();
this.CaptureGroup = 1;
}
/// <summary>
/// Gets or sets the group number of the capture group with 1 being being the
/// first group in PathRegex.
/// </summary>
public int CaptureGroup { get; set; }
/// <summary>
/// Gets or sets the offset to make the resulting capture group a zero-based
/// number.
/// </summary>
public int CaptureOffset { get; set; }
/// <summary>
/// Gets or sets the method for retrieving the path for the entity.
/// </summary>
public Func<Entity, string> GetPath { get; set; }
/// <summary>
/// Gets or sets the regular expression to identify a matching path.
/// </summary>
public string? PathRegex { get; set; }
/// <inheritdoc />
public virtual Entity Apply(
Entity entity,
TimeService timeService)
{
// Get the path and match it.
string path = this.GetPath(entity);
Regex? regex = this.GetRegex();
Match match = regex?.Match(path)
?? throw new NullReferenceException(
"PathRegex was not configured for the "
+ this.GetType().Name
+ ".");
if (!match.Success)
{
return entity;
}
if (match.Groups.Count < 2)
{
throw new InvalidOperationException(
"There must be at least one capture group in '"
+ this.PathRegex
+ "'.");
}
// Figure out the index/number of this entry.
string numberValue = match.Groups[this.CaptureGroup].Value;
if (!int.TryParse(numberValue, out int number))
{
throw new FormatException(
path + ": Cannot parse '" + numberValue + "' as integer.");
}
number += this.CaptureOffset;
// Pass it onto the extending class.
return this.Apply(entity, number, timeService);
}
/// <inheritdoc />
public virtual bool CanApply(Entity entity)
{
string path = this.GetPath(entity);
Regex? regex = this.GetRegex();
Match match = regex?.Match(path)
?? throw new NullReferenceException(
"PathRegex was not configured for the "
+ this.GetType().Name
+ ".");
return match.Success;
}
/// <summary>
/// Applies the schedule according to the normalized number (after
/// CaptureOffset).
/// </summary>
protected abstract Entity Apply(
Entity entity,
int number,
TimeService timeService);
private Regex? GetRegex()
{
return this.PathRegex == null
? null
: new Regex(this.PathRegex);
}
}

View file

@ -0,0 +1,86 @@
using System;
using MfGames.Gallium;
using MfGames.Nitride.Generators;
using NodaTime;
namespace MfGames.Nitride.Temporal.Schedules;
/// <summary>
/// A schedule that uses the `UPath` of the entity to determine an offset from
/// a starting point and calculates the information from there.
/// </summary>
[WithProperties]
public partial class PeriodicPathRegexSchedule : PathRegexScheduleBase
{
public PeriodicPathRegexSchedule()
{
this.CaptureOffset = -1;
}
/// <summary>
/// Gets or sets the period between each matching entity. The period is
/// amount of time between each item based on the number. So the first will
/// be on the ScheduleDate, the second SchedulePeriod later, the third
/// SchedulePeriod after the second, etc. More precisely, the date for
/// any item TimeSpan * ((int)CaptureGroup + (int)CaptureOffset).
/// </summary>
public string? SchedulePeriod { get; set; }
/// <summary>
/// Gets or sets the schedule period as a TimeSpan object. This is converted
/// from SchedulePeriod using https://github.com/pengowray/TimeSpanParser.
/// If the value is null or blank or "immediate", then this will be instant
/// (TimeSpan.Zero).
/// </summary>
public virtual TimeSpan? SchedulePeriodTimeSpan
{
get => TimeSpanHelper.Parse(this.SchedulePeriod);
set => this.SchedulePeriod = value?.ToString();
}
/// <summary>
/// Gets or sets when the first item is scheduled.
/// </summary>
public DateTime? ScheduleStart { get; set; }
/// <inheritdoc />
protected override Entity Apply(
Entity entity,
int number,
TimeService timeService)
{
// If we have a "never", then we skip it.
TimeSpan? period = this.SchedulePeriodTimeSpan;
DateTime? start = this.ScheduleStart;
if (period == null || start == null)
{
return entity;
}
// Figure out the time from the start.
DateTime when = start.Value + period.Value * number;
Instant instant = timeService.CreateInstant(when);
// If the time hasn't past, then we don't apply it.
Instant now = timeService.Clock.GetCurrentInstant();
return instant > now
? entity
: this.Apply(entity, number, instant);
}
/// <summary>
/// Applies the schedule to the entity based on the number and instant
/// given.
/// </summary>
protected virtual Entity Apply(
Entity entity,
int number,
Instant instant)
{
return entity.Set(instant);
}
}

View file

@ -10,7 +10,7 @@ date (as provided by the `MfGames.Nitride.Temporal` package).
A schedule is not needed when the date is in the path or inside a component. Using
`MfGames.Nitride.Temporal.SetFromComponent` or `MfGames.Nitride.Temporal.SetFromPath`
would be more than sufficient. This is for dynamically changing entities based on
the date based on `Timekeeper`.
the date based on `TimeService`.
## Configuring
@ -28,10 +28,10 @@ builder.UseTemporalSchedules();
A schedule is a class that implements `ISchedule` which has the following methods:
- CanApply(Entity) ⟶ bool
- This returns true if the schedule can apply to the given entity.
- This returns true if the schedule can apply to the given entity.
- Apply(Entity) ⟶ Entity
- This makes the changes for the schedule on the entity and returns the results.
- If the entity doesn't apply, then it should return the entity without changes.
- This makes the changes for the schedule on the entity and returns the results.
- If the entity doesn't apply, then it should return the entity without changes.
## ApplySchedules
@ -53,18 +53,32 @@ The following properties are available (along with their corresponding `With*` m
- `IList<ISchedule> Schedules`
- Contains the list of schedules to apply against entities.
- Defaults to an empty list.
- `Timekeeper Timekeeper`
- `TimeService TimeService`
- Used to determine when the site is being generated.
- Defaults to the one provided by `MfGames.Nitride.Temporal`.
### Numerical Path-Based Schedules
## Provided Schedules
A number of schedules are provided as part of this library.
### Schedule Periods
In all cases, the `SchedulePeriod` is a string that parses into a `TimeSpan` object
that determines the amount of time between two successive entities of the same
schedule. It is parsed using [TimeSpanParser](https://github.com/pengowray/TimeSpanParser)
and allow for `TimeSpan` formatting (such as "1.00:00:00"), descriptions such as
"4 days" or "1 month". In addition, it also handles `instant` (non-case sensitive)
for a zero length time or effectively all at once or `never` (also not case sensitive)
to never apply to it.
### Periodic Path Regex Schedules
A common pattern for using schedules is to dole out a numerical series of posts over
a period of time. For example, a weekly posting of chapters or comic strips. In these
cases, there is usually a number in the path such as `chapter-01` or `comic-0244`. The
schedule starts at a certain point and then continues every day or week as needed.
The `NumericalPathSchedule` encapsulates this pattern. It uses the `UPath` component
The `PeriodicPathRegexSchedule` encapsulates this pattern. It uses the `UPath` component
of the entity and compares it against a regular expression that captures the numerical
part of the path. If it matches, then the schedule sets the date equal to the starting
point plus the "period" for every one past the first.
@ -81,11 +95,12 @@ var entities = new List<Entity>
};
var schedules = new List<ISchedule>
{
new NumericalPathSchedule
new PeriodicPathRegexSchedule
{
PathRegex = "chapter-(\d+),
ScheduleStart = DateTime.Parse("2023-01-01"),
SchedulePeriod = SchedulePeriod.Week,
SchedulePeriod = "1 week",
// Alternatively, SchedulePeriodTimeSpan = TimeSpan.FromDays(7),
},
}
@ -95,22 +110,22 @@ return entities.Run(op.WithSchedules(schedules));
This will have chapter-01 have an `Instant` component set to 2023-01-01, the second
chapter will be set to 2023-01-08, and the third at 2023-01-15.
`NumericalPathSchedule` has the following properties:
`PeriodicPathRegexSchedule` has the following properties:
- `Func<Entity, string> GetPath`
- An override to allow retrieving a different function.
- Defaults to `entity.Get<UPath>().ToString()`
- `Regex PathRegex`
- The regular expression that retrieves the number.
- Defaults to `^.*(\d+)` which grabs the last number found.
- Defaults to `^.*?(\d+)[^\d]*$` which grabs the last number found.
- `DateTime ScheduleStart`
- The date that the schedule starts.
- No default.
- `SchedulePeriod SchedulePeriod`
- Values:
- Instant (meaning everything at once, default)
- Week
- Day
- `string SchedulePeriod`
- Parsed using https://github.com/pengowray/TimeSpanParser
- Supports any `TimeSpan` value, also "2 weeks" and Humanizer formatted values
- `TimeSpan SchedulePeriodTimeSpan`
- Parsed from `SchedulePeriod`
- `int CaptureGroup`
- The numerical index of the capture group.
- Defaults to `1` because the first match in Regex is 1.
@ -126,7 +141,7 @@ There is also a virtual method for applying the schedule.
#### Overriding Logic
The default operation of a `NumericalPathSchedule` is to only set the `Instant`
The default operation of a `PeriodicPathRegexSchedule` is to only set the `Instant`
component of the entity. The class can be extended to have more site-specific
entries including adding more properties to the schedule and applying them.
@ -167,14 +182,54 @@ Commonly, this schedule will be put into a JSON or YAML file.
```yaml
schedules:
# Patron and Ko-Fi subscribers get it all at once
- pathRegex: chapters/chapter-\d+
scheduleDate: 2025-01-01
- pathRegex: chapters/chapter-(\d+)
scheduleStart: 2025-01-01
schedulePeriod: instant # Because we overrode the default to be weekly.
access: subscribers
# The first fifteen chapters (01-15) were released at the rate of one
# per week starting in 2024. This will replace all the schedules above
# it.
- path: chapters/chapter-(0\d|1[1-5])
scheduleDate: 2030-01-01
scheduleStart: 2030-01-01
access: public
```
### Indexed Path Regex Schedules
A variant on the periodic schedule is the `IndexedPathRegexSchedule`. This evolved
from the periodic one in order to simplify the regular expressions at the expense
of having a more complex structure. It uses the same regular expression as
`PeriodicPathRegexSchedule` including `CaptureGroup` (still defaults to 1) and
`CaptureOffset` (which defaults to 0) but instead of having the schedule date
and period, those are relegated to a `Dictionary<int, IndexedSchedule>` (or a
class that extends `IndexedSchedule`).
Using the below example:
```yaml
schedules:
pathRegex: chapters/chapter-(\d+)
indexes:
1:
scheduleStart: 2025-01-01
schedulePeriod: instant
access: subscribers
10:
scheduleStart: 2030-01-01
schedulePeriod: 1 week
access: subscribers
30:
schedulePeriod: never
```
For a given calculated number (such as `chapter-01` being `1`), the schedule picks the
highest index that is not replaced by a higher number. So, chapters 1-9 would use the 2025
date and be instantly available on that date while chapters 10 and higher would use the
2030 date and be doled out a week at a time.
While this example doesn't use the top level as a sequence, it could be used to handle
multiple overlapping schedules (like the first where two changes), but it is more
verbose while describing it.
For the inner schedule, the number is normalized to be 0-based automatically for each one
so the above example would have 1, 10, and 30 all pass in `0` into the `Apply` function.

View file

@ -0,0 +1,51 @@
using System;
using MfGames.Gallium;
using MfGames.Nitride.Generators;
using NodaTime;
namespace MfGames.Nitride.Temporal.Schedules;
/// <summary>
/// A schedule that goes against all entities it is applied to.
/// </summary>
[WithProperties]
public partial class SimplePathSchedule : ISchedule
{
/// <summary>
/// Gets or sets when the first item is scheduled.
/// </summary>
public DateTime? ScheduleStart { get; set; }
/// <inheritdoc />
public virtual Entity Apply(
Entity entity,
TimeService timeService)
{
DateTime start = this.ScheduleStart
?? throw new NullReferenceException(
"Cannot use a schedule without a start date.");
Instant instant = timeService.CreateInstant(start);
// If the time hasn't past, then we don't apply it.
Instant now = timeService.Clock.GetCurrentInstant();
return instant > now
? entity
: this.Apply(entity, instant);
}
/// <inheritdoc />
public virtual bool CanApply(Entity entity)
{
return true;
}
protected virtual Entity Apply(
Entity entity,
Instant instant)
{
return entity.Set(instant);
}
}

View file

@ -0,0 +1,35 @@
using System;
using TimeSpanParserUtil;
namespace MfGames.Nitride.Temporal.Schedules;
public static class TimeSpanHelper
{
public static TimeSpan? Parse(string? input)
{
if (string.IsNullOrWhiteSpace(input))
{
return null;
}
if (input.Equals(
"immediate",
StringComparison.InvariantCultureIgnoreCase)
|| input.Equals(
"instant",
StringComparison.InvariantCultureIgnoreCase))
{
return TimeSpan.Zero;
}
if (input.Equals(
"never",
StringComparison.InvariantCultureIgnoreCase))
{
return null;
}
return TimeSpanParser.Parse(input);
}
}

View file

@ -6,10 +6,10 @@ public class ApplySchedulesValidator : AbstractValidator<ApplySchedules>
{
public ApplySchedulesValidator()
{
this.RuleFor(x => x.Schedules)
.NotEmpty();
this.RuleFor(x => x.GetSchedules)
.NotNull();
this.RuleFor(x => x.Timekeeper)
this.RuleFor(x => x.TimeService)
.NotNull();
}
}

View file

@ -20,14 +20,14 @@ public class DatePipelineCommandOption : IPipelineCommandOption
{
private readonly ILogger logger;
private readonly Timekeeper timekeeper;
private readonly TimeService timeService;
public DatePipelineCommandOption(
ILogger logger,
Timekeeper timekeeper)
TimeService timeService)
{
this.logger = logger.ForContext<Instant>();
this.timekeeper = timekeeper;
this.timeService = timeService;
this.Option = new Option<DateTime>("--date")
{
@ -53,15 +53,15 @@ public class DatePipelineCommandOption : IPipelineCommandOption
// date for the entire run.
var local = LocalDateTime.FromDateTime(value.Value);
ZonedDateTime zoned =
local.InZoneStrictly(this.timekeeper.DateTimeZone);
local.InZoneStrictly(this.timeService.DateTimeZone);
var instant = zoned.ToInstant();
this.timekeeper.Clock = new FakeClock(instant);
this.timeService.Clock = new FakeClock(instant);
}
// Report the date we are processing.
Instant now = this.timekeeper.Clock.GetCurrentInstant();
ZonedDateTime dateTime = now.InZone(this.timekeeper.DateTimeZone);
Instant now = this.timeService.Clock.GetCurrentInstant();
ZonedDateTime dateTime = now.InZone(this.timeService.DateTimeZone);
string formatted = dateTime.ToString("G", CultureInfo.InvariantCulture);
this.logger.Information("Setting date/time to {When:l}", formatted);

View file

@ -18,13 +18,13 @@ namespace MfGames.Nitride.Temporal.Cli;
/// </summary>
public class ExpiresPipelineCommandOption : IPipelineCommandOption
{
private readonly Timekeeper clock;
private readonly TimeService clock;
private readonly ILogger logger;
public ExpiresPipelineCommandOption(
ILogger logger,
Timekeeper clock,
TimeService clock,
string? defaultValue = null)
{
this.logger = logger.ForContext<Instant>();

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using FluentValidation;
@ -24,10 +25,10 @@ public partial class CreateDateIndexes : OperationBase, IResolvingOperation
public CreateDateIndexes(
IValidator<CreateDateIndexes> validator,
Timekeeper timekeeper)
TimeService timeService)
{
this.validator = validator;
this.Timekeeper = timekeeper;
this.TimeService = timeService;
}
/// <summary>
@ -50,10 +51,12 @@ public partial class CreateDateIndexes : OperationBase, IResolvingOperation
/// </summary>
public int LessThanEqualCollapse { get; set; }
public Timekeeper Timekeeper { get; }
public TimeService TimeService { get; }
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
// Validate our input.
this.validator.ValidateAndThrow(this);
@ -133,7 +136,7 @@ public partial class CreateDateIndexes : OperationBase, IResolvingOperation
Entity? first = pair.Value[0];
string? nextKey = this.Timekeeper
string? nextKey = this.TimeService
.ToDateTime(first.Get<Instant>())
.ToString(this.Formats[i + 1]);
@ -163,7 +166,7 @@ public partial class CreateDateIndexes : OperationBase, IResolvingOperation
List<Dictionary<string, List<Entity>>> grouped,
Entity entity)
{
var dateTime = this.Timekeeper.ToDateTime(instant);
var dateTime = this.TimeService.ToDateTime(instant);
for (int i = 0; i < this.Formats.Count; i++)
{

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Threading;
using FluentValidation;
@ -19,20 +20,22 @@ public partial class FilterOutExpiredInstant : OperationBase
public FilterOutExpiredInstant(
IValidator<FilterOutExpiredInstant> validator,
Timekeeper clock)
TimeService clock)
{
this.validator = validator;
this.Timekeeper = clock;
this.TimeService = clock;
}
public Timekeeper Timekeeper { get; set; }
public TimeService TimeService { get; set; }
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);
if (!this.Timekeeper.Expiration.HasValue)
if (!this.TimeService.Expiration.HasValue)
{
return input;
}
@ -45,7 +48,7 @@ public partial class FilterOutExpiredInstant : OperationBase
Instant instant,
CanExpire _)
{
Instant expiration = this.Timekeeper.Expiration!.Value;
Instant expiration = this.TimeService.Expiration!.Value;
bool isExpired = instant.CompareTo(expiration) < 0;
return isExpired ? null : entity;

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Threading;
using MfGames.Gallium;
using MfGames.Nitride.Generators;
@ -14,17 +15,19 @@ namespace MfGames.Nitride.Temporal;
[WithProperties]
public partial class FilterOutFutureInstant : OperationBase
{
public FilterOutFutureInstant(Timekeeper timekeeper)
public FilterOutFutureInstant(TimeService timeService)
{
this.Timekeeper = timekeeper;
this.TimeService = timeService;
}
public Timekeeper Timekeeper { get; set; }
public TimeService TimeService { get; set; }
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
Instant now = this.Timekeeper.Clock.GetCurrentInstant();
Instant now = this.TimeService.Clock.GetCurrentInstant();
return input
.SelectEntity<Instant>(

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using FluentValidation;
@ -16,11 +17,11 @@ namespace MfGames.Nitride.Temporal;
/// </summary>
public class SetInstantFromComponent<TComponent> : OperationBase
{
private readonly Timekeeper clock;
private readonly TimeService clock;
private readonly IValidator<SetInstantFromComponent<TComponent>> validator;
public SetInstantFromComponent(Timekeeper clock)
public SetInstantFromComponent(TimeService clock)
{
// TODO: Figure out why Autofac won't let us register IValidator of generic classes.
this.validator = new SetInstantFromComponentValidator<TComponent>();
@ -39,7 +40,9 @@ public class SetInstantFromComponent<TComponent> : OperationBase
}
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);
@ -70,6 +73,7 @@ public class SetInstantFromComponent<TComponent> : OperationBase
{
case null:
return entity;
case Instant direct:
instant = direct;

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Threading;
using FluentValidation;
@ -22,13 +23,13 @@ namespace MfGames.Nitride.Temporal;
[WithProperties]
public partial class SetInstantFromPath : OperationBase
{
private readonly Timekeeper clock;
private readonly TimeService clock;
private readonly IValidator<SetInstantFromPath> validator;
public SetInstantFromPath(
IValidator<SetInstantFromPath> validator,
Timekeeper clock)
TimeService clock)
{
this.validator = validator;
this.clock = clock;
@ -43,7 +44,9 @@ public partial class SetInstantFromPath : OperationBase
public Regex? PathRegex { get; set; }
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);
@ -64,15 +67,9 @@ public partial class SetInstantFromPath : OperationBase
// Create an Instant from this.
Instant instant = this.clock.CreateInstant(
Convert.ToInt32(
match.Groups["year"]
.Value),
Convert.ToInt32(
match.Groups["month"]
.Value),
Convert.ToInt32(
match.Groups["day"]
.Value));
Convert.ToInt32(match.Groups["year"].Value),
Convert.ToInt32(match.Groups["month"].Value),
Convert.ToInt32(match.Groups["day"].Value));
return entity.Set(instant);
}

View file

@ -45,8 +45,8 @@ public static class NitrideTemporalBuilderExtensions
context =>
{
ILogger logger = context.Resolve<ILogger>();
Timekeeper
clock = context.Resolve<Timekeeper>();
TimeService
clock = context.Resolve<TimeService>();
return new ExpiresPipelineCommandOption(
logger,
@ -65,12 +65,12 @@ public static class NitrideTemporalBuilderExtensions
scope) =>
{
ILogger logger = scope.Resolve<ILogger>();
Timekeeper timekeeper = scope.Resolve<Timekeeper>();
TimeService timeService = scope.Resolve<TimeService>();
timekeeper.DateTimeZone = config.DateTimeZone;
timeService.DateTimeZone = config.DateTimeZone;
logger.Verbose(
"Setting time zone to {Zone:l}",
timekeeper.DateTimeZone);
timeService.DateTimeZone);
});
}

View file

@ -10,7 +10,7 @@ public class NitrideTemporalModule : Module
builder.RegisterOperators(this);
builder.RegisterValidators(this);
builder.RegisterType<Timekeeper>()
builder.RegisterType<TimeService>()
.AsSelf()
.SingleInstance();

View file

@ -10,9 +10,9 @@ namespace MfGames.Nitride.Temporal;
/// the desire time zone along with various methods for processing parsed
/// DateTime objects into NodaTime.Instant.
/// </summary>
public class Timekeeper
public class TimeService
{
public Timekeeper()
public TimeService()
{
// We use FakeClock because we don't want time to advance in the
// middle of running, just in case we are just a few seconds before
@ -28,6 +28,16 @@ public class Timekeeper
/// </summary>
public IClock Clock { get; set; }
/// <summary>
/// Gets the current date time.
/// </summary>
public DateTime CurrentDateTime => this.ToDateTime(this.CurrentInstant);
/// <summary>
/// Gets the current instant.
/// </summary>
public Instant CurrentInstant => this.Clock.GetCurrentInstant();
/// <summary>
/// Gets or sets the date time zone used for processing.
/// </summary>

View file

@ -6,7 +6,7 @@ public class CreateDateIndexesValidator : AbstractValidator<CreateDateIndexes>
{
public CreateDateIndexesValidator()
{
this.RuleFor(a => a.Timekeeper)
this.RuleFor(a => a.TimeService)
.NotNull();
this.RuleFor(a => a.CreateIndex)

View file

@ -7,7 +7,7 @@ public class FilterOutExpiredInstantValidator
{
public FilterOutExpiredInstantValidator()
{
this.RuleFor(x => x.Timekeeper)
this.RuleFor(x => x.TimeService)
.NotNull();
}
}

View file

@ -7,7 +7,7 @@ public class FilterOutFutureInstantValidator
{
public FilterOutFutureInstantValidator()
{
this.RuleFor(x => x.Timekeeper)
this.RuleFor(x => x.TimeService)
.NotNull();
}
}

View file

@ -10,12 +10,12 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj" />
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="MfGames.Gallium" Version="0.4.0" />
<PackageReference Include="YamlDotNet" Version="12.0.0" />
<PackageReference Include="MfGames.Gallium" Version="0.4.0"/>
<PackageReference Include="YamlDotNet" Version="12.0.0"/>
</ItemGroup>
<!-- Include the source generator -->

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using MfGames.Gallium;
using MfGames.Nitride.Contents;
@ -44,7 +45,9 @@ public class ParseYamlHeader<TModel> : OperationBase
private bool RemoveHeader { get; set; }
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
// Set up the YAML parsing.
DeserializerBuilder builder =

View file

@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Threading;
using System.Threading.Tasks;
using MfGames.Nitride.Pipelines;
@ -48,6 +49,9 @@ public class BuildCommand : Command, ICommandHandler
/// <inheritdoc />
public async Task<int> InvokeAsync(InvocationContext context)
{
// Get the cancellation token so we can be interrupted.
CancellationToken cancellationToken = context.GetCancellationToken();
// Process any injected options.
this.logger.Debug(
"Processing {Count:N0} pipeline options",
@ -63,7 +67,7 @@ public class BuildCommand : Command, ICommandHandler
// all the pipelines once and then quits when it finishes.
this.logger.Information("Running pipelines");
int pipelinesResults = await this.pipelines.RunAsync();
int pipelinesResults = await this.pipelines.RunAsync(cancellationToken);
return pipelinesResults;
}

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using FluentValidation;
@ -67,7 +68,9 @@ public partial class CreateOrUpdateIndex : OperationBase
} = null!;
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
// Make sure we have sane data.
this.validator.ValidateAndThrow(this);

View file

@ -3,6 +3,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using FluentValidation;
@ -92,7 +93,9 @@ public partial class EntityScanner : OperationBase
}
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
// Make sure we have sane data.
this.validator.ValidateAndThrow(this);

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using FluentValidation;
@ -46,7 +47,9 @@ public partial class LinkEntitySequence : OperationBase, IResolvingOperation
}
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
// Make sure everything is good.
this.validator.ValidateAndThrow(this);

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Threading;
using MfGames.Gallium;
@ -10,6 +11,9 @@ public interface IOperation
/// Runs the input entities through the operation and returns the results.
/// </summary>
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
IEnumerable<Entity> Run(IEnumerable<Entity> input);
IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default);
}

View file

@ -15,22 +15,22 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="6.4.0" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="FluentValidation" Version="11.2.1" />
<PackageReference Include="MfGames.Gallium" Version="0.4.0" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="MfGames.ToolBuilder" Version="1.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
<PackageReference Include="Serilog" Version="2.11.0" />
<PackageReference Include="Serilog.Extensions.Autofac.DependencyInjection" Version="5.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="5.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="Zio" Version="0.15.0" />
<PackageReference Include="Autofac" Version="6.4.0"/>
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="8.0.0"/>
<PackageReference Include="FluentValidation" Version="11.2.1"/>
<PackageReference Include="MfGames.Gallium" Version="0.4.0"/>
<PackageReference Include="Humanizer.Core" Version="2.14.1"/>
<PackageReference Include="MfGames.ToolBuilder" Version="1.0.0"/>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1"/>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0"/>
<PackageReference Include="Serilog" Version="2.11.0"/>
<PackageReference Include="Serilog.Extensions.Autofac.DependencyInjection" Version="5.0.0"/>
<PackageReference Include="Serilog.Extensions.Hosting" Version="5.0.1"/>
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0"/>
<PackageReference Include="SerilogAnalyzer" Version="0.15.0"/>
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1"/>
<PackageReference Include="System.Linq.Async" Version="6.0.1"/>
<PackageReference Include="Zio" Version="0.15.0"/>
</ItemGroup>
<!-- Include the source generator -->

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Threading;
using MfGames.Gallium;
@ -15,11 +16,13 @@ public static class NitrideOperationExtensions
/// </summary>
/// <param name="input">The entities to perform the operation against.</param>
/// <param name="operation">The operation to run.</param>
/// <param name="cancellationToken">The cancellation token of the request.</param>
/// <returns>The results of the operation.</returns>
public static IEnumerable<Entity> Run(
this IEnumerable<Entity> input,
IOperation operation)
IOperation operation,
CancellationToken cancellationToken = default)
{
return operation.Run(input);
return operation.Run(input, cancellationToken);
}
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Threading;
using MfGames.Gallium;
@ -10,5 +11,7 @@ namespace MfGames.Nitride;
public abstract class OperationBase : IOperation
{
/// <inheritdoc />
public abstract IEnumerable<Entity> Run(IEnumerable<Entity> input);
public abstract IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default);
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Threading;
using MfGames.Gallium;
@ -22,6 +23,9 @@ public interface IPipeline
/// entities.
/// </summary>
/// <param name="entities">The entities to process.</param>
/// <param name="cancellationToken">The token for cancelling processing.</param>
/// <returns>The resulting entities after the process runs.</returns>
IAsyncEnumerable<Entity> RunAsync(IEnumerable<Entity> entities);
IAsyncEnumerable<Entity> RunAsync(
IEnumerable<Entity> entities,
CancellationToken cancellationToken = default);
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Threading;
using MfGames.Gallium;
@ -41,12 +42,12 @@ public abstract class PipelineBase : IPipeline
/// <inheritdoc />
public abstract IAsyncEnumerable<Entity> RunAsync(
IEnumerable<Entity> entities);
IEnumerable<Entity> entities,
CancellationToken cancellationToken = default);
/// <inheritdoc />
public override string ToString()
{
return this.GetType()
.Name;
return this.GetType().Name;
}
}

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Humanizer;
@ -48,8 +49,9 @@ public class PipelineManager
/// Runs all of the pipelines in the appropriate order while running
/// across multiple threads.
/// </summary>
/// <param name="cancellationToken">The token for cancelling processing.</param>
/// <returns>A task with zero for success or otherwise an error code.</returns>
public Task<int> RunAsync()
public Task<int> RunAsync(CancellationToken cancellationToken)
{
// Make sure everything is setup.
DateTime started = DateTime.UtcNow;
@ -66,14 +68,18 @@ public class PipelineManager
"pipeline".ToQuantity(this.pipelines.Count));
Task[] tasks = this.entries
.Select(x => Task.Run(async () => await x.RunAsync()))
.Select(
x => Task.Run(
async () => await x.RunAsync(cancellationToken),
cancellationToken))
.ToArray();
var report = TimeSpan.FromSeconds(15);
while (!Task.WaitAll(tasks, report))
{
var waiting = this.entries.Where(x => !x.IsFinished)
var waiting = this.entries
.Where(x => !x.IsFinished)
.ToList();
this.logger.Debug(
@ -81,14 +87,15 @@ public class PipelineManager
"pipeline".ToQuantity(waiting.Count));
IOrderedEnumerable<IGrouping<PipelineRunnerState, PipelineRunner>>
states =
waiting.GroupBy(x => x.State, x => x)
.OrderBy(x => (int)x.Key);
states = waiting
.GroupBy(x => x.State, x => x)
.OrderBy(x => (int)x.Key);
foreach (IGrouping<PipelineRunnerState, PipelineRunner>? state in
states)
{
var statePipelines = state.OrderBy(x => x.Pipeline.ToString())
var statePipelines = state
.OrderBy(x => x.Pipeline.ToString())
.ToList();
this.logger.Verbose(
@ -106,8 +113,8 @@ public class PipelineManager
}
// Figure out our return code.
bool hasErrors =
this.entries.Any(x => x.State == PipelineRunnerState.Errored);
bool hasErrors = this.entries
.Any(x => x.State == PipelineRunnerState.Errored);
this.logger.Information(
"Completed in {Elapsed}",
@ -144,20 +151,22 @@ public class PipelineManager
// Wrap all the pipelines into entries. We do this before the next
// step so we can have the entries depend on the entries.
this.entries = this.pipelines.Select(x => this.createEntry(x))
this.entries = this.pipelines
.Select(x => this.createEntry(x))
.ToList();
// Go through and connect the pipelines together.
foreach (PipelineRunner? entry in this.entries)
{
var dependencies = entry.Pipeline.GetDependencies()
var dependencies = entry.Pipeline
.GetDependencies()
.ToList();
foreach (IPipeline? dependency in dependencies)
{
// Get the entry for the dependency.
PipelineRunner dependencyPipeline =
this.entries.Single(x => x.Pipeline == dependency);
PipelineRunner dependencyPipeline = this.entries
.Single(x => x.Pipeline == dependency);
// Set up the bi-directional connection.
entry.Incoming.Add(dependencyPipeline);

View file

@ -149,7 +149,7 @@ public class PipelineRunner
/// Executes the pipeline, including waiting for any or all
/// dependencies.
/// </summary>
public async Task RunAsync()
public async Task RunAsync(CancellationToken cancellationToken = default)
{
try
{
@ -194,7 +194,7 @@ public class PipelineRunner
// Run the pipeline. This may not be resolved until we gather
// the output below.
await this.RunPipeline(input);
await this.RunPipeline(input, cancellationToken);
// At this point, we are completely done with our inputs, so signal
// to them in case they have to clean up any of their structures.
@ -274,12 +274,14 @@ public class PipelineRunner
return input;
}
private async Task RunPipeline(List<Entity> input)
private async Task RunPipeline(
List<Entity> input,
CancellationToken cancellationToken)
{
// Get the sequence of data, but this doesn't drain the enumeration.
List<Entity> output = await this.Pipeline
.RunAsync(input)
.ToListAsync();
.RunAsync(input, cancellationToken)
.ToListAsync(cancellationToken);
// Gather all the output.
this.logger.Verbose("{Pipeline:l}: Gathering output", this.Pipeline);

View file

@ -6,22 +6,22 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MfGames.Nitride.IO\MfGames.Nitride.IO.csproj" />
<ProjectReference Include="..\MfGames.Nitride.Tests\MfGames.Nitride.Tests.csproj" />
<ProjectReference Include="..\..\src\MfGames.Nitride.IO\MfGames.Nitride.IO.csproj"/>
<ProjectReference Include="..\MfGames.Nitride.Tests\MfGames.Nitride.Tests.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="CompareNETObjects" Version="4.78.0" />
<PackageReference Include="MfGames.Gallium" Version="0.4.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1" />
<PackageReference Include="JunitXml.TestLogger" Version="3.0.114" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="CompareNETObjects" Version="4.78.0"/>
<PackageReference Include="MfGames.Gallium" Version="0.4.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1"/>
<PackageReference Include="JunitXml.TestLogger" Version="3.0.114"/>
<PackageReference Include="Newtonsoft.Json" Version="13.0.2"/>
<PackageReference Include="xunit" Version="2.4.2"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Zio" Version="0.15.0" />
<PackageReference Include="Zio" Version="0.15.0"/>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>

View file

@ -6,10 +6,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MfGames.Gallium" Version="0.4.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1" />
<PackageReference Include="JunitXml.TestLogger" Version="3.0.114" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="MfGames.Gallium" Version="0.4.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1"/>
<PackageReference Include="JunitXml.TestLogger" Version="3.0.114"/>
<PackageReference Include="xunit" Version="2.4.2"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@ -21,9 +21,9 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MfGames.Nitride.Tests\MfGames.Nitride.Tests.csproj" />
<ProjectReference Include="..\..\src\MfGames.Nitride.Json\MfGames.Nitride.Json.csproj" />
<ProjectReference Include="..\..\src\MfGames.Nitride\MfGames.Nitride.csproj" />
<ProjectReference Include="..\MfGames.Nitride.Tests\MfGames.Nitride.Tests.csproj"/>
<ProjectReference Include="..\..\src\MfGames.Nitride.Json\MfGames.Nitride.Json.csproj"/>
<ProjectReference Include="..\..\src\MfGames.Nitride\MfGames.Nitride.csproj"/>
</ItemGroup>
</Project>

View file

@ -6,18 +6,18 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MfGames.Nitride.Markdown\MfGames.Nitride.Markdown.csproj" />
<ProjectReference Include="..\MfGames.Nitride.Tests\MfGames.Nitride.Tests.csproj" />
<ProjectReference Include="..\..\src\MfGames.Nitride.Markdown\MfGames.Nitride.Markdown.csproj"/>
<ProjectReference Include="..\MfGames.Nitride.Tests\MfGames.Nitride.Tests.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Markdig" Version="0.30.4" />
<PackageReference Include="MfGames.Gallium" Version="0.4.0" />
<PackageReference Include="MfGames.Markdown.Gemtext" Version="1.2.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1" />
<PackageReference Include="JunitXml.TestLogger" Version="3.0.114" />
<PackageReference Include="Slugify.Core" Version="3.0.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="Markdig" Version="0.30.4"/>
<PackageReference Include="MfGames.Gallium" Version="0.4.0"/>
<PackageReference Include="MfGames.Markdown.Gemtext" Version="1.2.2"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1"/>
<PackageReference Include="JunitXml.TestLogger" Version="3.0.114"/>
<PackageReference Include="Slugify.Core" Version="3.0.0"/>
<PackageReference Include="xunit" Version="2.4.2"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View file

@ -6,21 +6,21 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MfGames.Nitride.IO\MfGames.Nitride.IO.csproj" />
<ProjectReference Include="..\..\src\MfGames.Nitride.Slugs\MfGames.Nitride.Slugs.csproj" />
<ProjectReference Include="..\MfGames.Nitride.Tests\MfGames.Nitride.Tests.csproj" />
<ProjectReference Include="..\..\src\MfGames.Nitride.IO\MfGames.Nitride.IO.csproj"/>
<ProjectReference Include="..\..\src\MfGames.Nitride.Slugs\MfGames.Nitride.Slugs.csproj"/>
<ProjectReference Include="..\MfGames.Nitride.Tests\MfGames.Nitride.Tests.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="MfGames.Gallium" Version="0.4.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1" />
<PackageReference Include="JunitXml.TestLogger" Version="3.0.114" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="MfGames.Gallium" Version="0.4.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1"/>
<PackageReference Include="JunitXml.TestLogger" Version="3.0.114"/>
<PackageReference Include="xunit" Version="2.4.2"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Zio" Version="0.15.0" />
<PackageReference Include="Zio" Version="0.15.0"/>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>

View file

@ -0,0 +1,260 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MfGames.Gallium;
using MfGames.Nitride.Tests;
using NodaTime;
using NodaTime.Testing;
using Xunit;
using Xunit.Abstractions;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
using Zio;
namespace MfGames.Nitride.Temporal.Schedules.Tests;
public class IndexedPathRegexScheduleTest : TemporalSchedulesTestBase
{
public IndexedPathRegexScheduleTest(ITestOutputHelper output)
: base(output)
{
}
[Fact]
public void DeserializedSetupWorks()
{
using TemporalSchedulesTestContext context = this.CreateContext();
// Create a numerical series of entities.
var input = new List<Entity>
{
new Entity().SetAll((UPath)"/chapter-01.md", new TestModel()),
new Entity().SetAll((UPath)"/chapter-02.md", new TestModel()),
new Entity().SetAll((UPath)"/chapter-03.md", new TestModel()),
};
TestModel model = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build()
.Deserialize<TestModel>(
string.Join(
"\n",
"---",
"access: custom",
"schedules:",
" pathRegex: chapter-(\\d+)",
" indexes:",
" 1:",
" scheduleStart: 2020-01-01",
" schedulePeriod: instant",
" access: t-1",
" 2:",
" scheduleStart: 2023-01-02",
" schedulePeriod: 1 week",
" access: t-2",
""));
var schedules = model.Schedules!;
// Create the operation and run it, but treat it as being set after the
// second but before the third item.
TimeService time = context.Resolve<TimeService>();
ApplySchedules op = context.Resolve<ApplySchedules>()
.WithGetSchedules(_ => new ISchedule[] { schedules });
var now = Instant.FromUtc(2023, 1, 3, 0, 0);
time.Clock = new FakeClock(now);
var actual = op
.Run(input)
.Select(
a => string.Format(
"{0} -- {1} -- {2}",
a.Get<UPath>().ToString(),
a.Has<Instant>()
? time
.ToDateTime(a.Get<Instant>())
.ToString("yyyy-MM-dd")
: "none",
a.Get<TestModel>().Access))
.ToList();
var expected = new List<string>
{
"/chapter-01.md -- 2020-01-01 -- t-1",
"/chapter-02.md -- 2023-01-02 -- t-2",
"/chapter-03.md -- none -- private",
};
TestHelper.CompareObjects(expected, actual);
}
[Fact]
public void ManualSetupWorks()
{
using TemporalSchedulesTestContext context = this.CreateContext();
// Create a numerical series of entities.
var input = new List<Entity>
{
new Entity().SetAll((UPath)"/chapter-01.md", new TestModel()),
new Entity().SetAll((UPath)"/chapter-02.md", new TestModel()),
new Entity().SetAll((UPath)"/chapter-03.md", new TestModel()),
};
var schedules = new TestRegexSchedule
{
Indexes = new Dictionary<int, TestSchedule>
{
[1] = new TestSchedule
{
ScheduleStart = DateTime.Parse("2023-01-01"),
Access = "public",
},
},
};
// Create the operation and run it, but treat it as being set after the
// second but before the third item.
TimeService time = context.Resolve<TimeService>();
ApplySchedules op = context.Resolve<ApplySchedules>()
.WithGetSchedules(_ => new ISchedule[] { schedules });
var now = Instant.FromUtc(2023, 1, 9, 0, 0);
time.Clock = new FakeClock(now);
var actual = op
.Run(input)
.Select(
a => string.Format(
"{0} -- {1} -- {2}",
a.Get<UPath>().ToString(),
a.Has<Instant>()
? time
.ToDateTime(a.Get<Instant>())
.ToString("yyyy-MM-dd")
: "none",
a.Get<TestModel>().Access))
.ToList();
var expected = new List<string>
{
"/chapter-01.md -- 2023-01-01 -- public",
"/chapter-02.md -- 2023-01-08 -- public",
"/chapter-03.md -- none -- private",
};
TestHelper.CompareObjects(expected, actual);
}
[Fact]
public void SequencedScheduleWorks()
{
using TemporalSchedulesTestContext context = this.CreateContext();
// Create a numerical series of entities.
var input = new List<Entity>
{
new Entity().SetAll((UPath)"/chapter-01.md", new TestModel()),
new Entity().SetAll((UPath)"/chapter-02.md", new TestModel()),
new Entity().SetAll((UPath)"/chapter-03.md", new TestModel()),
new Entity().SetAll((UPath)"/chapter-04.md", new TestModel()),
new Entity().SetAll((UPath)"/chapter-05.md", new TestModel()),
new Entity().SetAll((UPath)"/chapter-06.md", new TestModel()),
};
var schedules = new TestRegexSchedule()
{
Indexes = new Dictionary<int, TestSchedule>
{
[1] = new()
{
ScheduleStart = DateTime.Parse("2020-01-01"),
Access = "subscriber",
SchedulePeriodTimeSpan = TimeSpan.FromDays(7),
},
[3] = new()
{
ScheduleStart = DateTime.Parse("2023-01-07"),
Access = "public",
SchedulePeriodTimeSpan = TimeSpan.Zero,
},
[5] = new()
{
SchedulePeriod = "never",
},
},
};
// Create the operation and run it, but treat it as being set after the
// second but before the third item.
TimeService time = context.Resolve<TimeService>();
ApplySchedules op = context.Resolve<ApplySchedules>()
.WithGetSchedules(_ => new ISchedule[] { schedules });
var now = Instant.FromUtc(2023, 1, 9, 0, 0);
time.Clock = new FakeClock(now);
var actual = op
.Run(input)
.Select(
a => string.Format(
"{0} -- {1} -- {2}",
a.Get<UPath>().ToString(),
a.Has<Instant>()
? time
.ToDateTime(a.Get<Instant>())
.ToString("yyyy-MM-dd")
: "none",
a.Get<TestModel>().Access))
.ToList();
var expected = new List<string>
{
"/chapter-01.md -- 2020-01-01 -- subscriber",
"/chapter-02.md -- 2020-01-08 -- subscriber",
"/chapter-03.md -- 2023-01-07 -- public",
"/chapter-04.md -- 2023-01-07 -- public",
"/chapter-05.md -- none -- private",
"/chapter-06.md -- none -- private",
};
TestHelper.CompareObjects(expected, actual);
}
public class TestModel
{
public string? Access { get; set; } = "private";
public TestRegexSchedule? Schedules { get; set; }
}
public class TestRegexSchedule : IndexedPathRegexSchedule<TestSchedule>
{
}
public class TestSchedule : IndexedSchedule
{
public TestSchedule()
{
this.SchedulePeriod = "1 week";
}
public string? Access { get; set; }
/// <inheritdoc />
protected override Entity Apply(
Entity entity,
int number,
Instant instant)
{
TestModel model = entity.Get<TestModel>();
model.Access = this.Access;
return entity.SetAll(instant, model);
}
}
}

View file

@ -18,9 +18,9 @@ using Zio;
namespace MfGames.Nitride.Temporal.Schedules.Tests;
public class NumericalPathScheduleTests : TemporalSchedulesTestBase
public class PeriodicPathRegexScheduleTest : TemporalSchedulesTestBase
{
public NumericalPathScheduleTests(ITestOutputHelper output)
public PeriodicPathRegexScheduleTest(ITestOutputHelper output)
: base(output)
{
}
@ -47,16 +47,18 @@ public class NumericalPathScheduleTests : TemporalSchedulesTestBase
"---",
"access: custom",
"schedules:",
" - scheduleStart: 2023-01-01",
" - pathRegex: chapter-(\\d+)",
" scheduleStart: 2023-01-01",
" schedulePeriod: 1 week",
" access: public",
""));
List<TestSchedule>? schedules = model.Schedules!;
List<TestRegexSchedule>? schedules = model.Schedules!;
// Create the operation and run it, but treat it as being set after the
// second but before the third item.
Timekeeper time = context.Resolve<Timekeeper>();
TimeService time = context.Resolve<TimeService>();
ApplySchedules op = context.Resolve<ApplySchedules>()
.WithSchedules(schedules);
.WithGetSchedules(_ => schedules);
var now = Instant.FromUtc(2023, 1, 9, 0, 0);
time.Clock = new FakeClock(now);
@ -98,7 +100,7 @@ public class NumericalPathScheduleTests : TemporalSchedulesTestBase
new Entity().SetAll((UPath)"/chapter-03.md", new TestModel()),
};
var schedules = new List<TestSchedule>
var schedules = new List<TestRegexSchedule>
{
new()
{
@ -109,9 +111,9 @@ public class NumericalPathScheduleTests : TemporalSchedulesTestBase
// Create the operation and run it, but treat it as being set after the
// second but before the third item.
Timekeeper time = context.Resolve<Timekeeper>();
TimeService time = context.Resolve<TimeService>();
ApplySchedules op = context.Resolve<ApplySchedules>()
.WithSchedules(schedules);
.WithGetSchedules(_ => schedules);
var now = Instant.FromUtc(2023, 1, 9, 0, 0);
time.Clock = new FakeClock(now);
@ -140,6 +142,62 @@ public class NumericalPathScheduleTests : TemporalSchedulesTestBase
TestHelper.CompareObjects(expected, actual);
}
[Fact]
public void ScheduleOffsetWorks()
{
using TemporalSchedulesTestContext context = this.CreateContext();
// Create a numerical series of entities.
var input = new List<Entity>
{
new Entity().SetAll((UPath)"/chapter-11.md", new TestModel()),
new Entity().SetAll((UPath)"/chapter-12.md", new TestModel()),
new Entity().SetAll((UPath)"/chapter-13.md", new TestModel()),
};
var schedules = new List<TestRegexSchedule>
{
new()
{
ScheduleStart = DateTime.Parse("2023-01-01"),
CaptureOffset = -11,
Access = "public",
},
};
// Create the operation and run it, but treat it as being set after the
// second but before the third item.
TimeService time = context.Resolve<TimeService>();
ApplySchedules op = context.Resolve<ApplySchedules>()
.WithGetSchedules(_ => schedules);
var now = Instant.FromUtc(2023, 1, 9, 0, 0);
time.Clock = new FakeClock(now);
var actual = op
.Run(input)
.Select(
a => string.Format(
"{0} -- {1} -- {2}",
a.Get<UPath>().ToString(),
a.Has<Instant>()
? time
.ToDateTime(a.Get<Instant>())
.ToString("yyyy-MM-dd")
: "none",
a.Get<TestModel>().Access))
.ToList();
var expected = new List<string>
{
"/chapter-11.md -- 2023-01-01 -- public",
"/chapter-12.md -- 2023-01-08 -- public",
"/chapter-13.md -- none -- private",
};
TestHelper.CompareObjects(expected, actual);
}
[Fact]
public void SequencedScheduleWorks()
{
@ -153,7 +211,7 @@ public class NumericalPathScheduleTests : TemporalSchedulesTestBase
new Entity().SetAll((UPath)"/chapter-03.md", new TestModel()),
};
var schedules = new List<TestSchedule>
var schedules = new List<TestRegexSchedule>
{
new()
{
@ -169,9 +227,9 @@ public class NumericalPathScheduleTests : TemporalSchedulesTestBase
// Create the operation and run it, but treat it as being set after the
// second but before the third item.
Timekeeper time = context.Resolve<Timekeeper>();
TimeService time = context.Resolve<TimeService>();
ApplySchedules op = context.Resolve<ApplySchedules>()
.WithSchedules(schedules);
.WithGetSchedules(_ => schedules);
var now = Instant.FromUtc(2023, 1, 9, 0, 0);
time.Clock = new FakeClock(now);
@ -204,14 +262,14 @@ public class NumericalPathScheduleTests : TemporalSchedulesTestBase
{
public string? Access { get; set; } = "private";
public List<TestSchedule>? Schedules { get; set; }
public List<TestRegexSchedule>? Schedules { get; set; }
}
public class TestSchedule : NumericalPathSchedule
public class TestRegexSchedule : PeriodicPathRegexSchedule
{
public TestSchedule()
public TestRegexSchedule()
{
this.SchedulePeriod = SchedulePeriod.Week;
this.SchedulePeriod = "1 week";
}
public string? Access { get; set; }

View file

@ -21,7 +21,7 @@ public class CreateDateIndexesTests : TemporalTestBase
public void MonthOnlyIndexes()
{
using TemporalTestContext context = this.CreateContext();
Timekeeper timekeeper = context.Resolve<Timekeeper>();
TimeService timeService = context.Resolve<TimeService>();
CreateDateIndexes op = context.Resolve<CreateDateIndexes>()
.WithFormats("yyyy-MM")
@ -30,11 +30,11 @@ public class CreateDateIndexesTests : TemporalTestBase
List<Entity> input = new()
{
new Entity().Add("page1")
.Add(timekeeper.CreateInstant(2021, 1, 2)),
.Add(timeService.CreateInstant(2021, 1, 2)),
new Entity().Add("page2")
.Add(timekeeper.CreateInstant(2021, 2, 2)),
.Add(timeService.CreateInstant(2021, 2, 2)),
new Entity().Add("page3")
.Add(timekeeper.CreateInstant(2022, 1, 2)),
.Add(timeService.CreateInstant(2022, 1, 2)),
};
List<Tuple<string, List<string>?, List<string>?>> actual =
@ -66,7 +66,7 @@ public class CreateDateIndexesTests : TemporalTestBase
public void YearMonthDayIndexes()
{
using TemporalTestContext context = this.CreateContext();
Timekeeper timekeeper = context.Resolve<Timekeeper>();
TimeService timeService = context.Resolve<TimeService>();
CreateDateIndexes op = context.Resolve<CreateDateIndexes>()
.WithFormats("yyyy/MM/dd", "yyyy/MM", "yyyy")
@ -75,11 +75,11 @@ public class CreateDateIndexesTests : TemporalTestBase
List<Entity> input = new()
{
new Entity().Add("page1")
.Add(timekeeper.CreateInstant(2021, 1, 2)),
.Add(timeService.CreateInstant(2021, 1, 2)),
new Entity().Add("page2")
.Add(timekeeper.CreateInstant(2021, 2, 2)),
.Add(timeService.CreateInstant(2021, 2, 2)),
new Entity().Add("page3")
.Add(timekeeper.CreateInstant(2022, 1, 2)),
.Add(timeService.CreateInstant(2022, 1, 2)),
};
List<Tuple<string, List<string>?, List<string>?>> actual =
@ -131,7 +131,7 @@ public class CreateDateIndexesTests : TemporalTestBase
public void YearMonthDayIndexesThreshold1()
{
using TemporalTestContext context = this.CreateContext();
Timekeeper timekeeper = context.Resolve<Timekeeper>();
TimeService timeService = context.Resolve<TimeService>();
CreateDateIndexes op = context.Resolve<CreateDateIndexes>()
.WithFormats("yyyy/MM/dd", "yyyy/MM", "yyyy")
@ -141,11 +141,11 @@ public class CreateDateIndexesTests : TemporalTestBase
List<Entity> input = new()
{
new Entity().Add("page1")
.Add(timekeeper.CreateInstant(2021, 1, 2)),
.Add(timeService.CreateInstant(2021, 1, 2)),
new Entity().Add("page2")
.Add(timekeeper.CreateInstant(2021, 2, 2)),
.Add(timeService.CreateInstant(2021, 2, 2)),
new Entity().Add("page3")
.Add(timekeeper.CreateInstant(2022, 1, 2)),
.Add(timeService.CreateInstant(2022, 1, 2)),
};
List<Tuple<string, List<string>?, List<string>?>> actual =
@ -197,7 +197,7 @@ public class CreateDateIndexesTests : TemporalTestBase
public void YearMonthIndexes()
{
using TemporalTestContext context = this.CreateContext();
Timekeeper timekeeper = context.Resolve<Timekeeper>();
TimeService timeService = context.Resolve<TimeService>();
CreateDateIndexes op = context.Resolve<CreateDateIndexes>()
.WithFormats("yyyy-MM", "yyyy")
@ -206,11 +206,11 @@ public class CreateDateIndexesTests : TemporalTestBase
List<Entity> input = new()
{
new Entity().Add("page1")
.Add(timekeeper.CreateInstant(2021, 1, 2)),
.Add(timeService.CreateInstant(2021, 1, 2)),
new Entity().Add("page2")
.Add(timekeeper.CreateInstant(2021, 2, 2)),
.Add(timeService.CreateInstant(2021, 2, 2)),
new Entity().Add("page3")
.Add(timekeeper.CreateInstant(2022, 1, 2)),
.Add(timeService.CreateInstant(2022, 1, 2)),
};
List<Tuple<string, List<string>?, List<string>?>> actual =
@ -250,7 +250,7 @@ public class CreateDateIndexesTests : TemporalTestBase
public void YearOnlyIndexes()
{
using TemporalTestContext context = this.CreateContext();
Timekeeper timekeeper = context.Resolve<Timekeeper>();
TimeService timeService = context.Resolve<TimeService>();
CreateDateIndexes op = context.Resolve<CreateDateIndexes>()
.WithFormats("yyyy")
@ -259,11 +259,11 @@ public class CreateDateIndexesTests : TemporalTestBase
List<Entity> input = new()
{
new Entity().Add("page1")
.Add(timekeeper.CreateInstant(2021, 1, 2)),
.Add(timeService.CreateInstant(2021, 1, 2)),
new Entity().Add("page2")
.Add(timekeeper.CreateInstant(2021, 2, 2)),
.Add(timeService.CreateInstant(2021, 2, 2)),
new Entity().Add("page3")
.Add(timekeeper.CreateInstant(2022, 1, 2)),
.Add(timeService.CreateInstant(2022, 1, 2)),
};
List<Tuple<string, List<string>?, List<string>?>> actual =

View file

@ -22,10 +22,10 @@ public class FilterOutFutureInstantTests : TemporalTestBase
{
// Create the context and set the timestamp to a constant value.
using TemporalTestContext context = this.CreateContext();
Timekeeper timekeeper = context.Resolve<Timekeeper>();
TimeService timeService = context.Resolve<TimeService>();
var now = Instant.FromUtc(2000, 6, 1, 0, 0);
timekeeper.Clock = new FakeClock(now);
timeService.Clock = new FakeClock(now);
// Create the entities.
List<Entity> input = new()
@ -52,10 +52,10 @@ public class FilterOutFutureInstantTests : TemporalTestBase
{
// Create the context and set the timestamp to a constant value.
using TemporalTestContext context = this.CreateContext();
Timekeeper timekeeper = context.Resolve<Timekeeper>();
TimeService timeService = context.Resolve<TimeService>();
var now = Instant.FromUtc(2000, 6, 1, 0, 0);
timekeeper.Clock = new FakeClock(now);
timeService.Clock = new FakeClock(now);
// Create the entities.
List<Entity> input = new()

View file

@ -6,17 +6,17 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MfGames.Nitride.Temporal\MfGames.Nitride.Temporal.csproj" />
<ProjectReference Include="..\MfGames.Nitride.Tests\MfGames.Nitride.Tests.csproj" />
<ProjectReference Include="..\..\src\MfGames.Nitride.Temporal\MfGames.Nitride.Temporal.csproj"/>
<ProjectReference Include="..\MfGames.Nitride.Tests\MfGames.Nitride.Tests.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="CompareNETObjects" Version="4.78.0" />
<PackageReference Include="MfGames.Gallium" Version="0.4.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1" />
<PackageReference Include="JunitXml.TestLogger" Version="3.0.114" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="CompareNETObjects" Version="4.78.0"/>
<PackageReference Include="MfGames.Gallium" Version="0.4.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1"/>
<PackageReference Include="JunitXml.TestLogger" Version="3.0.114"/>
<PackageReference Include="Newtonsoft.Json" Version="13.0.2"/>
<PackageReference Include="xunit" Version="2.4.2"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View file

@ -8,13 +8,13 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CompareNETObjects" Version="4.78.0" />
<PackageReference Include="MfGames.Gallium" Version="0.4.0" />
<PackageReference Include="MfGames.TestSetup" Version="1.0.6" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1" />
<PackageReference Include="Serilog.Sinks.XUnit" Version="3.0.3" />
<PackageReference Include="JunitXml.TestLogger" Version="3.0.114" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="CompareNETObjects" Version="4.78.0"/>
<PackageReference Include="MfGames.Gallium" Version="0.4.0"/>
<PackageReference Include="MfGames.TestSetup" Version="1.0.6"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1"/>
<PackageReference Include="Serilog.Sinks.XUnit" Version="3.0.3"/>
<PackageReference Include="JunitXml.TestLogger" Version="3.0.114"/>
<PackageReference Include="xunit" Version="2.4.2"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
@ -26,7 +26,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MfGames.Nitride\MfGames.Nitride.csproj" />
<ProjectReference Include="..\..\src\MfGames.Nitride\MfGames.Nitride.csproj"/>
</ItemGroup>
</Project>

View file

@ -6,10 +6,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MfGames.Gallium" Version="0.4.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1" />
<PackageReference Include="JunitXml.TestLogger" Version="3.0.114" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="MfGames.Gallium" Version="0.4.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1"/>
<PackageReference Include="JunitXml.TestLogger" Version="3.0.114"/>
<PackageReference Include="xunit" Version="2.4.2"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@ -21,9 +21,9 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MfGames.Nitride.Tests\MfGames.Nitride.Tests.csproj" />
<ProjectReference Include="..\..\src\MfGames.Nitride.Yaml\MfGames.Nitride.Yaml.csproj" />
<ProjectReference Include="..\..\src\MfGames.Nitride\MfGames.Nitride.csproj" />
<ProjectReference Include="..\MfGames.Nitride.Tests\MfGames.Nitride.Tests.csproj"/>
<ProjectReference Include="..\..\src\MfGames.Nitride.Yaml\MfGames.Nitride.Yaml.csproj"/>
<ProjectReference Include="..\..\src\MfGames.Nitride\MfGames.Nitride.csproj"/>
</ItemGroup>
</Project>