diff --git a/src/.editorconfig b/.editorconfig similarity index 100% rename from src/.editorconfig rename to .editorconfig diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f28a66d..7d04d4f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,17 +15,17 @@ jobs: - name: Setup uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 9.0.x - name: Restore - run: dotnet restore src + run: dotnet restore - name: Build - run: dotnet build src --no-restore --configuration Release + run: dotnet build --no-restore --configuration Release - name: Test - run: dotnet test src --no-build --configuration Release --verbosity normal + run: dotnet test --no-build --configuration Release --verbosity normal - name: Publish - run: dotnet publish src/Analog --configuration Release -p:PublishSingleFile=true --self-contained + run: dotnet publish src --configuration Release -p:PublishSingleFile=true --self-contained - name: Zip - run: zip /home/runner/work/analog/analog-linux-x64 /home/runner/work/analog/analog/src/Analog/bin/Release/net8.0/linux-x64/publish/* + run: zip /home/runner/work/analog/analog-linux-x64 /home/runner/work/analog/analog/src/bin/Release/net8.0/linux-x64/publish/* - name: Upload uses: actions/upload-artifact@v4 with: @@ -39,18 +39,18 @@ jobs: - name: Setup uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 9.0.x - name: Restore run: dotnet restore src - name: Build - run: dotnet build src --no-restore --configuration Release + run: dotnet build --no-restore --configuration Release - name: Test - run: dotnet test src --no-build --configuration Release --verbosity normal + run: dotnet test --no-build --configuration Release --verbosity normal - name: Publish - run: dotnet publish src/Analog --configuration Release -p:PublishSingleFile=true --self-contained + run: dotnet publish src --configuration Release -p:PublishSingleFile=true --self-contained - name: Zip shell: pwsh - run: Compress-Archive -Path D:\a\analog\analog\src\Analog\bin\Release\net8.0\win-x64\publish\* -DestinationPath D:\a\analog\analog-win-x64.zip + run: Compress-Archive -Path D:\a\analog\analog\src\bin\Release\net8.0\win-x64\publish\* -DestinationPath D:\a\analog\analog-win-x64.zip - name: Upload uses: actions/upload-artifact@v4 with: @@ -64,17 +64,17 @@ jobs: - name: Setup uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 9.0.x - name: Restore run: dotnet restore src - name: Build - run: dotnet build src --no-restore --configuration Release + run: dotnet build --no-restore --configuration Release - name: Test - run: dotnet test src --no-build --configuration Release --verbosity normal + run: dotnet test --no-build --configuration Release --verbosity normal - name: Publish - run: dotnet publish src/Analog --configuration Release -p:PublishSingleFile=true --self-contained + run: dotnet publish src --configuration Release -p:PublishSingleFile=true --self-contained - name: Zip - run: zip /Users/runner/work/analog/analog-osx-arm64 /Users/runner/work/analog/analog/src/Analog/bin/Release/net8.0/osx-arm64/publish/* + run: zip /Users/runner/work/analog/analog-osx-arm64 /Users/runner/work/analog/analog/src/bin/Release/net8.0/osx-arm64/publish/* - name: Upload uses: actions/upload-artifact@v4 with: diff --git a/.gitignore b/.gitignore index c6a47aa..104b544 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,484 @@ -.vscode/ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from `dotnet new gitignore` + +# dotenv files +.env + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory .vs/ -.idea/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.idea + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Vim temporary swap files +*.swp diff --git a/analog.sln b/analog.sln new file mode 100644 index 0000000..d6d124f --- /dev/null +++ b/analog.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Analog", "src\Analog.fsproj", "{4A65E52A-645B-4AC8-9AFB-9F676E448CF9}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Analog.Tests", "test\Analog.Tests.fsproj", "{3BBACB6E-2AAA-433A-BF91-A40A2217FE37}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4A65E52A-645B-4AC8-9AFB-9F676E448CF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A65E52A-645B-4AC8-9AFB-9F676E448CF9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A65E52A-645B-4AC8-9AFB-9F676E448CF9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A65E52A-645B-4AC8-9AFB-9F676E448CF9}.Release|Any CPU.Build.0 = Release|Any CPU + {3BBACB6E-2AAA-433A-BF91-A40A2217FE37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3BBACB6E-2AAA-433A-BF91-A40A2217FE37}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3BBACB6E-2AAA-433A-BF91-A40A2217FE37}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3BBACB6E-2AAA-433A-BF91-A40A2217FE37}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/src/.gitignore b/src/.gitignore deleted file mode 100644 index 104b544..0000000 --- a/src/.gitignore +++ /dev/null @@ -1,484 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from `dotnet new gitignore` - -# dotenv files -.env - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET -project.lock.json -project.fragment.lock.json -artifacts/ - -# Tye -.tye/ - -# ASP.NET Scaffolding -ScaffoldingReadMe.txt - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.tlog -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Coverlet is a free, cross platform Code Coverage Tool -coverage*.json -coverage*.xml -coverage*.info - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio 6 auto-generated project file (contains which files were open etc.) -*.vbp - -# Visual Studio 6 workspace and project file (working project files containing files to include in project) -*.dsw -*.dsp - -# Visual Studio 6 technical files -*.ncb -*.aps - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# Visual Studio History (VSHistory) files -.vshistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ - -# Fody - auto-generated XML schema -FodyWeavers.xsd - -# VS Code files for those working on multiple tools -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -*.code-workspace - -# Local History for Visual Studio Code -.history/ - -# Windows Installer files from build outputs -*.cab -*.msi -*.msix -*.msm -*.msp - -# JetBrains Rider -*.sln.iml -.idea - -## -## Visual studio for Mac -## - - -# globs -Makefile.in -*.userprefs -*.usertasks -config.make -config.status -aclocal.m4 -install-sh -autom4te.cache/ -*.tar.gz -tarballs/ -test-results/ - -# Mac bundle stuff -*.dmg -*.app - -# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore -# General -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore -# Windows thumbnail cache files -Thumbs.db -ehthumbs.db -ehthumbs_vista.db - -# Dump file -*.stackdump - -# Folder config file -[Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp - -# Windows shortcuts -*.lnk - -# Vim temporary swap files -*.swp diff --git a/src/Analog.Tests/FilterEvaluatorTest.fs b/src/Analog.Tests/FilterEvaluatorTest.fs deleted file mode 100644 index 764acd4..0000000 --- a/src/Analog.Tests/FilterEvaluatorTest.fs +++ /dev/null @@ -1,111 +0,0 @@ -module Analog.Tests.FilterEvaluatorTest - -open System -open Xunit -open FsUnit.Xunit -open Analog - -[] -let ``Evaluate Const filter with Boolean literal`` () = - let entry = Map.empty - let filter = Filter.Const (Literal.Boolean true) - let result = FilterEvaluator.evaluate entry filter - result |> should equal true - -[] -let ``Evaluate Field filter with matching field in entry`` () = - let entry = Map.ofList [ "key", Literal.Boolean true ] - let filter = Filter.Field "key" - let result = FilterEvaluator.evaluate entry filter - result |> should equal true - -[] -let ``Evaluate Field filter with non-matching field in entry`` () = - let entry = Map.ofList [ "key", Literal.Boolean true ] - let filter = Filter.Field "missingKey" - let result = FilterEvaluator.evaluate entry filter - result |> should equal false - -[] -let ``Evaluate Binary Equal filter with matching String literals`` () = - let entry = Map.empty - let filter = - Filter.Binary( - Filter.Const (Literal.String "test"), - Operator.Equal, - Filter.Const (Literal.String "test") - ) - let result = FilterEvaluator.evaluate entry filter - result |> should equal true - -[] -let ``Evaluate Binary NotEqual filter with non-matching Number literals`` () = - let entry = Map.empty - let filter = - Filter.Binary( - Filter.Const (Literal.Number 1.0), - Operator.NotEqual, - Filter.Const (Literal.Number 2.0) - ) - let result = FilterEvaluator.evaluate entry filter - result |> should equal true - -[] -let ``Evaluate Binary GreaterThan filter with Timestamp literals`` () = - let entry = Map.empty - let filter = - Filter.Binary( - Filter.Const (Literal.Timestamp (DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero))), - Operator.GreaterThan, - Filter.Const (Literal.Timestamp (DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero))) - ) - let result = FilterEvaluator.evaluate entry filter - result |> should equal true - -[] -let ``Evaluate Binary And filter with Boolean literals`` () = - let entry = Map.empty - let filter = - Filter.Binary( - Filter.Const (Literal.Boolean true), - Operator.And, - Filter.Const (Literal.Boolean true) - ) - let result = FilterEvaluator.evaluate entry filter - result |> should equal true - -[] -let ``Evaluate Binary Or filter with one Boolean literal true`` () = - let entry = Map.empty - let filter = - Filter.Binary( - Filter.Const (Literal.Boolean false), - Operator.Or, - Filter.Const (Literal.Boolean true) - ) - let result = FilterEvaluator.evaluate entry filter - result |> should equal true - -[] -let ``Evaluate Binary Equal filter with mismatched Literal types`` () = - let entry = Map.empty - let filter = - Filter.Binary( - Filter.Const (Literal.String "test"), - Operator.Equal, - Filter.Const (Literal.Number 42.0) - ) - let result = FilterEvaluator.evaluate entry filter - result |> should equal false - -[] -let ``Evaluate Binary GreaterThan filter with invalid field in entry`` () = - let entry = Map.ofList [ "key", Literal.Number 10.0 ] - let filter = - Filter.Binary( - Filter.Field "key", - Operator.GreaterThan, - Filter.Const (Literal.Number 20.0) - ) - let result = FilterEvaluator.evaluate entry filter - result |> should equal false diff --git a/src/Analog.Tests/FilterParserTest.fs b/src/Analog.Tests/FilterParserTest.fs deleted file mode 100644 index aeeb423..0000000 --- a/src/Analog.Tests/FilterParserTest.fs +++ /dev/null @@ -1,60 +0,0 @@ -module Analog.Tests.FilterParserTest - -open System - -open FsUnitTyped -open Xunit - -open Analog - -let parse = FilterParser.expression |> ParserRunner.run - -[] -let ``parse should correctly parse a constant string`` () = - parse "'hello'" |> shouldEqual (Result.Ok(Const(String "hello"))) - -[] -let ``parse should correctly parse a constant number`` () = - parse "42.5" |> shouldEqual (Result.Ok(Const(Literal.Number 42.5))) - -[] -let ``parse should correctly parse a constant boolean (true)`` () = - parse "true" |> shouldEqual (Result.Ok(Const(Boolean true))) - -[] -let ``parse should correctly parse a constant boolean (false)`` () = - parse "false" |> shouldEqual (Result.Ok(Const(Boolean false))) - -[] -let ``parse should correctly parse a field identifier`` () = - parse "fieldName" |> shouldEqual (Result.Ok(Field "fieldName")) - -[] -let ``parse should correctly parse a simple binary expression`` () = - parse "'value' = fieldName" - |> shouldEqual (Result.Ok(Binary(Const(String "value"), Equal, Field "fieldName"))) - -[] -let ``parse should correctly parse a complex binary expression`` () = - parse "'value' = fieldName & 42 > 10" - |> shouldEqual ( - Result.Ok( - Binary( - Binary(Const(String "value"), Equal, Field "fieldName"), - And, - Binary(Const(Literal.Number 42.0), GreaterThan, Const(Literal.Number 10.0)) - ) - ) - ) - -[] -let ``parse should correctly parse a timestamp`` () = - let input = "2024-01-03T12:34:56+00:00" - let result = parse input - result |> shouldEqual (Result.Ok(Const(Timestamp(DateTimeOffset.Parse input)))) - -[] -let ``parse should return an error for invalid input`` () = - match parse "'unterminated string" with - | Ok _ -> failwith "parse should return an error for invalid input" - | Error _ -> () diff --git a/src/Analog.Tests/Program.fs b/src/Analog.Tests/Program.fs deleted file mode 100644 index 0695f84..0000000 --- a/src/Analog.Tests/Program.fs +++ /dev/null @@ -1 +0,0 @@ -module Program = let [] main _ = 0 diff --git a/src/Analog/Analog.fsproj b/src/Analog.fsproj similarity index 76% rename from src/Analog/Analog.fsproj rename to src/Analog.fsproj index 0a0c3d4..db4926c 100644 --- a/src/Analog/Analog.fsproj +++ b/src/Analog.fsproj @@ -1,17 +1,19 @@  - net8.0 + net9.0 Exe + + - + diff --git a/src/Analog/Core.fs b/src/Analog/Core.fs deleted file mode 100644 index 46f89ca..0000000 --- a/src/Analog/Core.fs +++ /dev/null @@ -1,208 +0,0 @@ -namespace Analog - -open System - -type Literal = - | String of string - | Number of float - | Boolean of bool - | Timestamp of DateTimeOffset - -type Entry = Map - -type Operator = - | Equal - | NotEqual - | GreaterThan - | GreaterThanOrEqual - | LessThan - | LessThanOrEqual - | And - | Or - -type Filter = - | Const of Literal - | Field of string - | Binary of Filter * Operator * Filter - -module ParserRunner = - open FParsec - - let run parser = - run parser - >> function - | Success(value, _, _) -> Result.Ok value - | Failure(error, _, _) -> Result.Error error - - let tryRun parser = - run parser - >> function - | Result.Ok value -> Option.Some value - | Result.Error _ -> Option.None - -module LiteralParser = - open FParsec - - let number: Parser<_, unit> = - pfloat - >>= fun res -> - if Double.IsInfinity res || Double.IsNaN res then - fail "Number cannot be infinite or NaN" - else - preturn res - |> attempt - |>> Literal.Number - - let boolean: Parser<_, unit> = - choice [ pstringCI "true" >>% true; pstringCI "false" >>% false ] - |>> Literal.Boolean - - let timestamp: Parser<_, unit> = - restOfLine false - >>= fun input -> - try - DateTimeOffset.Parse input |> preturn |>> Literal.Timestamp - with err -> - fail err.Message - |> attempt - - let string: Parser<_, unit> = restOfLine true |>> Literal.String - - let literal: Parser<_, unit> = choice [ timestamp; number; boolean; string ] - -module EntryParser = - open GrokNet - - type private RawEntry = Map - - let create txt = - try - Grok txt |> Result.Ok - with err -> - Result.Error err.Message - - let value = - "\[%{TIMESTAMP_ISO8601:timestamp}\] \[%{LOGLEVEL:loglevel}\] %{GREEDYDATA:message}" - |> Grok - - let private group list (key, value) = - match list with - | [] -> [ Map([ key, value ]) ] - | head :: tail -> - if head |> Map.containsKey key then - [ Map([ key, value ]); head ] @ tail - else - (head |> Map.add key value) :: tail - - let private parseRaw (entry: RawEntry) = - entry - |> Map.map (fun _ -> ParserRunner.tryRun LiteralParser.literal) - |> Map.filter (fun _ value -> value |> Option.isSome) - |> Map.map (fun _ value -> value |> Option.get) - - let parse text (grok: Grok) : Entry list = - grok.Parse text - |> Seq.map (fun i -> i.Key, i.Value.ToString()) - |> Seq.fold group List.empty - |> List.map parseRaw - |> List.rev - -module FilterParser = - open FParsec - - let string: Parser<_, unit> = - skipChar '\'' >>. manyCharsTill anyChar (skipChar '\'') |>> String .>> spaces - - let number: Parser<_, unit> = pfloat |>> Literal.Number .>> spaces - - let boolean: Parser<_, unit> = LiteralParser.boolean .>> spaces - - let timestamp: Parser<_, unit> = LiteralParser.timestamp .>> spaces - - let constant: Parser<_, unit> = - choice [ string; timestamp; number; boolean ] |>> Const - - let field: Parser<_, unit> = many1Chars (letter <|> digit) |>> Field .>> spaces - - let term: Parser<_, unit> = choice [ constant; field ] - - let expression: Parser<_, unit> = - let precedence = OperatorPrecedenceParser() - precedence.TermParser <- choice [ constant; field ] - let add = precedence.AddOperator - let binary operator left right = Binary(left, operator, right) - add (InfixOperator("&", spaces, 1, Associativity.Left, binary And)) - add (InfixOperator("|", spaces, 2, Associativity.Left, binary Or)) - add (InfixOperator(">", spaces, 3, Associativity.None, binary GreaterThan)) - add (InfixOperator(">=", spaces, 4, Associativity.None, binary GreaterThanOrEqual)) - add (InfixOperator("<", spaces, 5, Associativity.None, binary LessThan)) - add (InfixOperator("<=", spaces, 6, Associativity.None, binary LessThanOrEqual)) - add (InfixOperator("=", spaces, 7, Associativity.None, binary Equal)) - add (InfixOperator("<>", spaces, 8, Associativity.None, binary NotEqual)) - precedence.ExpressionParser - -module FilterEvaluator = - - type private Eval = - | Temp of Literal option - | Final of bool - - let private compareLiteral (left: Literal option) (right: Literal option) comparer = - match left, right with - | Some left, Some right -> - match left, right with - | String _, String _ -> comparer left right - | Number _, Number _ -> comparer left right - | Boolean _, Boolean _ -> comparer left right - | Timestamp _, Timestamp _ -> comparer left right - | _ -> false - | _ -> false - - let private combineLiteral (left: Literal option) (right: Literal option) combiner = - match left, right with - | Some left, Some right -> - match left, right with - | Boolean left, Boolean right -> combiner left right - | _ -> false - | _ -> false - - let private wrapFinal = Literal.Boolean >> Option.Some - - let private compareEvaluation (left: Eval) (right: Eval) comparer = - match left, right with - | Temp left, Temp right -> compareLiteral left right comparer - | Temp left, Final right -> compareLiteral left (wrapFinal right) comparer - | Final left, Temp right -> compareLiteral (wrapFinal left) right comparer - | Final left, Final right -> compareLiteral (wrapFinal left) (wrapFinal right) comparer - - let private combineEvaluation (left: Eval) (right: Eval) combiner = - match left, right with - | Temp left, Temp right -> combineLiteral left right combiner - | Temp left, Final right -> combineLiteral left (wrapFinal right) combiner - | Final left, Temp right -> combineLiteral (wrapFinal left) right combiner - | Final left, Final right -> combineLiteral (wrapFinal left) (wrapFinal right) combiner - - let private evalOperator (left: Eval) (operator: Operator) (right: Eval) = - match operator with - | Equal -> compareEvaluation left right (=) - | NotEqual -> compareEvaluation left right (<>) - | GreaterThan -> compareEvaluation left right (>) - | GreaterThanOrEqual -> compareEvaluation left right (>=) - | LessThan -> compareEvaluation left right (<) - | LessThanOrEqual -> compareEvaluation left right (<=) - | And -> combineEvaluation left right (&&) - | Or -> combineEvaluation left right (||) - - let rec private eval (expression: Filter) (entry: Entry) : Eval = - match expression with - | Filter.Const right -> right |> Option.Some |> Eval.Temp - | Filter.Field field -> entry |> Map.tryFind field |> Eval.Temp - | Filter.Binary(left, operator, right) -> - let left = eval left entry - let right = eval right entry - evalOperator left operator right |> Eval.Final - - let evaluate entry expression = - match eval expression entry with - | Temp temp -> temp |> Option.isSome - | Final final -> final \ No newline at end of file diff --git a/src/Core.fs b/src/Core.fs new file mode 100644 index 0000000..0a01e9d --- /dev/null +++ b/src/Core.fs @@ -0,0 +1,63 @@ +[] +module Analog.Core + +module FParsec = + open FParsec + open System + + let pFloatFinite: Parser<_, unit> = + pfloat + >>= fun res -> + if Double.IsInfinity res || Double.IsNaN res then + fail "Number cannot be infinite or NaN" + else + preturn res + |> attempt + + let pBoolean: Parser<_, unit> = + choice [ pstringCI "true" >>% true; pstringCI "false" >>% false ] + + let pDateTimeOffset: Parser<_, unit> = + restOfLine false + >>= fun input -> + try + DateTimeOffset.Parse input |> preturn + with err -> + fail err.Message + |> attempt + + let skipSingleQuote: Parser<_, unit> = skipChar '\'' + + let pSingleQuotedString = skipSingleQuote >>. manyCharsTill anyChar skipSingleQuote + +module GrokNet = + open GrokNet + + let grokCreate pattern = + try + Grok(pattern) |> Result.Ok + with err -> + $"Grok initialization failed with error: {err.Message}" |> Result.Error + + let grokParse text (pattern: Grok) = + try + pattern.Parse text |> Result.Ok + with err -> + $"Grok parsing failed with error: {err.Message}" |> Result.Error + + let grokDefault = + match grokCreate "\[%{TIMESTAMP_ISO8601:timestamp}\] \[%{LOGLEVEL:loglevel}\] %{GREEDYDATA:message}" with + | Ok value -> value + | Error error -> failwith error + + let grokFold (result: GrokResult) = + (List.empty>, result) + ||> Seq.fold (fun list item -> + match list with + | [] -> [ Map([ item.Key, string item.Value ]) ] + | head :: tail -> + if head |> Map.containsKey item.Key then + [ Map([ item.Key, string item.Value ]); head ] @ tail + else + (head |> Map.add item.Key (string item.Value)) :: tail) + |> List.rev diff --git a/src/Filter.fs b/src/Filter.fs new file mode 100644 index 0000000..0eefb7c --- /dev/null +++ b/src/Filter.fs @@ -0,0 +1,123 @@ +module Analog.Filter + +open Log +open FParsec + +[] +type Operator = + | Equal + | NotEqual + | GreaterThan + | GreaterThanOrEqual + | LessThan + | LessThanOrEqual + | And + | Or + +[] +type Expression = + | Value of LogLiteral + | Field of string + | Binary of Expression * Operator * Expression + +let private pExpression: Parser<_, unit> = + let pExpressionField: Parser<_, unit> = + many1Chars (letter <|> digit) |>> Expression.Field .>> spaces + + let pExpressionValue: Parser<_, unit> = + choice + [ pSingleQuotedString |>> LogLiteral.String .>> spaces + pDateTimeOffset |>> LogLiteral.Timestamp .>> spaces + pFloatFinite |>> LogLiteral.Number .>> spaces + pBoolean |>> LogLiteral.Boolean .>> spaces ] + |>> Expression.Value + + let pPrecedenceTerm = choice [ pExpressionValue; pExpressionField ] + + let pPrecedence = OperatorPrecedenceParser() + + pPrecedence.TermParser <- pPrecedenceTerm + + let add = pPrecedence.AddOperator + let bin op left right = Expression.Binary(left, op, right) + + add (InfixOperator("&", spaces, 1, Associativity.Left, bin Operator.And)) + add (InfixOperator("|", spaces, 2, Associativity.Left, bin Operator.Or)) + add (InfixOperator(">", spaces, 3, Associativity.None, bin Operator.GreaterThan)) + add (InfixOperator(">=", spaces, 4, Associativity.None, bin Operator.GreaterThanOrEqual)) + add (InfixOperator("<", spaces, 5, Associativity.None, bin Operator.LessThan)) + add (InfixOperator("<=", spaces, 6, Associativity.None, bin Operator.LessThanOrEqual)) + add (InfixOperator("=", spaces, 7, Associativity.None, bin Operator.Equal)) + add (InfixOperator("<>", spaces, 8, Associativity.None, bin Operator.NotEqual)) + + pPrecedence.ExpressionParser + +let parse expression = + match run pExpression expression with + | Success(value, _, _) -> Result.Ok value + | Failure(error, _, _) -> Result.Error error + +[] +type private Evaluation = + | Value of LogLiteral option + | Final of bool + +let evaluateFilter entry expression = + let compareLiteral (left: LogLiteral option) (right: LogLiteral option) comparer = + match left, right with + | Some left, Some right -> + match left, right with + | LogLiteral.String _, LogLiteral.String _ -> comparer left right + | LogLiteral.Number _, LogLiteral.Number _ -> comparer left right + | LogLiteral.Boolean _, LogLiteral.Boolean _ -> comparer left right + | LogLiteral.Timestamp _, LogLiteral.Timestamp _ -> comparer left right + | _ -> false + | _ -> false + + let combineLiteral (left: LogLiteral option) (right: LogLiteral option) combiner = + match left, right with + | Some left, Some right -> + match left, right with + | LogLiteral.Boolean left, LogLiteral.Boolean right -> combiner left right + | _ -> false + | _ -> false + + let wrapFinal = LogLiteral.Boolean >> Option.Some + + let compareEvaluation (left: Evaluation) (right: Evaluation) comparer = + match left, right with + | Evaluation.Value left, Evaluation.Value right -> compareLiteral left right comparer + | Evaluation.Value left, Evaluation.Final right -> compareLiteral left (wrapFinal right) comparer + | Evaluation.Final left, Evaluation.Value right -> compareLiteral (wrapFinal left) right comparer + | Evaluation.Final left, Evaluation.Final right -> compareLiteral (wrapFinal left) (wrapFinal right) comparer + + let combineEvaluation (left: Evaluation) (right: Evaluation) combiner = + match left, right with + | Evaluation.Value left, Evaluation.Value right -> combineLiteral left right combiner + | Evaluation.Value left, Evaluation.Final right -> combineLiteral left (wrapFinal right) combiner + | Evaluation.Final left, Evaluation.Value right -> combineLiteral (wrapFinal left) right combiner + | Evaluation.Final left, Evaluation.Final right -> combineLiteral (wrapFinal left) (wrapFinal right) combiner + + let evaluateOperator (left: Evaluation) (operator: Operator) (right: Evaluation) = + match operator with + | Operator.Equal -> compareEvaluation left right (=) + | Operator.NotEqual -> compareEvaluation left right (<>) + | Operator.GreaterThan -> compareEvaluation left right (>) + | Operator.GreaterThanOrEqual -> compareEvaluation left right (>=) + | Operator.LessThan -> compareEvaluation left right (<) + | Operator.LessThanOrEqual -> compareEvaluation left right (<=) + | Operator.And -> combineEvaluation left right (&&) + | Operator.Or -> combineEvaluation left right (||) + + let rec loop (expression: Expression) (entry: LogEntry) : Evaluation = + match expression with + | Expression.Value right -> right |> Option.Some |> Evaluation.Value + | Expression.Field field -> entry |> Map.tryFind field |> Evaluation.Value + | Expression.Binary(left, operator, right) -> + let left = loop left entry + let right = loop right entry + evaluateOperator left operator right |> Evaluation.Final + + match loop expression entry with + | Evaluation.Value temp -> temp.IsSome + | Evaluation.Final final -> final diff --git a/src/Log.fs b/src/Log.fs new file mode 100644 index 0000000..f344777 --- /dev/null +++ b/src/Log.fs @@ -0,0 +1,31 @@ +module Analog.Log + +open System + +open FParsec + +[] +type LogLiteral = + | String of string + | Number of float + | Boolean of bool + | Timestamp of DateTimeOffset + +type LogEntry = Map + +let private pLogLiteral: Parser<_, unit> = + choice + [ pDateTimeOffset |>> LogLiteral.Timestamp + pFloatFinite |>> LogLiteral.Number + pBoolean |>> LogLiteral.Boolean + restOfLine true |>> LogLiteral.String ] + +let parse: Map list -> Map list = + List.map ( + Map.toSeq + >> Seq.choose (fun (key, value) -> + match run pLogLiteral value with + | Success(value, _, _) -> Option.Some(key, value) + | Failure _ -> Option.None) + >> Map.ofSeq + ) diff --git a/src/Analog/Program.fs b/src/Program.fs similarity index 60% rename from src/Analog/Program.fs rename to src/Program.fs index 61608f2..ad990c4 100644 --- a/src/Analog/Program.fs +++ b/src/Program.fs @@ -2,11 +2,15 @@ open System.IO open System.Text.Json open Analog +open GrokNet +open Log open Argu +open Microsoft.FSharp.Core open Spectre.Console open Spectre.Console.Json -type Argument = +[] +type Command = | [] File of string | [] Pattern of string | [] Filter of string @@ -21,37 +25,39 @@ type Argument = let import files = files |> List.map File.ReadAllText |> List.reduce (fun all next -> all + next) + let parse pattern text = pattern - |> Option.map EntryParser.create - |> Option.defaultValue (EntryParser.value |> Result.Ok) - |> Result.map (EntryParser.parse text) + |> Option.map grokCreate + |> Option.defaultValue (grokDefault |> Result.Ok) + |> Result.bind (grokParse text) + |> Result.map grokFold + |> Result.map Log.parse -let filter filter entries = - filter - |> Option.map (ParserRunner.run FilterParser.expression) +let filter expression entries = + expression + |> Option.map Filter.parse |> Option.map (fun res -> res |> Result.bind (fun filter -> entries |> Result.map (fun entries -> filter, entries))) |> Option.map (fun result -> result - |> Result.map (fun (filter, entries) -> - entries |> List.filter (fun entry -> FilterEvaluator.evaluate entry filter))) + |> Result.map (fun (filter, entries) -> entries |> List.filter (fun entry -> Filter.evaluateFilter entry filter))) |> Option.defaultValue entries -let handle (args: ParseResults) = - import (args.GetResults Argument.File) - |> parse (args.TryGetResult Argument.Pattern) - |> filter (args.TryGetResult Argument.Filter) +let handle (args: ParseResults) = + import (args.GetResults Command.File) + |> parse (args.TryGetResult Command.Pattern) + |> filter (args.TryGetResult Command.Filter) -let normalize (entry: Entry) = +let normalize (entry: LogEntry) = entry |> Map.map (fun _ value -> match value with - | String value -> box value - | Number value -> box value - | Boolean value -> box value - | Timestamp value -> box value) + | LogLiteral.String value -> box value + | LogLiteral.Number value -> box value + | LogLiteral.Boolean value -> box value + | LogLiteral.Timestamp value -> box value) let print entries = entries @@ -63,7 +69,7 @@ let print entries = let args = try ArgumentParser - .Create() + .Create() .ParseCommandLine(Environment.GetCommandLineArgs() |> Array.skip 1) |> Result.Ok with err -> diff --git a/src/analog.sln b/src/analog.sln deleted file mode 100644 index e364731..0000000 --- a/src/analog.sln +++ /dev/null @@ -1,51 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Analog", "Analog\Analog.fsproj", "{7A9C59B7-C5F5-449F-A446-DD2703323686}" -EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Analog.Tests", "Analog.Tests\Analog.Tests.fsproj", "{D742D202-8FE7-42B8-B728-ACF3C3B28880}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|arm64 = Debug|arm64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|arm64 = Release|arm64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {7A9C59B7-C5F5-449F-A446-DD2703323686}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7A9C59B7-C5F5-449F-A446-DD2703323686}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7A9C59B7-C5F5-449F-A446-DD2703323686}.Debug|arm64.ActiveCfg = Debug|Any CPU - {7A9C59B7-C5F5-449F-A446-DD2703323686}.Debug|arm64.Build.0 = Debug|Any CPU - {7A9C59B7-C5F5-449F-A446-DD2703323686}.Debug|x86.ActiveCfg = Debug|Any CPU - {7A9C59B7-C5F5-449F-A446-DD2703323686}.Debug|x86.Build.0 = Debug|Any CPU - {7A9C59B7-C5F5-449F-A446-DD2703323686}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7A9C59B7-C5F5-449F-A446-DD2703323686}.Release|Any CPU.Build.0 = Release|Any CPU - {7A9C59B7-C5F5-449F-A446-DD2703323686}.Release|arm64.ActiveCfg = Release|Any CPU - {7A9C59B7-C5F5-449F-A446-DD2703323686}.Release|arm64.Build.0 = Release|Any CPU - {7A9C59B7-C5F5-449F-A446-DD2703323686}.Release|x86.ActiveCfg = Release|Any CPU - {7A9C59B7-C5F5-449F-A446-DD2703323686}.Release|x86.Build.0 = Release|Any CPU - {D742D202-8FE7-42B8-B728-ACF3C3B28880}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D742D202-8FE7-42B8-B728-ACF3C3B28880}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D742D202-8FE7-42B8-B728-ACF3C3B28880}.Debug|arm64.ActiveCfg = Debug|Any CPU - {D742D202-8FE7-42B8-B728-ACF3C3B28880}.Debug|arm64.Build.0 = Debug|Any CPU - {D742D202-8FE7-42B8-B728-ACF3C3B28880}.Debug|x86.ActiveCfg = Debug|Any CPU - {D742D202-8FE7-42B8-B728-ACF3C3B28880}.Debug|x86.Build.0 = Debug|Any CPU - {D742D202-8FE7-42B8-B728-ACF3C3B28880}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D742D202-8FE7-42B8-B728-ACF3C3B28880}.Release|Any CPU.Build.0 = Release|Any CPU - {D742D202-8FE7-42B8-B728-ACF3C3B28880}.Release|arm64.ActiveCfg = Release|Any CPU - {D742D202-8FE7-42B8-B728-ACF3C3B28880}.Release|arm64.Build.0 = Release|Any CPU - {D742D202-8FE7-42B8-B728-ACF3C3B28880}.Release|x86.ActiveCfg = Release|Any CPU - {D742D202-8FE7-42B8-B728-ACF3C3B28880}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {06FB59AB-911B-498D-89BF-3DF9DA28C2E7} - EndGlobalSection -EndGlobal diff --git a/src/Analog.Tests/Analog.Tests.fsproj b/test/Analog.Tests.fsproj similarity index 79% rename from src/Analog.Tests/Analog.Tests.fsproj rename to test/Analog.Tests.fsproj index 7a9ee78..01b662f 100644 --- a/src/Analog.Tests/Analog.Tests.fsproj +++ b/test/Analog.Tests.fsproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 false false @@ -16,21 +16,21 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/Analog.Tests/EntryParserTest.fs b/test/EntryParserTest.fs similarity index 56% rename from src/Analog.Tests/EntryParserTest.fs rename to test/EntryParserTest.fs index 2c3ce3d..cce0e69 100644 --- a/src/Analog.Tests/EntryParserTest.fs +++ b/test/EntryParserTest.fs @@ -2,14 +2,22 @@ open System open Analog -open FsUnit.Xunit +open GrokNet +open Swensen.Unquote open Xunit +open Log let parse txt = - EntryParser.value |> EntryParser.parse txt + grokDefault + |> grokParse txt + |> Result.map grokFold + |> Result.map Log.parse + |> function + | Result.Ok value -> value + | Result.Error err -> failwith err -let stringOf = Literal.String -let timestampOf = DateTimeOffset.Parse >> Literal.Timestamp +let stringOf = LogLiteral.String +let timestampOf = DateTimeOffset.Parse >> LogLiteral.Timestamp [] let ``parse single log line`` () = @@ -20,9 +28,9 @@ let ``parse single log line`` () = |> Map.ofList |> List.singleton - "[2024-11-17 14:30:55] [INFO] User logged in successfully" - |> parse - |> should equal expected + let actual = parse "[2024-11-17 14:30:55] [INFO] User logged in successfully" + + test <@ actual = expected @> [] let ``parse two log lines`` () = @@ -37,6 +45,8 @@ let ``parse two log lines`` () = |> Map.ofList ] |> List.ofSeq - "[2024-11-17 14:30:55] [INFO] User logged in successfully\n[2024-11-17 14:31:00] [ERROR] Failed to authenticate user" - |> parse - |> should equal expected + let actual = + parse + "[2024-11-17 14:30:55] [INFO] User logged in successfully\n[2024-11-17 14:31:00] [ERROR] Failed to authenticate user" + + test <@ actual = expected @> diff --git a/test/FilterEvaluatorTest.fs b/test/FilterEvaluatorTest.fs new file mode 100644 index 0000000..b53dcb5 --- /dev/null +++ b/test/FilterEvaluatorTest.fs @@ -0,0 +1,123 @@ +module Analog.Tests.FilterEvaluatorTest + +open System +open Analog +open Swensen.Unquote +open Xunit +open Log +open Filter + +[] +let ``Evaluate Const filter with Boolean literal`` () = + let entry = Map.empty + let filter = Expression.Value(LogLiteral.Boolean true) + let result = evaluateFilter entry filter + test <@ result = true @> + +[] +let ``Evaluate Field filter with matching field in entry`` () = + let entry = Map.ofList [ "key", LogLiteral.Boolean true ] + let filter = Expression.Field "key" + let result = evaluateFilter entry filter + test <@ result = true @> + +[] +let ``Evaluate Field filter with non-matching field in entry`` () = + let entry = Map.ofList [ "key", LogLiteral.Boolean true ] + let filter = Expression.Field "missingKey" + let result = evaluateFilter entry filter + test <@ result = false @> + +[] +let ``Evaluate Binary Equal filter with matching String literals`` () = + let entry = Map.empty + + let filter = + Expression.Binary( + Expression.Value(LogLiteral.String "test"), + Operator.Equal, + Expression.Value(LogLiteral.String "test") + ) + + let result = evaluateFilter entry filter + test <@ result = true @> + +[] +let ``Evaluate Binary NotEqual filter with non-matching Number literals`` () = + let entry = Map.empty + + let filter = + Expression.Binary( + Expression.Value(LogLiteral.Number 1.0), + Operator.NotEqual, + Expression.Value(LogLiteral.Number 2.0) + ) + + let result = evaluateFilter entry filter + test <@ result = true @> + +[] +let ``Evaluate Binary GreaterThan filter with Timestamp literals`` () = + let entry = Map.empty + + let filter = + Expression.Binary( + Expression.Value(LogLiteral.Timestamp(DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero))), + Operator.GreaterThan, + Expression.Value(LogLiteral.Timestamp(DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero))) + ) + + let result = evaluateFilter entry filter + test <@ result = true @> + +[] +let ``Evaluate Binary And filter with Boolean literals`` () = + let entry = Map.empty + + let filter = + Expression.Binary( + Expression.Value(LogLiteral.Boolean true), + Operator.And, + Expression.Value(LogLiteral.Boolean true) + ) + + let result = evaluateFilter entry filter + test <@ result = true @> + +[] +let ``Evaluate Binary Or filter with one Boolean literal true`` () = + let entry = Map.empty + + let filter = + Expression.Binary( + Expression.Value(LogLiteral.Boolean false), + Operator.Or, + Expression.Value(LogLiteral.Boolean true) + ) + + let result = evaluateFilter entry filter + test <@ result = true @> + +[] +let ``Evaluate Binary Equal filter with mismatched Literal types`` () = + let entry = Map.empty + + let filter = + Expression.Binary( + Expression.Value(LogLiteral.String "test"), + Operator.Equal, + Expression.Value(LogLiteral.Number 42.0) + ) + + let result = evaluateFilter entry filter + test <@ result = false @> + +[] +let ``Evaluate Binary GreaterThan filter with invalid field in entry`` () = + let entry = Map.ofList [ "key", LogLiteral.Number 10.0 ] + + let filter = + Expression.Binary(Expression.Field "key", Operator.GreaterThan, Expression.Value(LogLiteral.Number 20.0)) + + let result = evaluateFilter entry filter + test <@ result = false @> diff --git a/test/FilterParserTest.fs b/test/FilterParserTest.fs new file mode 100644 index 0000000..d0e779a --- /dev/null +++ b/test/FilterParserTest.fs @@ -0,0 +1,98 @@ +module Analog.Tests.FilterParserTest + + +open System +open Analog +open Swensen.Unquote +open Xunit + +open Analog.Filter +open Analog.Log + +let parse text = Filter.parse text + +[] +let ``parse should correctly parse a constant string`` () = + let actual = parse "'hello'" + + let expected: Result = + Result.Ok(Expression.Value(LogLiteral.String "hello")) + + test <@ actual = expected @> + +[] +let ``parse should correctly parse a constant number`` () = + let actual = parse "42.5" + let expected: Result = Result.Ok(Expression.Value(LogLiteral.Number 42.5)) + test <@ actual = expected @> + +[] +let ``parse should correctly parse a constant boolean (true)`` () = + let actual = parse "true" + let expected: Result = Result.Ok(Expression.Value(LogLiteral.Boolean true)) + test <@ actual = expected @> + +[] +let ``parse should correctly parse a constant boolean (false)`` () = + let actual = parse "false" + let expected: Result = Result.Ok(Expression.Value(LogLiteral.Boolean false)) + test <@ actual = expected @> + +[] +let ``parse should correctly parse a field identifier`` () = + let actual = parse "fieldName" + let expected: Result = Result.Ok(Expression.Field "fieldName") + test <@ actual = expected @> + +[] +let ``parse should correctly parse a simple binary expression`` () = + let actual = parse "'FilterExpression.Value' = fieldName" + + let expected: Result = + Result.Ok( + Expression.Binary( + Expression.Value(LogLiteral.String "FilterExpression.Value"), + Operator.Equal, + Expression.Field "fieldName" + ) + ) + + test <@ actual = expected @> + +[] +let ``parse should correctly parse a complex binary expression`` () = + let actual = parse "'FilterExpression.Value' = fieldName & 42 > 10" + + let expected: Result = + Result.Ok( + Expression.Binary( + Expression.Binary( + Expression.Value(LogLiteral.String "FilterExpression.Value"), + Operator.Equal, + Expression.Field "fieldName" + ), + Operator.And, + Expression.Binary( + Expression.Value(LogLiteral.Number 42.0), + Operator.GreaterThan, + Expression.Value(LogLiteral.Number 10.0) + ) + ) + ) + + test <@ actual = expected @> + +[] +let ``parse should correctly parse a timestamp`` () = + let actual = parse "2024-01-03T12:34:56+00:00" + + let expected: Result = + Result.Ok(Expression.Value(LogLiteral.Timestamp(DateTimeOffset.Parse "2024-01-03T12:34:56+00:00"))) + + test <@ actual = expected @> + +[] +let ``parse should return an error for invalid input`` () = + match parse "'unterminated string" with + | Ok _ -> failwith "parse should return an error for invalid input" + | Error _ -> () diff --git a/test/Program.fs b/test/Program.fs new file mode 100644 index 0000000..80c6d84 --- /dev/null +++ b/test/Program.fs @@ -0,0 +1,3 @@ +module Program = + [] + let main _ = 0