Utility types for stable, short human-readable identifiers used in the editor. Ids contains base36 id generation and validation. Several record structs (NodeId, PortId, EdgeId, ParamId) wrap string values, provide constructors New/Parse/TryParse, IsValid checks, comparison and ToString.
namespace Editor.Prism.Core;
/// <summary>
/// Short, stable, human-readable identifiers.
/// <para>
/// Ids are minted once and never rewritten. That is what keeps git diffs small and lets
/// diagnostics, bookmarks and cross-document references survive a save.
/// </para>
/// </summary>
public static class Ids
{
/// <summary>Number of base36 characters in a freshly minted id.</summary>
public const int ShortIdLength = 8;
const string Alphabet = "0123456789abcdefghijklmnopqrstuvwxyz";
/// <summary>Mint a new base36 short id. 36^8 ≈ 2.8e12 values, collision-checked per document by the caller.</summary>
public static string NewShortId( int length = ShortIdLength )
{
if ( length < 1 ) length = 1;
var chars = new char[length];
var rng = System.Random.Shared;
for ( int i = 0; i < length; i++ )
{
chars[i] = Alphabet[rng.Next( Alphabet.Length )];
}
return new string( chars );
}
/// <summary>True when <paramref name="value"/> is non-empty and contains only base36 characters.</summary>
public static bool IsWellFormed( string value )
{
if ( string.IsNullOrEmpty( value ) ) return false;
foreach ( var c in value )
{
var ok = ( c >= '0' && c <= '9' ) || ( c >= 'a' && c <= 'z' );
if ( !ok ) return false;
}
return true;
}
}
/// <summary>Identifies a node within a document. Stable for the lifetime of the node.</summary>
public readonly record struct NodeId( string Value ) : IComparable<NodeId>
{
/// <summary>The unset id. Serializes as an empty string and never matches a real node.</summary>
public static readonly NodeId None = default;
/// <summary>True when this id refers to something.</summary>
public bool IsValid => !string.IsNullOrEmpty( Value );
/// <summary>Mint a new id.</summary>
public static NodeId New() => new( Ids.NewShortId() );
/// <summary>Wrap an existing string. Never throws; an empty string produces <see cref="None"/>.</summary>
public static NodeId Parse( string value ) => string.IsNullOrEmpty( value ) ? None : new NodeId( value );
/// <summary>Wrap an existing string, reporting whether it was usable.</summary>
public static bool TryParse( string value, out NodeId id )
{
id = Parse( value );
return id.IsValid;
}
/// <inheritdoc/>
public int CompareTo( NodeId other ) => string.CompareOrdinal( Value, other.Value );
/// <inheritdoc/>
public override string ToString() => Value ?? string.Empty;
}
/// <summary>Identifies a port within a node. Matches the port's declared name.</summary>
public readonly record struct PortId( string Value ) : IComparable<PortId>
{
/// <summary>The unset id.</summary>
public static readonly PortId None = default;
/// <summary>True when this id refers to something.</summary>
public bool IsValid => !string.IsNullOrEmpty( Value );
/// <summary>
/// Port ids are authored, not minted — they come from the <c>[In]</c>/<c>[Out]</c> property name
/// or from <see cref="Editor.Prism.Model.PortBuilder"/>. This exists only for synthetic ports.
/// </summary>
public static PortId New() => new( Ids.NewShortId() );
/// <summary>Wrap an existing string. Never throws.</summary>
public static PortId Parse( string value ) => string.IsNullOrEmpty( value ) ? None : new PortId( value );
/// <summary>Wrap an existing string, reporting whether it was usable.</summary>
public static bool TryParse( string value, out PortId id )
{
id = Parse( value );
return id.IsValid;
}
/// <inheritdoc/>
public int CompareTo( PortId other ) => string.CompareOrdinal( Value, other.Value );
/// <inheritdoc/>
public override string ToString() => Value ?? string.Empty;
}
/// <summary>Identifies an edge within a document.</summary>
public readonly record struct EdgeId( string Value ) : IComparable<EdgeId>
{
/// <summary>The unset id.</summary>
public static readonly EdgeId None = default;
/// <summary>True when this id refers to something.</summary>
public bool IsValid => !string.IsNullOrEmpty( Value );
/// <summary>Mint a new id.</summary>
public static EdgeId New() => new( Ids.NewShortId() );
/// <summary>Wrap an existing string. Never throws.</summary>
public static EdgeId Parse( string value ) => string.IsNullOrEmpty( value ) ? None : new EdgeId( value );
/// <summary>Wrap an existing string, reporting whether it was usable.</summary>
public static bool TryParse( string value, out EdgeId id )
{
id = Parse( value );
return id.IsValid;
}
/// <inheritdoc/>
public int CompareTo( EdgeId other ) => string.CompareOrdinal( Value, other.Value );
/// <inheritdoc/>
public override string ToString() => Value ?? string.Empty;
}
/// <summary>Identifies a blackboard parameter or keyword within a document.</summary>
public readonly record struct ParamId( string Value ) : IComparable<ParamId>
{
/// <summary>The unset id.</summary>
public static readonly ParamId None = default;
/// <summary>True when this id refers to something.</summary>
public bool IsValid => !string.IsNullOrEmpty( Value );
/// <summary>Mint a new id.</summary>
public static ParamId New() => new( Ids.NewShortId() );
/// <summary>Wrap an existing string. Never throws.</summary>
public static ParamId Parse( string value ) => string.IsNullOrEmpty( value ) ? None : new ParamId( value );
/// <summary>Wrap an existing string, reporting whether it was usable.</summary>
public static bool TryParse( string value, out ParamId id )
{
id = Parse( value );
return id.IsValid;
}
/// <inheritdoc/>
public int CompareTo( ParamId other ) => string.CompareOrdinal( Value, other.Value );
/// <inheritdoc/>
public override string ToString() => Value ?? string.Empty;
}