This repository has been archived on 2023-02-02. You can view files and clone it, but cannot push or open issues or pull requests.
mfgames-nitride-cil/src/MfGames.Nitride.Temporal.Sc.../README.md

8.5 KiB

Schedules

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).

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 TimeService.

Configuring

To use the modules, either the NitrideTemporalSchedulesModule can be added or the builder extension method can be used.

NitrideBuilder builder;

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.

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.
  • TimeService TimeService
    • Used to determine when the site is being generated.
    • Defaults to the one provided by MfGames.Nitride.Temporal.

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 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 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.

In this example:

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 PeriodicPathRegexSchedule
    {
        PathRegex = "chapter-(\d+),
        ScheduleStart = DateTime.Parse("2023-01-01"),
        SchedulePeriod = "1 week",
        // Alternatively, SchedulePeriodTimeSpan = TimeSpan.FromDays(7),
    },
}

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.

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+)[^\d]*$ which grabs the last number found.
  • DateTime ScheduleStart
    • The date that the schedule starts.
    • No default.
  • string SchedulePeriod
  • 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.
  • 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 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.

/// <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.

schedules:
  # Patron and Ko-Fi subscribers get it all at once
  - 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])
    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:

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.