using System; using System.Collections.Generic; using System.Linq; using FluentValidation; using MfGames.Gallium; using Serilog; namespace MfGames.Nitride.Entities; /// /// A Nitride operation that creates and merges entities that are intended /// to be indexes of another entity. Examples of this would be year and month /// archive pages for a blog or a tag/category pages for associated data. This /// uses the scanner to determine how many index entities are needed and then /// merges existing entities with their data or creates new indexes for ones /// that don't already have an index. /// /// /// This makes the assumption that there is one index per page. /// [WithProperties] public partial class CreateOrUpdateIndex : OperationBase { private readonly ILogger logger; private readonly IValidator validator; public CreateOrUpdateIndex( ILogger logger, IValidator validator) { this.validator = validator; this.logger = logger.ForContext(typeof(CreateOrUpdateIndex)); } /// /// Creates an index for a given key. This will not be called for any /// index that has been already created. If this is null, no new indexes /// will be made. /// public Func, Entity>? CreateIndex { get; set; } = null!; /// /// Gets or sets the function to retrieve the key from an existing /// index page. If this returns null, then the entity is considered not /// to be an index page. /// public Func GetIndexKey { get; set; } = null!; /// /// Gets or sets the scanner that provides the keys. /// public EntityScanner Scanner { get; set; } = null!; /// /// Updates an existing index entity to include new information. /// public Func, Entity> UpdateIndex { get; set; } = null!; /// public override IEnumerable Run(IEnumerable input) { // Make sure we have sane data. this.validator.ValidateAndThrow(this); // Get the list of all the scanned entities. var scanned = this.Scanner.GetScannedResults() .ToDictionary(x => x.Key, x => x.Value); // We loop through the results and look for index entities. Any one we // find, we update with the existing entries. If we get to the end and // still have any left over, we create those pages. HashSet existing = new(); foreach (Entity? entity in input) { // See if this entity is an index for anything. string? key = this.GetIndexKey(entity); if (key == null) { // Not an index page, we don't need to pay attention. yield return entity; } else { // This is an existing entity page that needs to be updated. IEnumerable entries = scanned.TryGetValue(key, out List? list) ? list : Array.Empty(); existing.Add(key); yield return this.UpdateIndex(entity, key, entries); } } // Once we're done with the list, we need to create the missing indexes. if (this.CreateIndex != null) { foreach (string key in scanned.Keys.Where( key => !existing.Contains(key))) { yield return this.CreateIndex(key, scanned[key]); } } // Report the results. this.logger.Debug( "Found {Old:N0} and created {New:N0} index pages for {Keys:N0} keys", existing.Count, scanned.Count - existing.Count, scanned.Keys.Count()); } }