Skip to content

refactor: replace LanguageExt with custom Functional types and extract RootCommand#16

Merged
recepgunes1 merged 2 commits intomainfrom
refactor/entrypoint
Feb 21, 2026
Merged

refactor: replace LanguageExt with custom Functional types and extract RootCommand#16
recepgunes1 merged 2 commits intomainfrom
refactor/entrypoint

Conversation

@recepgunes1
Copy link
Copy Markdown
Member

@recepgunes1 recepgunes1 commented Feb 20, 2026

Summary by CodeRabbit

  • New Features

    • Added a startup banner displayed when launching the CLI (disable with the --silent option).
  • Refactor

    • Replaced an external functional library with a project-specific implementation for maintainability.
    • Restructured the root CLI and reorganized subcommands for clearer initialization and command handling.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 20, 2026

Walkthrough

The PR replaces LanguageExt with an internal functional library (Error, Fin, Option), adds a RootCommand class for CLI wiring and banner rendering, refactors commands to use the local functional types, and updates Program.cs and project dependencies (adds Figgle packages, removes LanguageExt).

Changes

Cohort / File(s) Summary
Command Dependency Refactoring
funURL.CLI/Commands/DecodeCommand.cs, funURL.CLI/Commands/EncodeCommand.cs, funURL.CLI/Commands/DedupeCommand.cs, funURL.CLI/Commands/ModifyCommand.cs, funURL.CLI/Commands/ParseCommand.cs
Replaced using LanguageExt with using funURL.CLI.Functional; changed option field types from System.CommandLine.Option<T> to local Option<T>; command wiring and behavior unchanged.
CLI Root Command
funURL.CLI/Commands/RootCommand.cs
Added new public RootCommand class (extends System.CommandLine.RootCommand) with a silent option, subcommand registration, Parse method that conditionally renders a Figgle banner, and a Create() factory.
Functional Types Library
funURL.CLI/Functional/Error.cs, funURL.CLI/Functional/Fin.cs
Added Error type and Fin<T> discriminated result type (Succ/Fail cases) with Map, Match, and ThrowIfFail implementations to replace LanguageExt usages.
Core & Program Changes
funURL.CLI/Core/UrlOperations.cs, funURL.CLI/Program.cs
Updated UrlOperations to use local functional namespace; Program.cs now uses RootCommand.Create(), introduces a shared CancellationTokenSource, calls root.Parse(args, cancellationToken) and parseResult.InvokeAsync(cancellationToken).
Project & Tests
funURL.CLI/funURL.CLI.csproj, funURL.CLI.Tests/UrlOperationsTests.cs
Removed LanguageExt.Core package; added Figgle and Figgle.Fonts; removed an unused LanguageExt using in tests.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant RootCmd as RootCommand
  participant ParseRes as ParseResult
  participant Invoker as Command Invoker
  participant Figgle as FiggleFonts

  User->>RootCmd: run CLI with args
  RootCmd->>RootCmd: Parse(args, cancellationToken)
  RootCmd-->>Figgle: (if not silent) render banner
  RootCmd->>ParseRes: return parse result
  User->>Invoker: await parseResult.InvokeAsync(cancellationToken)
  Invoker->>RootCmd: execute selected subcommand
  Invoker-->>User: command output / exit
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Refactor/go to csharp #10: Introduced the original C# CLI that this PR refactors by replacing LanguageExt with an internal functional implementation.

Suggested reviewers

  • ahsentekd

Poem

🐇🌿
I hopped through namespaces, tidy and light,
Swapped in Fin and Error, kept behavior tight.
Figgle paints banners, subcommands take flight—
A rabbit’s small patch, functional and bright.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: replacing LanguageExt with custom Functional types and extracting RootCommand, which aligns with the primary modifications across the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/entrypoint

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
funURL.CLI/Commands/RootCommand.cs (1)

24-27: CancellationToken parameter has no meaningful use — consider removing it.

After fixing the WriteAsync call above, the only remaining usage of cancellationToken disappears. base.Parse(args, null) takes ParserConfiguration?, not a token. The parameter adds noise to the call site in Program.cs without providing any cancellation semantics during parse.

♻️ Simplified signature
-    public ParseResult Parse(IReadOnlyList<string> args, CancellationToken cancellationToken)
+    public ParseResult Parse(IReadOnlyList<string> args)
     {
         var parseResult = base.Parse(args, null);

         if (!parseResult.GetValue(silentOption))
         {
-            Console.Out.Write(FiggleFonts.Standard.Render("funURL").AsMemory(), cancellationToken);
+            Console.Out.Write(FiggleFonts.Standard.Render("funURL"));
         }

         return parseResult;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@funURL.CLI/Commands/RootCommand.cs` around lines 24 - 27, The Parse method's
CancellationToken parameter is unused and should be removed: change the method
signature of RootCommand.Parse to Parse(IReadOnlyList<string> args) and update
the internal call to base.Parse(args, null) (no token), then find and update all
call sites (e.g., Program.cs) that pass a CancellationToken to this Parse
overload so they call the new signature instead; ensure any overrides or
interface implementations referencing Parse(IReadOnlyList<string>,
CancellationToken) are updated or removed to keep signatures consistent.
funURL.CLI/Functional/Fin.cs (2)

12-12: Consider adding a Bind/FlatMap operation.

Map only lifts T → TResult functions. When a chained step itself returns Fin<TResult> (e.g., two consecutive validation calls), the caller ends up with Fin<Fin<TResult>>. Bind collapses that:

♻️ Proposed addition
 public abstract Fin<TResult> Map<TResult>(Func<T, TResult> mapper);

+public abstract Fin<TResult> Bind<TResult>(Func<T, Fin<TResult>> binder);

Add to SuccCase:

+public override Fin<TResult> Bind<TResult>(Func<T, Fin<TResult>> binder) => binder(value);

Add to FailCase:

+public override Fin<TResult> Bind<TResult>(Func<T, Fin<TResult>> binder) => error;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@funURL.CLI/Functional/Fin.cs` at line 12, The current Fin<T> only exposes Map
which cannot flatten nested Fin results; add an abstract Bind (aka FlatMap)
method with signature Bind<TResult>(Func<T, Fin<TResult>> binder) to Fin,
implement it in SuccCase to invoke and return binder(this.Value), and implement
it in FailCase to return the existing failure instance typed as Fin<TResult>
(propagating the failure without invoking the binder); update usages that chain
operations returning Fin to use Bind instead of Map to avoid Fin<Fin<TResult>>.

14-14: Match only supports Task-returning delegates.

Any synchronous match (e.g., transforming to a string) requires Task.FromResult(...) boilerplate. A generic synchronous overload avoids this:

♻️ Proposed addition
 public abstract Task Match(Func<T, Task> Succ, Func<Error, Task> Fail);

+public abstract TResult Match<TResult>(Func<T, TResult> Succ, Func<Error, TResult> Fail);

Add to SuccCase:

+public override TResult Match<TResult>(Func<T, TResult> Succ, Func<Error, TResult> Fail) => Succ(value);

Add to FailCase:

+public override TResult Match<TResult>(Func<T, TResult> Succ, Func<Error, TResult> Fail) => Fail(error);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@funURL.CLI/Functional/Fin.cs` at line 14, The Match API only accepts
Task-returning delegates, forcing callers to use Task.FromResult for synchronous
results; add generic synchronous overloads to support returning TResult
directly: add a new overload signature Match<TResult>(Func<T, TResult> Succ,
Func<Error, TResult> Fail) to the abstract base and implement it in SuccCase and
FailCase by invoking the provided sync delegates and wrapping their return
values with Task.FromResult<TResult>, ensuring existing async Match(Func<T,
Task<TResult>>, Func<Error, Task<TResult>>) remains available for async callers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@funURL.CLI/Commands/RootCommand.cs`:
- Around line 28-31: The banner write is using fire-and-forget
Console.Out.WriteAsync in RootCommand.cs inside the
parseResult.GetValue(silentOption) check which can lose output and swallow IO
exceptions; replace the async call with the synchronous
Console.Out.Write/WriteLine overload that takes the rendered banner string (use
FiggleFonts.Standard.Render("funURL")) so the write happens inline and
exceptions propagate, and remove the unused cancellationToken from that call.

In `@funURL.CLI/funURL.CLI.csproj`:
- Around line 33-34: Replace the runtime Figgle packages with the Figgle source
generator to avoid AOT/runtime font loading: remove the PackageReference entries
for "Figgle" and "Figgle.Fonts" and add a PackageReference for
"Figgle.Generator" (v0.6.5); then update any places that call Figgle rendering
at runtime to use the [StaticFiggle] attribute so text like "funURL" is rendered
at compile time; ensure project still has <PublishAot>true</PublishAot> and
<PublishTrimmed>true</PublishTrimmed> and verify no runtime Figgle assembly is
required after the change.

In `@funURL.CLI/Program.cs`:
- Line 14: RootCommand.Parse starts a fire-and-forget banner write via
Console.Out.WriteAsync which allows the banner to race with command output; fix
it in RootCommand.Parse by awaiting the Console.Out.WriteAsync Task (i.e., await
Console.Out.WriteAsync(...)) and ensure Parse's async signature returns a Task
that completes only after the write finishes so callers (e.g., Program.cs
calling rootCommand.Parse(...).InvokeAsync(...)) will await the banner write;
update the Parse method signature/implementation if needed so Program.cs's
existing await continues to await completion.

---

Nitpick comments:
In `@funURL.CLI/Commands/RootCommand.cs`:
- Around line 24-27: The Parse method's CancellationToken parameter is unused
and should be removed: change the method signature of RootCommand.Parse to
Parse(IReadOnlyList<string> args) and update the internal call to
base.Parse(args, null) (no token), then find and update all call sites (e.g.,
Program.cs) that pass a CancellationToken to this Parse overload so they call
the new signature instead; ensure any overrides or interface implementations
referencing Parse(IReadOnlyList<string>, CancellationToken) are updated or
removed to keep signatures consistent.

In `@funURL.CLI/Functional/Fin.cs`:
- Line 12: The current Fin<T> only exposes Map which cannot flatten nested Fin
results; add an abstract Bind (aka FlatMap) method with signature
Bind<TResult>(Func<T, Fin<TResult>> binder) to Fin, implement it in SuccCase to
invoke and return binder(this.Value), and implement it in FailCase to return the
existing failure instance typed as Fin<TResult> (propagating the failure without
invoking the binder); update usages that chain operations returning Fin to use
Bind instead of Map to avoid Fin<Fin<TResult>>.
- Line 14: The Match API only accepts Task-returning delegates, forcing callers
to use Task.FromResult for synchronous results; add generic synchronous
overloads to support returning TResult directly: add a new overload signature
Match<TResult>(Func<T, TResult> Succ, Func<Error, TResult> Fail) to the abstract
base and implement it in SuccCase and FailCase by invoking the provided sync
delegates and wrapping their return values with Task.FromResult<TResult>,
ensuring existing async Match(Func<T, Task<TResult>>, Func<Error,
Task<TResult>>) remains available for async callers.

Comment on lines +28 to +31
if (!parseResult.GetValue(silentOption))
{
Console.Out.WriteAsync(FiggleFonts.Standard.Render("funURL").AsMemory(), cancellationToken);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fire-and-forget WriteAsync — banner may not appear, exceptions are silently swallowed.

Console.Out.WriteAsync(...) returns a ValueTask that is never awaited. Because Parse is a synchronous method, this is a fire-and-forget: the banner write races against the command's own output (typically lost), and any IOException from the write becomes an unobserved task exception. Use the synchronous overload instead.

🐛 Proposed fix
-        Console.Out.WriteAsync(FiggleFonts.Standard.Render("funURL").AsMemory(), cancellationToken);
+        Console.Out.Write(FiggleFonts.Standard.Render("funURL"));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@funURL.CLI/Commands/RootCommand.cs` around lines 28 - 31, The banner write is
using fire-and-forget Console.Out.WriteAsync in RootCommand.cs inside the
parseResult.GetValue(silentOption) check which can lose output and swallow IO
exceptions; replace the async call with the synchronous
Console.Out.Write/WriteLine overload that takes the rendered banner string (use
FiggleFonts.Standard.Render("funURL")) so the write happens inline and
exceptions propagate, and remove the unused cancellationToken from that call.

Comment on lines +33 to +34
<PackageReference Include="Figgle" Version="0.6.5" />
<PackageReference Include="Figgle.Fonts" Version="0.6.5" />
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find and examine the project file
find . -name "funURL.CLI.csproj" -type f | head -5

Repository: HappyHackingSpace/funURL

Length of output: 98


🏁 Script executed:

# Query NuGet for Figgle and Figgle.Generator information
curl -s "https://api.nuget.org/v3/registration5-semver1/figgle/index.json" \
  | python3 -c "
import json, sys
try:
    data = json.load(sys.stdin)
    pages = data.get('items', [])
    if pages:
        latest = pages[-1].get('items', [])[-1] if pages[-1].get('items') else {}
        entry = latest.get('catalogEntry', {})
        print('Figgle latest:', entry.get('version', 'N/A'))
        print('Listed:', entry.get('listed', 'N/A'))
except:
    print('Figgle: Error parsing')
"

Repository: HappyHackingSpace/funURL

Length of output: 101


🏁 Script executed:

# Check for Figgle.Generator
curl -s "https://api.nuget.org/v3/registration5-semver1/figgle.generator/index.json" \
  | python3 -c "
import json, sys
try:
    data = json.load(sys.stdin)
    pages = data.get('items', [])
    versions = []
    for page in pages:
        for item in page.get('items', []):
            v = item.get('catalogEntry', {}).get('version', '')
            if v:
                versions.append(v)
    if versions:
        print('Figgle.Generator exists: YES')
        print('Latest versions:', versions[-3:])
    else:
        print('Figgle.Generator: NOT FOUND on NuGet')
except Exception as e:
    print(f'Figgle.Generator: Error - {e}')
"

Repository: HappyHackingSpace/funURL

Length of output: 141


🏁 Script executed:

# Check for Figgle.Fonts
curl -s "https://api.nuget.org/v3/registration5-semver1/figgle.fonts/index.json" \
  | python3 -c "
import json, sys
try:
    data = json.load(sys.stdin)
    pages = data.get('items', [])
    if pages:
        latest = pages[-1].get('items', [])[-1] if pages[-1].get('items') else {}
        entry = latest.get('catalogEntry', {})
        print('Figgle.Fonts latest:', entry.get('version', 'N/A'))
except:
    print('Figgle.Fonts: Error parsing')
"

Repository: HappyHackingSpace/funURL

Length of output: 94


🏁 Script executed:

# Read the csproj file to verify PublishAot and PublishTrimmed settings
cat funURL.CLI/funURL.CLI.csproj

Repository: HappyHackingSpace/funURL

Length of output: 1826


🏁 Script executed:

# Get detailed metadata for Figgle 0.6.5 to check for AOT compatibility
curl -s "https://api.nuget.org/v3/registration5-semver1/figgle/0.6.5.json" \
  | python3 -c "
import json, sys
try:
    data = json.load(sys.stdin)
    entry = data.get('catalogEntry', {})
    print('Figgle 0.6.5 Details:')
    print('  Version:', entry.get('version'))
    print('  Tags:', entry.get('tags', 'N/A'))
    print('  Description:', entry.get('description', 'N/A')[:200] if entry.get('description') else 'N/A')
    # Check for AOT indicator (may not exist in standard metadata)
    print('  IsAotCompatible:', entry.get('isAotCompatible', 'Not explicitly set'))
except Exception as e:
    print(f'Error: {e}')
"

Repository: HappyHackingSpace/funURL

Length of output: 132


🏁 Script executed:

# Get Figgle.Generator metadata to confirm source-generation capability
curl -s "https://api.nuget.org/v3/registration5-semver1/figgle.generator/0.6.5.json" \
  | python3 -c "
import json, sys
try:
    data = json.load(sys.stdin)
    entry = data.get('catalogEntry', {})
    print('Figgle.Generator 0.6.5 Details:')
    print('  Version:', entry.get('version'))
    print('  Tags:', entry.get('tags', 'N/A'))
    print('  Description:', entry.get('description', 'N/A')[:300] if entry.get('description') else 'N/A')
    print('  DependencyGroups:', entry.get('dependencyGroups', []))
except Exception as e:
    print(f'Error: {e}')
"

Repository: HappyHackingSpace/funURL

Length of output: 142


🏁 Script executed:

# Search online documentation or GitHub for Figgle AOT compatibility info
# Look for mentions of Figgle.Generator as an AOT-friendly alternative
curl -s "https://api.github.com/repos/drewnoakes/figgle/contents/README.md" \
  | python3 -c "
import json, sys, base64
try:
    data = json.load(sys.stdin)
    if 'content' in data:
        content = base64.b64decode(data['content']).decode('utf-8')
        # Look for mentions of Generator or AOT
        lines = content.split('\n')
        for i, line in enumerate(lines):
            if 'generator' in line.lower() or 'aot' in line.lower():
                print(f'Line {i}: {line}')
except:
    print('Could not fetch README')
"

Repository: HappyHackingSpace/funURL

Length of output: 1160


Consider using Figgle.Generator source generator instead of Figgle + Figgle.Fonts.

This project publishes with <PublishAot>true</PublishAot> and <PublishTrimmed>true</PublishTrimmed>, but Figgle targets .NET Standard 2.0 and is not AOT-annotated. Unvalidated third-party dependencies can silently break AOT guarantees even when no warnings are reported at publish time.

Figgle.Generator (available on NuGet at the same version 0.6.5) is a source generator that, for statically-known text like "funURL", embeds the rendered text directly into the assembly at compile time. This eliminates the runtime font-loading entirely and removes the need to ship the ~500KB Figgle.Fonts package. Per the official documentation, "if all Figgle text is rendered this way, you don't have to ship any Figgle assembly with your app."

Replace both Figgle and Figgle.Fonts with Figgle.Generator and use the [StaticFiggle] attribute to render the text at compile time.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@funURL.CLI/funURL.CLI.csproj` around lines 33 - 34, Replace the runtime
Figgle packages with the Figgle source generator to avoid AOT/runtime font
loading: remove the PackageReference entries for "Figgle" and "Figgle.Fonts" and
add a PackageReference for "Figgle.Generator" (v0.6.5); then update any places
that call Figgle rendering at runtime to use the [StaticFiggle] attribute so
text like "funURL" is rendered at compile time; ensure project still has
<PublishAot>true</PublishAot> and <PublishTrimmed>true</PublishTrimmed> and
verify no runtime Figgle assembly is required after the change.

Comment thread funURL.CLI/Program.cs Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@funURL.CLI/Commands/RootCommand.cs`:
- Around line 24-34: Parse calls base.Parse(args, null) in
Parse(IReadOnlyList<string> args, CancellationToken cancellationToken) which may
be using the wrong overload; verify your System.CommandLine version and replace
the null second argument with the appropriate value for that overload (for
example pass the incoming cancellationToken if an overload accepts a
CancellationToken, or pass a properly-typed default
CliConfiguration/CliConfiguration? rather than null) so that RootCommand.Parse
uses the intended API; check the Parse method usage in RootCommand.Parse, the
base.Parse invocation, and ensure silentOption and the
FiggleFonts.Standard.Render banner logic remain unchanged.

In `@funURL.CLI/Program.cs`:
- Around line 12-16: The Parse method is now async so the code should await it
before invoking; ensure you call RootCommand.Create(), then await
rootCommand.Parse(args, cancellationToken) into parseResult and then await
parseResult.InvokeAsync(cancellationToken: cancellationToken) (using the
parseResult local and the methods RootCommand.Create, Parse, and InvokeAsync) so
the two-step await pattern is applied correctly.

@recepgunes1 recepgunes1 merged commit c7834c4 into main Feb 21, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants