refactor: replace LanguageExt with custom Functional types and extract RootCommand#16
refactor: replace LanguageExt with custom Functional types and extract RootCommand#16recepgunes1 merged 2 commits intomainfrom
Conversation
WalkthroughThe 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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
funURL.CLI/Commands/RootCommand.cs (1)
24-27:CancellationTokenparameter has no meaningful use — consider removing it.After fixing the
WriteAsynccall above, the only remaining usage ofcancellationTokendisappears.base.Parse(args, null)takesParserConfiguration?, not a token. The parameter adds noise to the call site inProgram.cswithout 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 aBind/FlatMapoperation.
Maponly liftsT → TResultfunctions. When a chained step itself returnsFin<TResult>(e.g., two consecutive validation calls), the caller ends up withFin<Fin<TResult>>.Bindcollapses 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:Matchonly supportsTask-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.
| if (!parseResult.GetValue(silentOption)) | ||
| { | ||
| Console.Out.WriteAsync(FiggleFonts.Standard.Render("funURL").AsMemory(), cancellationToken); | ||
| } |
There was a problem hiding this comment.
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.
| <PackageReference Include="Figgle" Version="0.6.5" /> | ||
| <PackageReference Include="Figgle.Fonts" Version="0.6.5" /> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find and examine the project file
find . -name "funURL.CLI.csproj" -type f | head -5Repository: 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.csprojRepository: 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.
There was a problem hiding this comment.
🤖 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.
Summary by CodeRabbit
New Features
Refactor