Editor-side helpers for reading and mutating an ArchPlan. Provides traversal (Everything, Parts, Units), find/host lookups, filing/unfiling/insert/remove operations, id utilities, and JSON highest-id scanning used when plan parts lack runtime types.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
namespace Sunless.Architecture;
// The accessors that replace fourteen bespoke finders, each of which was a copy of the same
// SelectMany(...).FirstOrDefault(x => x.Id == id). The caller names the type it wants; the manifest that claims
// that type says where it is filed, so core reaches a module's parts without naming one. Every generic member has an
// ArchKind-keyed twin beside it, for a caller holding an ident off disk and no type to go with it.
public static class ArchPlanStore
{
// Every authored object in the plan, walked through the manifests rather than through a hand-listed traversal
// that forgets whatever was added last. A part nothing claims is simply not walked - that is a kind this build
// has no manifest for, and its bytes ride in the payload bag untouched.
public static IEnumerable<object> Everything( this ArchPlan plan, ArchKinds table = null )
{
if ( plan is null )
{
yield break;
}
var kinds = table ?? ArchKinds.Load();
var seen = new HashSet<object>( ReferenceEqualityComparer.Instance );
var pending = new Queue<object>();
foreach ( var root in plan.Units.Cast<object>().Concat( plan.Assemblies ).Concat( plan.Instances ) )
{
pending.Enqueue( root );
}
while ( pending.Count > 0 )
{
var host = pending.Dequeue();
if ( host is null || !seen.Add( host ) )
{
continue;
}
yield return host;
foreach ( var filed in kinds.All.Where( manifest => Hosts( manifest, host ) ).SelectMany( manifest => manifest.Filed( plan, host ) ) )
{
pending.Enqueue( filed );
}
}
}
public static IEnumerable<T> Parts<T>( this ArchPlan plan, ArchKinds table = null ) where T : class
{
return plan.Everything( table ).OfType<T>();
}
// The same sweep keyed on a kind rather than on a type. A kind stands at the plan root, on hosts, or on both, so
// the two are walked together and reference identity keeps whatever overlaps from coming back twice.
public static IEnumerable<object> Parts( this ArchPlan plan, ArchKind kind, ArchKinds table = null )
{
if ( plan is null )
{
yield break;
}
var kinds = table ?? ArchKinds.Load();
var manifest = kinds.For( kind );
if ( manifest is null )
{
yield break;
}
var seen = new HashSet<object>( ReferenceEqualityComparer.Instance );
foreach ( var part in manifest.Filed( plan, null ).Where( seen.Add ) )
{
yield return part;
}
foreach ( var host in plan.Everything( kinds ).Where( candidate => Hosts( manifest, candidate ) ) )
{
foreach ( var part in manifest.Filed( plan, host ).Where( seen.Add ) )
{
yield return part;
}
}
}
public static T Find<T>( this ArchPlan plan, int id, ArchKinds table = null ) where T : class
{
return id == 0 ? null : plan.Parts<T>( table ).FirstOrDefault( part => IdOf( part ) == id );
}
public static object Find( this ArchPlan plan, ArchKind kind, int id, ArchKinds table = null )
{
return id == 0 ? null : plan.Parts( kind, table ).FirstOrDefault( part => IdOf( part ) == id );
}
// An id arriving with no kind beside it, which is how a selection, a link and an mcp argument all arrive.
public static object Find( this ArchPlan plan, int id, ArchKinds table = null )
{
return id == 0 ? null : plan.Everything( table ).FirstOrDefault( part => IdOf( part ) == id );
}
public static THost HostOf<THost>( this ArchPlan plan, object part, ArchKinds table = null ) where THost : class
{
if ( part is null )
{
return null;
}
var kinds = table ?? ArchKinds.Load();
return plan.Everything( kinds ).OfType<THost>().FirstOrDefault( host => Files( plan, kinds, host, part ) );
}
// Neither end of the question named: the part is an object and so is what comes back.
public static object HostOf( this ArchPlan plan, object part, ArchKinds table = null )
{
if ( part is null )
{
return null;
}
var kinds = table ?? ArchKinds.Load();
return plan.Everything( kinds ).FirstOrDefault( host => Files( plan, kinds, host, part ) );
}
public static IEnumerable<T> Units<T>( this ArchPlan plan ) where T : ArchUnit => plan.Units.OfType<T>();
// Read off the unit's own stored kind, so a unit whose module is not installed still answers as itself.
public static IEnumerable<ArchUnit> Units( this ArchPlan plan, ArchKind kind ) => plan.Units.Where( unit => unit.Kind == kind );
// One host's own parts of one kind, in the order the manifest files them - which is the order the stack applies
// them in, so an index taken from this read is the index to seat against.
public static IEnumerable<object> Filed( this ArchPlan plan, ArchKind kind, object host, ArchKinds table = null )
{
// Only the rootward ask walks the document; a host answers off its own list, so demanding a plan there
// would force one through helpers that hold a host and nothing else, purely to be ignored.
if ( host is null && plan is null )
{
return Enumerable.Empty<object>();
}
var manifest = (table ?? ArchKinds.Load()).For( kind );
return manifest is null ? Enumerable.Empty<object>() : manifest.Filed( plan, host );
}
// For a helper that holds the host and no plan. Named apart from the extension so the reader sees that nothing
// is being asked of the document.
public static IEnumerable<object> FiledOn( ArchKind kind, object host, ArchKinds table = null )
{
return host is null ? Enumerable.Empty<object>() : Filed( null, kind, host, table );
}
// For a call site that can still name what it is after: the kind is found from the type rather than given, and a
// type no kind claims falls back to whatever this host holds of it.
public static IEnumerable<T> Filed<T>( this ArchPlan plan, object host, ArchKinds table = null ) where T : class
{
var kinds = table ?? ArchKinds.Load();
return kinds.Claiming( typeof( T ) ) is { } manifest
? manifest.Filed( plan, host ).OfType<T>()
: kinds.All.Where( entry => Hosts( entry, host ) ).SelectMany( entry => entry.Filed( plan, host ) ).OfType<T>();
}
// Counted where the list can count itself, so asking whether a host holds anything never builds a list to
// measure - the ten Count reads a HasContent used to make were ten free answers.
public static int CountFiled( this ArchPlan plan, ArchKind kind, object host, ArchKinds table = null )
{
return plan.Filed( kind, host, table ).Count();
}
public static int CountFiled<T>( this ArchPlan plan, object host, ArchKinds table = null ) where T : class
{
return plan.Filed<T>( host, table ).Count();
}
// Stops at the first kind that holds something, which is all HasContent ever wanted to know.
public static bool AnyFiled( this ArchPlan plan, object host, ArchKinds table = null )
{
if ( plan is null )
{
return false;
}
var kinds = table ?? ArchKinds.Load();
return kinds.All.Where( manifest => Hosts( manifest, host ) ).Any( manifest => manifest.Filed( plan, host ).Any() );
}
// Appended through the manifest, the only thing that knows which of a host's several lists this part belongs in.
public static bool File( this ArchPlan plan, ArchKind kind, object host, object payload, ArchKinds table = null )
{
return (table ?? ArchKinds.Load()).For( kind )?.File( host, payload ) == true;
}
// The index counts within the KIND's filed order, not within the list underneath it: two kinds can share one
// list, and a walkway seated before the second walkway must not land before the second room.
public static bool Insert( this ArchPlan plan, ArchKind kind, object host, int index, object payload, ArchKinds table = null )
{
var manifest = plan is null || payload is null ? null : (table ?? ArchKinds.Load()).For( kind );
var folder = manifest?.Folder( plan, host );
if ( folder is null )
{
return false;
}
var seat = SeatFor( folder, manifest.Filed( plan, host ).ToList(), index );
if ( !manifest.File( host, payload ) )
{
return false;
}
var landed = SeatOf( folder, payload );
if ( landed >= 0 && landed != seat )
{
folder.RemoveAt( landed );
folder.Insert( seat, payload );
}
return true;
}
public static bool RemoveAt( this ArchPlan plan, ArchKind kind, object host, int index, ArchKinds table = null )
{
var manifest = plan is null || index < 0 ? null : (table ?? ArchKinds.Load()).For( kind );
var filed = manifest?.Filed( plan, host ).ToList();
return filed is not null && index < filed.Count && manifest.Unfile( plan, filed[index] );
}
// Materialised before anything is dropped, or the predicate walks a list being edited under it.
public static int RemoveAll( this ArchPlan plan, ArchKind kind, object host, Func<object, bool> predicate, ArchKinds table = null )
{
var manifest = plan is null || predicate is null ? null : (table ?? ArchKinds.Load()).For( kind );
if ( manifest is null )
{
return 0;
}
return manifest.Filed( plan, host ).Where( predicate ).ToList().Count( doomed => manifest.Unfile( plan, doomed ) );
}
// Keyed on the payload, because a part is dropped by a caller holding the thing rather than its kind. A payload
// no manifest claims is offered round the table, since two kinds sharing one type leave one of them unclaimed.
public static bool Unfile( this ArchPlan plan, object payload, ArchKinds table = null )
{
if ( plan is null || payload is null )
{
return false;
}
var kinds = table ?? ArchKinds.Load();
return kinds.Claiming( payload )?.Unfile( plan, payload ) == true
|| kinds.All.Any( manifest => manifest.Unfile( plan, payload ) );
}
// Never cached: hotload carries a PropertyInfo from the assembly the editor had before the edit.
public static int IdOf( object payload )
{
return payload?.GetType().GetProperty( "Id" )?.GetValue( payload ) is int id ? id : 0;
}
// The id watermark inside bytes no type in this editor claims. Without this pass the parts of a kind this build
// cannot read are invisible to the allocator, which hands their ids out again - and two objects sharing an id
// are one object in the scene, standing where two were meant to.
public static int HighestIdIn( IReadOnlyDictionary<string, JsonElement> payloads )
{
return payloads is null ? 0 : payloads.Values.Select( HighestIdIn ).DefaultIfEmpty( 0 ).Max();
}
public static int HighestIdIn( JsonElement element )
{
switch ( element.ValueKind )
{
case JsonValueKind.Object:
var highest = 0;
foreach ( var property in element.EnumerateObject() )
{
var found = property.NameEquals( "Id" ) && property.Value.ValueKind == JsonValueKind.Number
&& property.Value.TryGetInt32( out var id )
? id
: HighestIdIn( property.Value );
highest = System.Math.Max( highest, found );
}
return highest;
case JsonValueKind.Array:
return element.EnumerateArray().Select( HighestIdIn ).DefaultIfEmpty( 0 ).Max();
default:
return 0;
}
}
static bool Hosts( IArchKindManifest manifest, object host )
{
return manifest.Hosts.Any( type => type.IsInstanceOfType( host ) );
}
static bool Files( ArchPlan plan, ArchKinds kinds, object host, object part )
{
return kinds.All.Where( manifest => Hosts( manifest, host ) )
.SelectMany( manifest => manifest.Filed( plan, host ) )
.Any( filed => ReferenceEquals( filed, part ) );
}
// Where in the folder the kind's own index lands. Past the end of what this kind holds is an append, which is
// also what a folder shared with another kind gives back when nothing of ours stands at that index.
static int SeatFor( System.Collections.IList folder, IReadOnlyList<object> filed, int index )
{
var seat = index >= 0 && index < filed.Count ? SeatOf( folder, filed[index] ) : -1;
return seat < 0 ? folder.Count : seat;
}
// By reference, because a part is identified by being that object - two parts that compare equal are still two.
static int SeatOf( System.Collections.IList folder, object payload )
{
for ( var seat = 0; seat < folder.Count; seat++ )
{
if ( ReferenceEquals( folder[seat], payload ) )
{
return seat;
}
}
return -1;
}
}