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()); #region Build /// /// Produces a concrete using values from an external object. /// /// Object providing positional values. public string Build(State parameterValues) => Build(parameterValues.GetState()); /// /// Produces a concrete by substituting into the template. /// /// Flat array of values that will be written in the order that parameters appear when segments are enumerated left‑to‑right. Any optional parameters must still appear as null if missing. /// The completed . /// If the supplied value count does not match (). public string Build(params object?[] parameterValues) { ArgumentOutOfRangeException.ThrowIfNotEqual(parameterValues.Length, GetParameterCount()); StringBuilder link = new(); link.Append(Protocol); link.Append("://"); link.Append(Host); int pvC = 0; foreach (var segment in Segments) { link.Append('/'); link.Append(segment.Name); bool startedQueryString = false; for (int i = 0; i < segment.Parameters.Count; i++) { if (parameterValues[pvC] is null) if (segment.Parameters[i].Position.HasFlag(Position.Optional)) continue; else throw new ArgumentException(string.Format(link_builder_argument_missing, pvC, segment.Parameters[i].Name)); if (segment.Parameters[i].Position.HasFlag(Position.Query) && Segments[^1] != segment) throw new ArgumentException(string.Format(link_builder_query_only_at_last, i)); if (segment.Parameters[i].Position.HasFlag(Position.Query)) if (!startedQueryString) { link.Append('?'); startedQueryString = true; } else link.Append('&'); if (segment.Parameters[i].Position.HasFlag(Position.Before)) link.Append(segment.Parameters[i].Name); if (segment.Parameters[i].Position.HasFlag(Position.Query)) link.Append('='); if (parameterValues[pvC] is not null) link.Append(parameterValues[pvC++]); else if (!segment.Parameters[i].Position.HasFlag(Position.Optional)) throw new ArgumentException(string.Format(link_builder_argument_missing, pvC, segment.Parameters[i].Name)); if (segment.Parameters[i].Position.HasFlag(Position.Query | Position.After)) throw new ArgumentException(string.Format(link_builder_incompatible_flag, nameof(Position.Query), nameof(Position.After))); if (segment.Parameters[i].Position.HasFlag(Position.After)) link.Append(segment.Parameters[i].Name); if (i + 1 < segment.Parameters.Count && segment.Separator is not null) link.Append(segment.Separator); } link.Append(segment.Suffix); } return link.ToString(); } #endregion } }