feat(schedules): implemented the basic schedule
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
This commit is contained in:
parent
c73805ae93
commit
d5b975c179
18 changed files with 620 additions and 430 deletions
|
@ -51,6 +51,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MfGames.Nitride.Json", "src
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MfGames.Nitride.Json.Tests", "tests\MfGames.Nitride.Json.Tests\MfGames.Nitride.Json.Tests.csproj", "{7CCC3A82-D5FE-4D54-9751-5E7985DE1F26}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MfGames.Nitride.Temporal.Schedules", "src\MfGames.Nitride.Temporal.Schedules\MfGames.Nitride.Temporal.Schedules.csproj", "{6AC8F985-B11B-44F4-A000-DFEAFEF59754}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MfGames.Nitride.Temporal.Schedules.Tests", "tests\MfGames.Nitride.Temporal.Schedules.Tests\MfGames.Nitride.Temporal.Schedules.Tests.csproj", "{CA009524-E64A-4380-874E-C9D19D868572}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
@ -316,6 +320,30 @@ Global
|
|||
{7CCC3A82-D5FE-4D54-9751-5E7985DE1F26}.Release|x64.Build.0 = Release|Any CPU
|
||||
{7CCC3A82-D5FE-4D54-9751-5E7985DE1F26}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{7CCC3A82-D5FE-4D54-9751-5E7985DE1F26}.Release|x86.Build.0 = Release|Any CPU
|
||||
{6AC8F985-B11B-44F4-A000-DFEAFEF59754}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6AC8F985-B11B-44F4-A000-DFEAFEF59754}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6AC8F985-B11B-44F4-A000-DFEAFEF59754}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{6AC8F985-B11B-44F4-A000-DFEAFEF59754}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{6AC8F985-B11B-44F4-A000-DFEAFEF59754}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{6AC8F985-B11B-44F4-A000-DFEAFEF59754}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{6AC8F985-B11B-44F4-A000-DFEAFEF59754}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6AC8F985-B11B-44F4-A000-DFEAFEF59754}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6AC8F985-B11B-44F4-A000-DFEAFEF59754}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{6AC8F985-B11B-44F4-A000-DFEAFEF59754}.Release|x64.Build.0 = Release|Any CPU
|
||||
{6AC8F985-B11B-44F4-A000-DFEAFEF59754}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{6AC8F985-B11B-44F4-A000-DFEAFEF59754}.Release|x86.Build.0 = Release|Any CPU
|
||||
{CA009524-E64A-4380-874E-C9D19D868572}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CA009524-E64A-4380-874E-C9D19D868572}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CA009524-E64A-4380-874E-C9D19D868572}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{CA009524-E64A-4380-874E-C9D19D868572}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{CA009524-E64A-4380-874E-C9D19D868572}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{CA009524-E64A-4380-874E-C9D19D868572}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{CA009524-E64A-4380-874E-C9D19D868572}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CA009524-E64A-4380-874E-C9D19D868572}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CA009524-E64A-4380-874E-C9D19D868572}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{CA009524-E64A-4380-874E-C9D19D868572}.Release|x64.Build.0 = Release|Any CPU
|
||||
{CA009524-E64A-4380-874E-C9D19D868572}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{CA009524-E64A-4380-874E-C9D19D868572}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{D480943C-764D-4A8A-B546-642ED10586BB} = {570184FD-ECE4-4EC8-86E1-C1265E17D647}
|
||||
|
@ -339,5 +367,7 @@ Global
|
|||
{2AAE2B69-A93D-4045-B7E6-A32ED08D0D65} = {251D9C68-34EB-439D-B167-688BCC47DA17}
|
||||
{9A0D2BEE-859A-4E74-8CA7-5E0FB7C2B113} = {570184FD-ECE4-4EC8-86E1-C1265E17D647}
|
||||
{7CCC3A82-D5FE-4D54-9751-5E7985DE1F26} = {251D9C68-34EB-439D-B167-688BCC47DA17}
|
||||
{6AC8F985-B11B-44F4-A000-DFEAFEF59754} = {570184FD-ECE4-4EC8-86E1-C1265E17D647}
|
||||
{CA009524-E64A-4380-874E-C9D19D868572} = {251D9C68-34EB-439D-B167-688BCC47DA17}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
|
@ -1,6 +1,85 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
using FluentValidation;
|
||||
|
||||
using MfGames.Gallium;
|
||||
using MfGames.Nitride.Generators;
|
||||
|
||||
namespace MfGames.Nitride.Temporal.Schedules;
|
||||
|
||||
public interface ApplySchedules
|
||||
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>
|
||||
[WithProperties]
|
||||
public partial class ApplySchedules : OperationBase
|
||||
{
|
||||
private readonly IValidator<ApplySchedules> validator;
|
||||
|
||||
public ApplySchedules(
|
||||
IValidator<ApplySchedules> validator,
|
||||
Timekeeper timekeeper)
|
||||
{
|
||||
this.Timekeeper = timekeeper;
|
||||
this.validator = validator;
|
||||
this.Schedules = new List<ISchedule>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ordered list of schedules to apply to the entities.
|
||||
/// </summary>
|
||||
public IList<ISchedule> Schedules { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timekeeper associated with this operation.
|
||||
/// </summary>
|
||||
public Timekeeper Timekeeper { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
|
||||
{
|
||||
this.validator.ValidateAndThrow(this);
|
||||
|
||||
return input.Select(this.Apply);
|
||||
}
|
||||
|
||||
public ApplySchedules WithSchedules<TItem>(IEnumerable<TItem> items)
|
||||
where TItem : ISchedule
|
||||
{
|
||||
this.Schedules = items.OfType<ISchedule>().ToList();
|
||||
return this;
|
||||
}
|
||||
|
||||
private Entity Apply(Entity entity)
|
||||
{
|
||||
foreach (ISchedule schedule in this.Schedules)
|
||||
{
|
||||
if (!schedule.CanApply(entity))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
entity = schedule.Apply(entity, this.Timekeeper);
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,17 +8,23 @@ namespace MfGames.Nitride.Temporal.Schedules;
|
|||
/// </summary>
|
||||
public interface ISchedule
|
||||
{
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// <returns>
|
||||
/// The modified entity, if the changes can be applied, otherwise the same
|
||||
/// entity.
|
||||
/// </returns>
|
||||
Entity Apply(
|
||||
Entity entity,
|
||||
Timekeeper timekeeper);
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a schedule applies to a given entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity to test.</param>
|
||||
/// <returns>True if the schedule applies to the given entity, otherwise false.</returns>
|
||||
bool CanApply(Entity entity);
|
||||
|
||||
/// <summary>
|
||||
/// Applies the schedule changes to the entity and returns the results.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity to update.</param>
|
||||
/// <returns>The modified entity, if the changes can be applied, otherwise the same entity.</returns>
|
||||
Entity Apply(Entity entity);
|
||||
}
|
||||
|
|
|
@ -11,12 +11,13 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Autofac" Version="6.4.0" />
|
||||
<PackageReference Include="MfGames.Gallium" Version="0.3.0" />
|
||||
<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" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MfGames.Nitride.Temporal\MfGames.Nitride.Temporal.csproj" />
|
||||
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
@ -4,6 +4,10 @@ using System.Text.RegularExpressions;
|
|||
using MfGames.Gallium;
|
||||
using MfGames.Nitride.Generators;
|
||||
|
||||
using NodaTime;
|
||||
|
||||
using Zio;
|
||||
|
||||
namespace MfGames.Nitride.Temporal.Schedules;
|
||||
|
||||
/// <summary>
|
||||
|
@ -11,7 +15,7 @@ namespace MfGames.Nitride.Temporal.Schedules;
|
|||
/// a starting point and calculates the information from there.
|
||||
/// </summary>
|
||||
[WithProperties]
|
||||
public partial class NumericalPathSchedule
|
||||
public partial class NumericalPathSchedule : ISchedule
|
||||
{
|
||||
public NumericalPathSchedule()
|
||||
{
|
||||
|
@ -21,13 +25,100 @@ public partial class NumericalPathSchedule
|
|||
this.CaptureOffset = -1;
|
||||
}
|
||||
|
||||
public Regex? PathRegex { get; set; }
|
||||
|
||||
public Func<Entity, string> GetPath { get; set; }
|
||||
public DateTime? ScheduleStart { get; set; }
|
||||
public SchedulePeriod SchedulePeriod { get; set; }
|
||||
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,41 +1,180 @@
|
|||
# Date Processing
|
||||
# Schedules
|
||||
|
||||
One of the common features of static websites are blogs which leads to having
|
||||
some form of date-centric processing of pages to build archive pages, calendars,
|
||||
and being able to write posts in the future.
|
||||
Scheduling posts and entries is fairly common process with blob posts but the
|
||||
methods for creating those schedules can vary drastically because it requires
|
||||
site- or project-specific elements such as which components or models to change
|
||||
and how. This package provides one approach to scheduling that "applies"
|
||||
changes (as described in JSON or YAML) to a given object based on the current
|
||||
date (as provided by the `MfGames.Nitride.Temporal` package).
|
||||
|
||||
With the component system, the date of a given file is simply attached to
|
||||
`Nodatime.Instant` component of the `Entity` object for the bulk of the
|
||||
processing.
|
||||
|
||||
## Supporting Time Zones
|
||||
|
||||
The concept of time zones while date processing is one that is frequently
|
||||
overlooked. A date is a date, right? However, most blogs and news sites have a
|
||||
concept of when a new day starts but it isn't always the same time as the server
|
||||
that is building the site. While a blog might be in America/Chicago time, a CI
|
||||
server could be set to UTC (such as Azure build servers) and the
|
||||
"day" may roll over fix or six hours before or after the blog's time.
|
||||
|
||||
This is why Nitride uses `Instant` for when pages are implemented. These are
|
||||
points in time that are independent of time zones, but we also provide tools for
|
||||
converting a date model or one from the path into a proper instant based on the
|
||||
blog's time zone.
|
||||
|
||||
## Why NodaTime?
|
||||
|
||||
We decided to use [NodaTime](https://nodatime.org/) instead of the built-in date
|
||||
time functions for a number of reasons, mainly because it has a more intuitive
|
||||
way of handling time zones
|
||||
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`.
|
||||
|
||||
## Configuring
|
||||
|
||||
There are two callbacks on `NitrideBuilder` that can be used to define the date
|
||||
and time processing for the blog.
|
||||
To use the modules, either the `NitrideTemporalSchedulesModule` can be added or
|
||||
the builder extension method can be used.
|
||||
|
||||
```csharp
|
||||
NitrideBuilder builder;
|
||||
|
||||
builder
|
||||
.ConfigureDates((NitrideClock clock) => clock.SetTimeZone())
|
||||
builder.UseTemporalSchedules();
|
||||
```
|
||||
|
||||
## ISchedule
|
||||
|
||||
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.
|
||||
- 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.
|
||||
|
||||
## ApplySchedules
|
||||
|
||||
The primary operation is `ApplySchedules` which take a sequence of schedule objects
|
||||
and applies each one in turn against every entity given to the operation.
|
||||
|
||||
```csharp
|
||||
IEnumerable<Entity> entities;
|
||||
IList<ISchedule> schedules;
|
||||
ApplySchedules op;
|
||||
|
||||
return op
|
||||
.WithSchedules(schedules)
|
||||
.Run(entities);
|
||||
```
|
||||
|
||||
The following properties are available (along with their corresponding `With*` methods):
|
||||
|
||||
- `IList<ISchedule> Schedules`
|
||||
- Contains the list of schedules to apply against entities.
|
||||
- Defaults to an empty list.
|
||||
- `Timekeeper Timekeeper`
|
||||
- Used to determine when the site is being generated.
|
||||
- Defaults to the one provided by `MfGames.Nitride.Temporal`.
|
||||
|
||||
### Numerical Path-Based 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
|
||||
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.
|
||||
|
||||
In this example:
|
||||
|
||||
```csharp
|
||||
ApplySchedule op;
|
||||
var entities = new List<Entity>
|
||||
{
|
||||
new Entity().Set((UPath) "chapter-01.md"),
|
||||
new Entity().Set((UPath) "chapter-02.md"),
|
||||
new Entity().Set((UPath) "chapter-03.md"),
|
||||
};
|
||||
var schedules = new List<ISchedule>
|
||||
{
|
||||
new NumericalPathSchedule
|
||||
{
|
||||
PathRegex = "chapter-(\d+),
|
||||
ScheduleStart = DateTime.Parse("2023-01-01"),
|
||||
SchedulePeriod = SchedulePeriod.Week,
|
||||
},
|
||||
}
|
||||
|
||||
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:
|
||||
|
||||
- `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.
|
||||
- `DateTime ScheduleStart`
|
||||
- The date that the schedule starts.
|
||||
- No default.
|
||||
- `SchedulePeriod SchedulePeriod`
|
||||
- Values:
|
||||
- Instant (meaning everything at once, default)
|
||||
- Week
|
||||
- Day
|
||||
- `int CaptureGroup`
|
||||
- The numerical index of the capture group.
|
||||
- Defaults to `1` because the first match in Regex is 1.
|
||||
- `int CaptureOffset`
|
||||
- The offset to make the capture group a zero-based number.
|
||||
- Defaults to `-1` which makes `chapter-01` the first entry.
|
||||
|
||||
There is also a virtual method for applying the schedule.
|
||||
|
||||
- `protected Entity ApplySchedule(Entity entity, int number, Instant instant)`
|
||||
- The callback method for applying the schedule to the entity.
|
||||
- This defaults to `return entity.Set(instant)`.
|
||||
|
||||
#### Overriding Logic
|
||||
|
||||
The default operation of a `NumericalPathSchedule` 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.
|
||||
|
||||
```csharp
|
||||
/// <summary>A model for the YAML front matter on a page.</summary>
|
||||
public class PageModel
|
||||
{
|
||||
/// <summary>Gets or sets the access key for the page.</summary>
|
||||
public string? Access { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the optional schedule for this page.</summary>
|
||||
List<PageSchedule>? Schedules { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>A schedule specific to this project.</summary>
|
||||
public class PageSchedule : NumericalPathSchedule
|
||||
{
|
||||
public PageSchedule()
|
||||
{
|
||||
// Set the default to weekly.
|
||||
this.SchedulePeriod = SchedulePeriod.Week;
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the access key for the page.</summary>
|
||||
public Access { get; set; }
|
||||
|
||||
protected override Entity Apply(Entity entity, int number, Instant instant)
|
||||
{
|
||||
var model = entity.Get<PageModel>();
|
||||
model.Access = this.Access;
|
||||
return entity.Set(instant, model);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
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
|
||||
access: public
|
||||
```
|
||||
|
|
|
@ -1,79 +1,17 @@
|
|||
using System;
|
||||
|
||||
using Autofac;
|
||||
|
||||
using MfGames.Nitride.Commands;
|
||||
using MfGames.Nitride.Temporal.Cli;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace MfGames.Nitride.Temporal.Schedules.Setup;
|
||||
|
||||
public static class NitrideTemporalSchedulesBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extends the builder to allow for configuring the temporal
|
||||
/// settings for generation.
|
||||
/// schedules during processing.
|
||||
/// </summary>
|
||||
public static NitrideBuilder UseTemporal(
|
||||
this NitrideBuilder builder,
|
||||
Action<NitrideTemporalConfiguration>? configure = null)
|
||||
public static NitrideBuilder UseTemporalSchedules(
|
||||
this NitrideBuilder builder)
|
||||
{
|
||||
// Get the configuration so we can set the various options.
|
||||
var config = new NitrideTemporalConfiguration();
|
||||
|
||||
configure?.Invoke(config);
|
||||
|
||||
// Add in the module registration.
|
||||
builder.ConfigureContainer(
|
||||
x =>
|
||||
{
|
||||
// Register the module.
|
||||
x.RegisterModule<NitrideTemporalModule>();
|
||||
|
||||
// Add in the CLI options.
|
||||
if (config.AddDateOptionToCommandLine)
|
||||
{
|
||||
x.RegisterType<DatePipelineCommandOption>()
|
||||
.As<IPipelineCommandOption>();
|
||||
}
|
||||
|
||||
if (config.AddExpireOptionToCommandLine
|
||||
&& config.Expiration != null)
|
||||
{
|
||||
x.Register(
|
||||
context =>
|
||||
{
|
||||
ILogger logger = context.Resolve<ILogger>();
|
||||
Timekeeper
|
||||
clock = context.Resolve<Timekeeper>();
|
||||
|
||||
return new ExpiresPipelineCommandOption(
|
||||
logger,
|
||||
clock,
|
||||
config.Expiration);
|
||||
})
|
||||
.As<IPipelineCommandOption>();
|
||||
}
|
||||
});
|
||||
|
||||
if (config.DateTimeZone != null)
|
||||
{
|
||||
builder.ConfigureSite(
|
||||
(
|
||||
_,
|
||||
scope) =>
|
||||
{
|
||||
ILogger logger = scope.Resolve<ILogger>();
|
||||
Timekeeper timekeeper = scope.Resolve<Timekeeper>();
|
||||
|
||||
timekeeper.DateTimeZone = config.DateTimeZone;
|
||||
logger.Verbose(
|
||||
"Setting time zone to {Zone:l}",
|
||||
timekeeper.DateTimeZone);
|
||||
});
|
||||
}
|
||||
|
||||
return builder;
|
||||
return builder.ConfigureContainer(
|
||||
x => x.RegisterModule<NitrideTemporalSchedulesModule>());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
using Autofac;
|
||||
|
||||
using MfGames.Nitride.Temporal.Setup;
|
||||
|
||||
namespace MfGames.Nitride.Temporal.Schedules.Setup;
|
||||
|
||||
public class NitrideTemporalSchedulesModule : Module
|
||||
|
@ -7,14 +9,8 @@ public class NitrideTemporalSchedulesModule : Module
|
|||
/// <inheritdoc />
|
||||
protected override void Load(ContainerBuilder builder)
|
||||
{
|
||||
builder.RegisterModule<NitrideTemporalModule>();
|
||||
builder.RegisterOperators(this);
|
||||
builder.RegisterValidators(this);
|
||||
|
||||
builder.RegisterType<Timekeeper>()
|
||||
.AsSelf()
|
||||
.SingleInstance();
|
||||
|
||||
builder.RegisterGeneric(typeof(SetInstantFromComponent<>))
|
||||
.As(typeof(SetInstantFromComponent<>));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,17 +2,14 @@ using FluentValidation;
|
|||
|
||||
namespace MfGames.Nitride.Temporal.Schedules.Validators;
|
||||
|
||||
public class CreateDateIndexesValidator : AbstractValidator<CreateDateIndexes>
|
||||
public class ApplySchedulesValidator : AbstractValidator<ApplySchedules>
|
||||
{
|
||||
public CreateDateIndexesValidator()
|
||||
public ApplySchedulesValidator()
|
||||
{
|
||||
this.RuleFor(a => a.Timekeeper)
|
||||
.NotNull();
|
||||
this.RuleFor(x => x.Schedules)
|
||||
.NotEmpty();
|
||||
|
||||
this.RuleFor(a => a.CreateIndex)
|
||||
.NotNull();
|
||||
|
||||
this.RuleFor(a => a.Formats)
|
||||
this.RuleFor(x => x.Timekeeper)
|
||||
.NotNull();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
using MfGames.Nitride.Generators;
|
||||
|
||||
namespace MfGames.Nitride.Temporal;
|
||||
|
||||
/// <summary>
|
||||
/// A marker component for identifying a post that can expire.
|
||||
/// </summary>
|
||||
public class CanExpire
|
||||
[SingletonComponent]
|
||||
public partial class CanExpire
|
||||
{
|
||||
public static CanExpire Instance { get; } = new();
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Autofac" Version="6.4.0" />
|
||||
<PackageReference Include="MfGames.Gallium" Version="0.3.0" />
|
||||
<PackageReference Include="MfGames.Gallium" Version="0.4.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="NodaTime" Version="3.1.2" />
|
||||
<PackageReference Include="NodaTime.Testing" Version="3.1.2" />
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
using Autofac;
|
||||
|
||||
namespace MfGames.Nitride.Temporal;
|
||||
namespace MfGames.Nitride.Temporal.Setup;
|
||||
|
||||
public class NitrideTemporalModule : Module
|
||||
{
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CompareNETObjects" Version="4.78.0" />
|
||||
<PackageReference Include="MfGames.Gallium" Version="0.3.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" />
|
||||
|
@ -25,6 +25,7 @@
|
|||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="YamlDotNet" Version="12.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -3,322 +3,228 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
|
||||
using MfGames.Gallium;
|
||||
using MfGames.Nitride.Temporal.Tests;
|
||||
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 TestPageModel
|
||||
public class NumericalPathScheduleTests : TemporalSchedulesTestBase
|
||||
{
|
||||
public string Access { get; set; }
|
||||
}
|
||||
|
||||
public class TestSchedule : NumericalPathSchedule
|
||||
{
|
||||
}
|
||||
|
||||
public class ApplySchedulesTests : TemporalSchedulesTestBase
|
||||
{
|
||||
public ApplySchedulesTests(ITestOutputHelper output)
|
||||
public NumericalPathScheduleTests(ITestOutputHelper output)
|
||||
: base(output)
|
||||
{
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MonthOnlyIndexes()
|
||||
public void DeserializedSetupWorks()
|
||||
{
|
||||
using TemporalSchedulesTestContext context = this.CreateContext();
|
||||
Timekeeper timekeeper = context.Resolve<Timekeeper>();
|
||||
|
||||
CreateDateIndexes op = context.Resolve<CreateDateIndexes>()
|
||||
.WithFormats("yyyy-MM")
|
||||
.WithCreateIndex(this.CreateIndex);
|
||||
|
||||
List<Entity> input = new()
|
||||
// Create a numerical series of entities.
|
||||
var input = new List<Entity>
|
||||
{
|
||||
new Entity().Add("page1")
|
||||
.Add(timekeeper.CreateInstant(2021, 1, 2)),
|
||||
new Entity().Add("page2")
|
||||
.Add(timekeeper.CreateInstant(2021, 2, 2)),
|
||||
new Entity().Add("page3")
|
||||
.Add(timekeeper.CreateInstant(2022, 1, 2)),
|
||||
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()),
|
||||
};
|
||||
|
||||
List<Tuple<string, List<string>?, List<string>?>> actual =
|
||||
this.GetActual(op, input);
|
||||
TestModel model = new DeserializerBuilder()
|
||||
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
||||
.Build()
|
||||
.Deserialize<TestModel>(
|
||||
string.Join(
|
||||
"\n",
|
||||
"---",
|
||||
"access: custom",
|
||||
"schedules:",
|
||||
" - scheduleStart: 2023-01-01",
|
||||
" access: public",
|
||||
""));
|
||||
List<TestSchedule>? schedules = model.Schedules!;
|
||||
|
||||
var expected = new List<Tuple<string, List<string>?, List<string>?>>
|
||||
{
|
||||
new(
|
||||
"index-2021-01",
|
||||
new List<string> { "page1" },
|
||||
new List<string>()),
|
||||
new(
|
||||
"index-2021-02",
|
||||
new List<string> { "page2" },
|
||||
new List<string>()),
|
||||
new(
|
||||
"index-2022-01",
|
||||
new List<string> { "page3" },
|
||||
new List<string>()),
|
||||
new("page1", null, null),
|
||||
new("page2", null, null),
|
||||
new("page3", null, null),
|
||||
};
|
||||
// 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>();
|
||||
ApplySchedules op = context.Resolve<ApplySchedules>()
|
||||
.WithSchedules(schedules);
|
||||
var now = Instant.FromUtc(2023, 1, 9, 0, 0);
|
||||
|
||||
TestHelper.CompareObjects(expected, actual);
|
||||
}
|
||||
time.Clock = new FakeClock(now);
|
||||
|
||||
[Fact]
|
||||
public void YearMonthDayIndexes()
|
||||
{
|
||||
using TemporalSchedulesTestContext context = this.CreateContext();
|
||||
Timekeeper timekeeper = context.Resolve<Timekeeper>();
|
||||
|
||||
CreateDateIndexes op = context.Resolve<CreateDateIndexes>()
|
||||
.WithFormats("yyyy/MM/dd", "yyyy/MM", "yyyy")
|
||||
.WithCreateIndex(this.CreateIndex);
|
||||
|
||||
List<Entity> input = new()
|
||||
{
|
||||
new Entity().Add("page1")
|
||||
.Add(timekeeper.CreateInstant(2021, 1, 2)),
|
||||
new Entity().Add("page2")
|
||||
.Add(timekeeper.CreateInstant(2021, 2, 2)),
|
||||
new Entity().Add("page3")
|
||||
.Add(timekeeper.CreateInstant(2022, 1, 2)),
|
||||
};
|
||||
|
||||
List<Tuple<string, List<string>?, List<string>?>> actual =
|
||||
this.GetActual(op, input);
|
||||
|
||||
var expected = new List<Tuple<string, List<string>?, List<string>?>>
|
||||
{
|
||||
new(
|
||||
"index-2021",
|
||||
new List<string>(),
|
||||
new List<string> { "index-2021/01", "index-2021/02" }),
|
||||
new(
|
||||
"index-2021/01",
|
||||
new List<string>(),
|
||||
new List<string> { "index-2021/01/02" }),
|
||||
new(
|
||||
"index-2021/01/02",
|
||||
new List<string> { "page1" },
|
||||
new List<string>()),
|
||||
new(
|
||||
"index-2021/02",
|
||||
new List<string>(),
|
||||
new List<string> { "index-2021/02/02" }),
|
||||
new(
|
||||
"index-2021/02/02",
|
||||
new List<string> { "page2" },
|
||||
new List<string>()),
|
||||
new(
|
||||
"index-2022",
|
||||
new List<string>(),
|
||||
new List<string> { "index-2022/01" }),
|
||||
new(
|
||||
"index-2022/01",
|
||||
new List<string>(),
|
||||
new List<string> { "index-2022/01/02" }),
|
||||
new(
|
||||
"index-2022/01/02",
|
||||
new List<string> { "page3" },
|
||||
new List<string>()),
|
||||
new("page1", null, null),
|
||||
new("page2", null, null),
|
||||
new("page3", null, null),
|
||||
};
|
||||
|
||||
TestHelper.CompareObjects(expected, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void YearMonthDayIndexesThreshold1()
|
||||
{
|
||||
using TemporalSchedulesTestContext context = this.CreateContext();
|
||||
Timekeeper timekeeper = context.Resolve<Timekeeper>();
|
||||
|
||||
CreateDateIndexes op = context.Resolve<CreateDateIndexes>()
|
||||
.WithFormats("yyyy/MM/dd", "yyyy/MM", "yyyy")
|
||||
.WithCreateIndex(this.CreateIndex)
|
||||
.WithLessThanEqualCollapse(1);
|
||||
|
||||
List<Entity> input = new()
|
||||
{
|
||||
new Entity().Add("page1")
|
||||
.Add(timekeeper.CreateInstant(2021, 1, 2)),
|
||||
new Entity().Add("page2")
|
||||
.Add(timekeeper.CreateInstant(2021, 2, 2)),
|
||||
new Entity().Add("page3")
|
||||
.Add(timekeeper.CreateInstant(2022, 1, 2)),
|
||||
};
|
||||
|
||||
List<Tuple<string, List<string>?, List<string>?>> actual =
|
||||
this.GetActual(op, input);
|
||||
|
||||
var expected = new List<Tuple<string, List<string>?, List<string>?>>
|
||||
{
|
||||
new(
|
||||
"index-2021",
|
||||
new List<string>(),
|
||||
new List<string> { "index-2021/01", "index-2021/02" }),
|
||||
new(
|
||||
"index-2021/01",
|
||||
new List<string> { "page1" },
|
||||
new List<string> { "index-2021/01/02" }),
|
||||
new(
|
||||
"index-2021/01/02",
|
||||
new List<string> { "page1" },
|
||||
new List<string>()),
|
||||
new(
|
||||
"index-2021/02",
|
||||
new List<string> { "page2" },
|
||||
new List<string> { "index-2021/02/02" }),
|
||||
new(
|
||||
"index-2021/02/02",
|
||||
new List<string> { "page2" },
|
||||
new List<string>()),
|
||||
new(
|
||||
"index-2022",
|
||||
new List<string> { "page3" },
|
||||
new List<string> { "index-2022/01" }),
|
||||
new(
|
||||
"index-2022/01",
|
||||
new List<string> { "page3" },
|
||||
new List<string> { "index-2022/01/02" }),
|
||||
new(
|
||||
"index-2022/01/02",
|
||||
new List<string> { "page3" },
|
||||
new List<string>()),
|
||||
new("page1", null, null),
|
||||
new("page2", null, null),
|
||||
new("page3", null, null),
|
||||
};
|
||||
|
||||
TestHelper.CompareObjects(expected, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void YearMonthIndexes()
|
||||
{
|
||||
using TemporalSchedulesTestContext context = this.CreateContext();
|
||||
Timekeeper timekeeper = context.Resolve<Timekeeper>();
|
||||
|
||||
CreateDateIndexes op = context.Resolve<CreateDateIndexes>()
|
||||
.WithFormats("yyyy-MM", "yyyy")
|
||||
.WithCreateIndex(this.CreateIndex);
|
||||
|
||||
List<Entity> input = new()
|
||||
{
|
||||
new Entity().Add("page1")
|
||||
.Add(timekeeper.CreateInstant(2021, 1, 2)),
|
||||
new Entity().Add("page2")
|
||||
.Add(timekeeper.CreateInstant(2021, 2, 2)),
|
||||
new Entity().Add("page3")
|
||||
.Add(timekeeper.CreateInstant(2022, 1, 2)),
|
||||
};
|
||||
|
||||
List<Tuple<string, List<string>?, List<string>?>> actual =
|
||||
this.GetActual(op, input);
|
||||
|
||||
var expected = new List<Tuple<string, List<string>?, List<string>?>>
|
||||
{
|
||||
new(
|
||||
"index-2021",
|
||||
new List<string>(),
|
||||
new List<string> { "index-2021-01", "index-2021-02" }),
|
||||
new(
|
||||
"index-2021-01",
|
||||
new List<string> { "page1" },
|
||||
new List<string>()),
|
||||
new(
|
||||
"index-2021-02",
|
||||
new List<string> { "page2" },
|
||||
new List<string>()),
|
||||
new(
|
||||
"index-2022",
|
||||
new List<string>(),
|
||||
new List<string> { "index-2022-01" }),
|
||||
new(
|
||||
"index-2022-01",
|
||||
new List<string> { "page3" },
|
||||
new List<string>()),
|
||||
new("page1", null, null),
|
||||
new("page2", null, null),
|
||||
new("page3", null, null),
|
||||
};
|
||||
|
||||
TestHelper.CompareObjects(expected, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void YearOnlyIndexes()
|
||||
{
|
||||
using TemporalSchedulesTestContext context = this.CreateContext();
|
||||
Timekeeper timekeeper = context.Resolve<Timekeeper>();
|
||||
|
||||
CreateDateIndexes op = context.Resolve<CreateDateIndexes>()
|
||||
.WithFormats("yyyy")
|
||||
.WithCreateIndex(this.CreateIndex);
|
||||
|
||||
List<Entity> input = new()
|
||||
{
|
||||
new Entity().Add("page1")
|
||||
.Add(timekeeper.CreateInstant(2021, 1, 2)),
|
||||
new Entity().Add("page2")
|
||||
.Add(timekeeper.CreateInstant(2021, 2, 2)),
|
||||
new Entity().Add("page3")
|
||||
.Add(timekeeper.CreateInstant(2022, 1, 2)),
|
||||
};
|
||||
|
||||
List<Tuple<string, List<string>?, List<string>?>> actual =
|
||||
this.GetActual(op, input);
|
||||
|
||||
var expected = new List<Tuple<string, List<string>?, List<string>?>>
|
||||
{
|
||||
new(
|
||||
"index-2021",
|
||||
new List<string> { "page1", "page2" },
|
||||
new List<string>()),
|
||||
new("index-2022", new List<string> { "page3" }, new List<string>()),
|
||||
new("page1", null, null),
|
||||
new("page2", null, null),
|
||||
new("page3", null, null),
|
||||
};
|
||||
|
||||
TestHelper.CompareObjects(expected, actual);
|
||||
}
|
||||
|
||||
private Entity CreateIndex(DateIndex a)
|
||||
{
|
||||
return new Entity().Add(a)
|
||||
.Add($"index-{a.Key}");
|
||||
}
|
||||
|
||||
private List<Tuple<string, List<string>?, List<string>?>> GetActual(
|
||||
CreateDateIndexes op,
|
||||
List<Entity> input)
|
||||
{
|
||||
var actual = op.Run(input)
|
||||
var actual = op
|
||||
.Run(input)
|
||||
.Select(
|
||||
x => new Tuple<string, List<string>?, List<string>?>(
|
||||
x.Get<string>(),
|
||||
x.GetOptional<DateIndex>()
|
||||
?.Entries.Select(a => a.Get<string>())
|
||||
.OrderBy(b => b)
|
||||
.ToList(),
|
||||
x.GetOptional<DateIndex>()
|
||||
?.Indexes.Select(a => a.Get<string>())
|
||||
.OrderBy(b => b)
|
||||
.ToList()))
|
||||
.OrderBy(x => x.Item1)
|
||||
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();
|
||||
|
||||
return actual;
|
||||
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 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 List<TestSchedule>
|
||||
{
|
||||
new()
|
||||
{
|
||||
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.
|
||||
Timekeeper time = context.Resolve<Timekeeper>();
|
||||
ApplySchedules op = context.Resolve<ApplySchedules>()
|
||||
.WithSchedules(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()),
|
||||
};
|
||||
|
||||
var schedules = new List<TestSchedule>
|
||||
{
|
||||
new()
|
||||
{
|
||||
ScheduleStart = DateTime.Parse("2023-01-01"),
|
||||
Access = "subscriber",
|
||||
},
|
||||
new()
|
||||
{
|
||||
ScheduleStart = DateTime.Parse("2023-01-07"),
|
||||
Access = "public",
|
||||
},
|
||||
};
|
||||
|
||||
// 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>();
|
||||
ApplySchedules op = context.Resolve<ApplySchedules>()
|
||||
.WithSchedules(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-07 -- public",
|
||||
"/chapter-02.md -- 2023-01-08 -- subscriber",
|
||||
"/chapter-03.md -- none -- private",
|
||||
};
|
||||
|
||||
TestHelper.CompareObjects(expected, actual);
|
||||
}
|
||||
|
||||
public class TestModel
|
||||
{
|
||||
public string? Access { get; set; } = "private";
|
||||
|
||||
public List<TestSchedule>? Schedules { get; set; }
|
||||
}
|
||||
|
||||
public class TestSchedule : NumericalPathSchedule
|
||||
{
|
||||
public TestSchedule()
|
||||
{
|
||||
this.SchedulePeriod = SchedulePeriod.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,8 @@ using Xunit.Abstractions;
|
|||
|
||||
namespace MfGames.Nitride.Temporal.Schedules.Tests;
|
||||
|
||||
public abstract class TemporalSchedulesTestBase : TestBase<TemporalTestContext>
|
||||
public abstract class TemporalSchedulesTestBase
|
||||
: TestBase<TemporalSchedulesTestContext>
|
||||
{
|
||||
protected TemporalSchedulesTestBase(ITestOutputHelper output)
|
||||
: base(output)
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
using Autofac;
|
||||
|
||||
using MfGames.Nitride.Temporal.Schedules.Setup;
|
||||
using MfGames.Nitride.Temporal.Setup;
|
||||
using MfGames.Nitride.Tests;
|
||||
|
||||
namespace MfGames.Nitride.Temporal.Schedules.Tests;
|
||||
|
||||
public class TemporalTestContext : NitrideTestContext
|
||||
public class TemporalSchedulesTestContext : NitrideTestContext
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void ConfigureContainer(ContainerBuilder builder)
|
||||
{
|
||||
base.ConfigureContainer(builder);
|
||||
builder.RegisterModule<NitrideTemporalModule>();
|
||||
builder.RegisterModule<NitrideTemporalSchedulesModule>();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CompareNETObjects" Version="4.78.0" />
|
||||
<PackageReference Include="MfGames.Gallium" Version="0.3.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" />
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using Autofac;
|
||||
|
||||
using MfGames.Nitride.Temporal.Setup;
|
||||
using MfGames.Nitride.Tests;
|
||||
|
||||
namespace MfGames.Nitride.Temporal.Tests;
|
||||
|
|
Reference in a new issue