using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using FluentValidation; using MfGames.Gallium; namespace MfGames.Nitride.Entities; /// /// Implements a Nitride operation that scans the entities as they are /// passed through the `Run` method and gathers information into a /// `Dictionary<string, List<Entity>>` which then can be /// queries by later operations. This handles making sure the entire /// input has been processed before operating. /// [WithProperties] public partial class EntityScanner : OperationBase { private readonly object locker; private readonly ConcurrentDictionary> results; private readonly IValidator validator; private bool done; public EntityScanner(IValidator validator) { this.validator = validator; this.locker = new object(); this.results = new ConcurrentDictionary>(); } /// /// Gets or sets a callback function that gets the keys associated with /// the given entity. /// public Func?> GetKeysFromEntity { get; set; } = null!; /// /// Gets the list of entities associated with the given key. if the key has not /// been /// seen, this returns an empty collection. /// /// The key to search for. /// A list of entities associated with the given key. /// /// If the input has not been /// completely processed, this exception is thrown. /// public IEnumerable GetScannedEntities(string key) { // Make sure we're done processing. this.CheckDone(); // We have the list, so return it or an empty list. if (this.results.TryGetValue(key, out List? list)) { return list.AsReadOnly(); } // We didn't have the list but we always return something. return Array.Empty(); } /// /// Gets a list of all known keys from the scanner. /// /// public IEnumerable GetScannedKeys() { this.CheckDone(); return this.results.Keys.ToImmutableList(); } /// /// Gets a dictionary of all the results from the scanner. /// /// public ImmutableDictionary> GetScannedResults() { this.CheckDone(); return this.results.ToImmutableDictionary(); } /// public override IEnumerable Run(IEnumerable input) { // Make sure we have sane data. this.validator.ValidateAndThrow(this); // Reset our done flag to handle re-entrant calls. lock (this.locker) { this.done = false; } // Loop through the entities and process each one. foreach (Entity? entity in input) { // Scan the given entity and see where it needs to be included. // The entity is added to each of the keys returned by this class. IEnumerable? keysFromEntity = this.GetKeysFromEntity(entity); if (keysFromEntity != null) { foreach (string key in keysFromEntity) { this.results.AddOrUpdate( key, _ => new List { entity }, ( _, list) => list.Union(new[] { entity }) .ToList()); } } // Finish processing this entity. yield return entity; } // We are done, so flip our flag and we're done processing. lock (this.locker) { this.done = true; } } private void CheckDone() { lock (this.locker) { // Make sure we are done, otherwise give a useful message. if (!this.done) { throw new InvalidOperationException( "Have not finished processing through the input for scanning. To finish" + " processing, have an operation that causes the enumerable to resolve. This can" + " can be as simple as a `.ToList()` operator or using another resolving operation" + " such as one implementing `IResolvingOperation` before any calls to `GetEntities`."); } } } }