From a086cfa02b99a89860f62ff980a1d376c66bb383 Mon Sep 17 00:00:00 2001 From: aeqw89 Date: Sat, 10 May 2025 17:20:33 +0300 Subject: [PATCH] Introduced some unit testing. Cleaned up some classes in Beam. Overhauled source link generation. --- Beam.Temporary.Cli/Program.cs | 1 + Beam.Tests/Beam.Tests.csproj | 27 ++++ Beam.Tests/SouceLinkBuilder.Tests.cs | 92 +++++++++++++ Beam.Tests/UnitTest1.cs | 7 + Beam.sln | 6 + Beam/CommonStateChangers.cs | 16 +++ Beam/DataBackedSourceLinkGenerator.cs | 9 -- Beam/DelegateBackedSourceLinkGenerator.cs | 48 ------- Beam/DownloadContext.cs | 4 +- Beam/IDocumentSourceLinkFactory.cs | 6 +- Beam/IStateChangeBehaviour.cs | 8 ++ Beam/IncrementationBehaviour.cs | 17 --- Beam/NumberedStateChanger.cs | 16 +++ Beam/Ordered.cs | 3 + Beam/OrderedSourceLinkGenerator.cs | 48 +++++++ Beam/PackagedSourceLinkGenerationData.cs | 18 --- Beam/ParallelDownloader.cs | 78 ----------- Beam/S.cs | 3 + Beam/SequentialDownloader.cs | 2 +- Beam/{DocumentSourceLink.cs => SourceLink.cs} | 8 +- Beam/SourceLinkBuilder.cs | 122 ++++++++++++++++++ Beam/SourceLinkEnumerable.cs | 10 +- Beam/State.cs | 22 ++++ 23 files changed, 386 insertions(+), 185 deletions(-) create mode 100644 Beam.Tests/Beam.Tests.csproj create mode 100644 Beam.Tests/SouceLinkBuilder.Tests.cs create mode 100644 Beam.Tests/UnitTest1.cs create mode 100644 Beam/CommonStateChangers.cs delete mode 100644 Beam/DataBackedSourceLinkGenerator.cs delete mode 100644 Beam/DelegateBackedSourceLinkGenerator.cs create mode 100644 Beam/IStateChangeBehaviour.cs delete mode 100644 Beam/IncrementationBehaviour.cs create mode 100644 Beam/NumberedStateChanger.cs create mode 100644 Beam/Ordered.cs create mode 100644 Beam/OrderedSourceLinkGenerator.cs delete mode 100644 Beam/PackagedSourceLinkGenerationData.cs delete mode 100644 Beam/ParallelDownloader.cs rename Beam/{DocumentSourceLink.cs => SourceLink.cs} (65%) create mode 100644 Beam/SourceLinkBuilder.cs create mode 100644 Beam/State.cs diff --git a/Beam.Temporary.Cli/Program.cs b/Beam.Temporary.Cli/Program.cs index 5958b63..21ce3e1 100644 --- a/Beam.Temporary.Cli/Program.cs +++ b/Beam.Temporary.Cli/Program.cs @@ -36,6 +36,7 @@ namespace Beam.Temporary.Cli { await using var sharedContext = await DataDictionaryContext.Create( SharedDataPath, + false, DataKind.Shared, logger, ConversionOptions diff --git a/Beam.Tests/Beam.Tests.csproj b/Beam.Tests/Beam.Tests.csproj new file mode 100644 index 0000000..5d4c268 --- /dev/null +++ b/Beam.Tests/Beam.Tests.csproj @@ -0,0 +1,27 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + diff --git a/Beam.Tests/SouceLinkBuilder.Tests.cs b/Beam.Tests/SouceLinkBuilder.Tests.cs new file mode 100644 index 0000000..d5f657a --- /dev/null +++ b/Beam.Tests/SouceLinkBuilder.Tests.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Beam.Tests { + public class SouceLinkBuilder { + [Fact] + public void ShouldConstruct_NoErrors() { + _ = new SourceLinkBuilder("example.com"); + } + [Fact] + public void ShouldBuild_NoErrors() { + _ = new SourceLinkBuilder("example.com").Build(); + } + [Fact] + public void ShouldBuild_Correctly() { + var k = new SourceLinkBuilder("example.com"); + Assert.NotNull(k); + var link = k.Build(); + Assert.Equal("https://example.com/", link.Link.ToString()); // trailing slash for RFC standardization stuff + } + [Fact] + public void ShouldBuild_SegmentAddsCorrectly() { + var k = new SourceLinkBuilder("example.com"); + Assert.NotNull(k); + k.AddSegment("folder1"); + var link = k.Build(); + Assert.Equal("https://example.com/folder1", link.Link.ToString()); + } + [Fact] + public void ShouldThrow_EmptySegmentsDisallowed() { + var k = new SourceLinkBuilder("example.com"); + Assert.NotNull(k); + Assert.Throws(() => { + k.AddSegment(""); + }); + } + [Theory] + [InlineData("folder1", "folder2", "folder3")] + [InlineData("f1", "f5", "f6")] + public void ShouldBuild_MultipleSegmentsCorrect(params string[] segments) { + var k = new SourceLinkBuilder("example.com"); + Assert.NotNull(k); + foreach (var segment in segments) { + k.AddSegment(segment); + } + + StringBuilder builder = new(); + builder.Append("https://example.com/"); + foreach(var segment in segments) { + builder.Append(segment + "/"); + } + + // Remove trailing slash + builder.Remove(builder.Length - 1, 1); + + var link = k.Build(); + + Assert.Equal(builder.ToString(), link.Link.ToString()); + } + [Fact] + public void ShouldBuild_SingleParameterCorrect() { + var k = new SourceLinkBuilder("example.com"); + Assert.NotNull(k); + k.AddSegment("f1"); + k.AddParameters(0, "?q="); + var link = k.Build("foo"); + Assert.Equal("https://example.com/f1?q=foo", link.Link.ToString()); + } + [Fact] + public void ShouldBuild_MultiParameterCorrect() { + var k = new SourceLinkBuilder("example.com"); + Assert.NotNull(k); + k.AddSegment("f1"); + k.AddParameters(0, "?q=", "?m="); + var link = k.Build("foo", "bar"); + Assert.Equal("https://example.com/f1?q=foo?m=bar", link.Link.ToString()); + } + [Fact] + public void ShouldBuild_MultiParameterCorrectWithSuffix() { + var k = new SourceLinkBuilder("example.com"); + Assert.NotNull(k); + k.AddSegment("f1", "&"); + k.AddParameters(0, "?q=", "?m="); + var link = k.Build("foo", "bar"); + Assert.Equal("https://example.com/f1?q=foo&?m=bar", link.Link.ToString()); + } + + } +} diff --git a/Beam.Tests/UnitTest1.cs b/Beam.Tests/UnitTest1.cs new file mode 100644 index 0000000..d10ff40 --- /dev/null +++ b/Beam.Tests/UnitTest1.cs @@ -0,0 +1,7 @@ +namespace Beam.Tests { + public class UnitTest1 { + [Fact] + public void Test1() { + } + } +} diff --git a/Beam.sln b/Beam.sln index f0c9e82..0d6084d 100644 --- a/Beam.sln +++ b/Beam.sln @@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Beam.Dynamic", "Beam.Dynami EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Beam.Exports", "Beam.Exports\Beam.Exports.csproj", "{7C0ADBC0-44D4-48F8-901B-9C93F1B1FFDC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Beam.Tests", "Beam.Tests\Beam.Tests.csproj", "{E26800C2-0518-49E8-88DF-A0B6ED97D4AB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,6 +35,10 @@ Global {7C0ADBC0-44D4-48F8-901B-9C93F1B1FFDC}.Debug|Any CPU.Build.0 = Debug|Any CPU {7C0ADBC0-44D4-48F8-901B-9C93F1B1FFDC}.Release|Any CPU.ActiveCfg = Release|Any CPU {7C0ADBC0-44D4-48F8-901B-9C93F1B1FFDC}.Release|Any CPU.Build.0 = Release|Any CPU + {E26800C2-0518-49E8-88DF-A0B6ED97D4AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E26800C2-0518-49E8-88DF-A0B6ED97D4AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E26800C2-0518-49E8-88DF-A0B6ED97D4AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E26800C2-0518-49E8-88DF-A0B6ED97D4AB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Beam/CommonStateChangers.cs b/Beam/CommonStateChangers.cs new file mode 100644 index 0000000..fca5416 --- /dev/null +++ b/Beam/CommonStateChangers.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Beam { + public static class CommonStateChangers { + public static IStateChangeBehaviour LastAsNumber => new NumberedStateChanger((x, i) => { + object last = x[^1]; + if (!int.TryParse(last.ToString(), out var number)) + throw new InvalidOperationException(S.M.StateChangeError); + x[^1] = number + i; + }); + } +} diff --git a/Beam/DataBackedSourceLinkGenerator.cs b/Beam/DataBackedSourceLinkGenerator.cs deleted file mode 100644 index bf178ce..0000000 --- a/Beam/DataBackedSourceLinkGenerator.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Beam { - public class DataBackedSourceLinkGenerator(PackagedSourceLinkGenerationData data, params object[] initialState) : DelegateBackedSourceLinkGenerator(data.GenerateLink, data.GetBehaviour(), initialState) {} -} diff --git a/Beam/DelegateBackedSourceLinkGenerator.cs b/Beam/DelegateBackedSourceLinkGenerator.cs deleted file mode 100644 index 78023fc..0000000 --- a/Beam/DelegateBackedSourceLinkGenerator.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Beam { - public delegate DocumentSourceLink LinkGenerator(params object[] ps); - public delegate object Incrementor(object obj, int amount); - - public class DelegateBackedSourceLinkGenerator : IEnumerator { - public LinkGenerator Generator { get; set; } - public IncrementationBehaviour Behaviour { get; } - private object[] InitialState; - - public DelegateBackedSourceLinkGenerator(LinkGenerator generator, IncrementationBehaviour behaviour, params object[] initialState) { - Generator = generator; - Behaviour = behaviour; - InitialState = (object[])initialState.Clone(); - State = (object[])initialState.Clone(); - - Reset(); - } - - public object[] State { get; set; } - public DocumentSourceLink Current { get; private set; } - - object IEnumerator.Current => Current; - - public void Dispose() { - return; - } - - public bool MoveNext() { - Behaviour.Apply(State, 1); - Current = Generator(State); - return Current.HasValue; - } - - public void Reset() { - State = (object[])InitialState.Clone(); - Behaviour.Apply(State, -1); - Current = Generator(State); - } - } -} diff --git a/Beam/DownloadContext.cs b/Beam/DownloadContext.cs index 4892d6a..6e1691e 100644 --- a/Beam/DownloadContext.cs +++ b/Beam/DownloadContext.cs @@ -22,13 +22,13 @@ namespace Beam { public IProgress? RetryReporter { get; set; } public AsyncDownloadFailurePredicate?[]? AsyncFailurePredicates { get; } public TimeSpan TimeOut { get; set; } - public IEnumerable Links { get; } + public IEnumerable Links { get; } public CancellationToken CancellationToken { get; } public DocumentCache Cache { get; private set; } = []; public ILogger? DownloadLogger { get; set; } public DownloadContext(HtmlWeb web, - IEnumerable links, + IEnumerable links, CancellationToken cancellationToken = default, HtmlTransformer? transformer = null, AsyncHtmlTransformer? asyncTransformer = null, diff --git a/Beam/IDocumentSourceLinkFactory.cs b/Beam/IDocumentSourceLinkFactory.cs index 5e9c9dd..feaeef9 100644 --- a/Beam/IDocumentSourceLinkFactory.cs +++ b/Beam/IDocumentSourceLinkFactory.cs @@ -1,8 +1,8 @@ namespace Beam { internal interface IDocumentSourceLinkFactory { - DocumentSourceLink GetNextLink(DocumentSourceLink current); - DocumentSourceLink GetPrecedingLink(DocumentSourceLink current); - DocumentSourceLink GetArbitraryLink(DocumentSourceLink current, int offset) => offset switch { + SourceLink GetNextLink(SourceLink current); + SourceLink GetPrecedingLink(SourceLink current); + SourceLink GetArbitraryLink(SourceLink current, int offset) => offset switch { 0 => current, > 0 => GetArbitraryLink(GetNextLink(current), offset - 1), < 0 => GetArbitraryLink(GetPrecedingLink(current), offset + 1) diff --git a/Beam/IStateChangeBehaviour.cs b/Beam/IStateChangeBehaviour.cs new file mode 100644 index 0000000..8fd823d --- /dev/null +++ b/Beam/IStateChangeBehaviour.cs @@ -0,0 +1,8 @@ +namespace Beam { + /// + /// Defines how a url template should should be updated, in what order, and by how much + /// + public interface IStateChangeBehaviour { + public void Apply(State state, object stimulus); + } +} diff --git a/Beam/IncrementationBehaviour.cs b/Beam/IncrementationBehaviour.cs deleted file mode 100644 index 6023ca4..0000000 --- a/Beam/IncrementationBehaviour.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Beam { - /// - /// Defines how a url template should should be updated, in what order, and by how much - /// - public struct IncrementationBehaviour { - public Dictionary Map { get; set; } - - public readonly void Apply(object[] objects, int amount) { - foreach(var (i, inc) in Map) { - if (i < objects.Length) - objects[i] = inc(objects[i], amount)?.ToString(); - else - throw new S.MapException(S.M.MapDoesNotMatchArgs); - } - } - } -} diff --git a/Beam/NumberedStateChanger.cs b/Beam/NumberedStateChanger.cs new file mode 100644 index 0000000..c97a523 --- /dev/null +++ b/Beam/NumberedStateChanger.cs @@ -0,0 +1,16 @@ +namespace Beam { + public class NumberedStateChanger(NumberedStateChanger.MoveState moveState) : IStateChangeBehaviour { + public delegate void MoveState(State state, int amount); + public MoveState MoveStateDlgte { get; set; } = moveState; + + public virtual void Apply(State state, object stimulus) { + if (stimulus is not int amount) + throw new ArgumentException(S.M.StimulusMustBeInt, nameof(stimulus)); + Apply(state, amount); + } + + public virtual void Apply(State state, int amount) { + MoveStateDlgte(state, amount); + } + } +} diff --git a/Beam/Ordered.cs b/Beam/Ordered.cs new file mode 100644 index 0000000..e096725 --- /dev/null +++ b/Beam/Ordered.cs @@ -0,0 +1,3 @@ +namespace Beam { + public record Ordered(T Data, int Order); +} diff --git a/Beam/OrderedSourceLinkGenerator.cs b/Beam/OrderedSourceLinkGenerator.cs new file mode 100644 index 0000000..2edee29 --- /dev/null +++ b/Beam/OrderedSourceLinkGenerator.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Beam { + //public delegate SourceLink LinkGenerator(params object[] ps); + //public delegate object Incrementor(object obj, int amount); + + public class OrderedSourceLinkGenerator : IEnumerator { + public SourceLinkBuilder Builder { get; set; } + public NumberedStateChanger Behaviour { get; } + private State InitialState; + + public OrderedSourceLinkGenerator(SourceLinkBuilder builder, NumberedStateChanger behaviour, params object[] initialState) { + Builder = builder; + Behaviour = behaviour; + InitialState = new State(initialState); + State = InitialState.Copy(); + + Reset(); + } + + public State State { get; set; } + public SourceLink Current { get; private set; } + + object IEnumerator.Current => Current; + + public void Dispose() { + return; + } + + public bool MoveNext() { + Behaviour.Apply(State, 1); + Current = Builder.Build(State); + return Current.HasValue; + } + + public void Reset() { + State = InitialState.Copy(); + Behaviour.Apply(State, -1); + Current = Builder.Build(State); + } + } +} diff --git a/Beam/PackagedSourceLinkGenerationData.cs b/Beam/PackagedSourceLinkGenerationData.cs deleted file mode 100644 index 50e3cd9..0000000 --- a/Beam/PackagedSourceLinkGenerationData.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Beam { - public struct PackagedSourceLinkGenerationData { - public string Template { get; set; } - public int IndexOfChapterIndex { get; set; } - - public readonly DocumentSourceLink GenerateLink(params object[] ps) - => new(string.Format(Template, ps)); - public IncrementationBehaviour GetBehaviour() { - return new IncrementationBehaviour() { - Map = new Dictionary() { { - IndexOfChapterIndex, - (x, i) => int.Parse(x.ToString() ?? throw new ArgumentException()) + i - } - } - }; - } - } -} diff --git a/Beam/ParallelDownloader.cs b/Beam/ParallelDownloader.cs deleted file mode 100644 index ebbfd2a..0000000 --- a/Beam/ParallelDownloader.cs +++ /dev/null @@ -1,78 +0,0 @@ -using HtmlAgilityPack; -using System.Collections; -using System.Collections.Concurrent; - -namespace Beam { - public record Ordered(T Data, int Order); - [Obsolete("Use chunk downloader instead.")] - public class ParallelDownloader(DownloadContext context, int maximumConcurrentDownloads = 4) : IAsyncEnumerator> { - - public DownloadContext Context { get; } = context; - public int MaximumConcurrentDownloads { get; } = maximumConcurrentDownloads; - - private Task? CacheFiller { get; set; } - private int Count = 0; - private ConcurrentBag> Cache { get; set; } = []; - public Ordered Current { get; set; } - - private UnitDownloader GetUnitDownloader() - => new(Context.Web, Context.AsyncTranformer, Context.AsyncFailurePredicates); - private ParallelOptions GetOptions() - => new() { - CancellationToken = Context.CancellationToken, - MaxDegreeOfParallelism = MaximumConcurrentDownloads - }; - - private async Task FillCache() { - List> chunk = []; - int i = 0; - foreach (var link in Context.Links.Take(MaximumConcurrentDownloads * 2)) - chunk.Add(new Ordered(link, i++)); - Console.WriteLine(chunk.Select((x) => $"{x.Order}: {x.Data.Link}").Aggregate((x, y) => $"{x}\n{y}")); - var unitDownloader = GetUnitDownloader(); - int downloadedCount = 0; - - await Parallel.ForEachAsync(chunk, GetOptions(), async (x, ct) => { - var (result, doc) = await unitDownloader.TryDownload([new Ordered(x.Data.Link.ToString(), x.Order)], ct, tryProgress: Context.RetryReporter); - if (!result || doc is null) { - Console.WriteLine($"FAILED to download {x.Data.Link}"); - return; - } - Cache.Add(new(doc, x.Order)); - Context.DownloadReporter?.Report(doc); - Interlocked.Increment(ref downloadedCount); - Interlocked.Increment(ref Count); - }); - - Console.WriteLine("Downloaded Chunk"); - CacheFiller = null; - } - - public async ValueTask MoveNextAsync() { - TimeSpan waited = TimeSpan.Zero; - TimeSpan delta = TimeSpan.FromSeconds(0.01); - while(waited < Context.TimeOut) { - if (Cache.Count < MaximumConcurrentDownloads && CacheFiller is null) // strange - CacheFiller ??= FillCache(); - - Cache.TryTake(out var k); - if (k is not null) { - Current = k; - return true; - } - - - - waited += delta; - await Task.Delay(delta); - } - - return false; - } - - public ValueTask DisposeAsync() { - GC.SuppressFinalize(this); - return ValueTask.CompletedTask; - } - } -} diff --git a/Beam/S.cs b/Beam/S.cs index dae2faa..709db9b 100644 --- a/Beam/S.cs +++ b/Beam/S.cs @@ -34,6 +34,9 @@ namespace Beam { public const string MapDoesNotMatchArgs = "Error; Map contains indicies that exceed the argument list passed."; public const string NewFragmentShouldBeFree = "Assertion Error: Could not acquire lock of newly created fragment"; public const string LinksCannotBeEmpty = "Cannot construct downloader with empty links collection!"; + public const string StimulusMustBeInt = "Stimulus must be an integer"; + public const string StateCastException = "State cannot be cast to T"; + public const string StateChangeError = "Something went wrong while changing the state."; } } } diff --git a/Beam/SequentialDownloader.cs b/Beam/SequentialDownloader.cs index 402ad14..32f71b8 100644 --- a/Beam/SequentialDownloader.cs +++ b/Beam/SequentialDownloader.cs @@ -8,7 +8,7 @@ namespace Beam { public ILogger? Logger { get; set; } public int LastOrder { get; set; } = 0; - protected IEnumerator LinksEnumerator; + protected IEnumerator LinksEnumerator; public Func> GetUnitDownloader { get; set; } diff --git a/Beam/DocumentSourceLink.cs b/Beam/SourceLink.cs similarity index 65% rename from Beam/DocumentSourceLink.cs rename to Beam/SourceLink.cs index d972e27..721b442 100644 --- a/Beam/DocumentSourceLink.cs +++ b/Beam/SourceLink.cs @@ -5,18 +5,18 @@ using System.Text; using System.Threading.Tasks; namespace Beam { - public readonly struct DocumentSourceLink(string link) { + public readonly struct SourceLink(string link) { private readonly string Link_ { get; } = link; public readonly Uri Link => new(Link_); public bool HasValue => !string.IsNullOrWhiteSpace(Link_); - public static DocumentSourceLink InvalidLink { get; } = new("https://invalid.link"); + public static SourceLink InvalidLink { get; } = new("invalid://link"); - public static bool operator ==(DocumentSourceLink lhs, DocumentSourceLink rhs) { + public static bool operator ==(SourceLink lhs, SourceLink rhs) { return lhs.Link == rhs.Link; } - public static bool operator !=(DocumentSourceLink lhs, DocumentSourceLink rhs) { + public static bool operator !=(SourceLink lhs, SourceLink rhs) { return lhs.Link != rhs.Link; } diff --git a/Beam/SourceLinkBuilder.cs b/Beam/SourceLinkBuilder.cs new file mode 100644 index 0000000..1236b76 --- /dev/null +++ b/Beam/SourceLinkBuilder.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Beam { + public class Parameter(string name) { + public string Name { get; set; } = name; + } + + public class LinkSegment(string name, string separator = "", string suffix = "") { + public string Name { get; set; } = name; + public List Parameters { get; set; } = []; + public string Separator { get; set; } = separator; + public string Suffix { get; set; } = suffix; + } + + public class SourceLinkBuilder(string host, string protocol = "https") { + public string Protocol { get; set; } = protocol; + public string Host { get; set; } = host; + public List Segments { get; set; } = []; + + public string GetSuffix(int segmentIndex) { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(segmentIndex, Segments.Count); + ArgumentOutOfRangeException.ThrowIfNegative(segmentIndex); + return Segments[segmentIndex].Suffix; + } + + public string GetSuffix() + => GetSuffix(Segments.Count - 1); + + public string GetSeparator(int segmentIndex) { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(segmentIndex, Segments.Count); + ArgumentOutOfRangeException.ThrowIfNegative(segmentIndex); + return Segments[segmentIndex].Separator; + } + + public string GetSeparator() + => GetSeparator(Segments.Count - 1); + + public void SetSuffix(int segmentIndex, string suffix) { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(segmentIndex, Segments.Count); + ArgumentOutOfRangeException.ThrowIfNegative(segmentIndex); + var seg = Segments[segmentIndex]; + seg.Suffix = suffix; + } + + public void SetSuffix(string suffix) + => SetSuffix(Segments.Count - 1, suffix); + + public void SetSeparator(int segmentIndex, string separator) { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(segmentIndex, Segments.Count); + ArgumentOutOfRangeException.ThrowIfNegative(segmentIndex); + var seg = Segments[segmentIndex]; + seg.Separator = separator; + } + + public void SetSeparator(string separator) + => SetSeparator(Segments.Count - 1, separator); + + public void AddSegment(string name, string? separator = null) { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + Segments.Add(new LinkSegment(name, separator)); + } + + 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)); + } + } + + public void AddParameters(params string[] parameters) + => AddParameters(Segments.Count - 1, parameters); + + 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); + } + + public void SetParameters(params string[] parameters) + => SetParameters(Segments.Count - 1, parameters); + + public int GetParameterCount() { + int count = 0; + foreach(var segment in Segments) { + count += segment.Parameters.Count; + } + + return count; + } + + public SourceLink 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); + for (int i = 0; i < segment.Parameters.Count; i++) { + link.Append(segment.Parameters[i].Name); + link.Append(parameterValues[pvC++]); + if (i + 1 < segment.Parameters.Count && segment.Separator is not null) + link.Append(segment.Separator); + } + } + + return new SourceLink(link.ToString()); + } + } +} diff --git a/Beam/SourceLinkEnumerable.cs b/Beam/SourceLinkEnumerable.cs index 3bc2b2f..4895ceb 100644 --- a/Beam/SourceLinkEnumerable.cs +++ b/Beam/SourceLinkEnumerable.cs @@ -6,17 +6,17 @@ using System.Text; using System.Threading.Tasks; namespace Beam { - public class SourceLinkEnumerable : IEnumerable { - private SourceLinkEnumerable(IEnumerator enumerator) { + public class SourceLinkEnumerable : IEnumerable { + private SourceLinkEnumerable(IEnumerator enumerator) { Enumerator = enumerator; } - public IEnumerator Enumerator { get; } + public IEnumerator Enumerator { get; } - public static SourceLinkEnumerable FromGenerator(IEnumerator generator) + public static SourceLinkEnumerable FromGenerator(IEnumerator generator) => new SourceLinkEnumerable(generator); - public IEnumerator GetEnumerator() { + public IEnumerator GetEnumerator() { return Enumerator; } diff --git a/Beam/State.cs b/Beam/State.cs new file mode 100644 index 0000000..4c05f44 --- /dev/null +++ b/Beam/State.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Beam { + public class State(object[] state) { + object[] state = state; + + public object[] GetState() => state; + public void SetState(object[] state) => this.state = state; + + public State Copy() + => new((object[])state.Clone()); + + public object this[Index i] { + get => state[i]; + set => state[i] = value; + } + } +}