From 93be131ad9d424fb9dfeba867c2a17c5be7f35ce Mon Sep 17 00:00:00 2001 From: Sciumo Date: Thu, 10 Jul 2025 15:35:27 -0400 Subject: [PATCH] Add simple web service --- Directory.Build.props | 4 +- SymSpell.SWS/Controllers/LookupController.cs | 33 +++++++++++++++ SymSpell.SWS/Program.cs | 35 ++++++++++++++++ SymSpell.SWS/Properties/launchSettings.json | 29 +++++++++++++ SymSpell.SWS/SymSpell.SWS.csproj | 20 +++++++++ SymSpell.SWS/appsettings.Development.json | 8 ++++ SymSpell.SWS/appsettings.json | 9 ++++ SymSpell.SWS/test_sws.py | 43 ++++++++++++++++++++ SymSpell.sln | 8 +++- test_sws.sh | 19 +++++++++ 10 files changed, 205 insertions(+), 3 deletions(-) create mode 100644 SymSpell.SWS/Controllers/LookupController.cs create mode 100644 SymSpell.SWS/Program.cs create mode 100644 SymSpell.SWS/Properties/launchSettings.json create mode 100644 SymSpell.SWS/SymSpell.SWS.csproj create mode 100644 SymSpell.SWS/appsettings.Development.json create mode 100644 SymSpell.SWS/appsettings.json create mode 100644 SymSpell.SWS/test_sws.py create mode 100755 test_sws.sh diff --git a/Directory.Build.props b/Directory.Build.props index 96bffb9..0d56107 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,9 +1,9 @@ - + all - + all diff --git a/SymSpell.SWS/Controllers/LookupController.cs b/SymSpell.SWS/Controllers/LookupController.cs new file mode 100644 index 0000000..6293f88 --- /dev/null +++ b/SymSpell.SWS/Controllers/LookupController.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Mvc; + +namespace SymSpellSWS.Controllers; + +[ApiController] +[Route("[controller]")] +public class LookupController : ControllerBase +{ + private readonly SymSpell _symSpell; + + public LookupController(SymSpell symSpell) + { + _symSpell = symSpell; + } + + [HttpGet] + public IEnumerable Get([FromQuery] string word, [FromQuery] string verbosity = "Closest") + { + if (string.IsNullOrWhiteSpace(word)) + { + return Enumerable.Empty(); + } + + var verbosityEnum = verbosity.ToLower() switch + { + "top" => SymSpell.Verbosity.Top, + "all" => SymSpell.Verbosity.All, + _ => SymSpell.Verbosity.Closest, + }; + + return _symSpell.Lookup(word, verbosityEnum); + } +} diff --git a/SymSpell.SWS/Program.cs b/SymSpell.SWS/Program.cs new file mode 100644 index 0000000..28b3ce7 --- /dev/null +++ b/SymSpell.SWS/Program.cs @@ -0,0 +1,35 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddControllers().AddJsonOptions(options => +{ + options.JsonSerializerOptions.IncludeFields = true; +}); + +// Configure and register SymSpell as a singleton +const int initialCapacity = 82765; +const int maxEditDistance = 2; +const int prefixLength = 7; +var symSpell = new SymSpell(initialCapacity, maxEditDistance, prefixLength); + +// Load dictionary +string path = Path.Combine(AppContext.BaseDirectory, "frequency_dictionary_en_82_765.txt"); +if (!symSpell.LoadDictionary(path, 0, 1)) +{ + Console.Error.WriteLine("Dictionary file not found: " + Path.GetFullPath(path)); + // App will start, but lookups will be incorrect. +} +builder.Services.AddSingleton(symSpell); + + +var app = builder.Build(); + +// Configure the HTTP request pipeline. + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/SymSpell.SWS/Properties/launchSettings.json b/SymSpell.SWS/Properties/launchSettings.json new file mode 100644 index 0000000..cf11f20 --- /dev/null +++ b/SymSpell.SWS/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5000", + "sslPort": 5001 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "SymSpell.SWS": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": false, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/SymSpell.SWS/SymSpell.SWS.csproj b/SymSpell.SWS/SymSpell.SWS.csproj new file mode 100644 index 0000000..9e3b375 --- /dev/null +++ b/SymSpell.SWS/SymSpell.SWS.csproj @@ -0,0 +1,20 @@ + + + + net9.0 + enable + enable + + + + + + + + + + PreserveNewest + + + + diff --git a/SymSpell.SWS/appsettings.Development.json b/SymSpell.SWS/appsettings.Development.json new file mode 100644 index 0000000..3e1a225 --- /dev/null +++ b/SymSpell.SWS/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Information" + } + } +} diff --git a/SymSpell.SWS/appsettings.json b/SymSpell.SWS/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/SymSpell.SWS/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/SymSpell.SWS/test_sws.py b/SymSpell.SWS/test_sws.py new file mode 100644 index 0000000..ad76d52 --- /dev/null +++ b/SymSpell.SWS/test_sws.py @@ -0,0 +1,43 @@ +import json +import ssl +import sys +import urllib.request + +def main(): + if len(sys.argv) < 2: + print("Certificate path not provided.", file=sys.stderr) + sys.exit(1) + + cert_path = sys.argv[1] + context = ssl.create_default_context(cafile=cert_path) + + try: + # The service redirects http to https, so let's use https directly + # We request Verbosity.All to get all suggestions within maxEditDistance + url = "https://localhost:5001/lookup?word=watevr&verbosity=All" + with urllib.request.urlopen(url, context=context) as response: + if response.status != 200: + print(f"Error: Received status code {response.status}", file=sys.stderr) + sys.exit(1) + + data = json.loads(response.read().decode()) + + if not data: + print("Error: Received empty response.", file=sys.stderr) + sys.exit(1) + + # Check if 'whatever' is in the list of suggestions + found = any(s.get('term') == 'whatever' for s in data if isinstance(s, dict)) + + if found: + print("Test passed!") + else: + print(f"Test failed: Expected a suggestion of 'whatever' in the results, but got: {json.dumps(data)}", file=sys.stderr) + sys.exit(1) + + except Exception as e: + print(f"An error occurred: {e}", file=sys.stderr) + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/SymSpell.sln b/SymSpell.sln index dc636a4..f4f47a0 100644 --- a/SymSpell.sln +++ b/SymSpell.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.27130.2027 MinimumVisualStudioVersion = 15.0.26124.0 @@ -25,6 +25,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "miscs", "miscs", "{B96EF85F RunThisFirst_dotnetrestore.bat = RunThisFirst_dotnetrestore.bat EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SymSpell.SWS", "SymSpell.SWS\SymSpell.SWS.csproj", "{6BFFBDBC-CB71-4451-BDF3-246446329B2B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -59,6 +61,10 @@ Global {9C78B67B-6782-4F28-B5F7-14C94E2CE017}.Debug|Any CPU.Build.0 = Debug|Any CPU {9C78B67B-6782-4F28-B5F7-14C94E2CE017}.Release|Any CPU.ActiveCfg = Release|Any CPU {9C78B67B-6782-4F28-B5F7-14C94E2CE017}.Release|Any CPU.Build.0 = Release|Any CPU + {6BFFBDBC-CB71-4451-BDF3-246446329B2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6BFFBDBC-CB71-4451-BDF3-246446329B2B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6BFFBDBC-CB71-4451-BDF3-246446329B2B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6BFFBDBC-CB71-4451-BDF3-246446329B2B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/test_sws.sh b/test_sws.sh new file mode 100755 index 0000000..b22fa8b --- /dev/null +++ b/test_sws.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -e + +CERT_PATH=SymSpell.SWS/sws-test-cert.pem +echo "Exporting dev cert for testing to $CERT_PATH..." +dotnet dev-certs https --format PEM --export-path $CERT_PATH + +# Ensure cert is removed and web service is killed on exit +trap 'echo "Stopping web service..."; kill $SWS_PID; echo "Cleaning up cert file..."; rm -f $CERT_PATH' EXIT + +echo "Starting SymSpell.SWS web service..." +dotnet run --project SymSpell.SWS/SymSpell.SWS.csproj & +SWS_PID=$! + +echo "Waiting for service to start..." +sleep 10 + +echo "Running test script..." +python3 SymSpell.SWS/test_sws.py $CERT_PATH