Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
274 changes: 274 additions & 0 deletions Dummies.UnitTests/AnyUriTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
#region Usings declarations

using System;
using System.Collections.Generic;

using JetBrains.Annotations;

using NFluent;

using Xunit;

#endregion

namespace Dummies.UnitTests;

[TestSubject(typeof(AnyUri))]
public sealed class AnyUriTests {

private const int FuzzSeeds = 500;
private const int SampleCount = 300;

#region Statics members declarations

// Valid by construction: across many seeds a family generator never throws and yields the expected URI kind.
private static void GeneratesAcross(Func<AnyContext, IAny<Uri>> build, UriKind? kind) {
for (int seed = 0; seed < FuzzSeeds; seed++) {
Uri value;
try { value = build(Any.WithSeed(seed)).Generate(); }
catch (Exception error) { Assert.Fail($"seed {seed}: {error.GetType().Name}: {error.Message}"); return; }

if (kind.HasValue) {
Check.WithCustomMessage($"seed {seed}: '{value.OriginalString}'")
.That(value.IsAbsoluteUri).IsEqualTo(kind.Value == UriKind.Absolute);
}
}
}

private static IEnumerable<Uri> Sample(IAny<Uri> generator) {
for (int i = 0; i < SampleCount; i++) { yield return generator.Generate(); }
}

private static IAny<Uri> Seeded(Func<AnyContext, IAny<Uri>> build) {
return build(Any.WithSeed(20260723));
}

#endregion

[Fact(DisplayName = "Every family is valid by construction: it never throws and yields the expected URI kind.")]
public void EveryFamilyGeneratesValidUris() {
GeneratesAcross(context => context.Uri(), null);
GeneratesAcross(context => context.Uri().Web(), UriKind.Absolute);
GeneratesAcross(context => context.Uri().WebSocket(), UriKind.Absolute);
GeneratesAcross(context => context.Uri().Ftp(), UriKind.Absolute);
GeneratesAcross(context => context.Uri().Mailto(), UriKind.Absolute);
GeneratesAcross(context => context.Uri().Relative(), UriKind.Relative);
}

[Fact(DisplayName = "The unconstrained generator reaches every family.")]
public void UnconstrainedReachesEveryFamily() {
HashSet<string> seen = new();
foreach (Uri value in Sample(Seeded(context => context.Uri()))) {
seen.Add(value.IsAbsoluteUri ? value.Scheme : "relative");
}

Check.That(seen.Contains("http") || seen.Contains("https")).IsTrue();
Check.That(seen.Contains("ws") || seen.Contains("wss")).IsTrue();
Check.That(seen.Contains("ftp")).IsTrue();
Check.That(seen.Contains("mailto")).IsTrue();
Check.That(seen.Contains("relative")).IsTrue();
}

[Fact(DisplayName = "Web reaches both http and https; each generated value is one of them.")]
public void WebReachesBothSchemes() {
HashSet<string> seen = new();
foreach (Uri value in Sample(Seeded(context => context.Uri().Web()))) {
seen.Add(value.Scheme);
Check.That(value.Scheme is "http" or "https").IsTrue();
}

Check.That(seen.Contains("http")).IsTrue();
Check.That(seen.Contains("https")).IsTrue();
}

[Fact(DisplayName = "WebSocket reaches both ws and wss.")]
public void WebSocketReachesBothSchemes() {
HashSet<string> seen = new();
foreach (Uri value in Sample(Seeded(context => context.Uri().WebSocket()))) {
seen.Add(value.Scheme);
Check.That(value.Scheme is "ws" or "wss").IsTrue();
}

Check.That(seen.Contains("ws")).IsTrue();
Check.That(seen.Contains("wss")).IsTrue();
}

[Fact(DisplayName = "UsingHttps pins the scheme to https.")]
public void UsingHttpsPinsTheScheme() {
foreach (Uri value in Sample(Seeded(context => context.Uri().Web().UsingHttps()))) {
Check.That(value.Scheme).IsEqualTo("https");
}
}

[Fact(DisplayName = "WithHost pins the host.")]
public void WithHostPinsTheHost() {
foreach (Uri value in Sample(Seeded(context => context.Uri().Web().WithHost("api.example.com")))) {
Check.That(value.Host).IsEqualTo("api.example.com");
}
}

[Fact(DisplayName = "WithPort pins the port.")]
public void WithPortPinsThePort() {
foreach (Uri value in Sample(Seeded(context => context.Uri().Web().WithPort(8443)))) {
Check.That(value.Port).IsEqualTo(8443);
}
}

[Fact(DisplayName = "WithoutPath renders the root path.")]
public void WithoutPathRendersRoot() {
foreach (Uri value in Sample(Seeded(context => context.Uri().Web().WithoutPath()))) {
Check.That(value.AbsolutePath).IsEqualTo("/");
}
}

[Fact(DisplayName = "WithPathSegments renders exactly that many segments.")]
public void WithPathSegmentsRendersThatManySegments() {
foreach (Uri value in Sample(Seeded(context => context.Uri().Web().WithHost("h.test").WithPathSegments(3)))) {
Check.That(value.AbsolutePath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries).Length).IsEqualTo(3);
}
}

[Fact(DisplayName = "WithUserInfo, WithQuery and WithFragment add their components.")]
public void OptionalComponentsAreIncluded() {
foreach (Uri value in Sample(Seeded(context => context.Uri().Web().WithUserInfo().WithQuery().WithFragment()))) {
Check.That(value.UserInfo.Length).IsStrictlyGreaterThan(0);
Check.That(value.Query.Length).IsStrictlyGreaterThan(0);
Check.That(value.Fragment.Length).IsStrictlyGreaterThan(0);
}
}

[Fact(DisplayName = "WithUserInfo(user, password) pins both parts.")]
public void WithUserInfoPinsBothParts() {
foreach (Uri value in Sample(Seeded(context => context.Uri().Ftp().WithUserInfo("admin", "s3cret")))) {
Check.That(value.UserInfo).IsEqualTo("admin:s3cret");
}
}

[Fact(DisplayName = "Ftp yields the ftp scheme with user-info.")]
public void FtpYieldsFtpScheme() {
foreach (Uri value in Sample(Seeded(context => context.Uri().Ftp().WithUserInfo()))) {
Check.That(value.Scheme).IsEqualTo("ftp");
Check.That(value.UserInfo.Length).IsStrictlyGreaterThan(0);
}
}

[Fact(DisplayName = "Mailto yields the mailto scheme and an address.")]
public void MailtoYieldsAnAddress() {
foreach (Uri value in Sample(Seeded(context => context.Uri().Mailto()))) {
Check.That(value.Scheme).IsEqualTo("mailto");
Check.That(value.OriginalString).StartsWith("mailto:");
Check.That(value.OriginalString).Contains("@");
}
}

[Fact(DisplayName = "Mailto pins the local-part and domain.")]
public void MailtoPinsLocalPartAndDomain() {
foreach (Uri value in Sample(Seeded(context => context.Uri().Mailto().WithLocalPart("john").WithDomain("example.test")))) {
Check.That(value.OriginalString).IsEqualTo("mailto:john@example.test");
}
}

[Fact(DisplayName = "Relative yields a relative reference (not absolute).")]
public void RelativeYieldsARelativeReference() {
foreach (Uri value in Sample(Seeded(context => context.Uri().Relative()))) {
Check.That(value.IsAbsoluteUri).IsFalse();
}
}

[Fact(DisplayName = "Relative().Rooted() starts the path with a slash.")]
public void RootedRelativeStartsWithSlash() {
foreach (Uri value in Sample(Seeded(context => context.Uri().Relative().Rooted().WithPathSegments(2)))) {
Check.That(value.OriginalString).StartsWith("/");
}
}

[Fact(DisplayName = "A second scheme pin conflicts, naming both sides.")]
public void SecondSchemePinConflicts() {
ConflictingAnyConstraintException conflict = Assert.Throws<ConflictingAnyConstraintException>(
() => Any.Uri().Web().UsingHttp().UsingHttps());

Check.That(conflict.Message).Contains("UsingHttps()");
Check.That(conflict.Message).Contains("UsingHttp()");
}

[Fact(DisplayName = "A second path constraint conflicts, naming both sides.")]
public void SecondPathConstraintConflicts() {
ConflictingAnyConstraintException conflict = Assert.Throws<ConflictingAnyConstraintException>(
() => Any.Uri().Web().WithoutPath().WithPathSegments(2));

Check.That(conflict.Message).Contains("WithPathSegments(2)");
Check.That(conflict.Message).Contains("WithoutPath()");
}

[Fact(DisplayName = "WithHost rejects a non-ASCII (IDN) host, pointing to punycode.")]
public void WithHostRejectsIdnHost() {
ArgumentException error = Assert.Throws<ArgumentException>(() => Any.Uri().Web().WithHost("münchen.de"));

Check.That(error.Message).Contains("punycode");
}

[Fact(DisplayName = "Host, user-info, port and segment-count arguments are validated as arguments.")]
public void ArgumentsAreValidated() {
Check.ThatCode(() => Any.Uri().Web().WithHost(null!)).Throws<ArgumentNullException>();
Check.ThatCode(() => Any.Uri().Web().WithHost("")).Throws<ArgumentException>();
Check.ThatCode(() => Any.Uri().Web().WithHost("bad host")).Throws<ArgumentException>();
Check.ThatCode(() => Any.Uri().Web().WithUserInfo("a:b")).Throws<ArgumentException>();
Check.ThatCode(() => Any.Uri().Web().WithPort(0)).Throws<ArgumentOutOfRangeException>();
Check.ThatCode(() => Any.Uri().Web().WithPort(70000)).Throws<ArgumentOutOfRangeException>();
Check.ThatCode(() => Any.Uri().Web().WithPathSegments(-1)).Throws<ArgumentOutOfRangeException>();
}

[Fact(DisplayName = "WithUserInfo(user) keeps the user and draws an arbitrary password.")]
public void WithUserInfoPinsUserAndDrawsPassword() {
foreach (Uri value in Sample(Seeded(context => context.Uri().Web().WithUserInfo("bob")))) {
Check.That(value.UserInfo).StartsWith("bob:");
Check.That(value.UserInfo.Length).IsStrictlyGreaterThan("bob:".Length);
}
}

[Fact(DisplayName = "A relative URI with an explicit 0 segments and nothing else fails at generation with a seed.")]
public void EmptyRelativeFailsAtGeneration() {
AnyGenerationException error = Assert.Throws<AnyGenerationException>(
() => Any.WithSeed(20260723).Uri().Relative().WithPathSegments(0).Generate());

Check.That(error.Seed).IsEqualTo(20260723);
}

[Fact(DisplayName = "A relative URI with 0 segments still generates when it carries a query.")]
public void ZeroSegmentRelativeWithQueryGenerates() {
Uri value = Any.WithSeed(1).Uri().Relative().WithPathSegments(0).WithQuery().Generate();

Check.That(value.IsAbsoluteUri).IsFalse();
Check.That(value.OriginalString).StartsWith("?");
}

[Fact(DisplayName = "WithHost rejects a non-canonical shorthand-IPv4 host that would not round-trip.")]
public void WithHostRejectsNonCanonicalHost() {
ArgumentException error = Assert.Throws<ArgumentException>(() => Any.Uri().Web().WithHost("123"));
Check.That(error.Message).Contains("canonical");

Check.ThatCode(() => Any.Uri().Web().WithHost("1.2")).Throws<ArgumentException>();
Check.ThatCode(() => Any.Uri().Web().WithHost("1.2.3.4")).DoesNotThrow();
Check.ThatCode(() => Any.Uri().Web().WithHost("api.example.com")).DoesNotThrow();
}

[Fact(DisplayName = "A seeded URI draw is reproducible: the same seed yields the same value.")]
public void SeededDrawIsReproducible() {
Uri first = Any.WithSeed(4242).Uri().Web().WithQuery().Generate();
Uri second = Any.WithSeed(4242).Uri().Web().WithQuery().Generate();

Check.That(second).IsEqualTo(first);
}

[Fact(DisplayName = "A URI generator composes like any other: through As into a domain value.")]
public void ComposesThroughAs() {
foreach (string scheme in Sample(Seeded(context => context.Uri().Web()).As(uri => uri.Scheme))) {
Check.That(scheme is "http" or "https").IsTrue();
}
}

private static IEnumerable<T> Sample<T>(IAny<T> generator) {
for (int i = 0; i < SampleCount; i++) { yield return generator.Generate(); }
}

}
12 changes: 12 additions & 0 deletions Dummies/Any.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,18 @@
return AnyPattern.FromPattern(AmbientRandomSource.Instance, pattern.ToString(), (pattern.Options & RegexOptions.IgnoreCase) != 0);
}

/// <summary>
/// Starts an arbitrary <see cref="System.Uri" /> generator drawing from the ambient random context.
/// Unconstrained, it yields any valid URI from the safe space — an absolute web (<c>http</c>/<c>https</c>),
/// WebSocket (<c>ws</c>/<c>wss</c>), FTP or mailto URI, or a relative reference. Narrow it to a family
/// (<c>Web()</c>, <c>WebSocket()</c>, <c>Ftp()</c>, <c>Mailto()</c>, <c>Relative()</c>) to reach that family's
/// component constraints; each narrowing returns a builder exposing only that family's valid components.
/// </summary>
/// <returns>A URI generator to narrow fluently.</returns>
public static AnyUri Uri() {
return new AnyUri(AmbientRandomSource.Instance, UriSpec.Unconstrained);
}

/// <summary>
/// Starts an arbitrary <see cref="int" /> generator drawing from the ambient random context. Unconstrained, it
/// draws from the full <see cref="int" /> range; chain constraints to express what the surrounding code
Expand Down Expand Up @@ -526,7 +538,7 @@
/// <typeparam name="TResult">The type of the composed value.</typeparam>
/// <returns>A generator of the composed value.</returns>
/// <exception cref="ArgumentNullException">Thrown when any argument is <c>null</c>.</exception>
public static IAny<TResult> Combine<T1, T2, T3, TResult>(IAny<T1> first, IAny<T2> second, IAny<T3> third, Func<T1, T2, T3, TResult> compose) {

Check warning on line 541 in Dummies/Any.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Reduce the number of generic parameters in the 'Any.Combine' method to no more than the 3 authorized.

Check warning on line 541 in Dummies/Any.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Reduce the number of generic parameters in the 'Any.Combine' method to no more than the 3 authorized.
if (first is null) { throw new ArgumentNullException(nameof(first)); }
if (second is null) { throw new ArgumentNullException(nameof(second)); }
if (third is null) { throw new ArgumentNullException(nameof(third)); }
Expand Down Expand Up @@ -559,7 +571,7 @@
/// <typeparam name="TResult">The type of the composed value.</typeparam>
/// <returns>A generator of the composed value.</returns>
/// <exception cref="ArgumentNullException">Thrown when any argument is <c>null</c>.</exception>
public static IAny<TResult> Combine<T1, T2, T3, T4, TResult>(IAny<T1> first, IAny<T2> second, IAny<T3> third, IAny<T4> fourth, Func<T1, T2, T3, T4, TResult> compose) {

Check warning on line 574 in Dummies/Any.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Reduce the number of generic parameters in the 'Any.Combine' method to no more than the 3 authorized.
if (first is null) { throw new ArgumentNullException(nameof(first)); }
if (second is null) { throw new ArgumentNullException(nameof(second)); }
if (third is null) { throw new ArgumentNullException(nameof(third)); }
Expand Down Expand Up @@ -596,7 +608,7 @@
/// <typeparam name="TResult">The type of the composed value.</typeparam>
/// <returns>A generator of the composed value.</returns>
/// <exception cref="ArgumentNullException">Thrown when any argument is <c>null</c>.</exception>
public static IAny<TResult> Combine<T1, T2, T3, T4, T5, TResult>(IAny<T1> first, IAny<T2> second, IAny<T3> third, IAny<T4> fourth, IAny<T5> fifth, Func<T1, T2, T3, T4, T5, TResult> compose) {

Check warning on line 611 in Dummies/Any.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Reduce the number of generic parameters in the 'Any.Combine' method to no more than the 3 authorized.
if (first is null) { throw new ArgumentNullException(nameof(first)); }
if (second is null) { throw new ArgumentNullException(nameof(second)); }
if (third is null) { throw new ArgumentNullException(nameof(third)); }
Expand Down Expand Up @@ -637,7 +649,7 @@
/// <typeparam name="TResult">The type of the composed value.</typeparam>
/// <returns>A generator of the composed value.</returns>
/// <exception cref="ArgumentNullException">Thrown when any argument is <c>null</c>.</exception>
public static IAny<TResult> Combine<T1, T2, T3, T4, T5, T6, TResult>(IAny<T1> first, IAny<T2> second, IAny<T3> third, IAny<T4> fourth, IAny<T5> fifth, IAny<T6> sixth, Func<T1, T2, T3, T4, T5, T6, TResult> compose) {

Check warning on line 652 in Dummies/Any.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Reduce the number of generic parameters in the 'Any.Combine' method to no more than the 3 authorized.
if (first is null) { throw new ArgumentNullException(nameof(first)); }
if (second is null) { throw new ArgumentNullException(nameof(second)); }
if (third is null) { throw new ArgumentNullException(nameof(third)); }
Expand Down Expand Up @@ -684,7 +696,7 @@
/// <exception cref="ArgumentNullException">Thrown when any argument is <c>null</c>.</exception>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters",
Justification = "Heterogeneous composition needs one generator parameter per part; the arity-8 ceiling is a deliberate ergonomic decision (ADR-0015), and a flat parameter list reads better at the call site than nested Combine calls.")]
public static IAny<TResult> Combine<T1, T2, T3, T4, T5, T6, T7, TResult>(IAny<T1> first, IAny<T2> second, IAny<T3> third, IAny<T4> fourth, IAny<T5> fifth, IAny<T6> sixth, IAny<T7> seventh, Func<T1, T2, T3, T4, T5, T6, T7, TResult> compose) {

Check warning on line 699 in Dummies/Any.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Reduce the number of generic parameters in the 'Any.Combine' method to no more than the 3 authorized.
if (first is null) { throw new ArgumentNullException(nameof(first)); }
if (second is null) { throw new ArgumentNullException(nameof(second)); }
if (third is null) { throw new ArgumentNullException(nameof(third)); }
Expand Down Expand Up @@ -736,7 +748,7 @@
/// <exception cref="ArgumentNullException">Thrown when any argument is <c>null</c>.</exception>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters",
Justification = "Heterogeneous composition needs one generator parameter per part; the arity-8 ceiling is a deliberate ergonomic decision (ADR-0015), and a flat parameter list reads better at the call site than nested Combine calls.")]
public static IAny<TResult> Combine<T1, T2, T3, T4, T5, T6, T7, T8, TResult>(IAny<T1> first, IAny<T2> second, IAny<T3> third, IAny<T4> fourth, IAny<T5> fifth, IAny<T6> sixth, IAny<T7> seventh, IAny<T8> eighth, Func<T1, T2, T3, T4, T5, T6, T7, T8, TResult> compose) {

Check warning on line 751 in Dummies/Any.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Reduce the number of generic parameters in the 'Any.Combine' method to no more than the 3 authorized.
if (first is null) { throw new ArgumentNullException(nameof(first)); }
if (second is null) { throw new ArgumentNullException(nameof(second)); }
if (third is null) { throw new ArgumentNullException(nameof(third)); }
Expand Down
9 changes: 9 additions & 0 deletions Dummies/AnyContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ public AnyPattern StringMatching(Regex pattern) {
return AnyPattern.FromPattern(_source, pattern.ToString(), (pattern.Options & RegexOptions.IgnoreCase) != 0);
}

/// <summary>
/// Starts an arbitrary <see cref="System.Uri" /> generator drawing from this context — same fluent surface as
/// <see cref="Any.Uri" />, deterministic under this context's seed.
/// </summary>
/// <returns>A URI generator to narrow fluently.</returns>
public AnyUri Uri() {
return new AnyUri(_source, UriSpec.Unconstrained);
}

/// <summary>
/// Starts an arbitrary <see cref="int" /> generator drawing from this context — same fluent surface as
/// <see cref="Any.Int32" />, deterministic under this context's seed.
Expand Down
78 changes: 78 additions & 0 deletions Dummies/AnyFtpUri.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
namespace Dummies;

/// <summary>
/// A generator of arbitrary <c>ftp</c> URIs — the classic <c>ftp://user:password@host/path</c> shape of legacy
/// code. An FTP URI carries user-info but <b>no query and no fragment</b>, so this builder does not expose them.
/// </summary>
public sealed class AnyFtpUri : IAny<Uri>, IHasRandomSource {

#region Fields declarations

private readonly RandomSource _source;
private readonly UriSpec _spec;

#endregion

internal AnyFtpUri(RandomSource source, UriSpec spec) {
_source = source;
_spec = spec;
}

RandomSource? IHasRandomSource.Source => _source;

/// <summary>Pins the host. Must be an ASCII host name (pass the punycode form for internationalized hosts).</summary>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="host" /> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="host" /> is empty, non-ASCII or not a valid host name.</exception>
public AnyFtpUri WithHost(string host) {
return new AnyFtpUri(_source, _spec.WithHost(UriSpec.RequireHost(host, nameof(host))));
}

/// <summary>Includes arbitrary <c>user:password</c> user-info.</summary>
public AnyFtpUri WithUserInfo() {
return new AnyFtpUri(_source, _spec.WithUserInfo(null, null));
}

/// <summary>Includes user-info with the given <paramref name="user" /> and an arbitrary password.</summary>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="user" /> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="user" /> contains a non-unreserved character.</exception>
public AnyFtpUri WithUserInfo(string user) {
return new AnyFtpUri(_source, _spec.WithUserInfo(UriSpec.RequireUserInfoPart(user, nameof(user)), null));
}

/// <summary>Includes the given <paramref name="user" /> and <paramref name="password" /> user-info.</summary>
/// <exception cref="ArgumentNullException">Thrown when an argument is <c>null</c>.</exception>
/// <exception cref="ArgumentException">Thrown when an argument contains a non-unreserved character.</exception>
public AnyFtpUri WithUserInfo(string user, string password) {
return new AnyFtpUri(_source, _spec.WithUserInfo(UriSpec.RequireUserInfoPart(user, nameof(user)), UriSpec.RequireUserInfoPart(password, nameof(password))));
}

/// <summary>Includes an arbitrary non-default port.</summary>
public AnyFtpUri WithPort() {
return new AnyFtpUri(_source, _spec.WithPort(null));
}

/// <summary>Includes the given <paramref name="port" />.</summary>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="port" /> is outside 1..65535.</exception>
public AnyFtpUri WithPort(int port) {
return new AnyFtpUri(_source, _spec.WithPort(UriSpec.RequirePort(port, nameof(port))));
}

/// <summary>Fixes the path to exactly <paramref name="count" /> segments. Declared once per generator.</summary>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="count" /> is negative.</exception>
/// <exception cref="ConflictingAnyConstraintException">Thrown when a path constraint is already declared.</exception>
public AnyFtpUri WithPathSegments(int count) {
return new AnyFtpUri(_source, _spec.WithPath(UriPathMode.Exact, UriSpec.RequireSegmentCount(count, nameof(count)), UriSpec.SegmentsLabel(count)));
}

/// <summary>Renders the root path (<c>/</c>) with no segments. Declared once per generator.</summary>
/// <exception cref="ConflictingAnyConstraintException">Thrown when a path constraint is already declared.</exception>
public AnyFtpUri WithoutPath() {
return new AnyFtpUri(_source, _spec.WithPath(UriPathMode.Root, 0, "WithoutPath()"));
}

/// <inheritdoc />
public Uri Generate() {
return _spec.Generate(_source);
}

}
Loading