diff --git a/MfGames.Nitride.sln b/MfGames.Nitride.sln
index 1e0b9d5..013765b 100644
--- a/MfGames.Nitride.sln
+++ b/MfGames.Nitride.sln
@@ -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
diff --git a/src/MfGames.Nitride.Temporal.Schedules/ApplySchedules.cs b/src/MfGames.Nitride.Temporal.Schedules/ApplySchedules.cs
index 55378d3..82eae2e 100644
--- a/src/MfGames.Nitride.Temporal.Schedules/ApplySchedules.cs
+++ b/src/MfGames.Nitride.Temporal.Schedules/ApplySchedules.cs
@@ -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
{
-
+ ///
+ /// Indicates that all the matching schedule periods are instant (zero time).
+ ///
+ Instant,
+
+ ///
+ /// Indicates that the entities will be scheduled a day apart.
+ ///
+ Day,
+
+ ///
+ /// Indicates that the entities will be scheduled seven days apart.
+ ///
+ Week,
+}
+
+///
+/// Applies schedules against the list of entities.
+///
+[WithProperties]
+public partial class ApplySchedules : OperationBase
+{
+ private readonly IValidator validator;
+
+ public ApplySchedules(
+ IValidator validator,
+ Timekeeper timekeeper)
+ {
+ this.Timekeeper = timekeeper;
+ this.validator = validator;
+ this.Schedules = new List();
+ }
+
+ ///
+ /// Gets or sets the ordered list of schedules to apply to the entities.
+ ///
+ public IList Schedules { get; set; }
+
+ ///
+ /// Gets or sets the timekeeper associated with this operation.
+ ///
+ public Timekeeper Timekeeper { get; set; }
+
+ ///
+ public override IEnumerable Run(IEnumerable input)
+ {
+ this.validator.ValidateAndThrow(this);
+
+ return input.Select(this.Apply);
+ }
+
+ public ApplySchedules WithSchedules(IEnumerable items)
+ where TItem : ISchedule
+ {
+ this.Schedules = items.OfType().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;
+ }
}
diff --git a/src/MfGames.Nitride.Temporal.Schedules/ISchedule.cs b/src/MfGames.Nitride.Temporal.Schedules/ISchedule.cs
index 3d556ec..a1fa15b 100644
--- a/src/MfGames.Nitride.Temporal.Schedules/ISchedule.cs
+++ b/src/MfGames.Nitride.Temporal.Schedules/ISchedule.cs
@@ -8,17 +8,23 @@ namespace MfGames.Nitride.Temporal.Schedules;
///
public interface ISchedule
{
+ ///
+ /// Applies the schedule changes to the entity and returns the results.
+ ///
+ /// The entity to update.
+ /// The timekeeper for apply.
+ ///
+ /// The modified entity, if the changes can be applied, otherwise the same
+ /// entity.
+ ///
+ Entity Apply(
+ Entity entity,
+ Timekeeper timekeeper);
+
///
/// Determines if a schedule applies to a given entity.
///
/// The entity to test.
/// True if the schedule applies to the given entity, otherwise false.
bool CanApply(Entity entity);
-
- ///
- /// Applies the schedule changes to the entity and returns the results.
- ///
- /// The entity to update.
- /// The modified entity, if the changes can be applied, otherwise the same entity.
- Entity Apply(Entity entity);
}
diff --git a/src/MfGames.Nitride.Temporal.Schedules/MfGames.Nitride.Temporal.Schedules.csproj b/src/MfGames.Nitride.Temporal.Schedules/MfGames.Nitride.Temporal.Schedules.csproj
index ee081a1..9871895 100644
--- a/src/MfGames.Nitride.Temporal.Schedules/MfGames.Nitride.Temporal.Schedules.csproj
+++ b/src/MfGames.Nitride.Temporal.Schedules/MfGames.Nitride.Temporal.Schedules.csproj
@@ -11,12 +11,13 @@
-
+
+
diff --git a/src/MfGames.Nitride.Temporal.Schedules/NumericalPathSchedule.cs b/src/MfGames.Nitride.Temporal.Schedules/NumericalPathSchedule.cs
index c964160..cd95618 100644
--- a/src/MfGames.Nitride.Temporal.Schedules/NumericalPathSchedule.cs
+++ b/src/MfGames.Nitride.Temporal.Schedules/NumericalPathSchedule.cs
@@ -4,6 +4,10 @@ using System.Text.RegularExpressions;
using MfGames.Gallium;
using MfGames.Nitride.Generators;
+using NodaTime;
+
+using Zio;
+
namespace MfGames.Nitride.Temporal.Schedules;
///
@@ -11,7 +15,7 @@ namespace MfGames.Nitride.Temporal.Schedules;
/// a starting point and calculates the information from there.
///
[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 GetPath { get; set; }
- public DateTime? ScheduleStart { get; set; }
- public SchedulePeriod SchedulePeriod { get; set; }
-
+ ///
+ /// Gets or sets the group number of the capture group with 1 being being the
+ /// first group in PathRegex.
+ ///
public int CaptureGroup { get; set; }
+ ///
+ /// Gets or sets the offset to make the resulting capture group a zero-based
+ /// number.
+ ///
public int CaptureOffset { get; set; }
+
+ ///
+ /// Gets or sets the method for retrieving the path for the entity.
+ ///
+ public Func GetPath { get; set; }
+
+ ///
+ /// Gets or sets the regular expression to identify a matching path.
+ ///
+ public Regex? PathRegex { get; set; }
+
+ ///
+ /// Gets or sets the period between each matching entity. More precisely,
+ /// the schedule will be TimeSpan * (CaptureGroup + CaptureOffset).
+ ///
+ public SchedulePeriod SchedulePeriod { get; set; }
+
+ ///
+ /// Gets or sets when the first item is scheduled.
+ ///
+ public DateTime? ScheduleStart { get; set; }
+
+ ///
+ 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);
+ }
+
+ ///
+ 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);
+ }
}
diff --git a/src/MfGames.Nitride.Temporal.Schedules/README.md b/src/MfGames.Nitride.Temporal.Schedules/README.md
index ee08cbc..a08c9eb 100644
--- a/src/MfGames.Nitride.Temporal.Schedules/README.md
+++ b/src/MfGames.Nitride.Temporal.Schedules/README.md
@@ -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 entities;
+IList schedules;
+ApplySchedules op;
+
+return op
+ .WithSchedules(schedules)
+ .Run(entities);
+```
+
+The following properties are available (along with their corresponding `With*` methods):
+
+- `IList 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
+{
+ 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
+{
+ 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 GetPath`
+ - An override to allow retrieving a different function.
+ - Defaults to `entity.Get().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
+/// A model for the YAML front matter on a page.
+public class PageModel
+{
+ /// Gets or sets the access key for the page.
+ public string? Access { get; set; }
+
+ /// Gets or sets the optional schedule for this page.
+ List? Schedules { get; set; }
+}
+
+/// A schedule specific to this project.
+public class PageSchedule : NumericalPathSchedule
+{
+ public PageSchedule()
+ {
+ // Set the default to weekly.
+ this.SchedulePeriod = SchedulePeriod.Week;
+ }
+
+ /// Gets or sets the access key for the page.
+ public Access { get; set; }
+
+ protected override Entity Apply(Entity entity, int number, Instant instant)
+ {
+ var model = entity.Get();
+ 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
```
diff --git a/src/MfGames.Nitride.Temporal.Schedules/Setup/NitrideTemporalSchedulesBuilderExtensions.cs b/src/MfGames.Nitride.Temporal.Schedules/Setup/NitrideTemporalSchedulesBuilderExtensions.cs
index 7234447..53fa846 100644
--- a/src/MfGames.Nitride.Temporal.Schedules/Setup/NitrideTemporalSchedulesBuilderExtensions.cs
+++ b/src/MfGames.Nitride.Temporal.Schedules/Setup/NitrideTemporalSchedulesBuilderExtensions.cs
@@ -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
{
///
/// Extends the builder to allow for configuring the temporal
- /// settings for generation.
+ /// schedules during processing.
///
- public static NitrideBuilder UseTemporal(
- this NitrideBuilder builder,
- Action? 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();
-
- // Add in the CLI options.
- if (config.AddDateOptionToCommandLine)
- {
- x.RegisterType()
- .As();
- }
-
- if (config.AddExpireOptionToCommandLine
- && config.Expiration != null)
- {
- x.Register(
- context =>
- {
- ILogger logger = context.Resolve();
- Timekeeper
- clock = context.Resolve();
-
- return new ExpiresPipelineCommandOption(
- logger,
- clock,
- config.Expiration);
- })
- .As();
- }
- });
-
- if (config.DateTimeZone != null)
- {
- builder.ConfigureSite(
- (
- _,
- scope) =>
- {
- ILogger logger = scope.Resolve();
- Timekeeper timekeeper = scope.Resolve();
-
- timekeeper.DateTimeZone = config.DateTimeZone;
- logger.Verbose(
- "Setting time zone to {Zone:l}",
- timekeeper.DateTimeZone);
- });
- }
-
- return builder;
+ return builder.ConfigureContainer(
+ x => x.RegisterModule());
}
}
diff --git a/src/MfGames.Nitride.Temporal.Schedules/Setup/NitrideTemporalSchedulesModule.cs b/src/MfGames.Nitride.Temporal.Schedules/Setup/NitrideTemporalSchedulesModule.cs
index ab79259..8e0d108 100644
--- a/src/MfGames.Nitride.Temporal.Schedules/Setup/NitrideTemporalSchedulesModule.cs
+++ b/src/MfGames.Nitride.Temporal.Schedules/Setup/NitrideTemporalSchedulesModule.cs
@@ -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
///
protected override void Load(ContainerBuilder builder)
{
+ builder.RegisterModule();
builder.RegisterOperators(this);
builder.RegisterValidators(this);
-
- builder.RegisterType()
- .AsSelf()
- .SingleInstance();
-
- builder.RegisterGeneric(typeof(SetInstantFromComponent<>))
- .As(typeof(SetInstantFromComponent<>));
}
}
diff --git a/src/MfGames.Nitride.Temporal.Schedules/Validators/ApplySchedulesValidator.cs b/src/MfGames.Nitride.Temporal.Schedules/Validators/ApplySchedulesValidator.cs
index b2a317c..e525913 100644
--- a/src/MfGames.Nitride.Temporal.Schedules/Validators/ApplySchedulesValidator.cs
+++ b/src/MfGames.Nitride.Temporal.Schedules/Validators/ApplySchedulesValidator.cs
@@ -2,17 +2,14 @@ using FluentValidation;
namespace MfGames.Nitride.Temporal.Schedules.Validators;
-public class CreateDateIndexesValidator : AbstractValidator
+public class ApplySchedulesValidator : AbstractValidator
{
- 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();
}
}
diff --git a/src/MfGames.Nitride.Temporal/CanExpire.cs b/src/MfGames.Nitride.Temporal/CanExpire.cs
index 3aa9efb..2b425b4 100644
--- a/src/MfGames.Nitride.Temporal/CanExpire.cs
+++ b/src/MfGames.Nitride.Temporal/CanExpire.cs
@@ -1,9 +1,11 @@
+using MfGames.Nitride.Generators;
+
namespace MfGames.Nitride.Temporal;
///
/// A marker component for identifying a post that can expire.
///
-public class CanExpire
+[SingletonComponent]
+public partial class CanExpire
{
- public static CanExpire Instance { get; } = new();
}
diff --git a/src/MfGames.Nitride.Temporal/MfGames.Nitride.Temporal.csproj b/src/MfGames.Nitride.Temporal/MfGames.Nitride.Temporal.csproj
index c51c119..1f29e72 100644
--- a/src/MfGames.Nitride.Temporal/MfGames.Nitride.Temporal.csproj
+++ b/src/MfGames.Nitride.Temporal/MfGames.Nitride.Temporal.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/src/MfGames.Nitride.Temporal/Setup/NitrideTemporalModule.cs b/src/MfGames.Nitride.Temporal/Setup/NitrideTemporalModule.cs
index ddd98dd..e27c746 100644
--- a/src/MfGames.Nitride.Temporal/Setup/NitrideTemporalModule.cs
+++ b/src/MfGames.Nitride.Temporal/Setup/NitrideTemporalModule.cs
@@ -1,6 +1,6 @@
using Autofac;
-namespace MfGames.Nitride.Temporal;
+namespace MfGames.Nitride.Temporal.Setup;
public class NitrideTemporalModule : Module
{
diff --git a/tests/MfGames.Nitride.Temporal.Schedules.Tests/MfGames.Nitride.Temporal.Schedules.Tests.csproj b/tests/MfGames.Nitride.Temporal.Schedules.Tests/MfGames.Nitride.Temporal.Schedules.Tests.csproj
index da6250d..b0e4ff2 100644
--- a/tests/MfGames.Nitride.Temporal.Schedules.Tests/MfGames.Nitride.Temporal.Schedules.Tests.csproj
+++ b/tests/MfGames.Nitride.Temporal.Schedules.Tests/MfGames.Nitride.Temporal.Schedules.Tests.csproj
@@ -12,7 +12,7 @@
-
+
@@ -25,6 +25,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
all
+
diff --git a/tests/MfGames.Nitride.Temporal.Schedules.Tests/NumericalPathScheduleTests.cs b/tests/MfGames.Nitride.Temporal.Schedules.Tests/NumericalPathScheduleTests.cs
index c161cce..72ca49c 100644
--- a/tests/MfGames.Nitride.Temporal.Schedules.Tests/NumericalPathScheduleTests.cs
+++ b/tests/MfGames.Nitride.Temporal.Schedules.Tests/NumericalPathScheduleTests.cs
@@ -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();
- CreateDateIndexes op = context.Resolve()
- .WithFormats("yyyy-MM")
- .WithCreateIndex(this.CreateIndex);
-
- List input = new()
+ // Create a numerical series of entities.
+ var input = new List
{
- 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?, List?>> actual =
- this.GetActual(op, input);
+ TestModel model = new DeserializerBuilder()
+ .WithNamingConvention(CamelCaseNamingConvention.Instance)
+ .Build()
+ .Deserialize(
+ string.Join(
+ "\n",
+ "---",
+ "access: custom",
+ "schedules:",
+ " - scheduleStart: 2023-01-01",
+ " access: public",
+ ""));
+ List? schedules = model.Schedules!;
- var expected = new List?, List?>>
- {
- new(
- "index-2021-01",
- new List { "page1" },
- new List()),
- new(
- "index-2021-02",
- new List { "page2" },
- new List()),
- new(
- "index-2022-01",
- new List { "page3" },
- new List()),
- 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();
+ ApplySchedules op = context.Resolve()
+ .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();
-
- CreateDateIndexes op = context.Resolve()
- .WithFormats("yyyy/MM/dd", "yyyy/MM", "yyyy")
- .WithCreateIndex(this.CreateIndex);
-
- List 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?, List?>> actual =
- this.GetActual(op, input);
-
- var expected = new List?, List?>>
- {
- new(
- "index-2021",
- new List(),
- new List { "index-2021/01", "index-2021/02" }),
- new(
- "index-2021/01",
- new List(),
- new List { "index-2021/01/02" }),
- new(
- "index-2021/01/02",
- new List { "page1" },
- new List()),
- new(
- "index-2021/02",
- new List(),
- new List { "index-2021/02/02" }),
- new(
- "index-2021/02/02",
- new List { "page2" },
- new List()),
- new(
- "index-2022",
- new List(),
- new List { "index-2022/01" }),
- new(
- "index-2022/01",
- new List(),
- new List { "index-2022/01/02" }),
- new(
- "index-2022/01/02",
- new List { "page3" },
- new List()),
- 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();
-
- CreateDateIndexes op = context.Resolve()
- .WithFormats("yyyy/MM/dd", "yyyy/MM", "yyyy")
- .WithCreateIndex(this.CreateIndex)
- .WithLessThanEqualCollapse(1);
-
- List 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?, List?>> actual =
- this.GetActual(op, input);
-
- var expected = new List?, List?>>
- {
- new(
- "index-2021",
- new List(),
- new List { "index-2021/01", "index-2021/02" }),
- new(
- "index-2021/01",
- new List { "page1" },
- new List { "index-2021/01/02" }),
- new(
- "index-2021/01/02",
- new List { "page1" },
- new List()),
- new(
- "index-2021/02",
- new List { "page2" },
- new List { "index-2021/02/02" }),
- new(
- "index-2021/02/02",
- new List { "page2" },
- new List()),
- new(
- "index-2022",
- new List { "page3" },
- new List { "index-2022/01" }),
- new(
- "index-2022/01",
- new List { "page3" },
- new List { "index-2022/01/02" }),
- new(
- "index-2022/01/02",
- new List { "page3" },
- new List()),
- 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();
-
- CreateDateIndexes op = context.Resolve()
- .WithFormats("yyyy-MM", "yyyy")
- .WithCreateIndex(this.CreateIndex);
-
- List 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?, List?>> actual =
- this.GetActual(op, input);
-
- var expected = new List?, List?>>
- {
- new(
- "index-2021",
- new List(),
- new List { "index-2021-01", "index-2021-02" }),
- new(
- "index-2021-01",
- new List { "page1" },
- new List()),
- new(
- "index-2021-02",
- new List { "page2" },
- new List()),
- new(
- "index-2022",
- new List(),
- new List { "index-2022-01" }),
- new(
- "index-2022-01",
- new List { "page3" },
- new List()),
- 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();
-
- CreateDateIndexes op = context.Resolve()
- .WithFormats("yyyy")
- .WithCreateIndex(this.CreateIndex);
-
- List 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?, List?>> actual =
- this.GetActual(op, input);
-
- var expected = new List?, List?>>
- {
- new(
- "index-2021",
- new List { "page1", "page2" },
- new List()),
- new("index-2022", new List { "page3" }, new List()),
- 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?, List?>> GetActual(
- CreateDateIndexes op,
- List input)
- {
- var actual = op.Run(input)
+ var actual = op
+ .Run(input)
.Select(
- x => new Tuple?, List?>(
- x.Get(),
- x.GetOptional()
- ?.Entries.Select(a => a.Get())
- .OrderBy(b => b)
- .ToList(),
- x.GetOptional()
- ?.Indexes.Select(a => a.Get())
- .OrderBy(b => b)
- .ToList()))
- .OrderBy(x => x.Item1)
+ a => string.Format(
+ "{0} -- {1} -- {2}",
+ a.Get().ToString(),
+ a.Has()
+ ? time
+ .ToDateTime(a.Get())
+ .ToString("yyyy-MM-dd")
+ : "none",
+ a.Get().Access))
.ToList();
- return actual;
+ var expected = new List
+ {
+ "/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
+ {
+ 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
+ {
+ 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();
+ ApplySchedules op = context.Resolve()
+ .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().ToString(),
+ a.Has()
+ ? time
+ .ToDateTime(a.Get())
+ .ToString("yyyy-MM-dd")
+ : "none",
+ a.Get().Access))
+ .ToList();
+
+ var expected = new List
+ {
+ "/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
+ {
+ 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
+ {
+ 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();
+ ApplySchedules op = context.Resolve()
+ .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().ToString(),
+ a.Has()
+ ? time
+ .ToDateTime(a.Get())
+ .ToString("yyyy-MM-dd")
+ : "none",
+ a.Get().Access))
+ .ToList();
+
+ var expected = new List
+ {
+ "/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? Schedules { get; set; }
+ }
+
+ public class TestSchedule : NumericalPathSchedule
+ {
+ public TestSchedule()
+ {
+ this.SchedulePeriod = SchedulePeriod.Week;
+ }
+
+ public string? Access { get; set; }
+
+ ///
+ protected override Entity Apply(
+ Entity entity,
+ int number,
+ Instant instant)
+ {
+ TestModel model = entity.Get();
+ model.Access = this.Access;
+ return entity.SetAll(instant, model);
+ }
}
}
diff --git a/tests/MfGames.Nitride.Temporal.Schedules.Tests/TemporalSchedulesTestBase.cs b/tests/MfGames.Nitride.Temporal.Schedules.Tests/TemporalSchedulesTestBase.cs
index a332cb7..8f989d4 100644
--- a/tests/MfGames.Nitride.Temporal.Schedules.Tests/TemporalSchedulesTestBase.cs
+++ b/tests/MfGames.Nitride.Temporal.Schedules.Tests/TemporalSchedulesTestBase.cs
@@ -4,7 +4,8 @@ using Xunit.Abstractions;
namespace MfGames.Nitride.Temporal.Schedules.Tests;
-public abstract class TemporalSchedulesTestBase : TestBase
+public abstract class TemporalSchedulesTestBase
+ : TestBase
{
protected TemporalSchedulesTestBase(ITestOutputHelper output)
: base(output)
diff --git a/tests/MfGames.Nitride.Temporal.Schedules.Tests/TemporalSchedulesTestContext.cs b/tests/MfGames.Nitride.Temporal.Schedules.Tests/TemporalSchedulesTestContext.cs
index 3bc1770..999f6e3 100644
--- a/tests/MfGames.Nitride.Temporal.Schedules.Tests/TemporalSchedulesTestContext.cs
+++ b/tests/MfGames.Nitride.Temporal.Schedules.Tests/TemporalSchedulesTestContext.cs
@@ -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
{
///
protected override void ConfigureContainer(ContainerBuilder builder)
{
base.ConfigureContainer(builder);
builder.RegisterModule();
+ builder.RegisterModule();
}
}
diff --git a/tests/MfGames.Nitride.Temporal.Tests/MfGames.Nitride.Temporal.Tests.csproj b/tests/MfGames.Nitride.Temporal.Tests/MfGames.Nitride.Temporal.Tests.csproj
index 1e9f90c..b4120aa 100644
--- a/tests/MfGames.Nitride.Temporal.Tests/MfGames.Nitride.Temporal.Tests.csproj
+++ b/tests/MfGames.Nitride.Temporal.Tests/MfGames.Nitride.Temporal.Tests.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/tests/MfGames.Nitride.Temporal.Tests/TemporalTestContext.cs b/tests/MfGames.Nitride.Temporal.Tests/TemporalTestContext.cs
index 186502f..5218afa 100644
--- a/tests/MfGames.Nitride.Temporal.Tests/TemporalTestContext.cs
+++ b/tests/MfGames.Nitride.Temporal.Tests/TemporalTestContext.cs
@@ -1,5 +1,6 @@
using Autofac;
+using MfGames.Nitride.Temporal.Setup;
using MfGames.Nitride.Tests;
namespace MfGames.Nitride.Temporal.Tests;