-
Notifications
You must be signed in to change notification settings - Fork 1
refactor: replace LanguageExt with custom Functional types and extract RootCommand #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,4 @@ | ||
| using funURL.CLI.Core; | ||
| using LanguageExt; | ||
|
|
||
| namespace funURL.CLI.Tests; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| using System.CommandLine; | ||
| using Figgle.Fonts; | ||
|
|
||
| namespace funURL.CLI.Commands; | ||
|
|
||
| /// <summary> | ||
| /// The root command for the funURL CLI. | ||
| /// </summary> | ||
| public class RootCommand : System.CommandLine.RootCommand | ||
| { | ||
| private readonly Option<bool> silentOption = new("--silent", "-s") { Description = "Suppress the startup banner", Recursive = true }; | ||
|
|
||
| private RootCommand() | ||
| : base("funURL - A Functional URL Swiss Army Knife") | ||
| { | ||
| Options.Add(silentOption); | ||
| Subcommands.Add(ParseCommand.Create()); | ||
| Subcommands.Add(ModifyCommand.Create()); | ||
| Subcommands.Add(EncodeCommand.Create()); | ||
| Subcommands.Add(DecodeCommand.Create()); | ||
| Subcommands.Add(DedupeCommand.Create()); | ||
| } | ||
|
|
||
| public async Task<ParseResult> Parse(IReadOnlyList<string> args, CancellationToken cancellationToken) | ||
| { | ||
| var parseResult = base.Parse(args, null); | ||
|
|
||
| if (!parseResult.GetValue(silentOption)) | ||
| { | ||
| await Console.Out.WriteAsync(FiggleFonts.Standard.Render("funURL").AsMemory(), cancellationToken); | ||
| } | ||
|
|
||
| return parseResult; | ||
| } | ||
|
|
||
| public static RootCommand Create() => new(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| namespace funURL.CLI.Functional; | ||
|
|
||
| internal sealed class Error(string message) | ||
| { | ||
| public string Message { get; } = message; | ||
|
|
||
| public static Error New(string message) => new(message); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| namespace funURL.CLI.Functional; | ||
|
|
||
| internal abstract class Fin<T> | ||
| { | ||
| public abstract bool IsSucc { get; } | ||
| public bool IsFail => !IsSucc; | ||
|
|
||
| public static Fin<T> Succ(T value) => new SuccCase(value); | ||
|
|
||
| public static implicit operator Fin<T>(Error error) => new FailCase(error); | ||
|
|
||
| public abstract Fin<TResult> Map<TResult>(Func<T, TResult> mapper); | ||
|
|
||
| public abstract Task Match(Func<T, Task> Succ, Func<Error, Task> Fail); | ||
|
|
||
| public abstract T ThrowIfFail(); | ||
|
|
||
| private sealed class SuccCase(T value) : Fin<T> | ||
| { | ||
| public override bool IsSucc => true; | ||
|
|
||
| public override Fin<TResult> Map<TResult>(Func<T, TResult> mapper) => Fin<TResult>.Succ(mapper(value)); | ||
|
|
||
| public override Task Match(Func<T, Task> Succ, Func<Error, Task> Fail) => Succ(value); | ||
|
|
||
| public override T ThrowIfFail() => value; | ||
| } | ||
|
|
||
| private sealed class FailCase(Error error) : Fin<T> | ||
| { | ||
| public override bool IsSucc => false; | ||
|
|
||
| public override Fin<TResult> Map<TResult>(Func<T, TResult> mapper) => error; | ||
|
|
||
| public override Task Match(Func<T, Task> Succ, Func<Error, Task> Fail) => Fail(error); | ||
|
|
||
| public override T ThrowIfFail() => throw new InvalidOperationException(error.Message); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,21 +1,16 @@ | ||
| using System.CommandLine; | ||
| using funURL.CLI.Commands; | ||
| using funURL.CLI.Commands; | ||
|
|
||
| using var cancellationTokenSource = new CancellationTokenSource(); | ||
| var cancellationToken = cancellationTokenSource.Token; | ||
|
|
||
| Console.CancelKeyPress += (_, e) => | ||
| { | ||
| e.Cancel = true; | ||
| cancellationTokenSource.Cancel(); | ||
| }; | ||
|
|
||
| var rootCommand = new RootCommand("funURL - A Functional URL Swiss Army Knife 🛠️") | ||
| { | ||
| ParseCommand.Create(), | ||
| ModifyCommand.Create(), | ||
| EncodeCommand.Create(), | ||
| DecodeCommand.Create(), | ||
| DedupeCommand.Create(), | ||
| }; | ||
| var rootCommand = RootCommand.Create(); | ||
|
|
||
| var parseResult = await rootCommand.Parse(args, cancellationToken); | ||
|
|
||
| return await rootCommand.Parse(args).InvokeAsync(cancellationToken: cancellationTokenSource.Token); | ||
| return await parseResult.InvokeAsync(cancellationToken: cancellationToken); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,7 +30,8 @@ | |
|
|
||
| <ItemGroup> | ||
| <InternalsVisibleTo Include="funURL.CLI.Tests" /> | ||
| <PackageReference Include="LanguageExt.Core" Version="4.4.9" /> | ||
| <PackageReference Include="Figgle" Version="0.6.5" /> | ||
| <PackageReference Include="Figgle.Fonts" Version="0.6.5" /> | ||
|
Comment on lines
+33
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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 This project publishes with
Replace both 🤖 Prompt for AI Agents |
||
| <PackageReference Include="System.CommandLine" Version="2.0.3" /> | ||
| </ItemGroup> | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fire-and-forget
WriteAsync— banner may not appear, exceptions are silently swallowed.Console.Out.WriteAsync(...)returns aValueTaskthat is never awaited. BecauseParseis a synchronous method, this is a fire-and-forget: the banner write races against the command's own output (typically lost), and anyIOExceptionfrom the write becomes an unobserved task exception. Use the synchronous overload instead.🐛 Proposed fix
🤖 Prompt for AI Agents