236 lines
8.5 KiB
Markdown
236 lines
8.5 KiB
Markdown
# 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 `Timekeeper`.
|
|
|
|
## Configuring
|
|
|
|
To use the modules, either the `NitrideTemporalSchedulesModule` can be added or
|
|
the builder extension method can be used.
|
|
|
|
```csharp
|
|
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.
|
|
|
|
```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`.
|
|
|
|
## 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 `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:
|
|
|
|
```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 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`
|
|
- 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.
|
|
- `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.
|
|
|
|
```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+)
|
|
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:
|
|
|
|
```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.
|