Editor/Prism/Core/Ids.cs

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.

Native Interop
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;
}