using System; using System.Collections.Generic; using FluentValidation; using Gallium; using Serilog; namespace Nitride.Entities; /// /// A Nitride operation that creates and merges entities that are intended /// to be indexes of another entity. For example, this could be year and /// month archive pages, tag or category pages. Support is given for /// merging existing pages so a description could be written from a file /// and then the index logic is automatically added. /// public class CreateIndexEntities : OperationBase where TIndexKey : notnull { // TODO: This does not use [WithProperties] because the source generator hasn't been taught how to do generics. private readonly ILogger logger; private readonly IValidator> validator; public CreateIndexEntities(ILogger logger) { // TODO: Figure out why Autofac won't let us register IValidator of generic classes. this.validator = new CreateIndexEntitiesValidator(); this.logger = logger.ForContext(typeof(CreateIndexEntities<>)); } /// /// Creates an index for a given key. This will not be called for any /// index that has been already created. /// public Func, Entity>? CreateIndexEntity { get; set; } /// /// 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? GetIndexEntityKey { get; set; } /// /// A method that gets the keys for a given entity. If this returns an /// empty list, then the entity will not added to an index. /// public Func>? GetIndexKeys { get; set; } /// /// Updates an existing index entity to include new information. /// public Func, Entity>? UpdateIndexEntity { get; set; } /// public override IEnumerable Run(IEnumerable input) { // Make sure we have sane data. this.validator.ValidateAndThrow(this); // We need to process two lists out of the output, so we need to put // it into a list so we can enumerate through it twice. This will // also cause the output to be reordered. Dictionary indexes = new(); Dictionary> indexed = new(); List results = new(); foreach (Entity? entity in input) { // See if we are an index page first. if (this.GetIndexEntityKey != null) { TIndexKey? indexKey = this.GetIndexEntityKey(entity); if (indexKey != null) { indexes[indexKey] = entity; continue; } } // We aren't an index, so check to see if this entity is // something to be indexed. foreach (TIndexKey indexedKey in this.GetIndexKeys!(entity)) { if (!indexed.TryGetValue(indexedKey, out List? list)) { indexed[indexedKey] = list = new List(); } list.Add(entity); } // Add to the non-index page list. results.Add(entity); } // Go through all the index pages and update them. We get a list of // all the pages in the index and pass them into the function to // update the existing index. Then we update the entity and add it // to the bottom of the results list. foreach ((TIndexKey key, Entity? oldIndex) in indexes) { if (!indexed.TryGetValue(key, out List? list)) { list = new List(); } Entity newEntity = this.UpdateIndexEntity!(oldIndex, key, list); results.Add(newEntity); } // Go through all the known index keys and create the missing pages. int created = 0; foreach ((TIndexKey key, List? list) in indexed) { // See if we already have a page, if we do, then we've already // processed that page and don't have to do anything. if (indexes.ContainsKey(key)) { continue; } // We don't have that page and need to add it to the list. Entity entity = this.CreateIndexEntity!(key, list); created++; results.Add(entity); } // Return the combined together version. this.logger.Debug( "Found {Old:N0} and created {New:N0} index pages for {Keys:N0} keys", indexes.Count, created, indexed.Count); return results; } public CreateIndexEntities WithCreateIndexEntity(Func, Entity>? callback) { this.CreateIndexEntity = callback; return this; } public CreateIndexEntities WithGetIndexEntityKey(Func? callback) { this.GetIndexEntityKey = callback; return this; } public CreateIndexEntities WithGetIndexKeys(Func>? callback) { this.GetIndexKeys = callback; return this; } public CreateIndexEntities WithUpdateIndexEntity( Func, Entity>? callback) { this.UpdateIndexEntity = callback; return this; } }