feat: reimplemented index page generation
This commit is contained in:
parent
3898396afe
commit
86403060e7
12 changed files with 463 additions and 404 deletions
|
@ -1,179 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using FluentValidation;
|
||||
|
||||
using Gallium;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace Nitride.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class CreateIndexEntities<TIndexKey> : 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<CreateIndexEntities<TIndexKey>> validator;
|
||||
|
||||
public CreateIndexEntities(ILogger logger)
|
||||
{
|
||||
// TODO: Figure out why Autofac won't let us register IValidator of generic classes.
|
||||
this.validator = new CreateIndexEntitiesValidator<TIndexKey>();
|
||||
this.logger = logger.ForContext(typeof(CreateIndexEntities<>));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an index for a given key. This will not be called for any
|
||||
/// index that has been already created.
|
||||
/// </summary>
|
||||
public Func<TIndexKey, IList<Entity>, Entity>? CreateIndexEntity
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public Func<Entity, TIndexKey?>? GetIndexEntityKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public Func<Entity, IEnumerable<TIndexKey>>? GetIndexKeys { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing index entity to include new information.
|
||||
/// </summary>
|
||||
public Func<Entity, TIndexKey, IList<Entity>, Entity>? UpdateIndexEntity
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<Entity> Run(IEnumerable<Entity> 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<TIndexKey, Entity> indexes = new();
|
||||
Dictionary<TIndexKey, List<Entity>> indexed = new();
|
||||
List<Entity> 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<Entity>? list))
|
||||
{
|
||||
indexed[indexedKey] = list = new List<Entity>();
|
||||
}
|
||||
|
||||
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<Entity>? list))
|
||||
{
|
||||
list = new List<Entity>();
|
||||
}
|
||||
|
||||
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<Entity>? 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<TIndexKey> WithCreateIndexEntity(Func<TIndexKey, IList<Entity>, Entity>? callback)
|
||||
{
|
||||
this.CreateIndexEntity = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CreateIndexEntities<TIndexKey> WithGetIndexEntityKey(Func<Entity, TIndexKey?>? callback)
|
||||
{
|
||||
this.GetIndexEntityKey = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CreateIndexEntities<TIndexKey> WithGetIndexKeys(Func<Entity, IEnumerable<TIndexKey>>? callback)
|
||||
{
|
||||
this.GetIndexKeys = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CreateIndexEntities<TIndexKey> WithUpdateIndexEntity(
|
||||
Func<Entity, TIndexKey, IList<Entity>, Entity>? callback)
|
||||
{
|
||||
this.UpdateIndexEntity = callback;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
using FluentValidation;
|
||||
|
||||
namespace Nitride.Entities;
|
||||
|
||||
public class CreateIndexEntitiesValidator<TIndexKey> : AbstractValidator<CreateIndexEntities<TIndexKey>>
|
||||
where TIndexKey : notnull
|
||||
{
|
||||
public CreateIndexEntitiesValidator()
|
||||
{
|
||||
this.RuleFor(x => x.CreateIndexEntity).NotNull();
|
||||
this.RuleFor(x => x.GetIndexKeys).NotNull();
|
||||
this.RuleFor(x => x.UpdateIndexEntity).NotNull();
|
||||
}
|
||||
}
|
114
src/Nitride/Entities/CreateOrUpdateIndex.cs
Normal file
114
src/Nitride/Entities/CreateOrUpdateIndex.cs
Normal file
|
@ -0,0 +1,114 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
using FluentValidation;
|
||||
|
||||
using Gallium;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace Nitride.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This makes the assumption that there is one index per page.
|
||||
/// </remarks>
|
||||
[WithProperties]
|
||||
public partial class CreateOrUpdateIndex : OperationBase
|
||||
{
|
||||
private readonly ILogger logger;
|
||||
|
||||
private readonly IValidator<CreateOrUpdateIndex> validator;
|
||||
|
||||
public CreateOrUpdateIndex(ILogger logger, IValidator<CreateOrUpdateIndex> validator)
|
||||
{
|
||||
this.validator = validator;
|
||||
this.logger = logger.ForContext(typeof(CreateOrUpdateIndex));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an index for a given key. This will not be called for any
|
||||
/// index that has been already created.
|
||||
/// </summary>
|
||||
public Func<string, IList<Entity>, Entity> CreateIndex { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public Func<Entity, string?> GetIndexKey { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the scanner that provides the keys.
|
||||
/// </summary>
|
||||
public EntityScanner Scanner { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing index entity to include new information.
|
||||
/// </summary>
|
||||
public Func<Entity, string, IEnumerable<Entity>, Entity> UpdateIndex { get; set; } = null!;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<Entity> Run(IEnumerable<Entity> 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<string> 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<Entity> entries =
|
||||
scanned.TryGetValue(key, out List<Entity>? list) ? list : Array.Empty<Entity>();
|
||||
|
||||
existing.Add(key);
|
||||
|
||||
yield return this.UpdateIndex(entity, key, entries);
|
||||
}
|
||||
}
|
||||
|
||||
// Once we're done with the list, we need to create the missing indexes.
|
||||
foreach (string? key in scanned.Keys)
|
||||
{
|
||||
if (existing.Contains(key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
14
src/Nitride/Entities/CreateOrUpdateIndexValidator.cs
Normal file
14
src/Nitride/Entities/CreateOrUpdateIndexValidator.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
using FluentValidation;
|
||||
|
||||
namespace Nitride.Entities;
|
||||
|
||||
public class CreateOrUpdateIndexValidator : AbstractValidator<CreateOrUpdateIndex>
|
||||
{
|
||||
public CreateOrUpdateIndexValidator()
|
||||
{
|
||||
this.RuleFor(x => x.Scanner).NotNull();
|
||||
this.RuleFor(x => x.GetIndexKey).NotNull();
|
||||
this.RuleFor(x => x.CreateIndex).NotNull();
|
||||
this.RuleFor(x => x.UpdateIndex).NotNull();
|
||||
}
|
||||
}
|
147
src/Nitride/Entities/EntityScanner.cs
Normal file
147
src/Nitride/Entities/EntityScanner.cs
Normal file
|
@ -0,0 +1,147 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
using FluentValidation;
|
||||
|
||||
using Gallium;
|
||||
|
||||
namespace Nitride.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[WithProperties]
|
||||
public partial class EntityScanner : OperationBase
|
||||
{
|
||||
private readonly object locker;
|
||||
|
||||
private readonly ConcurrentDictionary<string, List<Entity>> results;
|
||||
|
||||
private readonly IValidator<EntityScanner> validator;
|
||||
|
||||
private bool done;
|
||||
|
||||
public EntityScanner(IValidator<EntityScanner> validator)
|
||||
{
|
||||
this.validator = validator;
|
||||
this.locker = new object();
|
||||
this.results = new ConcurrentDictionary<string, List<Entity>>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a callback function that gets the keys associated with
|
||||
/// the given entity.
|
||||
/// </summary>
|
||||
public Func<Entity, IEnumerable<string>?> GetKeysFromEntity { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of entities associated with the given key. if the key has not
|
||||
/// been
|
||||
/// seen, this returns an empty collection.
|
||||
/// </summary>
|
||||
/// <param name="key">The key to search for.</param>
|
||||
/// <returns>A list of entities associated with the given key.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// If the input has not been
|
||||
/// completely processed, this exception is thrown.
|
||||
/// </exception>
|
||||
public IEnumerable<Entity> 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<Entity>? list))
|
||||
{
|
||||
return list.AsReadOnly();
|
||||
}
|
||||
|
||||
// We didn't have the list but we always return something.
|
||||
return Array.Empty<Entity>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of all known keys from the scanner.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<string> GetScannedKeys()
|
||||
{
|
||||
this.CheckDone();
|
||||
return this.results.Keys.ToImmutableList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a dictionary of all the results from the scanner.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public ImmutableDictionary<string, List<Entity>> GetScannedResults()
|
||||
{
|
||||
this.CheckDone();
|
||||
|
||||
return this.results.ToImmutableDictionary();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<Entity> Run(IEnumerable<Entity> 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<string>? keysFromEntity = this.GetKeysFromEntity(entity);
|
||||
|
||||
if (keysFromEntity != null)
|
||||
{
|
||||
foreach (string key in keysFromEntity)
|
||||
{
|
||||
this.results.AddOrUpdate(
|
||||
key,
|
||||
_ => new List<Entity> { 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`.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
src/Nitride/Entities/EntityScannerValidator.cs
Normal file
11
src/Nitride/Entities/EntityScannerValidator.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
using FluentValidation;
|
||||
|
||||
namespace Nitride.Entities;
|
||||
|
||||
public class EntityScannerValidator : AbstractValidator<EntityScanner>
|
||||
{
|
||||
public EntityScannerValidator()
|
||||
{
|
||||
this.RuleFor(x => x.GetKeysFromEntity).NotNull();
|
||||
}
|
||||
}
|
|
@ -1,188 +0,0 @@
|
|||
# Entities
|
||||
|
||||
Nitride is based on an Entity Component System (ECS) in the way it handles the
|
||||
various input documents, images, feeds, and other parts that make up a website.
|
||||
Implementing it this way makes it easier to create a distinction between the
|
||||
different entities (much like Statiq.Web uses the DocumentType)
|
||||
but allows for adding new components along the way without having the C#
|
||||
limitations of a sealed enumeration or needing to implement a Javascript-style
|
||||
enum for identification. Instead, if an entity needs to be identified as being
|
||||
Markdown, an image, or a database query, it just adds a component to represent
|
||||
that information.
|
||||
|
||||
The basic entity is just a simple object with an internal identifier. These
|
||||
entities are also immutable. Functions that appear to manipulate actually clone,
|
||||
make the change, and then return the results.
|
||||
|
||||
```c#
|
||||
var entity = new Entity();
|
||||
Console.WriteLine("Entity Id: {0}", entity.Id);
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
By itself, an entity doesn't have any meaning or purpose. These are described by
|
||||
a generic collection of components that are added to the entity. Each component
|
||||
has a type and then an instance of that type. These are added to the entity with
|
||||
the `Add` command. If a type is not given, it is assumed to be the same type as
|
||||
the parameter, but a base class or interface can be given to allow different
|
||||
types to be stored in a specific component.
|
||||
|
||||
In effect, the type of the component is the key. Having two different types,
|
||||
even with the same object, would be considered two distinct objects.
|
||||
|
||||
```c#
|
||||
string mimeType = "text/plain";
|
||||
Entity entity = new Entity();
|
||||
Assert.Equal(0, entity.Count);
|
||||
|
||||
Entity newEntity = entity.Add(mimeType);
|
||||
Assert.Equal(0, entity.Count);
|
||||
Assert.Equal(1, newEntity.Count);
|
||||
Assert.Equal(entity.Id, newEntity.Id);
|
||||
Assert.Equal(entity, newEntity);
|
||||
|
||||
newEntity = newEntity.Add<object>(mimeType);
|
||||
Assert.Equal(2, newEntity.Count);
|
||||
```
|
||||
|
||||
The basic operations for entity components are:
|
||||
|
||||
- `Add<TType>(component)`: Adds a component as the given type. If there is
|
||||
already a component there, an exception will be thrown.
|
||||
- `Add(component)`: As `Add<TType>(component)` but the `TType` is the same as
|
||||
`component.GetType()`.
|
||||
- `Remove<TType>()`: Removes any component of the given type, if exists. If
|
||||
there is no such component, then nothing happens.
|
||||
- `Remove(component)`: Same as `Remove<TType>` with the component given
|
||||
determining the `TType`.
|
||||
- `Set<TType>(component)`: Adds or updates a component of the given type.
|
||||
- `Set(component)`: Same as `Set<TType>` with the component given determining
|
||||
the `TType`.
|
||||
- `Copy()`: Creates a copy of the entity and assigns it a new identifier.
|
||||
- `ExactCopy()`: Creates a copy of the entity with the same identifier.
|
||||
|
||||
As above, all of these return a new entity (or the same one if no change is made
|
||||
to the entity).
|
||||
|
||||
### Query Components
|
||||
|
||||
- `bool Has<TType>()`: Returns a value indicating whether the entity has the
|
||||
given component type.
|
||||
- `TType Get<TType>()`: Returns the value of the registered component. If there
|
||||
is no such object, this will throw an exception.
|
||||
- `TType? GetOptional<TType>()`: Returns the value of the registered component.
|
||||
If there is no such object, this will return the default value.
|
||||
- `bool TryGet<TType>(out TType component)`: Attempt to get the component. If it
|
||||
cannot be retrieved, then this will return `false` and `component` is
|
||||
undefined.
|
||||
|
||||
## Collections
|
||||
|
||||
To keep with the patterns of C#, working with collection of entities uses normal
|
||||
LINQ operations. For example, to combine two sets of entities together,
|
||||
the `Union` LINQ command can be used:
|
||||
|
||||
```c#
|
||||
IEnumerable<Entity> entities1;
|
||||
IEnumerable<Entity> entities2;
|
||||
IEnumerable<Entity> all = entities1.Union(entities2);
|
||||
```
|
||||
|
||||
To work with the ECS, additional extension methods have been written that allow
|
||||
for filtering or working with those entities.
|
||||
|
||||
### HasComponents
|
||||
|
||||
The `HasComponents` is a set of overrides that checks to see if the given entity
|
||||
has the requisite components. If they don't, then that entity is filtered out.
|
||||
|
||||
```c#
|
||||
IEnumerable<Entity> entities;
|
||||
|
||||
var filtered1 = entities.HasComponents<C1>();
|
||||
var filtered2 = entities.HasComponents<C1, C2>();
|
||||
var filtered3 = entities.HasComponents<C1, C2, C3>();
|
||||
```
|
||||
|
||||
### NotComponents
|
||||
|
||||
`NotComponents` is effectively the reverse of `HasComponents` in that if the
|
||||
entity has the given components, they are filtered out. This also allows up to
|
||||
three different components.
|
||||
|
||||
This also allows the developer to ask for an entity that has two components but
|
||||
not have a different of two with:
|
||||
|
||||
```c#
|
||||
IEnumerable<Entity> entities;
|
||||
var filtered = entities
|
||||
.HasComponents<C1, C2>()
|
||||
.NotComonents<C3, C4>();
|
||||
```
|
||||
|
||||
### ForComponents
|
||||
|
||||
`ForComponents` allows for a lambda to be performed on entities that have the
|
||||
given components while passing all the entities on through the function. This is
|
||||
much like the `ForEach` combined with `Select` in that the changed or updated
|
||||
entity will be passed on.
|
||||
|
||||
```c#
|
||||
var entities = new Entities[]
|
||||
{
|
||||
new Entity().Add("value1"),
|
||||
new Entity().Add(2),
|
||||
new Entity().Add(3).Add("value2"),
|
||||
};
|
||||
var filtered = entities
|
||||
.ForComponents<string>((entity, value) => entity.Set(value + "!"));
|
||||
|
||||
Assert.Equal(
|
||||
new[] {
|
||||
"value1!",
|
||||
null,
|
||||
"value2!",
|
||||
},
|
||||
filtered.Select(x => x.GetOptional<string>()));
|
||||
```
|
||||
|
||||
There are also three overloads allowing up to three components to be pulled out
|
||||
with the lambda.
|
||||
|
||||
### SetComponents, AddComponents, RemoveComponents
|
||||
|
||||
`SetComponents` (as the corresponding `AddComponents`, and `RemoveComponents`)
|
||||
basically perform the same operation on the entire list. They also have the
|
||||
three overloads to allow one to three components be manipulated in a single
|
||||
call.
|
||||
|
||||
```c#
|
||||
IEnumerable<Entity> entities;
|
||||
var updated = entities
|
||||
.AddComponents<object>(mimeType)
|
||||
.AddComponents(mimeType)
|
||||
.RemoveComponents<object>()
|
||||
.SetComponents<object>(mimeType)
|
||||
.SetComponent(mimeType);
|
||||
```
|
||||
|
||||
### MergeEntities
|
||||
|
||||
`MergeComponents` combines multiple entities together if they have the same `Id`
|
||||
field. The two sides of the comparison are the presence of a specific component.
|
||||
|
||||
```c#
|
||||
IEnumerable<Entity> entities;
|
||||
var combined = entities
|
||||
.MergeEntities<C1, C2>(
|
||||
(entity1, c1, entity2, c2) => entity1.Set(c2));
|
||||
```
|
||||
|
||||
## Files, Paths, and Content
|
||||
|
||||
Entities do not have an integral concept of being a file or having contents from
|
||||
the disk or anywhere else. Much of this is implemented as components from the
|
||||
Nitride.IO assembly which uses [Zio](https://github.com/xoofx/zio)
|
||||
for the underlying library, but can be easily replaced with a different IO
|
||||
layer (or even the straight System.IO).
|
9
src/Nitride/IResolvingOperation.cs
Normal file
9
src/Nitride/IResolvingOperation.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace Nitride;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates an operation that resolved (completely processes the input)
|
||||
/// before returning from the `Run` operation.
|
||||
/// </summary>
|
||||
public interface IResolvingOperation : IOperation
|
||||
{
|
||||
}
|
|
@ -15,21 +15,21 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Autofac" Version="6.4.0"/>
|
||||
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="8.0.0"/>
|
||||
<PackageReference Include="FluentValidation" Version="11.0.2"/>
|
||||
<PackageReference Include="Gallium" Version="1.0.2"/>
|
||||
<PackageReference Include="Humanizer.Core" Version="2.14.1"/>
|
||||
<PackageReference Include="MfGames.ToolBuilder" Version="4.0.2"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0"/>
|
||||
<PackageReference Include="Serilog" Version="2.11.0"/>
|
||||
<PackageReference Include="Serilog.Extensions.Autofac.DependencyInjection" Version="5.0.0"/>
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="4.2.0"/>
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1"/>
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0"/>
|
||||
<PackageReference Include="System.CommandLine" Version="[2.0.0-beta3.22114.1,)"/>
|
||||
<PackageReference Include="Zio" Version="0.15.0"/>
|
||||
<PackageReference Include="Autofac" Version="6.4.0" />
|
||||
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="FluentValidation" Version="11.0.2" />
|
||||
<PackageReference Include="Gallium" Version="1.0.2" />
|
||||
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
|
||||
<PackageReference Include="MfGames.ToolBuilder" Version="4.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog" Version="2.11.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Autofac.DependencyInjection" Version="5.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="4.2.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" />
|
||||
<PackageReference Include="System.CommandLine" Version="[2.0.0-beta3.22114.1,)" />
|
||||
<PackageReference Include="Zio" Version="0.15.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Include the source generator -->
|
||||
|
|
|
@ -28,7 +28,6 @@ public class NitrideModule : Module
|
|||
// Operations
|
||||
builder.RegisterValidators(this);
|
||||
builder.RegisterOperators(this);
|
||||
builder.RegisterGeneric(typeof(CreateIndexEntities<>)).As(typeof(CreateIndexEntities<>));
|
||||
|
||||
// Commands
|
||||
builder.RegisterType<BuildCommand>().AsSelf().As<Command>().SingleInstance();
|
||||
|
|
146
tests/Nitride.Tests/Entities/CreateOrUpdateIndexTests.cs
Normal file
146
tests/Nitride.Tests/Entities/CreateOrUpdateIndexTests.cs
Normal file
|
@ -0,0 +1,146 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
using Gallium;
|
||||
|
||||
using Nitride.Entities;
|
||||
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Nitride.Tests.Entities;
|
||||
|
||||
public class CreateOrUpdateIndexTests : NitrideTestBase, IDisposable
|
||||
{
|
||||
private readonly NitrideTestContext context;
|
||||
|
||||
private readonly IOperation op;
|
||||
|
||||
private readonly EntityScanner scanner;
|
||||
|
||||
public CreateOrUpdateIndexTests(ITestOutputHelper output)
|
||||
: base(output)
|
||||
{
|
||||
this.context = this.CreateContext();
|
||||
|
||||
this.scanner = this.context.Resolve<EntityScanner>().WithGetKeysFromEntity(e => e.GetOptional<List<string>>());
|
||||
this.op = this.context.Resolve<CreateOrUpdateIndex>()
|
||||
.WithScanner(this.scanner)
|
||||
.WithGetIndexKey(x => x.Get<string>().Contains("index") ? x.Get<string>().Replace("index", "cat") : null)
|
||||
.WithCreateIndex((key, list) => new Entity().Add(key.Replace("cat", "index")).Add(list.ToList()))
|
||||
.WithUpdateIndex((index, key, list) => index.Add(list.ToList()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateMultipleIndexes()
|
||||
{
|
||||
List<Entity> input = new()
|
||||
{
|
||||
new Entity().Add("page1").Add(new List<string> { "cat1" }),
|
||||
new Entity().Add("page2").Add(new List<string> { "cat2" }),
|
||||
new Entity().Add("page3").Add(new List<string> { "cat1" }),
|
||||
};
|
||||
|
||||
List<Tuple<string, List<string>?>> actual = this.GetActual(input);
|
||||
|
||||
Assert.Equal(
|
||||
new[]
|
||||
{
|
||||
new Tuple<string, List<string>?>("index1", new List<string> { "page1", "page3" }),
|
||||
new Tuple<string, List<string>?>("index2", new List<string> { "page2" }),
|
||||
new Tuple<string, List<string>?>("page1", null),
|
||||
new Tuple<string, List<string>?>("page2", null),
|
||||
new Tuple<string, List<string>?>("page3", null),
|
||||
},
|
||||
actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateNestedIndexes()
|
||||
{
|
||||
List<Entity> input = new()
|
||||
{
|
||||
new Entity().Add("index2").Add(new List<string> { "cat1" }),
|
||||
new Entity().Add("page2").Add(new List<string> { "cat2" }),
|
||||
new Entity().Add("page3").Add(new List<string> { "cat1" }),
|
||||
};
|
||||
|
||||
List<Tuple<string, List<string>?>> actual = this.GetActual(input);
|
||||
|
||||
Assert.Equal(
|
||||
new[]
|
||||
{
|
||||
new Tuple<string, List<string>?>("index1", new List<string> { "index2", "page3" }),
|
||||
new Tuple<string, List<string>?>("index2", new List<string> { "page2" }),
|
||||
new Tuple<string, List<string>?>("page2", null),
|
||||
new Tuple<string, List<string>?>("page3", null),
|
||||
},
|
||||
actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateSimpleIndex()
|
||||
{
|
||||
List<Entity> input = new()
|
||||
{
|
||||
new Entity().Add("page1").Add(new List<string> { "cat1" }),
|
||||
};
|
||||
|
||||
List<Tuple<string, List<string>?>> actual = this.GetActual(input);
|
||||
|
||||
Assert.Equal(
|
||||
new[]
|
||||
{
|
||||
new Tuple<string, List<string>?>("index1", new List<string> { "page1" }),
|
||||
new Tuple<string, List<string>?>("page1", null),
|
||||
},
|
||||
actual);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
this.context.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateSimpleIndex()
|
||||
{
|
||||
List<Entity> input = new()
|
||||
{
|
||||
new Entity().Add("index1"),
|
||||
new Entity().Add("page1").Add(new List<string> { "cat1" }),
|
||||
};
|
||||
|
||||
var output = this.scanner.Run(input).ToList().Run(this.op).ToList();
|
||||
var actual = output
|
||||
.Select(
|
||||
x => new Tuple<string, List<string>?>(
|
||||
x.Get<string>(),
|
||||
x.GetOptional<List<Entity>>()?.Select(y => y.Get<string>()).OrderBy(y => y).ToList()))
|
||||
.OrderBy(x => x.Item1)
|
||||
.ToList();
|
||||
|
||||
Assert.Equal(
|
||||
new[]
|
||||
{
|
||||
new Tuple<string, List<string>?>("index1", new List<string> { "page1" }),
|
||||
new Tuple<string, List<string>?>("page1", null),
|
||||
},
|
||||
actual);
|
||||
}
|
||||
|
||||
private List<Tuple<string, List<string>?>> GetActual(List<Entity> input)
|
||||
{
|
||||
var output = this.scanner.Run(input).ToList().Run(this.op).ToList();
|
||||
var actual = output
|
||||
.Select(
|
||||
x => new Tuple<string, List<string>?>(
|
||||
x.Get<string>(),
|
||||
x.GetOptional<List<Entity>>()?.Select(y => y.Get<string>()).OrderBy(y => y).ToList()))
|
||||
.OrderBy(x => x.Item1)
|
||||
.ToList();
|
||||
return actual;
|
||||
}
|
||||
}
|
|
@ -8,12 +8,12 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Gallium" Version="1.0.2"/>
|
||||
<PackageReference Include="MfGames.TestSetup" Version="1.0.4"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0"/>
|
||||
<PackageReference Include="Serilog.Sinks.XUnit" Version="3.0.3"/>
|
||||
<PackageReference Include="JunitXml.TestLogger" Version="3.0.114"/>
|
||||
<PackageReference Include="xunit" Version="2.4.1"/>
|
||||
<PackageReference Include="Gallium" Version="1.0.2" />
|
||||
<PackageReference Include="MfGames.TestSetup" Version="1.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
||||
<PackageReference Include="Serilog.Sinks.XUnit" Version="3.0.3" />
|
||||
<PackageReference Include="JunitXml.TestLogger" Version="3.0.114" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
@ -25,7 +25,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Nitride\Nitride.csproj"/>
|
||||
<ProjectReference Include="..\..\src\Nitride\Nitride.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
Reference in a new issue