using System.Text;
using Beam.Abstractions;
using Beam.Models;
using static Beam.Exceptions.Exceptions;
namespace Beam.Data {
///
/// Describes where a token should be inserted relative to the run‑time value
/// that ultimately replaces it when building a .
///
[Flags]
public enum Position {
///
/// The parameter name is written before its run‑time value
/// (e.g. id42 when the name is id and the value is 42).
///
Before = 0b01,
///
/// The parameter name is written after its run‑time value
/// (e.g. 42id).
///
After = 0b10,
///
/// The parameter name is written both before and after the value
/// (e.g. id42id).
///
BeforeAndAfter = 0b11,
///
/// The parameter is optional, and is omitted if missing.
///
Optional = 0b100,
///
/// The parameter is a query parameter, and should be decorated with ? and &
///
Query = 0b1000,
}
///
/// Represents a single placeholder that will be substituted when a link is built.
///
/// Identifier that appears in the final link according to .
/// Controls whether is written before, after, or on both sides of the value.
public class Parameter(string name, Position position = Position.Before) {
///
/// Gets or sets the identifier that frames the value when the link is rendered.
///
public string Name { get; set; } = name;
///
/// Gets or sets the position at which is emitted relative to the run‑time value.
///
public Position Position { get; set; } = position;
///
/// Creates a shallow copy whose mutable members are detached from the original instance.
///
/// A new with identical and .
public Parameter Clone()
=> new(Name, Position);
}
///
/// Describes one path segment of a URL and the collection of tokens that belong to it.
///
/// Literal part of the segment that precedes the parameters (may be empty).
/// Optional string placed between adjacent parameters when more than one is present.
/// Optional string appended after the last parameter in the segment.
public class LinkSegment(string name, string separator = "", string suffix = "") {
///
/// Gets or sets the literal path component for this segment.
///
public string Name { get; set; } = name;
///
/// Gets or sets the collection of parameters that appear within the segment.
///
public List Parameters { get; set; } = [];
///
/// Gets or sets the string inserted between parameters when the segment is rendered.
///
public string Separator { get; set; } = separator;
///
/// Gets or sets the string appended after the last parameter when the segment is rendered.
///
public string Suffix { get; set; } = suffix;
///
/// Produces a deep copy whose list contains cloned objects.
///
public LinkSegment Clone()
=> new LinkSegment(Name, Separator, Suffix) {
Parameters = [.. Parameters.Select(static x => x.Clone())]
};
///
/// Replaces with a new set whose items all use .
///
/// Parameter identifiers to add.
/// This instance for fluent calls.
public LinkSegment WithParameters(params string[] parameters) {
Parameters = parameters.Select(static x => new Parameter(x)).ToList();
return this;
}
///
/// Replaces with a new set using explicit name/position tuples.
///
/// Tuples of parameter identifier and desired position.
/// This instance for fluent calls.
public LinkSegment WithParameters(params (string, Position)[] parameters) {
Parameters = parameters.Select(static x => new Parameter(x.Item1, x.Item2)).ToList();
return this;
}
}
///
/// Fluent helper for composing strongly‑typed, template‑driven source links.
///
///
/// 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.
///
/// DNS host name (e.g. api.example.com).
/// Transport protocol; defaults to https.
public class LinkBuilder(string host, string protocol = "https") : ILinkBuilder {
///
/// Gets or sets the scheme part of the URL (e.g. https, http).
///
public string Protocol { get; set; } = protocol;
///
/// Gets or sets the host portion of the URL.
///
public string Host { get; set; } = host;
///
/// Gets or sets the ordered collection of path segments.
///
public List Segments { get; set; } = [];
///
/// Produces a deep copy whose and contained collections are detached from the original.
///
public LinkBuilder Clone()
=> new LinkBuilder(Host, Protocol) {
Segments = [.. Segments.Select(static x => x.Clone())]
};
#region Helpers – suffix & separator
///
/// Returns the suffix of the ‑th segment.
///
/// Zero‑based segment index.
/// If is outside the valid range.
public string GetSuffix(int segmentIndex) {
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(segmentIndex, Segments.Count);
ArgumentOutOfRangeException.ThrowIfNegative(segmentIndex);
return Segments[segmentIndex].Suffix;
}
///
/// Returns the suffix of the last segment.
///
public string GetSuffix()
=> GetSuffix(Segments.Count - 1);
///
/// Returns the separator used by the ‑th segment.
///
/// Zero‑based segment index.
/// If is outside the valid range.
public string GetSeparator(int segmentIndex) {
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(segmentIndex, Segments.Count);
ArgumentOutOfRangeException.ThrowIfNegative(segmentIndex);
return Segments[segmentIndex].Separator;
}
///
/// Returns the separator of the last segment.
///
public string GetSeparator()
=> GetSeparator(Segments.Count - 1);
///
/// Assigns a new suffix to the ‑th segment.
///
/// Zero‑based segment index.
/// String appended after the segment's parameters.
/// If is outside the valid range.
public void SetSuffix(int segmentIndex, string suffix) {
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(segmentIndex, Segments.Count);
ArgumentOutOfRangeException.ThrowIfNegative(segmentIndex);
Segments[segmentIndex].Suffix = suffix;
}
///
/// Assigns a new suffix to the last segment.
///
public void SetSuffix(string suffix)
=> SetSuffix(Segments.Count - 1, suffix);
///
/// Assigns a new separator to the ‑th segment.
///
/// Zero‑based segment index.
/// String inserted between parameters belonging to the same segment.
/// If is outside the valid range.
public void SetSeparator(int segmentIndex, string separator) {
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(segmentIndex, Segments.Count);
ArgumentOutOfRangeException.ThrowIfNegative(segmentIndex);
Segments[segmentIndex].Separator = separator;
}
///
/// Assigns a new separator to the last segment.
///
public void SetSeparator(string separator)
=> SetSeparator(Segments.Count - 1, separator);
#endregion
#region Segment manipulation
///
/// Appends a new segment.
///
/// Literal portion of the segment.
/// Optional separator for subsequent parameters; keeps the current default.
/// If is , empty, or whitespace.
public void AddSegment(string name, string? separator = null) {
ArgumentException.ThrowIfNullOrWhiteSpace(name);
Segments.Add(new LinkSegment(name, separator));
}
///
/// Replaces the whole collection with the supplied , each represented as a .
///
/// This instance for fluent calls.
public LinkBuilder WithSegments(params IEnumerable segments) {
Segments = segments.Select(static x => new LinkSegment(x)).ToList();
return this;
}
///
/// Replaces the collection with empty segments.
///
/// Number of segments to create.
public LinkBuilder WithSegments(int count)
=> WithSegments(Enumerable.Repeat("", count));
#endregion
#region Parameter manipulation (fluent)
///
/// Replaces parameters of the ‑th segment using the supplied identifiers.
///
public LinkBuilder WithParameters(int i, params string[] parameters) {
Segments[i].WithParameters(parameters);
return this;
}
///
/// Replaces parameters of the ‑th segment using explicit name/position tuples.
///
public LinkBuilder WithParameters(int i, params (string, Position)[] parameters) {
Segments[i].WithParameters(parameters);
return this;
}
#endregion
#region Parameter manipulation (imperative)
///
/// Adds new parameters to the specified segment without clearing existing ones.
///
/// Zero‑based segment index.
/// Identifiers of parameters to add.
/// If is invalid.
/// If any parameter identifier is , empty or whitespace.
public void AddParameters(int segmentIndex, params string[] parameters) {
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(segmentIndex, Segments.Count);
ArgumentOutOfRangeException.ThrowIfNegative(segmentIndex);
var seg = Segments[segmentIndex];
foreach (var parameter in parameters) {
ArgumentException.ThrowIfNullOrWhiteSpace(parameter);
seg.Parameters.Add(new Parameter(parameter));
}
}
///
/// Adds parameters to the last segment.
///
public void AddParameters(params string[] parameters)
=> AddParameters(Segments.Count - 1, parameters);
///
/// Replaces the entire parameter list of the specified segment.
///
public void SetParameters(int segmentIndex, params string[] parameters) {
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(segmentIndex, Segments.Count);
ArgumentOutOfRangeException.ThrowIfNegative(segmentIndex);
var seg = Segments[segmentIndex];
seg.Parameters.Clear();
AddParameters(segmentIndex, parameters);
}
///
/// Replaces the parameter list of the last segment.
///
public void SetParameters(params string[] parameters)
=> SetParameters(Segments.Count - 1, parameters);
#endregion
///
/// Returns the total number of tokens across all segments.
///
public int GetParameterCount() {
int count = 0;
foreach (var segment in Segments) {
count += segment.Parameters.Count;
}
return count;
}
public string Build(IReadOnlyState state)
=> Build(state.GetState().ToArray