@@ -5,116 +5,290 @@ using System.Text;
using System.Threading.Tasks ;
using System.Threading.Tasks ;
namespace Beam {
namespace Beam {
/// <summary>
/// Describes where a <see cref="Parameter"/> token should be inserted relative to the run‑ time value
/// that ultimately replaces it when building a <see cref="SourceLink"/>.
/// </summary>
[Flags]
[Flags]
public enum Position {
public enum Position {
Before = 0 b01 ,
/// <summary>
After = 0 b10 ,
/// The parameter name is written <em>before</em> its run‑ time value
BeforeAndAfter = 0 b11
/// (e.g. <c>id42</c> when the name is <c>id</c> and the value is <c>42</c>).
/// </summary>
Before = 0 b01 ,
/// <summary>
/// The parameter name is written <em>after</em> its run‑ time value
/// (e.g. <c>42id</c>).
/// </summary>
After = 0 b10 ,
/// <summary>
/// The parameter name is written both before and after the value
/// (e.g. <c>id42id</c>).
/// </summary>
BeforeAndAfter = 0 b11
}
}
/// <summary>
/// Represents a single placeholder that will be substituted when a link is built.
/// </summary>
/// <param name="name">Identifier that appears in the final link according to <paramref name="position"/>.</param>
/// <param name="position">Controls whether <paramref name="name"/> is written before, after, or on both sides of the value.</param>
public class Parameter ( string name , Position position = Position . Before ) {
public class Parameter ( string name , Position position = Position . Before ) {
/// <summary>
/// Gets or sets the identifier that frames the value when the link is rendered.
/// </summary>
public string Name { get ; set ; } = name ;
public string Name { get ; set ; } = name ;
/// <summary>
/// Gets or sets the position at which <see cref="Name"/> is emitted relative to the run‑ time value.
/// </summary>
public Position Position { get ; set ; } = position ;
public Position Position { get ; set ; } = position ;
/// <summary>
/// Creates a shallow copy whose mutable members are detached from the original instance.
/// </summary>
/// <returns>A new <see cref="Parameter"/> with identical <see cref="Name"/> and <see cref="Position"/>.</returns>
public Parameter Clone ( )
= > new ( Name , Position ) ;
}
}
/// <summary>
/// Describes one path segment of a URL and the collection of <see cref="Parameter"/> tokens that belong to it.
/// </summary>
/// <param name="name">Literal part of the segment that precedes the parameters (may be empty).</param>
/// <param name="separator">Optional string placed <em>between</em> adjacent parameters when more than one is present.</param>
/// <param name="suffix">Optional string appended <em>after</em> the last parameter in the segment.</param>
public class LinkSegment ( string name , string separator = "" , string suffix = "" ) {
public class LinkSegment ( string name , string separator = "" , string suffix = "" ) {
/// <summary>
/// Gets or sets the literal path component for this segment.
/// </summary>
public string Name { get ; set ; } = name ;
public string Name { get ; set ; } = name ;
/// <summary>
/// Gets or sets the collection of parameters that appear within the segment.
/// </summary>
public List < Parameter > Parameters { get ; set ; } = [ ] ;
public List < Parameter > Parameters { get ; set ; } = [ ] ;
/// <summary>
/// Gets or sets the string inserted between parameters when the segment is rendered.
/// </summary>
public string Separator { get ; set ; } = separator ;
public string Separator { get ; set ; } = separator ;
/// <summary>
/// Gets or sets the string appended after the last parameter when the segment is rendered.
/// </summary>
public string Suffix { get ; set ; } = suffix ;
public string Suffix { get ; set ; } = suffix ;
/// <summary>
/// Produces a deep copy whose <see cref="Parameters"/> list contains cloned <see cref="Parameter"/> objects.
/// </summary>
public LinkSegment Clone ( )
= > new LinkSegment ( Name , Separator , Suffix ) {
Parameters = [ . . Parameters . Select ( static x = > x . Clone ( ) ) ]
} ;
/// <summary>
/// Replaces <see cref="Parameters"/> with a new set whose items all use <see cref="Position.Before"/>.
/// </summary>
/// <param name="parameters">Parameter identifiers to add.</param>
/// <returns>This instance for fluent calls.</returns>
public LinkSegment WithParameters ( params string [ ] parameters ) {
public LinkSegment WithParameters ( params string [ ] parameters ) {
Parameters = parameters . Select ( ( x ) = > new Parameter ( x ) ) . ToList ( ) ;
Parameters = parameters . Select ( static x = > new Parameter ( x ) ) . ToList ( ) ;
return this ;
return this ;
}
}
/// <summary>
/// Replaces <see cref="Parameters"/> with a new set using explicit name/position tuples.
/// </summary>
/// <param name="parameters">Tuples of parameter identifier and desired position.</param>
/// <returns>This instance for fluent calls.</returns>
public LinkSegment WithParameters ( params ( string , Position ) [ ] parameters ) {
public LinkSegment WithParameters ( params ( string , Position ) [ ] parameters ) {
Parameters = parameters . Select ( ( x ) = > new Parameter ( x . Item1 , x . Item2 ) ) . ToList ( ) ;
Parameters = parameters . Select ( static x = > new Parameter ( x . Item1 , x . Item2 ) ) . ToList ( ) ;
return this ;
return this ;
}
}
}
}
/// <summary>
/// Fluent helper for composing strongly‑ typed, template‑ driven source links.
/// </summary>
/// <remarks>
/// The builder captures a static template (protocol, host, path segments and their parameters) and is later supplied with
/// a flat array of values that populate all parameters in left‑ to‑ right order.
/// </remarks>
/// <param name="host">DNS host name (e.g. <c>api.example.com</c>).</param>
/// <param name="protocol">Transport protocol; defaults to <c>https</c>.</param>
public class SourceLinkBuilder ( string host , string protocol = "https" ) {
public class SourceLinkBuilder ( string host , string protocol = "https" ) {
/// <summary>
/// Gets or sets the scheme part of the URL (e.g. <c>https</c>, <c>http</c>).
/// </summary>
public string Protocol { get ; set ; } = protocol ;
public string Protocol { get ; set ; } = protocol ;
/// <summary>
/// Gets or sets the host portion of the URL.
/// </summary>
public string Host { get ; set ; } = host ;
public string Host { get ; set ; } = host ;
/// <summary>
/// Gets or sets the ordered collection of path segments.
/// </summary>
public List < LinkSegment > Segments { get ; set ; } = [ ] ;
public List < LinkSegment > Segments { get ; set ; } = [ ] ;
/// <summary>
/// Produces a deep copy whose <see cref="Segments"/> and contained collections are detached from the original.
/// </summary>
public SourceLinkBuilder Clone ( )
= > new SourceLinkBuilder ( Protocol , Host ) {
Segments = [ . . Segments . Select ( static x = > x . Clone ( ) ) ]
} ;
#region Helpers – suffix & separator
/// <summary>
/// Returns the suffix of the <paramref name="segmentIndex"/>‑ th segment.
/// </summary>
/// <param name="segmentIndex">Zero‑ based segment index.</param>
/// <exception cref="ArgumentOutOfRangeException">If <paramref name="segmentIndex"/> is outside the valid range.</exception>
public string GetSuffix ( int segmentIndex ) {
public string GetSuffix ( int segmentIndex ) {
ArgumentOutOfRangeException . ThrowIfGreaterThanOrEqual ( segmentIndex , Segments . Count ) ;
ArgumentOutOfRangeException . ThrowIfGreaterThanOrEqual ( segmentIndex , Segments . Count ) ;
ArgumentOutOfRangeException . ThrowIfNegative ( segmentIndex ) ;
ArgumentOutOfRangeException . ThrowIfNegative ( segmentIndex ) ;
return Segments [ segmentIndex ] . Suffix ;
return Segments [ segmentIndex ] . Suffix ;
}
}
/// <summary>
/// Returns the suffix of the last segment.
/// </summary>
public string GetSuffix ( )
public string GetSuffix ( )
= > GetSuffix ( Segments . Count - 1 ) ;
= > GetSuffix ( Segments . Count - 1 ) ;
/// <summary>
/// Returns the separator used by the <paramref name="segmentIndex"/>‑ th segment.
/// </summary>
/// <param name="segmentIndex">Zero‑ based segment index.</param>
/// <exception cref="ArgumentOutOfRangeException">If <paramref name="segmentIndex"/> is outside the valid range.</exception>
public string GetSeparator ( int segmentIndex ) {
public string GetSeparator ( int segmentIndex ) {
ArgumentOutOfRangeException . ThrowIfGreaterThanOrEqual ( segmentIndex , Segments . Count ) ;
ArgumentOutOfRangeException . ThrowIfGreaterThanOrEqual ( segmentIndex , Segments . Count ) ;
ArgumentOutOfRangeException . ThrowIfNegative ( segmentIndex ) ;
ArgumentOutOfRangeException . ThrowIfNegative ( segmentIndex ) ;
return Segments [ segmentIndex ] . Separator ;
return Segments [ segmentIndex ] . Separator ;
}
}
/// <summary>
/// Returns the separator of the last segment.
/// </summary>
public string GetSeparator ( )
public string GetSeparator ( )
= > GetSeparator ( Segments . Count - 1 ) ;
= > GetSeparator ( Segments . Count - 1 ) ;
/// <summary>
/// Assigns a new suffix to the <paramref name="segmentIndex"/>‑ th segment.
/// </summary>
/// <param name="segmentIndex">Zero‑ based segment index.</param>
/// <param name="suffix">String appended after the segment's parameters.</param>
/// <exception cref="ArgumentOutOfRangeException">If <paramref name="segmentIndex"/> is outside the valid range.</exception>
public void SetSuffix ( int segmentIndex , string suffix ) {
public void SetSuffix ( int segmentIndex , string suffix ) {
ArgumentOutOfRangeException . ThrowIfGreaterThanOrEqual ( segmentIndex , Segments . Count ) ;
ArgumentOutOfRangeException . ThrowIfGreaterThanOrEqual ( segmentIndex , Segments . Count ) ;
ArgumentOutOfRangeException . ThrowIfNegative ( segmentIndex ) ;
ArgumentOutOfRangeException . ThrowIfNegative ( segmentIndex ) ;
var seg = Segments [ segmentIndex ] ;
Segments [ segmentIndex ] . Suffix = suffix ;
seg . Suffix = suffix ;
}
}
/// <summary>
/// Assigns a new suffix to the last segment.
/// </summary>
public void SetSuffix ( string suffix )
public void SetSuffix ( string suffix )
= > SetSuffix ( Segments . Count - 1 , suffix ) ;
= > SetSuffix ( Segments . Count - 1 , suffix ) ;
/// <summary>
/// Assigns a new separator to the <paramref name="segmentIndex"/>‑ th segment.
/// </summary>
/// <param name="segmentIndex">Zero‑ based segment index.</param>
/// <param name="separator">String inserted between parameters belonging to the same segment.</param>
/// <exception cref="ArgumentOutOfRangeException">If <paramref name="segmentIndex"/> is outside the valid range.</exception>
public void SetSeparator ( int segmentIndex , string separator ) {
public void SetSeparator ( int segmentIndex , string separator ) {
ArgumentOutOfRangeException . ThrowIfGreaterThanOrEqual ( segmentIndex , Segments . Count ) ;
ArgumentOutOfRangeException . ThrowIfGreaterThanOrEqual ( segmentIndex , Segments . Count ) ;
ArgumentOutOfRangeException . ThrowIfNegative ( segmentIndex ) ;
ArgumentOutOfRangeException . ThrowIfNegative ( segmentIndex ) ;
var seg = Segments [ segmentIndex ] ;
Segments [ segmentIndex ] . Separator = separator ;
seg . Separator = separator ;
}
}
/// <summary>
/// Assigns a new separator to the last segment.
/// </summary>
public void SetSeparator ( string separator )
public void SetSeparator ( string separator )
= > SetSeparator ( Segments . Count - 1 , separator ) ;
= > SetSeparator ( Segments . Count - 1 , separator ) ;
#endregion
#region Segment manipulation
/// <summary>
/// Appends a new segment.
/// </summary>
/// <param name="name">Literal portion of the segment.</param>
/// <param name="separator">Optional separator for subsequent parameters; <see langword="null"/> keeps the current default.</param>
/// <exception cref="ArgumentException">If <paramref name="name"/> is <see langword="null"/>, empty, or whitespace.</exception>
public void AddSegment ( string name , string? separator = null ) {
public void AddSegment ( string name , string? separator = null ) {
ArgumentException . ThrowIfNullOrWhiteSpace ( name ) ;
ArgumentException . ThrowIfNullOrWhiteSpace ( name ) ;
Segments . Add ( new LinkSegment ( name , separator ) ) ;
Segments . Add ( new LinkSegment ( name , separator ) ) ;
}
}
/// <summary>
/// Replaces the whole <see cref="Segments"/> collection with the supplied <paramref name="segments"/>, each represented as a <see cref="LinkSegment"/>.
/// </summary>
/// <returns>This instance for fluent calls.</returns>
public SourceLinkBuilder WithSegments ( params IEnumerable < string > segments ) {
public SourceLinkBuilder WithSegments ( params IEnumerable < string > segments ) {
Segments = segments . Select ( ( x ) = > new LinkSegment ( x ) ) . ToList ( ) ;
Segments = segments . Select ( static x = > new LinkSegment ( x ) ) . ToList ( ) ;
return this ;
return this ;
}
}
/// <summary>
/// Replaces the <see cref="Segments"/> collection with <paramref name="count"/> empty segments.
/// </summary>
/// <param name="count">Number of segments to create.</param>
public SourceLinkBuilder WithSegments ( int count )
public SourceLinkBuilder WithSegments ( int count )
= > WithSegments ( Enumerable . Repeat ( "" , count ) ) ;
= > WithSegments ( Enumerable . Repeat ( "" , count ) ) ;
#endregion
#region Parameter manipulation ( fluent )
/// <summary>
/// Replaces parameters of the <paramref name="i"/>‑ th segment using the supplied identifiers.
/// </summary>
public SourceLinkBuilder WithParameters ( int i , params string [ ] parameters ) {
public SourceLinkBuilder WithParameters ( int i , params string [ ] parameters ) {
Segments [ i ]
Segments [ i ] . WithParameters ( parameters ) ;
. WithParameters ( parameters ) ;
return this ;
return this ;
}
}
/// <summary>
/// Replaces parameters of the <paramref name="i"/>‑ th segment using explicit name/position tuples.
/// </summary>
public SourceLinkBuilder WithParameters ( int i , params ( string , Position ) [ ] parameters ) {
public SourceLinkBuilder WithParameters ( int i , params ( string , Position ) [ ] parameters ) {
Segments [ i ]
Segments [ i ] . WithParameters ( parameters ) ;
. WithParameters ( parameters ) ;
return this ;
return this ;
}
}
#endregion
#region Parameter manipulation ( imperative )
/// <summary>
/// Adds new parameters to the specified segment without clearing existing ones.
/// </summary>
/// <param name="segmentIndex">Zero‑ based segment index.</param>
/// <param name="parameters">Identifiers of parameters to add.</param>
/// <exception cref="ArgumentOutOfRangeException">If <paramref name="segmentIndex"/> is invalid.</exception>
/// <exception cref="ArgumentException">If any parameter identifier is <see langword="null"/>, empty or whitespace.</exception>
public void AddParameters ( int segmentIndex , params string [ ] parameters ) {
public void AddParameters ( int segmentIndex , params string [ ] parameters ) {
ArgumentOutOfRangeException . ThrowIfGreaterThanOrEqual ( segmentIndex , Segments . Count ) ;
ArgumentOutOfRangeException . ThrowIfGreaterThanOrEqual ( segmentIndex , Segments . Count ) ;
ArgumentOutOfRangeException . ThrowIfNegative ( segmentIndex ) ;
ArgumentOutOfRangeException . ThrowIfNegative ( segmentIndex ) ;
var seg = Segments [ segmentIndex ] ;
var seg = Segments [ segmentIndex ] ;
foreach ( var parameter in parameters ) {
foreach ( var parameter in parameters ) {
ArgumentException . ThrowIfNullOrWhiteSpace ( parameter ) ;
ArgumentException . ThrowIfNullOrWhiteSpace ( parameter ) ;
seg . Parameters . Add ( new Parameter ( parameter ) ) ;
seg . Parameters . Add ( new Parameter ( parameter ) ) ;
}
}
}
}
/// <summary>
/// Adds parameters to the last segment.
/// </summary>
public void AddParameters ( params string [ ] parameters )
public void AddParameters ( params string [ ] parameters )
= > AddParameters ( Segments . Count - 1 , parameters ) ;
= > AddParameters ( Segments . Count - 1 , parameters ) ;
/// <summary>
/// Replaces the entire parameter list of the specified segment.
/// </summary>
public void SetParameters ( int segmentIndex , params string [ ] parameters ) {
public void SetParameters ( int segmentIndex , params string [ ] parameters ) {
ArgumentOutOfRangeException . ThrowIfGreaterThanOrEqual ( segmentIndex , Segments . Count ) ;
ArgumentOutOfRangeException . ThrowIfGreaterThanOrEqual ( segmentIndex , Segments . Count ) ;
ArgumentOutOfRangeException . ThrowIfNegative ( segmentIndex ) ;
ArgumentOutOfRangeException . ThrowIfNegative ( segmentIndex ) ;
@@ -123,21 +297,38 @@ namespace Beam {
AddParameters ( segmentIndex , parameters ) ;
AddParameters ( segmentIndex , parameters ) ;
}
}
/// <summary>
/// Replaces the parameter list of the last segment.
/// </summary>
public void SetParameters ( params string [ ] parameters )
public void SetParameters ( params string [ ] parameters )
= > SetParameters ( Segments . Count - 1 , parameters ) ;
= > SetParameters ( Segments . Count - 1 , parameters ) ;
#endregion
/// <summary>
/// Returns the total number of <see cref="Parameter"/> tokens across all segments.
/// </summary>
public int GetParameterCount ( ) {
public int GetParameterCount ( ) {
int count = 0 ;
int count = 0 ;
foreach ( var segment in Segments ) {
foreach ( var segment in Segments ) {
count + = segment . Parameters . Count ;
count + = segment . Parameters . Count ;
}
}
return count ;
return count ;
}
}
#region Build
/// <summary>
/// Produces a concrete <see cref="SourceLink"/> using values from an external <see cref="State"/> object.
/// </summary>
/// <param name="parameterValues">Object providing positional values.</param>
public SourceLink Build ( State parameterValues )
public SourceLink Build ( State parameterValues )
= > Build ( parameterValues . GetState ( ) ) ;
= > Build ( parameterValues . GetState ( ) ) ;
/// <summary>
/// Produces a concrete <see cref="SourceLink"/> by substituting <paramref name="parameterValues"/> into the template.
/// </summary>
/// <param name="parameterValues">Flat array of values that will be written in the order that parameters appear when segments are enumerated left‑ to‑ right.</param>
/// <returns>The completed <see cref="SourceLink"/>.</returns>
/// <exception cref="ArgumentOutOfRangeException">If the supplied value count does not match <see cref="GetParameterCount"/>().</exception>
public SourceLink Build ( params object [ ] parameterValues ) {
public SourceLink Build ( params object [ ] parameterValues ) {
ArgumentOutOfRangeException . ThrowIfNotEqual ( parameterValues . Length , GetParameterCount ( ) ) ;
ArgumentOutOfRangeException . ThrowIfNotEqual ( parameterValues . Length , GetParameterCount ( ) ) ;
@@ -145,22 +336,29 @@ namespace Beam {
link . Append ( Protocol ) ;
link . Append ( Protocol ) ;
link . Append ( "://" ) ;
link . Append ( "://" ) ;
link . Append ( Host ) ;
link . Append ( Host ) ;
int pvC = 0 ;
int pvC = 0 ;
foreach ( var segment in Segments ) {
foreach ( var segment in Segments ) {
link . Append ( '/' ) ;
link . Append ( '/' ) ;
link . Append ( segment . Name ) ;
link . Append ( segment . Name ) ;
for ( int i = 0 ; i < segment . Parameters . Count ; i + + ) {
for ( int i = 0 ; i < segment . Parameters . Count ; i + + ) {
if ( segment . Parameters [ i ] . Position . HasFlag ( Position . Before ) )
if ( segment . Parameters [ i ] . Position . HasFlag ( Position . Before ) )
link . Append ( segment . Parameters [ i ] . Name ) ;
link . Append ( segment . Parameters [ i ] . Name ) ;
link . Append ( parameterValues [ pvC + + ] ) ;
link . Append ( parameterValues [ pvC + + ] ) ;
if ( segment . Parameters [ i ] . Position . HasFlag ( Position . After ) )
if ( segment . Parameters [ i ] . Position . HasFlag ( Position . After ) )
link . Append ( segment . Parameters [ i ] . Name ) ;
link . Append ( segment . Parameters [ i ] . Name ) ;
if ( i + 1 < segment . Parameters . Count & & segment . Separator is not null )
if ( i + 1 < segment . Parameters . Count & & segment . Separator is not null )
link . Append ( segment . Separator ) ;
link . Append ( segment . Separator ) ;
}
}
link . Append ( segment . Suffix ) ;
}
}
return new SourceLink ( link . ToString ( ) ) ;
return new SourceLink ( link . ToString ( ) ) ;
}
}
#endregion
}
}
}
}