├── .gitattributes ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .paket ├── paket.bootstrapper.exe └── paket.targets ├── .travis.yml ├── Chessie.sln ├── LICENSE.txt ├── NuGet.config ├── README.md ├── RELEASE_NOTES.md ├── appveyor.yml ├── build.cmd ├── build.fsx ├── build.sh ├── docs ├── content │ ├── a-tale-of-3-nightclubs-fsharp.fsx │ ├── a-tale-of-3-nightclubs.md │ ├── index.md │ ├── pass-warn-fail.fsx │ └── railway.fsx ├── files │ └── img │ │ ├── logo.pdn │ │ └── logo.png └── tools │ ├── generate.fsx │ └── templates │ └── template.cshtml ├── global.json ├── lib └── README.md ├── paket.dependencies ├── paket.lock ├── src └── Chessie │ ├── AssemblyInfo.fs │ ├── Chessie.fsproj │ ├── ErrorHandling.fs │ ├── paket.references │ ├── paket.template │ └── project.json └── tests ├── Chessie.CSharp.Test ├── Chessie.CSharp.Test.csproj ├── ExtensionsTests.cs ├── NightClubsValidation.cs ├── Program.cs ├── Properties │ └── AssemblyInfo.cs ├── SimpleValidation.cs ├── paket.references └── project.json ├── Chessie.Tests ├── BuilderTests.fs ├── Chessie.Tests.fsproj ├── NightClubs.fs ├── Program.fs ├── SimpleValidation.fs ├── TrialTests.fs ├── paket.references └── project.json └── Chessie.VisualBasic.Tests ├── Chessie.VisualBasic.Tests.vbproj ├── NightClubsValidation.vb └── SimpleValidation.vb /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp text=auto eol=lf 6 | *.fs diff=csharp text=auto eol=lf 7 | *.fsi diff=csharp text=auto eol=lf 8 | *.fsx diff=csharp text=auto eol=lf 9 | *.sln text eol=crlf merge=union 10 | *.csproj merge=union 11 | *.vbproj merge=union 12 | *.fsproj merge=union 13 | *.dbproj merge=union 14 | 15 | # Standard to msysgit 16 | *.doc diff=astextplain 17 | *.DOC diff=astextplain 18 | *.docx diff=astextplain 19 | *.DOCX diff=astextplain 20 | *.dot diff=astextplain 21 | *.DOT diff=astextplain 22 | *.pdf diff=astextplain 23 | *.PDF diff=astextplain 24 | *.rtf diff=astextplain 25 | *.RTF diff=astextplain 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | Please provide a succinct description of your issue. 4 | 5 | ### Repro steps 6 | 7 | Please provide the steps required to reproduce the problem 8 | 9 | 1. Step A 10 | 11 | 2. Step B 12 | 13 | ### Expected behavior 14 | 15 | Please provide a description of the behavior you expect. 16 | 17 | ### Actual behavior 18 | 19 | Please provide a description of the actual behavior you observe. 20 | 21 | ### Known workarounds 22 | 23 | Please provide a description of any known workarounds. 24 | 25 | ### Related information 26 | 27 | * Operating system 28 | * Branch 29 | * .NET Runtime, CoreCLR or Mono Version 30 | * Performance information, links to performance testing scripts 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # .NET cli 10 | 11 | project.lock.json 12 | 13 | # Xamarin Studio / monodevelop user-specific 14 | *.userprefs 15 | 16 | # Build results 17 | 18 | [Dd]ebug/ 19 | [Rr]elease/ 20 | x64/ 21 | build/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets 26 | !packages/*/build/ 27 | 28 | # MSTest test Results 29 | [Tt]est[Rr]esult*/ 30 | [Bb]uild[Ll]og.* 31 | 32 | *_i.c 33 | *_p.c 34 | *.ilk 35 | *.meta 36 | *.obj 37 | *.pch 38 | *.pdb 39 | *.pgc 40 | *.pgd 41 | *.rsp 42 | *.sbr 43 | *.tlb 44 | *.tli 45 | *.tlh 46 | *.tmp 47 | *.tmp_proj 48 | *.log 49 | *.vspscc 50 | *.vssscc 51 | .builds 52 | *.pidb 53 | *.log 54 | *.scc 55 | 56 | # Visual C++ cache files 57 | ipch/ 58 | *.aps 59 | *.ncb 60 | *.opensdf 61 | *.sdf 62 | *.cachefile 63 | 64 | # Visual Studio profiler 65 | *.psess 66 | *.vsp 67 | *.vspx 68 | 69 | # Guidance Automation Toolkit 70 | *.gpState 71 | 72 | # ReSharper is a .NET coding add-in 73 | _ReSharper*/ 74 | *.[Rr]e[Ss]harper 75 | 76 | # TeamCity is a build add-in 77 | _TeamCity* 78 | 79 | # DotCover is a Code Coverage Tool 80 | *.dotCover 81 | 82 | # NCrunch 83 | *.ncrunch* 84 | .*crunch*.local.xml 85 | 86 | # Installshield output folder 87 | [Ee]xpress/ 88 | 89 | # DocProject is a documentation generator add-in 90 | DocProject/buildhelp/ 91 | DocProject/Help/*.HxT 92 | DocProject/Help/*.HxC 93 | DocProject/Help/*.hhc 94 | DocProject/Help/*.hhk 95 | DocProject/Help/*.hhp 96 | DocProject/Help/Html2 97 | DocProject/Help/html 98 | 99 | # Click-Once directory 100 | publish/ 101 | 102 | # Publish Web Output 103 | *.Publish.xml 104 | 105 | # Enable nuget.exe in the .nuget folder (though normally executables are not tracked) 106 | !.nuget/NuGet.exe 107 | 108 | # Windows Azure Build Output 109 | csx 110 | *.build.csdef 111 | 112 | # Windows Store app package directory 113 | AppPackages/ 114 | 115 | # Others 116 | sql/ 117 | *.Cache 118 | ClientBin/ 119 | [Ss]tyle[Cc]op.* 120 | ~$* 121 | *~ 122 | *.dbmdl 123 | *.[Pp]ublish.xml 124 | *.pfx 125 | *.publishsettings 126 | 127 | # RIA/Silverlight projects 128 | Generated_Code/ 129 | 130 | # Backup & report files from converting an old project file to a newer 131 | # Visual Studio version. Backup files are not needed, because we have git ;-) 132 | _UpgradeReport_Files/ 133 | Backup*/ 134 | UpgradeLog*.XML 135 | UpgradeLog*.htm 136 | 137 | # SQL Server files 138 | App_Data/*.mdf 139 | App_Data/*.ldf 140 | 141 | 142 | #LightSwitch generated files 143 | GeneratedArtifacts/ 144 | _Pvt_Extensions/ 145 | ModelManifest.xml 146 | 147 | # ========================= 148 | # Windows detritus 149 | # ========================= 150 | 151 | # Windows image file caches 152 | Thumbs.db 153 | ehthumbs.db 154 | 155 | # Folder config file 156 | Desktop.ini 157 | 158 | # Recycle Bin used on file shares 159 | $RECYCLE.BIN/ 160 | 161 | # Mac desktop service store files 162 | .DS_Store 163 | 164 | # =================================================== 165 | # Exclude F# project specific directories and files 166 | # =================================================== 167 | 168 | # NuGet Packages Directory 169 | packages/ 170 | 171 | # Generated documentation folder 172 | docs/output/ 173 | 174 | # Temp folder used for publishing docs 175 | temp/ 176 | 177 | # Test results produced by build 178 | TestResults.xml 179 | TestResult.xml 180 | 181 | # Nuget outputs 182 | nuget/*.nupkg 183 | release.cmd 184 | release.sh 185 | localpackages/ 186 | paket-files 187 | *.orig 188 | .paket/paket.exe 189 | docs/content/license.md 190 | docs/content/release-notes.md 191 | *.bak 192 | docs/tools/FSharp.Formatting.svclog 193 | .fake 194 | -------------------------------------------------------------------------------- /.paket/paket.bootstrapper.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/Chessie/d422372020a6c054c1395434b465de07d73edd8b/.paket/paket.bootstrapper.exe -------------------------------------------------------------------------------- /.paket/paket.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | true 7 | $(MSBuildThisFileDirectory) 8 | $(MSBuildThisFileDirectory)..\ 9 | $(PaketRootPath)paket.lock 10 | $(PaketRootPath)paket-files\paket.restore.cached 11 | /Library/Frameworks/Mono.framework/Commands/mono 12 | mono 13 | 14 | 15 | 16 | 17 | $(PaketRootPath)paket.exe 18 | $(PaketToolsPath)paket.exe 19 | "$(PaketExePath)" 20 | $(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)" 21 | 22 | 23 | 24 | 25 | 26 | $(MSBuildProjectFullPath).paket.references 27 | 28 | 29 | 30 | 31 | $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references 32 | 33 | 34 | 35 | 36 | $(MSBuildProjectDirectory)\paket.references 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | $(PaketCommand) restore --references-file "$(PaketReferences)" 49 | 50 | RestorePackages; $(BuildDependsOn); 51 | 52 | 53 | 54 | true 55 | 56 | 57 | 58 | $([System.IO.File]::ReadAllText('$(PaketRestoreCacheFile)')) 59 | $([System.IO.File]::ReadAllText('$(PaketLockFilePath)')) 60 | true 61 | false 62 | true 63 | 64 | 65 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | 3 | #dotnet cli require Ubuntu 14.04 4 | sudo: required 5 | dist: trusty 6 | 7 | #dotnet cli require OSX 10.10 8 | osx_image: xcode7.1 9 | 10 | addons: 11 | apt: 12 | packages: 13 | - gettext 14 | - libcurl4-openssl-dev 15 | - libicu-dev 16 | - libssl-dev 17 | - libunwind8 18 | - zlib1g 19 | 20 | os: 21 | - osx 22 | 23 | env: 24 | - CLI_VERSION="1.0.0-beta-002071" 25 | 26 | before_install: 27 | # Download script to install dotnet cli 28 | - curl -L --create-dirs https://raw.githubusercontent.com/dotnet/cli/rel/1.0.0/scripts/obtain/install.sh -o ./scripts/obtain/install.sh 29 | - find ./scripts -name "*.sh" -exec chmod +x {} \; 30 | - export DOTNET_INSTALL_DIR="$PWD/.dotnetcli" 31 | # use bash to workaround bug https://github.com/dotnet/cli/issues/1725 32 | - sudo bash ./scripts/obtain/install.sh --channel "preview" --version "$CLI_VERSION" --install-dir "$DOTNET_INSTALL_DIR" --no-path 33 | # add dotnet to PATH 34 | - export PATH="$DOTNET_INSTALL_DIR:$PATH" 35 | # Workaround "Too many open files" 36 | - ulimit -n 1024 37 | # show dotnet info 38 | - which dotnet 39 | - dotnet --info 40 | 41 | script: 42 | - ./build.sh All 43 | -------------------------------------------------------------------------------- /Chessie.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio 2012 3 | VisualStudioVersion = 12.0.31101.0 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".paket", ".paket", "{63297B98-5CED-492C-A5B7-A5B4F73CF142}" 6 | ProjectSection(SolutionItems) = preProject 7 | paket.dependencies = paket.dependencies 8 | paket.lock = paket.lock 9 | EndProjectSection 10 | EndProject 11 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{A6A6AF7D-D6E3-442D-9B1E-58CC91879BE1}" 12 | EndProject 13 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{83F16175-43B1-4C90-A1EE-8E351C33435D}" 14 | ProjectSection(SolutionItems) = preProject 15 | docs\tools\generate.fsx = docs\tools\generate.fsx 16 | docs\tools\templates\template.cshtml = docs\tools\templates\template.cshtml 17 | EndProjectSection 18 | EndProject 19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "content", "content", "{8E6D5255-776D-4B61-85F9-73C37AA1FB9A}" 20 | ProjectSection(SolutionItems) = preProject 21 | docs\content\a-tale-of-3-nightclubs-fsharp.fsx = docs\content\a-tale-of-3-nightclubs-fsharp.fsx 22 | docs\content\a-tale-of-3-nightclubs.md = docs\content\a-tale-of-3-nightclubs.md 23 | docs\content\index.md = docs\content\index.md 24 | docs\content\railway.fsx = docs\content\railway.fsx 25 | docs\content\pass-warn-fail.fsx = docs\content\pass-warn-fail.fsx 26 | EndProjectSection 27 | EndProject 28 | Project("{f2a71f9b-5d33-465a-a702-920d77279786}") = "Chessie", "src\Chessie\Chessie.fsproj", "{5C095A65-1BF4-4C24-82D2-A07E8C258789}" 29 | EndProject 30 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "project", "project", "{BF60BC93-E09B-4E5F-9D85-95A519479D54}" 31 | ProjectSection(SolutionItems) = preProject 32 | build.fsx = build.fsx 33 | README.md = README.md 34 | RELEASE_NOTES.md = RELEASE_NOTES.md 35 | EndProjectSection 36 | EndProject 37 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{ED8079DD-2B06-4030-9F0F-DC548F98E1C4}" 38 | EndProject 39 | Project("{f2a71f9b-5d33-465a-a702-920d77279786}") = "Chessie.Tests", "tests\Chessie.Tests\Chessie.Tests.fsproj", "{5D8D2601-26EF-42C7-9703-8BDDDA015CB8}" 40 | EndProject 41 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chessie.CSharp.Test", "tests\Chessie.CSharp.Test\Chessie.CSharp.Test.csproj", "{FC460F0A-F3C6-4314-BD2C-9F4218B902BC}" 42 | EndProject 43 | Global 44 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 45 | Debug|Any CPU = Debug|Any CPU 46 | Release|Any CPU = Release|Any CPU 47 | EndGlobalSection 48 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 49 | {5C095A65-1BF4-4C24-82D2-A07E8C258789}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {5C095A65-1BF4-4C24-82D2-A07E8C258789}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {5C095A65-1BF4-4C24-82D2-A07E8C258789}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {5C095A65-1BF4-4C24-82D2-A07E8C258789}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {5D8D2601-26EF-42C7-9703-8BDDDA015CB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {5D8D2601-26EF-42C7-9703-8BDDDA015CB8}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {5D8D2601-26EF-42C7-9703-8BDDDA015CB8}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {5D8D2601-26EF-42C7-9703-8BDDDA015CB8}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {FC460F0A-F3C6-4314-BD2C-9F4218B902BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 58 | {FC460F0A-F3C6-4314-BD2C-9F4218B902BC}.Debug|Any CPU.Build.0 = Debug|Any CPU 59 | {FC460F0A-F3C6-4314-BD2C-9F4218B902BC}.Release|Any CPU.ActiveCfg = Release|Any CPU 60 | {FC460F0A-F3C6-4314-BD2C-9F4218B902BC}.Release|Any CPU.Build.0 = Release|Any CPU 61 | EndGlobalSection 62 | GlobalSection(NestedProjects) = preSolution 63 | {83F16175-43B1-4C90-A1EE-8E351C33435D} = {A6A6AF7D-D6E3-442D-9B1E-58CC91879BE1} 64 | {8E6D5255-776D-4B61-85F9-73C37AA1FB9A} = {A6A6AF7D-D6E3-442D-9B1E-58CC91879BE1} 65 | {5D8D2601-26EF-42C7-9703-8BDDDA015CB8} = {ED8079DD-2B06-4030-9F0F-DC548F98E1C4} 66 | {FC460F0A-F3C6-4314-BD2C-9F4218B902BC} = {ED8079DD-2B06-4030-9F0F-DC548F98E1C4} 67 | EndGlobalSection 68 | GlobalSection(SolutionProperties) = preSolution 69 | HideSolutionNode = FALSE 70 | EndGlobalSection 71 | EndGlobal -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [](http://issuestats.com/github/fsprojects/Chessie) 2 | [](http://issuestats.com/github/fsprojects/Chessie) 3 | [](https://ci.appveyor.com/project/SteffenForkmann/chessie) 4 | [](https://travis-ci.org/fsprojects/Chessie) 5 | 6 | # Chessie 7 | 8 | This project brings railway-oriented programming to .NET. 9 | 10 | Documentation: http://fsprojects.github.io/Chessie 11 | 12 | ## Maintainer(s) 13 | 14 | - [@forki](https://github.com/forki) 15 | - [@pblasucci](https://github.com/pblasucci) 16 | - [@mexx](https://github.com/mexx) 17 | - [@theimowski](https://github.com/theimowski) 18 | 19 | The default maintainer account for projects under "fsprojects" is [@fsprojectsgit](https://github.com/fsprojectsgit) - F# Community Project Incubation Space (repo management) 20 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | #### 0.6.0 - 21.07.2016 2 | * New version for .NETStandard 1.6 and coreclr 3 | 4 | #### 0.5.1 - 23.03.2016 5 | * Added fsharp friendly version of try called Catch - https://github.com/fsprojects/Chessie/pull/40 6 | * VB.NET support - https://github.com/fsprojects/Chessie/pull/39 7 | 8 | #### 0.4.0 - 17.01.2016 9 | * New C# helpers - https://github.com/fsprojects/Chessie/pull/34 https://github.com/fsprojects/Chessie/pull/35 https://github.com/fsprojects/Chessie/pull/36 10 | * Adding small helpers to make tutorial easier - https://github.com/fsprojects/Chessie/pull/23 11 | * BUGFIX: SelectMany implementation changed - https://github.com/fsprojects/Chessie/pull/27 12 | 13 | #### 0.2.0 - 23.03.2015 14 | * BREAKING: Fixed conflicts reported in https://github.com/fsprojects/Chessie/issues/17 15 | 16 | #### 0.1.1 - 23.03.2015 17 | * Improve C# signatures 18 | 19 | #### 0.1.0 - 19.03.2015 20 | * Aligned naming of library elements (`trail`, `TrailBuilder`, and `module Trial`) 21 | * Added Pass/Warn/Fail semantics 22 | 23 | #### 0.0.23 - 12.03.2015 24 | * Improving C# support 25 | 26 | #### 0.0.10 - 10.03.2015 27 | * Extended the builder class - https://github.com/fsprojects/Chessie/pull/11 28 | * New asyncTrial builder class - https://github.com/fsprojects/Chessie/pull/12 29 | 30 | #### 0.0.9 - 04.03.2015 31 | * Improving C# support 32 | 33 | #### 0.0.8 - 04.03.2015 34 | * Added dependency on FSharp.Core nuget package 35 | 36 | #### 0.0.7 - 28.02.2015 37 | * Added eitherTee - https://github.com/fsprojects/Chessie/pull/7 38 | 39 | #### 0.0.6 - 27.02.2015 40 | * First iteration on naming 41 | 42 | #### 0.0.2 - 27.02.2015 43 | * XML-docs added 44 | 45 | #### 0.0.1 - 26.02.2015 46 | * Extracted ROP from Paket 47 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | os: Visual Studio 2015 2 | 3 | init: 4 | - git config --global core.autocrlf input 5 | 6 | build_script: 7 | - cmd: build.cmd 8 | 9 | test: off 10 | version: 0.0.1.{build} 11 | artifacts: 12 | - path: bin 13 | name: bin 14 | -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | cls 3 | 4 | .paket\paket.bootstrapper.exe 5 | if errorlevel 1 ( 6 | exit /b %errorlevel% 7 | ) 8 | 9 | .paket\paket.exe restore 10 | if errorlevel 1 ( 11 | exit /b %errorlevel% 12 | ) 13 | 14 | IF NOT EXIST build.fsx ( 15 | .paket\paket.exe update 16 | packages\build\FAKE\tools\FAKE.exe init.fsx 17 | ) 18 | packages\build\FAKE\tools\FAKE.exe build.fsx %* 19 | -------------------------------------------------------------------------------- /build.fsx: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------- 2 | // FAKE build script 3 | // -------------------------------------------------------------------------------------- 4 | 5 | #r @"packages/build/FAKE/tools/FakeLib.dll" 6 | #r @"packages/build/FAKE/tools/Newtonsoft.Json.dll" 7 | 8 | open Fake 9 | open Fake.Git 10 | open Fake.AssemblyInfoFile 11 | open Fake.ReleaseNotesHelper 12 | open System 13 | open System.IO 14 | #if MONO 15 | #else 16 | #load "packages/build/SourceLink.Fake/tools/Fake.fsx" 17 | open SourceLink 18 | #endif 19 | 20 | // -------------------------------------------------------------------------------------- 21 | // START TODO: Provide project-specific details below 22 | // -------------------------------------------------------------------------------------- 23 | 24 | // Information about the project are used 25 | // - for version and project name in generated AssemblyInfo file 26 | // - by the generated NuGet package 27 | // - to run tests and to publish documentation on GitHub gh-pages 28 | // - for documentation, you also need to edit info in "docs/tools/generate.fsx" 29 | 30 | // The name of the project 31 | // (used by attributes in AssemblyInfo, name of a NuGet package and directory in 'src') 32 | let project = "Chessie" 33 | 34 | // Short summary of the project 35 | // (used as description in AssemblyInfo and as a short summary for NuGet package) 36 | let summary = "Railway-oriented programming for .NET" 37 | 38 | // Longer description of the project 39 | // (used as a description for NuGet package; line breaks are automatically cleaned up) 40 | let description = "Railway-oriented programming for .NET" 41 | 42 | // List of author names (for NuGet package) 43 | let authors = [ "Steffen Forkmann"; "Max Malook"; "Tomasz Heimowski" ] 44 | 45 | // Tags for your project (for NuGet package) 46 | let tags = "rop, fsharp, F#" 47 | 48 | // File system information 49 | let solutionFile = "Chessie.sln" 50 | 51 | // Pattern specifying assemblies to be tested using NUnit 52 | let testAssemblies = "tests/**/bin/Release/*Tests*.dll" 53 | 54 | // Git configuration (used for publishing documentation in gh-pages branch) 55 | // The profile where the project is posted 56 | let gitOwner = "fsprojects" 57 | let gitHome = "https://github.com/" + gitOwner 58 | 59 | // The name of the project on GitHub 60 | let gitName = "Chessie" 61 | 62 | // The url for the raw files hosted 63 | let gitRaw = environVarOrDefault "gitRaw" "https://raw.github.com/fsprojects" 64 | 65 | // -------------------------------------------------------------------------------------- 66 | // END TODO: The rest of the file includes standard build steps 67 | // -------------------------------------------------------------------------------------- 68 | 69 | // Read additional information from the release notes document 70 | let release = LoadReleaseNotes "RELEASE_NOTES.md" 71 | 72 | let genFSAssemblyInfo (projectPath) = 73 | let projectName = System.IO.Path.GetFileNameWithoutExtension(projectPath) 74 | let folderName = System.IO.Path.GetDirectoryName(projectPath) 75 | let basePath = "src" @@ folderName 76 | let fileName = basePath @@ "AssemblyInfo.fs" 77 | CreateFSharpAssemblyInfo fileName 78 | [ Attribute.Title (projectName) 79 | Attribute.Product project 80 | Attribute.Description summary 81 | Attribute.Version release.AssemblyVersion 82 | Attribute.FileVersion release.AssemblyVersion 83 | Fake.AssemblyInfoFile.Attribute("Extension", "", "System.Runtime.CompilerServices") ] 84 | 85 | let genCSAssemblyInfo (projectPath) = 86 | let projectName = System.IO.Path.GetFileNameWithoutExtension(projectPath) 87 | let folderName = System.IO.Path.GetDirectoryName(projectPath) 88 | let basePath = folderName @@ "Properties" 89 | let fileName = basePath @@ "AssemblyInfo.cs" 90 | CreateCSharpAssemblyInfo fileName 91 | [ Attribute.Title (projectName) 92 | Attribute.Product project 93 | Attribute.Description summary 94 | Attribute.Version release.AssemblyVersion 95 | Attribute.FileVersion release.AssemblyVersion ] 96 | 97 | // Generate assembly info files with the right version & up-to-date information 98 | Target "AssemblyInfo" (fun _ -> 99 | let fsProjs = !! "src/**/*.fsproj" 100 | let csProjs = !! "src/**/*.csproj" 101 | fsProjs |> Seq.iter genFSAssemblyInfo 102 | csProjs |> Seq.iter genCSAssemblyInfo 103 | ) 104 | 105 | // -------------------------------------------------------------------------------------- 106 | // Clean build results 107 | 108 | Target "Clean" (fun _ -> 109 | CleanDirs ["bin"; "temp"] 110 | !! "**/project.lock.json" |> DeleteFiles 111 | ) 112 | 113 | Target "CleanDocs" (fun _ -> 114 | CleanDirs ["docs/output"] 115 | ) 116 | 117 | // -------------------------------------------------------------------------------------- 118 | // Build library & test project 119 | 120 | Target "Build" (fun _ -> 121 | !! solutionFile 122 | |> MSBuildRelease "" "Rebuild" 123 | |> ignore 124 | ) 125 | 126 | // -------------------------------------------------------------------------------------- 127 | // Run the unit tests using test runner 128 | 129 | Target "RunTests" (fun _ -> 130 | !! testAssemblies 131 | |> NUnit (fun p -> 132 | { p with 133 | DisableShadowCopy = true 134 | TimeOut = TimeSpan.FromMinutes 20. 135 | OutputFile = "TestResults.xml" }) 136 | ) 137 | 138 | #if MONO 139 | #else 140 | // -------------------------------------------------------------------------------------- 141 | // SourceLink allows Source Indexing on the PDB generated by the compiler, this allows 142 | // the ability to step through the source code of external libraries https://github.com/ctaggart/SourceLink 143 | 144 | Target "SourceLink" (fun _ -> 145 | let baseUrl = sprintf "%s/%s/{0}/%%var2%%" gitRaw (project.ToLower()) 146 | use repo = new GitRepo(__SOURCE_DIRECTORY__) 147 | //F# projects 148 | !! "src/**/*.fsproj" 149 | |> Seq.iter (fun f -> 150 | let proj = VsProj.LoadRelease f 151 | logfn "source linking %s" proj.OutputFilePdb 152 | let files = proj.Compiles -- "**/AssemblyInfo.fs" 153 | repo.VerifyChecksums files 154 | proj.VerifyPdbChecksums files 155 | proj.CreateSrcSrv baseUrl repo.Commit (repo.Paths files) 156 | Pdbstr.exec proj.OutputFilePdb proj.OutputFilePdbSrcSrv 157 | ) 158 | //C# projects 159 | !! "src/**/*.csproj" 160 | |> Seq.iter (fun f -> 161 | let proj = VsProj.LoadRelease f 162 | logfn "source linking %s" proj.OutputFilePdb 163 | let files = proj.Compiles -- "**/AssemblyInfo.cs" 164 | repo.VerifyChecksums files 165 | proj.VerifyPdbChecksums files 166 | proj.CreateSrcSrv baseUrl repo.Commit (repo.Paths files) 167 | Pdbstr.exec proj.OutputFilePdb proj.OutputFilePdbSrcSrv 168 | ) 169 | ) 170 | 171 | #endif 172 | 173 | // -------------------------------------------------------------------------------------- 174 | // Build a NuGet package 175 | 176 | Target "NuGet" (fun _ -> 177 | Paket.Pack(fun p -> 178 | { p with 179 | OutputPath = "./temp" 180 | Version = release.NugetVersion 181 | ReleaseNotes = toLines release.Notes}) 182 | ) 183 | 184 | Target "PublishNuget" (fun _ -> 185 | Paket.Push(fun p -> 186 | { p with 187 | WorkingDir = "./temp" }) 188 | ) 189 | 190 | 191 | // -------------------------------------------------------------------------------------- 192 | // Generate the documentation 193 | 194 | Target "GenerateReferenceDocs" (fun _ -> 195 | if not <| executeFSIWithArgs "docs/tools" "generate.fsx" ["--define:RELEASE"; "--define:REFERENCE"] [] then 196 | failwith "generating reference documentation failed" 197 | ) 198 | 199 | let generateHelp' fail debug = 200 | let args = 201 | if debug then ["--define:HELP"] 202 | else ["--define:RELEASE"; "--define:HELP"] 203 | if executeFSIWithArgs "docs/tools" "generate.fsx" args [] then 204 | traceImportant "Help generated" 205 | else 206 | if fail then 207 | failwith "generating help documentation failed" 208 | else 209 | traceImportant "generating help documentation failed" 210 | 211 | let generateHelp fail = 212 | generateHelp' fail false 213 | 214 | Target "GenerateHelp" (fun _ -> 215 | DeleteFile "docs/content/release-notes.md" 216 | CopyFile "docs/content/" "RELEASE_NOTES.md" 217 | Rename "docs/content/release-notes.md" "docs/content/RELEASE_NOTES.md" 218 | 219 | DeleteFile "docs/content/license.md" 220 | CopyFile "docs/content/" "LICENSE.txt" 221 | Rename "docs/content/license.md" "docs/content/LICENSE.txt" 222 | 223 | generateHelp true 224 | ) 225 | 226 | Target "GenerateHelpDebug" (fun _ -> 227 | DeleteFile "docs/content/release-notes.md" 228 | CopyFile "docs/content/" "RELEASE_NOTES.md" 229 | Rename "docs/content/release-notes.md" "docs/content/RELEASE_NOTES.md" 230 | 231 | DeleteFile "docs/content/license.md" 232 | CopyFile "docs/content/" "LICENSE.txt" 233 | Rename "docs/content/license.md" "docs/content/LICENSE.txt" 234 | 235 | generateHelp' true true 236 | ) 237 | 238 | Target "KeepRunning" (fun _ -> 239 | use watcher = new FileSystemWatcher(DirectoryInfo("docs/content").FullName,"*.*") 240 | watcher.EnableRaisingEvents <- true 241 | watcher.Changed.Add(fun e -> generateHelp false) 242 | watcher.Created.Add(fun e -> generateHelp false) 243 | watcher.Renamed.Add(fun e -> generateHelp false) 244 | watcher.Deleted.Add(fun e -> generateHelp false) 245 | 246 | traceImportant "Waiting for help edits. Press any key to stop." 247 | 248 | System.Console.ReadKey() |> ignore 249 | 250 | watcher.EnableRaisingEvents <- false 251 | watcher.Dispose() 252 | ) 253 | 254 | Target "GenerateDocs" DoNothing 255 | 256 | let createIndexFsx lang = 257 | let content = """(*** hide ***) 258 | // This block of code is omitted in the generated HTML documentation. Use 259 | // it to define helpers that you do not want to show in the documentation. 260 | #I "../../../bin" 261 | 262 | (** 263 | F# Project Scaffold ({0}) 264 | ========================= 265 | *) 266 | """ 267 | let targetDir = "docs/content" @@ lang 268 | let targetFile = targetDir @@ "index.fsx" 269 | ensureDirectory targetDir 270 | System.IO.File.WriteAllText(targetFile, System.String.Format(content, lang)) 271 | 272 | Target "AddLangDocs" (fun _ -> 273 | let args = System.Environment.GetCommandLineArgs() 274 | if args.Length < 4 then 275 | failwith "Language not specified." 276 | 277 | args.[3..] 278 | |> Seq.iter (fun lang -> 279 | if lang.Length <> 2 && lang.Length <> 3 then 280 | failwithf "Language must be 2 or 3 characters (ex. 'de', 'fr', 'ja', 'gsw', etc.): %s" lang 281 | 282 | let templateFileName = "template.cshtml" 283 | let templateDir = "docs/tools/templates" 284 | let langTemplateDir = templateDir @@ lang 285 | let langTemplateFileName = langTemplateDir @@ templateFileName 286 | 287 | if System.IO.File.Exists(langTemplateFileName) then 288 | failwithf "Documents for specified language '%s' have already been added." lang 289 | 290 | ensureDirectory langTemplateDir 291 | Copy langTemplateDir [ templateDir @@ templateFileName ] 292 | 293 | createIndexFsx lang) 294 | ) 295 | 296 | // -------------------------------------------------------------------------------------- 297 | // Release Scripts 298 | 299 | Target "ReleaseDocs" (fun _ -> 300 | let tempDocsDir = "temp/gh-pages" 301 | CleanDir tempDocsDir 302 | Repository.cloneSingleBranch "" (gitHome + "/" + gitName + ".git") "gh-pages" tempDocsDir 303 | 304 | CopyRecursive "docs/output" tempDocsDir true |> tracefn "%A" 305 | StageAll tempDocsDir 306 | Git.Commit.Commit tempDocsDir (sprintf "Update generated documentation for version %s" release.NugetVersion) 307 | Branches.push tempDocsDir 308 | ) 309 | 310 | #load "paket-files/build/fsharp/FAKE/modules/Octokit/Octokit.fsx" 311 | open Octokit 312 | 313 | Target "Release" (fun _ -> 314 | StageAll "" 315 | Git.Commit.Commit "" (sprintf "Bump version to %s" release.NugetVersion) 316 | Branches.push "" 317 | 318 | Branches.tag "" release.NugetVersion 319 | Branches.pushTag "" "origin" release.NugetVersion 320 | 321 | // release on github 322 | createClient (getBuildParamOrDefault "github-user" "") (getBuildParamOrDefault "github-pw" "") 323 | |> createDraft gitOwner gitName release.NugetVersion (release.SemVer.PreRelease <> None) release.Notes 324 | 325 | |> releaseDraft 326 | |> Async.RunSynchronously 327 | ) 328 | 329 | Target "BuildPackage" DoNothing 330 | 331 | 332 | // -------------------------------------------------------------------------------------- 333 | // .NET Core SDK and .NET Core 334 | 335 | let assertExitCodeZero x = if x = 0 then () else failwithf "Command failed with exit code %i" x 336 | 337 | Target "SetVersionInProjectJSON" (fun _ -> 338 | !! "./**/project.json" 339 | |> Seq.iter (DotNet.SetVersionInProjectJson release.NugetVersion) 340 | ) 341 | 342 | Target "Build.NetCore" (fun _ -> 343 | DotNet.Restore id 344 | 345 | !! "src/**/project.json" 346 | |> DotNet.Build id 347 | ) 348 | 349 | Target "RunTests.NetCore" (fun _ -> 350 | !! "tests/**/project.json" 351 | |> DotNet.Test id 352 | ) 353 | 354 | let isDotnetSDKInstalled = DotNet.isInstalled() 355 | 356 | Target "Nuget.AddNetCore" (fun _ -> 357 | !! "src/**/project.json" 358 | |> DotNet.Pack id 359 | 360 | let nupkg = sprintf "../../temp/Chessie.%s.nupkg" (release.NugetVersion) 361 | let netcoreNupkg = sprintf "bin/Release/Chessie.%s.nupkg" (release.NugetVersion) 362 | 363 | Shell.Exec("dotnet", sprintf """mergenupkg --source "%s" --other "%s" --framework netstandard1.6 """ nupkg netcoreNupkg, "src/Chessie/") |> assertExitCodeZero 364 | ) 365 | 366 | // -------------------------------------------------------------------------------------- 367 | // Run all targets by default. Invoke 'build ' to override 368 | 369 | Target "All" DoNothing 370 | 371 | "Clean" 372 | ==> "AssemblyInfo" 373 | ==> "SetVersionInProjectJSON" 374 | ==> "Build" 375 | =?> ("Build.NetCore", isDotnetSDKInstalled) 376 | ==> "RunTests" 377 | =?> ("RunTests.NetCore", isDotnetSDKInstalled) 378 | =?> ("GenerateReferenceDocs",isLocalBuild) 379 | =?> ("GenerateDocs",isLocalBuild) 380 | ==> "All" 381 | =?> ("ReleaseDocs",isLocalBuild) 382 | 383 | "All" 384 | #if MONO 385 | #else 386 | =?> ("SourceLink", Pdbstr.tryFind().IsSome ) 387 | #endif 388 | ==> "NuGet" 389 | =?> ("Nuget.AddNetCore", isDotnetSDKInstalled) 390 | ==> "BuildPackage" 391 | 392 | "CleanDocs" 393 | ==> "GenerateHelp" 394 | ==> "GenerateReferenceDocs" 395 | ==> "GenerateDocs" 396 | 397 | "CleanDocs" 398 | ==> "GenerateHelpDebug" 399 | 400 | "GenerateHelp" 401 | ==> "KeepRunning" 402 | 403 | "ReleaseDocs" 404 | ==> "Release" 405 | 406 | "BuildPackage" 407 | ==> "PublishNuget" 408 | ==> "Release" 409 | 410 | RunTargetOrDefault "All" 411 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if test "$OS" = "Windows_NT" 3 | then 4 | # use .Net 5 | 6 | .paket/paket.bootstrapper.exe 7 | exit_code=$? 8 | if [ $exit_code -ne 0 ]; then 9 | exit $exit_code 10 | fi 11 | 12 | .paket/paket.exe restore 13 | exit_code=$? 14 | if [ $exit_code -ne 0 ]; then 15 | exit $exit_code 16 | fi 17 | 18 | [ ! -e build.fsx ] && .paket/paket.exe update 19 | [ ! -e build.fsx ] && packages/build/FAKE/tools/FAKE.exe init.fsx 20 | packages/build/FAKE/tools/FAKE.exe $@ --fsiargs -d:MONO build.fsx 21 | else 22 | # use mono 23 | mono .paket/paket.bootstrapper.exe 24 | exit_code=$? 25 | if [ $exit_code -ne 0 ]; then 26 | exit $exit_code 27 | fi 28 | 29 | mono .paket/paket.exe restore 30 | exit_code=$? 31 | if [ $exit_code -ne 0 ]; then 32 | exit $exit_code 33 | fi 34 | 35 | [ ! -e build.fsx ] && mono .paket/paket.exe update 36 | [ ! -e build.fsx ] && mono packages/build/FAKE/tools/FAKE.exe init.fsx 37 | mono packages/build/FAKE/tools/FAKE.exe $@ --fsiargs -d:MONO build.fsx 38 | fi 39 | -------------------------------------------------------------------------------- /docs/content/a-tale-of-3-nightclubs-fsharp.fsx: -------------------------------------------------------------------------------- 1 | (*** hide ***) 2 | #I "../../bin" 3 | 4 | (** 5 | 6 | # A Tale of 3 Nightclubs 7 | 8 | This F# tutorial is based on a [Scalaz tutorial](https://gist.github.com/oxbowlakes/970717) by Chris Marshall and was originally ported to [fsharpx](https://github.com/fsprojects/fsharpx/blob/master/tests/FSharpx.CSharpTests/ValidationExample.cs) by Mauricio Scheffer. 9 | 10 | Additional resources: 11 | 12 | * Railway Oriented Programming by Scott Wlaschin - A functional approach to error handling 13 | * [Blog post](http://fsharpforfunandprofit.com/posts/recipe-part2/) 14 | * [Slide deck](http://www.slideshare.net/ScottWlaschin/railway-oriented-programming) 15 | * [Video](https://vimeo.com/97344498) 16 | 17 | ## Part Zero : 10:15 Saturday Night 18 | 19 | We start by referencing Chessie and opening the ErrorHandling module and define a simple domain for nightclubs: 20 | 21 | *) 22 | 23 | #r "Chessie.dll" 24 | 25 | open Chessie.ErrorHandling 26 | 27 | type Sobriety = 28 | | Sober 29 | | Tipsy 30 | | Drunk 31 | | Paralytic 32 | | Unconscious 33 | 34 | type Gender = 35 | | Male 36 | | Female 37 | 38 | type Person = 39 | { Gender : Gender 40 | Age : int 41 | Clothes : string Set 42 | Sobriety : Sobriety } 43 | 44 | (** 45 | Now we define some validation methods that all nightclubs will perform: 46 | *) 47 | 48 | module Club = 49 | let checkAge (p : Person) = 50 | if p.Age < 18 then fail "Too young!" 51 | elif p.Age > 40 then fail "Too old!" 52 | else ok p 53 | 54 | let checkClothes (p : Person) = 55 | if p.Gender = Male && not (p.Clothes.Contains "Tie") then fail "Smarten up!" 56 | elif p.Gender = Female && p.Clothes.Contains "Trainers" then fail "Wear high heels" 57 | else ok p 58 | 59 | let checkSobriety (p : Person) = 60 | match p.Sobriety with 61 | | Drunk | Paralytic | Unconscious -> fail "Sober up!" 62 | | _ -> ok p 63 | 64 | (** 65 | ## Part One : Clubbed to Death 66 | 67 | Now let's compose some validation checks via syntactic sugar: 68 | *) 69 | 70 | module ClubbedToDeath = 71 | open Club 72 | 73 | let costToEnter p = 74 | trial { 75 | let! a = checkAge p 76 | let! b = checkClothes a 77 | let! c = checkSobriety b 78 | return 79 | match c.Gender with 80 | | Female -> 0m 81 | | Male -> 5m 82 | } 83 | 84 | (** 85 | Let's see how the validation works in action: 86 | *) 87 | 88 | let Ken = { Person.Gender = Male; Age = 28; Clothes = set ["Tie"; "Shirt"]; Sobriety = Tipsy } 89 | let Dave = { Person.Gender = Male; Age = 41; Clothes = set ["Tie"; "Jeans"]; Sobriety = Sober } 90 | let Ruby = { Person.Gender = Female; Age = 25; Clothes = set ["High heels"]; Sobriety = Tipsy } 91 | 92 | ClubbedToDeath.costToEnter Dave 93 | // [fsi:val it : Chessie.ErrorHandling.Result = Bad ["Too old!"]] 94 | 95 | ClubbedToDeath.costToEnter Ken 96 | // [fsi:val it : Result = Ok (5M,[])] 97 | 98 | ClubbedToDeath.costToEnter Ruby 99 | // [fsi:val it : Result = Ok (0M,[])] 100 | 101 | ClubbedToDeath.costToEnter { Ruby with Age = 17 } 102 | // [fsi:val it : Chessie.ErrorHandling.Result = Bad ["Too young!"]] 103 | 104 | (** 105 | The thing to note here is how the Validations can be composed together in a computation expression. 106 | The type system is making sure that failures flow through your computation in a safe manner. 107 | 108 | ## Part Two : Club Tropicana 109 | 110 | Part One showed monadic composition, which from the perspective of Validation is *fail-fast*. That is, any failed check short-circuits subsequent checks. This nicely models nightclubs in the real world, as anyone who has dashed home for a pair of smart shoes and returned, only to be told that your tie does not pass muster, will attest. 111 | 112 | But what about an ideal nightclub? One that tells you *everything* that is wrong with you. 113 | 114 | Applicative functors to the rescue! 115 | 116 | Let's compose some validation checks that accumulate failures : 117 | *) 118 | 119 | module ClubTropicana = 120 | open Club 121 | 122 | let costToEnter p = 123 | trial { 124 | let a = checkAge p 125 | let b = checkClothes p 126 | let c = checkSobriety p 127 | 128 | let! result::_ = [a;b;c] |> collect 129 | 130 | return 131 | match result.Gender with 132 | | Female -> 0m 133 | | Male -> 7.5m 134 | } 135 | 136 | (** 137 | The usage is the same as above except that as a result we will get either a success or a list of accumulated error messages from all the checks. 138 | 139 | Dave tried the second nightclub after a few more drinks in the pub: 140 | *) 141 | 142 | let daveParalytic = { Person.Gender = Male; Age = 41; Clothes = set ["Tie"; "Shirt"]; Sobriety = Paralytic } 143 | 144 | ClubTropicana.costToEnter daveParalytic 145 | // val it : Result = Error: Too old! 146 | // Sober up! 147 | 148 | (** 149 | So, what have we done? Well, with a *tiny change* (and no changes to the individual checks themselves), we have completely changed the behaviour to accumulate all errors, rather than halting at the first sign of trouble. Imagine trying to do this using exceptions, with ten checks. 150 | 151 | ## Part Three : Gay bar 152 | 153 | And for those wondering how to do this with a *very long list* of checks here is a solution: 154 | *) 155 | 156 | module GayBar = 157 | open Club 158 | 159 | let checkGender (p : Person) = 160 | if p.Gender = Male then ok p 161 | else fail "Men Only" 162 | 163 | let costToEnter p = 164 | trial { 165 | let! result::_ = 166 | [checkGender; checkAge; checkClothes; checkSobriety] 167 | |> List.map(fun f -> f p) 168 | |> collect 169 | return 170 | match result.Gender with 171 | | Female -> 0m 172 | | Male -> 7.5m 173 | } 174 | 175 | (** 176 | The usage is the same as above: 177 | *) 178 | 179 | let person = { Person.Gender = Male; Age = 59; Clothes = set ["Jeans"]; Sobriety = Paralytic } 180 | 181 | GayBar.costToEnter person 182 | // val it : Result = Error: Too old! 183 | // Smarten up! 184 | // Sober up! -------------------------------------------------------------------------------- /docs/content/a-tale-of-3-nightclubs.md: -------------------------------------------------------------------------------- 1 | # A Tale of 3 Nightclubs 2 | 3 | This C# tutorial is based on a [Scalaz tutorial](https://gist.github.com/oxbowlakes/970717) by Chris Marshall and was originally ported to [fsharpx](https://github.com/fsprojects/fsharpx/blob/master/tests/FSharpx.CSharpTests/ValidationExample.cs) by Mauricio Scheffer. 4 | 5 | Additional resources: 6 | 7 | * Railway Oriented Programming by Scott Wlaschin - A functional approach to error handling 8 | * [Blog post](http://fsharpforfunandprofit.com/posts/recipe-part2/) 9 | * [Slide deck](http://www.slideshare.net/ScottWlaschin/railway-oriented-programming) 10 | * [Video](https://vimeo.com/97344498) 11 | 12 | ## Part Zero : 10:15 Saturday Night 13 | 14 | We start by referencing Chessie and opening the ErrorHandling module and define a simple domain for nightclubs: 15 | 16 | [lang=csharp] 17 | using Chessie.ErrorHandling; 18 | using Chessie.ErrorHandling.CSharp; 19 | 20 | 21 | enum Sobriety { Sober, Tipsy, Drunk, Paralytic, Unconscious } 22 | enum Gender { Male, Female } 23 | 24 | class Person 25 | { 26 | public Gender Gender { get; private set; } 27 | public int Age { get; private set; } 28 | public List Clothes { get; private set; } 29 | public Sobriety Sobriety { get; private set; } 30 | 31 | public Person(Gender gender, int age, List clothes, Sobriety sobriety) 32 | { 33 | this.Gender = gender; 34 | this.Age = age; 35 | this.Clothes = clothes; 36 | this.Sobriety = sobriety; 37 | } 38 | } 39 | 40 | Now we define some validation methods that all nightclubs will perform: 41 | 42 | [lang=csharp] 43 | class Club 44 | { 45 | public static Result CheckAge(Person p) 46 | { 47 | if (p.Age < 18) 48 | return Result.FailWith("Too young!"); 49 | if (p.Age > 40) 50 | return Result.FailWith("Too old!"); 51 | return Result.Succeed(p); 52 | } 53 | 54 | public static Result CheckClothes(Person p) 55 | { 56 | if (p.Gender == Gender.Male && !p.Clothes.Contains("Tie")) 57 | return Result.FailWith("Smarten up!"); 58 | if (p.Gender == Gender.Female && p.Clothes.Contains("Trainers")) 59 | return Result.FailWith("Wear high heels!"); 60 | return Result.Succeed(p); 61 | } 62 | 63 | public static Result CheckSobriety(Person p) 64 | { 65 | if (new[] { Sobriety.Drunk, Sobriety.Paralytic, Sobriety.Unconscious }.Contains(p.Sobriety)) 66 | return Result.FailWith("Sober up!"); 67 | return Result.Succeed(p); 68 | } 69 | } 70 | 71 | ## Part One : Clubbed to Death 72 | 73 | Now let's compose some validation checks via syntactic sugar and LINQ: 74 | 75 | [lang=csharp] 76 | class ClubbedToDeath 77 | { 78 | public static Result CostToEnter(Person p) 79 | { 80 | return from a in Club.CheckAge(p) 81 | from b in Club.CheckClothes(a) 82 | from c in Club.CheckSobriety(b) 83 | select c.Gender == Gender.Female ? 0m : 5m; 84 | } 85 | } 86 | 87 | Let's see how the validation works in action: 88 | 89 | [lang=csharp] 90 | var Dave = new Person(Gender.Male, 41, new List { "Tie", "Jeans" }, Sobriety.Sober); 91 | var costDave = ClubbedToDeath.CostToEnter(Dave); // Error "Too old!" 92 | 93 | var Ken = new Person(Gender.Male, 28, new List { "Tie", "Shirt" }, Sobriety.Tipsy); 94 | var costKen = ClubbedToDeath.CostToEnter(Ken); // Success 5 95 | 96 | 97 | We can even use pattern matching on the result: 98 | 99 | [lang=csharp] 100 | var Ruby = new Person(Gender.Female, 25, new List { "High heels" }, Sobriety.Tipsy); 101 | var costRuby = ClubbedToDeath.CostToEnter(Ruby); 102 | costRuby.Match( 103 | (x, msgs) => { 104 | Console.WriteLine("Cost for Ruby: {0}", x); 105 | }, 106 | msgs => { 107 | Console.WriteLine("Ruby is not allowed to enter: {0}", msgs); 108 | }); 109 | 110 | The thing to note here is how the Validations can be composed together in a computation expression. 111 | The type system is making sure that failures flow through your computation in a safe manner. 112 | 113 | ## Part Two : Club Tropicana 114 | 115 | Part One showed monadic composition, which from the perspective of Validation is *fail-fast*. That is, any failed check short-circuits subsequent checks. This nicely models nightclubs in the real world, as anyone who has dashed home for a pair of smart shoes and returned, only to be told that your tie does not pass muster, will attest. 116 | 117 | But what about an ideal nightclub? One that tells you *everything* that is wrong with you. 118 | 119 | Applicative functors to the rescue! 120 | 121 | Let's compose some validation checks that accumulate failures using LINQ sugar: 122 | 123 | [lang=csharp] 124 | class ClubTropicana 125 | { 126 | public static Result CostToEnter(Person p) 127 | { 128 | return from c in Club.CheckAge(p) 129 | join x in Club.CheckClothes(p) on 1 equals 1 130 | join y in Club.CheckSobriety(p) on 1 equals 1 131 | select c.Gender == Gender.Female ? 0m : 7.5m; 132 | } 133 | } 134 | 135 | The usage is the same as above except that as a result we will get either a success or a list of accumulated error messages from all the checks. 136 | 137 | Dave tried the second nightclub after a few more drinks in the pub: 138 | 139 | [lang=csharp] 140 | var daveParalytic = new Person( 141 | age: 41, 142 | clothes: new List { "Tie", "Shirt" }, 143 | gender: Gender.Male, 144 | sobriety: Sobriety.Paralytic); 145 | 146 | var costDaveParalytic = ClubTropicana.CostToEnter(daveParalytic); 147 | 148 | costDaveParalytic.Match( 149 | ifSuccess: (x, msgs) => Console.WriteLine("Cost for Dave: {0}", x), 150 | ifFailure: errs => Console.WriteLine("Dave is not allowed to enter:\n{0}", String.Join("\n", errs))); 151 | 152 | This is the result: 153 | 154 | Dave is not allowed to enter: 155 | Too old! 156 | Sober up! 157 | 158 | So, what have we done? Well, with a *tiny change* (and no changes to the individual checks themselves), we have completely changed the behaviour to accumulate all errors, rather than halting at the first sign of trouble. Imagine trying to do this using exceptions, with ten checks. 159 | 160 | ## Part Three : Gay bar 161 | 162 | And for those wondering how to do this with a *very long list* of checks here is a solution: 163 | 164 | [lang=csharp] 165 | class GayBar 166 | { 167 | public static Result CheckGender (Person p) 168 | { 169 | if (p.Gender == Gender.Male) 170 | return Result.Succeed(p); 171 | return Result.FailWith("Men only"); 172 | } 173 | 174 | public static Result CostToEnter(Person p) 175 | { 176 | return new List>> { CheckGender, Club.CheckAge, Club.CheckClothes, Club.CheckSobriety } 177 | .Select(check => check(p)) 178 | .Collect() 179 | .Select(x => x[0].Age + 1.5m); 180 | } 181 | } 182 | 183 | 184 | The usage is the same as above: 185 | 186 | [lang=csharp] 187 | var person = new Person( 188 | gender: Gender.Male, 189 | age: 59, 190 | clothes: new List { "Jeans" }, 191 | sobriety: Sobriety.Paralytic); 192 | var cost = GayBar.CostToEnter(person); 193 | cost.Match( 194 | ifSuccess: (x, msgs) => Console.WriteLine("Cost for person: {0}", x), 195 | ifFailure: errs => Console.WriteLine("Person is not allowed to enter:\n{0}", String.Join("\n", errs))); 196 | -------------------------------------------------------------------------------- /docs/content/index.md: -------------------------------------------------------------------------------- 1 | Chessie 2 | ======= 3 | 4 | This project brings railway-oriented programming to .NET. 5 | 6 | 7 | 8 | 9 | 10 | The Chessie library can be installed from NuGet: 11 | PM> Install-Package Chessie 12 | 13 | 14 | 15 | 16 | 17 | Using Chessie with Paket 18 | ------------------------ 19 | 20 | Chessie is a single-file module, so it's convienient to get it with [Paket GitHub dependencies][deps]. 21 | To do so, just add following line to your `paket.dependencies` file: 22 | 23 | github fsprojects/Chessie src/Chessie/ErrorHandling.fs 24 | 25 | and following line to your `paket.references` file for the desired project: 26 | 27 | File:ErrorHandling.fs . 28 | 29 | 30 | Samples & documentation 31 | ----------------------- 32 | 33 | * Read the [tutorial](railway.html) to see how to use Chessie for railway-oriented programming. 34 | * [API Reference](reference/index.html) contains automatically generated documentation for all types, modules and functions in the library. 35 | This includes additional brief samples on using most of the functions. 36 | 37 | Contributing and copyright 38 | -------------------------- 39 | 40 | The project is hosted on [GitHub][gh] where you can [report issues][issues], fork 41 | the project and submit pull requests. If you're adding a new public API, please also 42 | consider adding [samples][content] that can be turned into a documentation. You might 43 | also want to read the [library design notes][readme] to understand how it works. 44 | 45 | The library is available under Apache 2.0 license, which allows modification and 46 | redistribution for both commercial and non-commercial purposes. For more information see the 47 | [License file][license] in the GitHub repository. 48 | 49 | [content]: https://github.com/fsprojects/Chessie/tree/master/docs/content 50 | [gh]: https://github.com/fsprojects/Chessie 51 | [issues]: https://github.com/fsprojects/Chessie/issues 52 | [readme]: https://github.com/fsprojects/Chessie/blob/master/README.md 53 | [license]: https://github.com/fsprojects/Chessie/blob/master/LICENSE.txt 54 | [deps]: https://fsprojects.github.io/Paket/github-dependencies.html 55 | -------------------------------------------------------------------------------- /docs/content/pass-warn-fail.fsx: -------------------------------------------------------------------------------- 1 | (*** hide ***) 2 | #I "../../bin" 3 | 4 | #r "Chessie.dll" 5 | open Chessie.ErrorHandling 6 | 7 | open System 8 | open System.Drawing 9 | 10 | (** 11 | Combining terminal and non-terminal errors 12 | === 13 | 14 | One feature of programming with "two-track" data is that once a value is on the _failure track_ 15 | operations against it may be skipped if they expect a value on the _passing track_. However, this 16 | is not always the desired behavior. In many cases, we will want to perform mulitple operations on a 17 | value and record any unusual or interesting results, but still keep thing on the _passing track_. 18 | One obvious scenario is when we want to validate multiple pieces of input (such as might be 19 | received from an HTTP request). 20 | *) 21 | 22 | type Applicant = 23 | { FullName :string * string 24 | DateOfBirth :DateTime 25 | FavoriteColor :Color option } 26 | 27 | (** 28 | Given some aggregate user-supplied data, we want to check each datum. 29 | *) 30 | 31 | let checkName getName msg request = 32 | let name = getName request 33 | if String.IsNullOrWhiteSpace name 34 | then // note the invalid datum, but stay on the _passing_ track 35 | request |> warn msg 36 | else // no issues, proceed on the "happy path" 37 | request |> pass 38 | 39 | let checkFirstName = checkName (fun {FullName = (name,_)} -> name) "First name is missing" 40 | let checkLastName = checkName (fun {FullName = (_,name)} -> name) "Last name is missing" 41 | 42 | let checkAge request = 43 | let dob = request.DateOfBirth 44 | let diff = DateTime.Today.Subtract dob 45 | if diff < TimeSpan.FromDays (18.0 * 365.0) 46 | then // note the invalid datum, but stay on the _passing_ track 47 | request |> warn "DateOfBirth is too recent" 48 | else // no issues, proceed on the "happy path" 49 | request |> pass 50 | 51 | (** 52 | Now we can combine our individual checks, returning the original value along-side any issues. 53 | *) 54 | 55 | (*** define-output: warnings ***) 56 | let checkApplicant request = 57 | request 58 | |> checkAge 59 | |> bind checkFirstName 60 | |> bind checkLastName 61 | 62 | let processRequest request = 63 | match checkApplicant request with 64 | | Pass _ -> printfn "All Good!!!" 65 | | Warn (_,log) -> printfn "Got some issues:" 66 | for msg in log do printfn " %s" msg 67 | | _ -> printfn "Something went horribly wrong." 68 | 69 | 70 | // good request 71 | processRequest {FullName = "John","Smith" 72 | DateOfBirth = DateTime (1995,12,21) 73 | FavoriteColor = None} 74 | // bad request 75 | processRequest {FullName = "Beck","" 76 | DateOfBirth = DateTime (2005,04,13) 77 | FavoriteColor = Some Color.Gold} 78 | 79 | (*** include-output: warnings ***) 80 | 81 | (** 82 | We can also mixed warnings in with operations that are terminal. In other words, we can still build 83 | workflows where certain operations switch the data over to the _failure track_. 84 | *) 85 | 86 | (*** define-output: two-and-three-track ***) 87 | let disallowPink request = 88 | match request.FavoriteColor with 89 | | Some c // no idea why we're being mean to this particular color 90 | when c = Color.Pink -> fail "Get outta here with that color!" 91 | | _ -> pass request 92 | 93 | let recheckApplicant request = 94 | request 95 | |> checkAge 96 | |> bind disallowPink 97 | |> bind checkFirstName 98 | |> bind checkLastName 99 | 100 | let reportMessages request = 101 | match recheckApplicant request with 102 | | Pass _ -> printfn "Nothing to report" 103 | | Warn (_,log) -> printfn "Got some issues:" 104 | for msg in log do printfn " %s" msg 105 | | Fail errors -> printfn "Got errors:" 106 | for msg in errors do printfn " %s" msg 107 | 108 | // terminal request 109 | reportMessages {FullName = "John","Smith" 110 | DateOfBirth = DateTime (1995,12,21) 111 | FavoriteColor = Some Color.Pink} 112 | 113 | // non-terminal request with warnings 114 | reportMessages {FullName = "","Smith" 115 | DateOfBirth = DateTime (1995,12,21) 116 | FavoriteColor = Some Color.Green} 117 | // good request 118 | reportMessages {FullName = "Bob","Smith" 119 | DateOfBirth = DateTime (1995,12,21) 120 | FavoriteColor = Some Color.Green} 121 | 122 | (*** include-output: two-and-three-track ***) 123 | 124 | (** 125 | In effect, we've turned "two-track" data into "three-track" data. But we can also flip this around. 126 | That's is, run operations over the data accumlating warnings. Then at the end, if we have any 127 | messages at all, switch to the _failure track_. 128 | *) 129 | 130 | (*** define-output: fail-on-warn ***) 131 | let warnOnNoColor request = 132 | match request.FavoriteColor with 133 | | None -> request |> warn "No color provided" 134 | | Some _ -> pass request 135 | 136 | let ``checkApplicant (again)`` request = 137 | request 138 | |> checkAge 139 | |> bind checkFirstName 140 | |> bind checkLastName 141 | |> bind warnOnNoColor 142 | 143 | let ``processRequest (again)`` request = 144 | // turn any warning messages into failure messages 145 | let result = request 146 | |> ``checkApplicant (again)`` 147 | |> failOnWarnings 148 | // now we only have 2 tracks on which the data may lay 149 | match result with 150 | | Fail errors -> for x in errors do printfn "ERROR! %s" x 151 | | _ -> printfn "SUCCESS!!!" 152 | 153 | // terminal request 154 | ``processRequest (again)`` {FullName = "","" 155 | DateOfBirth = DateTime.Today 156 | FavoriteColor = None} 157 | 158 | (*** include-output: fail-on-warn ***) 159 | -------------------------------------------------------------------------------- /docs/content/railway.fsx: -------------------------------------------------------------------------------- 1 | (*** hide ***) 2 | #I "../../bin" 3 | 4 | (** 5 | 6 | Using Chessie for Railway-oriented programming (ROP) 7 | ==================================================== 8 | 9 | This tutorial is based on an article about [Railway-oriented programming](http://fsharpforfunandprofit.com/posts/recipe-part2/) by Scott Wlaschin. 10 | 11 | Additional resources: 12 | 13 | * Railway Oriented Programming by Scott Wlaschin - A functional approach to error handling 14 | * [Slide deck](http://www.slideshare.net/ScottWlaschin/railway-oriented-programming) 15 | * [Video](https://vimeo.com/97344498) 16 | 17 | We start by referencing Chessie and opening the ErrorHandling module: 18 | *) 19 | 20 | #r "Chessie.dll" 21 | 22 | open Chessie.ErrorHandling 23 | 24 | (** 25 | Now we define some simple validation functions: 26 | *) 27 | 28 | type Request = 29 | { Name : string 30 | EMail : string } 31 | 32 | let validateInput input = 33 | if input.Name = "" then fail "Name must not be blank" 34 | elif input.EMail = "" then fail "Email must not be blank" 35 | else ok input // happy path 36 | 37 | let validate1 input = 38 | if input.Name = "" then fail "Name must not be blank" 39 | else ok input 40 | 41 | let validate2 input = 42 | if input.Name.Length > 50 then fail "Name must not be longer than 50 chars" 43 | else ok input 44 | 45 | let validate3 input = 46 | if input.EMail = "" then fail "Email must not be blank" 47 | else ok input 48 | 49 | let combinedValidation = 50 | // connect the two-tracks together 51 | validate1 52 | >> bind validate2 53 | >> bind validate3 54 | 55 | (** 56 | Let's use these with some basic combinators: 57 | *) 58 | 59 | { Name = ""; EMail = "" } 60 | |> combinedValidation 61 | // [fsi:val it : Chessie.ErrorHandling.Result =] 62 | // [fsi: Bad ["Name must not be blank"]] 63 | 64 | { Name = "Scott"; EMail = "" } 65 | |> combinedValidation 66 | // [fsi:val it : Chessie.ErrorHandling.Result =] 67 | // [fsi: bad ["Email must not be blank"]] 68 | 69 | { Name = "ScottScottScottScottScottScottScottScottScottScottScottScottScottScottScottScottScottScottScott" 70 | EMail = "" } 71 | |> combinedValidation 72 | // [fsi:val it : Chessie.ErrorHandling.Result =] 73 | // [fsi: Bad ["Name must not be longer than 50 chars" ]] 74 | 75 | { Name = "Scott"; EMail = "scott@chessie.com" } 76 | |> combinedValidation 77 | |> returnOrFail 78 | // [fsi:val it : Request = {Name = "Scott"; EMail = "scott@chessie.com";}] 79 | 80 | 81 | let canonicalizeEmail input = { input with EMail = input.EMail.Trim().ToLower() } 82 | 83 | let usecase = 84 | combinedValidation 85 | >> (lift canonicalizeEmail) 86 | 87 | { Name = "Scott"; EMail = "SCOTT@CHESSIE.com" } 88 | |> usecase 89 | |> returnOrFail 90 | // [fsi:val it : Request = {Name = "Scott"; EMail = "scott@chessie.com";}] 91 | 92 | { Name = ""; EMail = "SCOTT@CHESSIE.com" } 93 | |> usecase 94 | // [fsi:val it : Result = Bad ["Name must not be blank"]] 95 | 96 | // a dead-end function 97 | let updateDatabase input = 98 | () // dummy dead-end function for now 99 | 100 | 101 | let log twoTrackInput = 102 | let success(x,msgs) = printfn "DEBUG. Success so far." 103 | let failure msgs = printf "ERROR. %A" msgs 104 | eitherTee success failure twoTrackInput 105 | 106 | let usecase2 = 107 | usecase 108 | >> (successTee updateDatabase) 109 | >> log 110 | 111 | 112 | { Name = "Scott"; EMail = "SCOTT@CHESSIE.com" } 113 | |> usecase2 114 | |> returnOrFail 115 | // [fsi:DEBUG. Success so far.] 116 | // [fsi:val it : Request = {Name = "Scott";] 117 | // [fsi: EMail = "scott@chessie.com";}] 118 | -------------------------------------------------------------------------------- /docs/files/img/logo.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/Chessie/d422372020a6c054c1395434b465de07d73edd8b/docs/files/img/logo.pdn -------------------------------------------------------------------------------- /docs/files/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/Chessie/d422372020a6c054c1395434b465de07d73edd8b/docs/files/img/logo.png -------------------------------------------------------------------------------- /docs/tools/generate.fsx: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------- 2 | // Builds the documentation from `.fsx` and `.md` files in the 'docs/content' directory 3 | // (the generated documentation is stored in the 'docs/output' directory) 4 | // -------------------------------------------------------------------------------------- 5 | 6 | // Binaries that have XML documentation (in a corresponding generated XML file) 7 | let referenceBinaries = [ "Chessie.dll" ] 8 | // Web site location for the generated documentation 9 | let website = "/Chessie" 10 | 11 | let githubLink = "http://github.com/fsprojects/Chessie" 12 | 13 | // Specify more information about your project 14 | let info = 15 | [ "project-name", "Chessie" 16 | "project-author", "Steffen Forkmann, Max Malook, Tomasz Heimowski" 17 | "project-summary", "Railway-oriented programming for .NET" 18 | "project-github", githubLink 19 | "project-nuget", "http://nuget.org/packages/Chessie" ] 20 | 21 | // -------------------------------------------------------------------------------------- 22 | // For typical project, no changes are needed below 23 | // -------------------------------------------------------------------------------------- 24 | 25 | #I "../../packages/build/FAKE/tools/" 26 | #load "../../packages/build/FSharp.Formatting/FSharp.Formatting.fsx" 27 | #r "FakeLib.dll" 28 | open Fake 29 | open System.IO 30 | open Fake.FileHelper 31 | open FSharp.Literate 32 | open FSharp.MetadataFormat 33 | 34 | // When called from 'build.fsx', use the public project URL as 35 | // otherwise, use the current 'output' directory. 36 | #if RELEASE 37 | let root = website 38 | #else 39 | let root = "file://" + (__SOURCE_DIRECTORY__ @@ "../output") 40 | #endif 41 | 42 | // Paths with template/source/output locations 43 | let bin = __SOURCE_DIRECTORY__ @@ "../../bin" 44 | let content = __SOURCE_DIRECTORY__ @@ "../content" 45 | let output = __SOURCE_DIRECTORY__ @@ "../output" 46 | let files = __SOURCE_DIRECTORY__ @@ "../files" 47 | let templates = __SOURCE_DIRECTORY__ @@ "templates" 48 | let formatting = __SOURCE_DIRECTORY__ @@ "../../packages/build/FSharp.Formatting/" 49 | let docTemplate = formatting @@ "templates/docpage.cshtml" 50 | 51 | // Where to look for *.csproj templates (in this order) 52 | let layoutRootsAll = new System.Collections.Generic.Dictionary() 53 | layoutRootsAll.Add("en",[ templates; formatting @@ "templates" 54 | formatting @@ "templates/reference" ]) 55 | subDirectories (directoryInfo templates) 56 | |> Seq.iter (fun d -> 57 | let name = d.Name 58 | if name.Length = 2 || name.Length = 3 then 59 | layoutRootsAll.Add( 60 | name, [templates @@ name 61 | formatting @@ "templates" 62 | formatting @@ "templates/reference" ])) 63 | 64 | // Copy static files and CSS + JS from F# Formatting 65 | let copyFiles () = 66 | CopyRecursive files output true |> Log "Copying file: " 67 | ensureDirectory (output @@ "content") 68 | CopyRecursive (formatting @@ "styles") (output @@ "content") true 69 | |> Log "Copying styles and scripts: " 70 | 71 | let references = 72 | if isMono then 73 | // Workaround compiler errors in Razor-ViewEngine 74 | let d = RazorEngine.Compilation.ReferenceResolver.UseCurrentAssembliesReferenceResolver() 75 | let loadedList = d.GetReferences () |> Seq.map (fun r -> r.GetFile()) |> Seq.cache 76 | // We replace the list and add required items manually as mcs doesn't like duplicates... 77 | let getItem name = loadedList |> Seq.find (fun l -> l.Contains name) 78 | [ (getItem "FSharp.Core").Replace("4.3.0.0", "4.3.1.0") 79 | Path.GetFullPath "./../../packages/FSharp.Compiler.Service/lib/net40/FSharp.Compiler.Service.dll" 80 | Path.GetFullPath "./../../packages/FSharp.Formatting/lib/net40/System.Web.Razor.dll" 81 | Path.GetFullPath "./../../packages/FSharp.Formatting/lib/net40/RazorEngine.dll" 82 | Path.GetFullPath "./../../packages/FSharp.Formatting/lib/net40/FSharp.Literate.dll" 83 | Path.GetFullPath "./../../packages/FSharp.Formatting/lib/net40/FSharp.CodeFormat.dll" 84 | Path.GetFullPath "./../../packages/FSharp.Formatting/lib/net40/FSharp.MetadataFormat.dll" ] 85 | |> Some 86 | else None 87 | 88 | // Build API reference from XML comments 89 | let buildReference () = 90 | CleanDir (output @@ "reference") 91 | let binaries = 92 | referenceBinaries 93 | |> List.map (fun lib-> bin @@ lib) 94 | MetadataFormat.Generate 95 | ( binaries, output @@ "reference", layoutRootsAll.["en"], 96 | parameters = ("root", root)::info, 97 | sourceRepo = githubLink @@ "tree/master", 98 | sourceFolder = __SOURCE_DIRECTORY__ @@ ".." @@ "..", 99 | publicOnly = true, libDirs = [bin], 100 | ?assemblyReferences = references ) 101 | 102 | // Build documentation from `fsx` and `md` files in `docs/content` 103 | let buildDocumentation () = 104 | let fsiEval = FsiEvaluator () 105 | let subdirs = Directory.EnumerateDirectories(content, "*", SearchOption.AllDirectories) 106 | for dir in Seq.append [content] subdirs do 107 | let sub = if dir.Length > content.Length then dir.Substring(content.Length + 1) else "." 108 | let langSpecificPath(lang, path:string) = 109 | path.Split([|'/'; '\\'|], System.StringSplitOptions.RemoveEmptyEntries) 110 | |> Array.exists(fun i -> i = lang) 111 | let layoutRoots = 112 | let key = layoutRootsAll.Keys |> Seq.tryFind (fun i -> langSpecificPath(i, dir)) 113 | match key with 114 | | Some lang -> layoutRootsAll.[lang] 115 | | None -> layoutRootsAll.["en"] // "en" is the default language 116 | Literate.ProcessDirectory 117 | ( dir, docTemplate, output @@ sub, replacements = ("root", root)::info, 118 | layoutRoots = layoutRoots, 119 | ?assemblyReferences = references, 120 | generateAnchors = true, 121 | fsiEvaluator = fsiEval) 122 | 123 | // Generate 124 | copyFiles() 125 | #if HELP 126 | buildDocumentation() 127 | #endif 128 | #if REFERENCE 129 | buildReference() 130 | #endif 131 | -------------------------------------------------------------------------------- /docs/tools/templates/template.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | @Title 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | fsharp.org 27 | github page 28 | 29 | @Properties["project-name"] 30 | 31 | 32 | 33 | 34 | @RenderBody() 35 | 36 | 37 | 38 | 39 | @Properties["project-name"] 40 | Home page 41 | 42 | Get Library via NuGet 43 | Source Code on GitHub 44 | License 45 | Release Notes 46 | 47 | F# Articles 48 | Railway-oriented programming 49 | Handling warnings and errors 50 | A Tale of 3 Nightclubs 51 | C# Articles 52 | A Tale of 3 Nightclubs 53 | 54 | Documentation 55 | API Reference 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "projects": [ "src", "tests" ] 3 | } 4 | -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 | This file is in the `lib` directory. 2 | 3 | Any **libraries** on which your project depends and which are **NOT managed via NuGet** should be kept **in this directory**. 4 | This typically includes custom builds of third-party software, private (i.e. to a company) codebases, and native libraries. 5 | 6 | --- 7 | NOTE: 8 | 9 | This file is a placeholder, used to preserve directory structure in Git. 10 | 11 | This file does not need to be edited. 12 | -------------------------------------------------------------------------------- /paket.dependencies: -------------------------------------------------------------------------------- 1 | source https://nuget.org/api/v2 2 | 3 | nuget FSharp.Core redirects:force 4 | 5 | group Build 6 | 7 | source https://nuget.org/api/v2 8 | 9 | nuget FSharp.Formatting 10 | nuget FAKE 11 | nuget SourceLink.Fake 12 | 13 | github fsharp/FAKE modules/Octokit/Octokit.fsx 14 | 15 | group Test 16 | 17 | source https://nuget.org/api/v2 18 | 19 | nuget NUnit ~> 2 20 | nuget NUnit.Runners ~> 2 21 | 22 | github forki/FsUnit FsUnit.fs -------------------------------------------------------------------------------- /paket.lock: -------------------------------------------------------------------------------- 1 | NUGET 2 | remote: https://www.nuget.org/api/v2 3 | FSharp.Core (4.0.0.1) - redirects: force 4 | 5 | GROUP Build 6 | NUGET 7 | remote: https://www.nuget.org/api/v2 8 | FAKE (4.34.2) 9 | FSharp.Compiler.Service (2.0.0.6) 10 | FSharp.Formatting (2.14.4) 11 | FSharp.Compiler.Service (2.0.0.6) 12 | FSharpVSPowerTools.Core (>= 2.3 < 2.4) 13 | FSharpVSPowerTools.Core (2.3) 14 | FSharp.Compiler.Service (>= 2.0.0.3) 15 | Microsoft.Bcl (1.1.10) - framework: net10, net11, net20, net30, net35, net40, net40-full 16 | Microsoft.Bcl.Build (>= 1.0.14) 17 | Microsoft.Bcl.Build (1.0.21) - import_targets: false, framework: net10, net11, net20, net30, net35, net40, net40-full 18 | Microsoft.Net.Http (2.2.29) - framework: net10, net11, net20, net30, net35, net40, net40-full 19 | Microsoft.Bcl (>= 1.1.10) 20 | Microsoft.Bcl.Build (>= 1.0.14) 21 | Octokit (0.20) 22 | Microsoft.Net.Http - framework: net10, net11, net20, net30, net35, net40, net40-full 23 | SourceLink.Fake (1.1) 24 | GITHUB 25 | remote: fsharp/FAKE 26 | modules/Octokit/Octokit.fsx (e83eb21f41e71ed7e0cd28e0205a931037266677) 27 | Octokit (>= 0.20) 28 | GROUP Test 29 | NUGET 30 | remote: https://www.nuget.org/api/v2 31 | NUnit (2.6.4) 32 | NUnit.Runners (2.6.4) 33 | GITHUB 34 | remote: forki/FsUnit 35 | FsUnit.fs (f536bd5ed0eba8b38d2b01ae79d64e4e14fbd0a6) -------------------------------------------------------------------------------- /src/Chessie/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | namespace System 2 | open System.Reflection 3 | open System.Runtime.CompilerServices 4 | 5 | [] 6 | [] 7 | [] 8 | [] 9 | [] 10 | [] 11 | do () 12 | 13 | module internal AssemblyVersionInformation = 14 | let [] Version = "0.6.0" 15 | let [] InformationalVersion = "0.6.0" 16 | -------------------------------------------------------------------------------- /src/Chessie/Chessie.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {5C095A65-1BF4-4C24-82D2-A07E8C258789} 8 | Library 9 | Chessie 10 | Chessie 11 | v4.0 12 | 4.3.0.0 13 | Chessie 14 | 15 | ..\..\ 16 | true 17 | false 18 | 19 | 20 | true 21 | full 22 | false 23 | false 24 | ..\..\bin 25 | DEBUG;TRACE 26 | 5 27 | ..\..\bin\Chessie.xml 28 | --warnon:1182 29 | 30 | 31 | pdbonly 32 | true 33 | true 34 | ..\..\bin 35 | TRACE 36 | 3 37 | ..\..\bin\Chessie.xml 38 | 39 | 40 | 11 41 | 42 | 43 | 44 | 45 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets 46 | 47 | 48 | 49 | 50 | $(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\3.1\Framework\v4.0\Microsoft.FSharp.Targets 51 | 52 | 53 | 54 | 55 | $(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\3.0\Framework\v4.0\Microsoft.FSharp.Targets 56 | 57 | 58 | 59 | 60 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | ..\..\packages\FSharp.Core\lib\net20\FSharp.Core.dll 85 | True 86 | True 87 | 88 | 89 | 90 | 91 | 92 | 93 | ..\..\packages\FSharp.Core\lib\net40\FSharp.Core.dll 94 | True 95 | True 96 | 97 | 98 | 99 | 100 | 101 | 102 | ..\..\packages\FSharp.Core\lib\portable-net45+monoandroid10+monotouch10+xamarinios10\FSharp.Core.dll 103 | True 104 | True 105 | 106 | 107 | 108 | 109 | 110 | 111 | ..\..\packages\FSharp.Core\lib\portable-net45+netcore45\FSharp.Core.dll 112 | True 113 | True 114 | 115 | 116 | 117 | 118 | 119 | 120 | ..\..\packages\FSharp.Core\lib\portable-net45+netcore45+wp8\FSharp.Core.dll 121 | True 122 | True 123 | 124 | 125 | 126 | 127 | 128 | 129 | ..\..\packages\FSharp.Core\lib\portable-net45+netcore45+wpa81+wp8\FSharp.Core.dll 130 | True 131 | True 132 | 133 | 134 | 135 | 136 | 137 | 138 | ..\..\packages\FSharp.Core\lib\portable-net45+sl5+netcore45\FSharp.Core.dll 139 | True 140 | True 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /src/Chessie/ErrorHandling.fs: -------------------------------------------------------------------------------- 1 | /// Contains error propagation functions and a computation expression builder for Railway-oriented programming. 2 | namespace Chessie.ErrorHandling 3 | 4 | open System 5 | 6 | /// Represents the result of a computation. 7 | type Result<'TSuccess, 'TMessage> = 8 | /// Represents the result of a successful computation. 9 | | Ok of 'TSuccess * 'TMessage list 10 | /// Represents the result of a failed computation. 11 | | Bad of 'TMessage list 12 | 13 | /// Creates a Failure result with the given messages. 14 | static member FailWith(messages:'TMessage seq) : Result<'TSuccess, 'TMessage> = Result<'TSuccess, 'TMessage>.Bad(messages |> Seq.toList) 15 | 16 | /// Creates a Failure result with the given message. 17 | static member FailWith(message:'TMessage) : Result<'TSuccess, 'TMessage> = Result<'TSuccess, 'TMessage>.Bad([message]) 18 | 19 | /// Creates a Success result with the given value. 20 | static member Succeed(value:'TSuccess) : Result<'TSuccess, 'TMessage> = Result<'TSuccess, 'TMessage>.Ok(value,[]) 21 | 22 | /// Creates a Success result with the given value and the given message. 23 | static member Succeed(value:'TSuccess,message:'TMessage) : Result<'TSuccess, 'TMessage> = Result<'TSuccess, 'TMessage>.Ok(value,[message]) 24 | 25 | /// Creates a Success result with the given value and the given message. 26 | static member Succeed(value:'TSuccess,messages:'TMessage seq) : Result<'TSuccess, 'TMessage> = Result<'TSuccess, 'TMessage>.Ok(value,messages |> Seq.toList) 27 | 28 | /// Executes the given function on a given success or captures the failure 29 | static member Try(func: Func<_>) : Result<'TSuccess,exn> = 30 | try 31 | Ok(func.Invoke(),[]) 32 | with 33 | | exn -> Bad[exn] 34 | 35 | /// Converts the result into a string. 36 | override this.ToString() = 37 | match this with 38 | | Ok(v,msgs) -> sprintf "OK: %A - %s" v (String.Join(Environment.NewLine, msgs |> Seq.map (fun x -> x.ToString()))) 39 | | Bad(msgs) -> sprintf "Error: %s" (String.Join(Environment.NewLine, msgs |> Seq.map (fun x -> x.ToString()))) 40 | 41 | /// Basic combinators and operators for error handling. 42 | [] 43 | module Trial = 44 | /// Wraps a value in a Success 45 | let inline ok<'TSuccess,'TMessage> (x:'TSuccess) : Result<'TSuccess,'TMessage> = Ok(x, []) 46 | 47 | /// Wraps a value in a Success 48 | let inline pass<'TSuccess,'TMessage> (x:'TSuccess) : Result<'TSuccess,'TMessage> = Ok(x, []) 49 | 50 | /// Wraps a value in a Success and adds a message 51 | let inline warn<'TSuccess,'TMessage> (msg:'TMessage) (x:'TSuccess) : Result<'TSuccess,'TMessage> = Ok(x,[msg]) 52 | 53 | /// Wraps a message in a Failure 54 | let inline fail<'TSuccess,'Message> (msg:'Message) : Result<'TSuccess,'Message> = Bad([ msg ]) 55 | 56 | /// Executes the given function on a given success or captures the exception in a failure 57 | let inline Catch f x = Result<_,_>.Try(fun () -> f x) 58 | 59 | /// Returns true if the result was not successful. 60 | let inline failed result = 61 | match result with 62 | | Bad _ -> true 63 | | _ -> false 64 | 65 | /// Takes a Result and maps it with fSuccess if it is a Success otherwise it maps it with fFailure. 66 | let inline either fSuccess fFailure trialResult = 67 | match trialResult with 68 | | Ok(x, msgs) -> fSuccess (x, msgs) 69 | | Bad(msgs) -> fFailure (msgs) 70 | 71 | /// If the given result is a Success the wrapped value will be returned. 72 | ///Otherwise the function throws an exception with Failure message of the result. 73 | let inline returnOrFail result = 74 | let inline raiseExn msgs = 75 | msgs 76 | |> Seq.map (sprintf "%O") 77 | |> String.concat (Environment.NewLine + "\t") 78 | |> failwith 79 | either fst raiseExn result 80 | 81 | /// Appends the given messages with the messages in the given result. 82 | let inline mergeMessages msgs result = 83 | let inline fSuccess (x, msgs2) = Ok(x, msgs @ msgs2) 84 | let inline fFailure errs = Bad(errs @ msgs) 85 | either fSuccess fFailure result 86 | 87 | /// If the result is a Success it executes the given function on the value. 88 | /// Otherwise the exisiting failure is propagated. 89 | let inline bind f result = 90 | let inline fSuccess (x, msgs) = f x |> mergeMessages msgs 91 | let inline fFailure (msgs) = Bad msgs 92 | either fSuccess fFailure result 93 | 94 | /// Flattens a nested result given the Failure types are equal 95 | let inline flatten (result : Result,_>) = 96 | result |> bind id 97 | 98 | /// If the result is a Success it executes the given function on the value. 99 | /// Otherwise the exisiting failure is propagated. 100 | /// This is the infix operator version of ErrorHandling.bind 101 | let inline (>>=) result f = bind f result 102 | 103 | /// If the wrapped function is a success and the given result is a success the function is applied on the value. 104 | /// Otherwise the exisiting error messages are propagated. 105 | let inline apply wrappedFunction result = 106 | match wrappedFunction, result with 107 | | Ok(f, msgs1), Ok(x, msgs2) -> Ok(f x, msgs1 @ msgs2) 108 | | Bad errs, Ok(_, _msgs) -> Bad(errs) 109 | | Ok(_, _msgs), Bad errs -> Bad(errs) 110 | | Bad errs1, Bad errs2 -> Bad(errs1 @ errs2) 111 | 112 | /// If the wrapped function is a success and the given result is a success the function is applied on the value. 113 | /// Otherwise the exisiting error messages are propagated. 114 | /// This is the infix operator version of ErrorHandling.apply 115 | let inline (<*>) wrappedFunction result = apply wrappedFunction result 116 | 117 | /// Lifts a function into a Result container and applies it on the given result. 118 | let inline lift f result = apply (ok f) result 119 | 120 | /// Maps a function over the existing error messages in case of failure. In case of success, the message type will be changed and warnings will be discarded. 121 | let inline mapFailure f result = 122 | match result with 123 | | Ok (v,_) -> ok v 124 | | Bad errs -> Bad (f errs) 125 | 126 | /// Lifts a function into a Result and applies it on the given result. 127 | /// This is the infix operator version of ErrorHandling.lift 128 | let inline () f result = lift f result 129 | 130 | /// Promote a function to a monad/applicative, scanning the monadic/applicative arguments from left to right. 131 | let inline lift2 f a b = f a <*> b 132 | 133 | /// If the result is a Success it executes the given success function on the value and the messages. 134 | /// If the result is a Failure it executes the given failure function on the messages. 135 | /// Result is propagated unchanged. 136 | let inline eitherTee fSuccess fFailure result = 137 | let inline tee f x = f x; x; 138 | tee (either fSuccess fFailure) result 139 | 140 | /// If the result is a Success it executes the given function on the value and the messages. 141 | /// Result is propagated unchanged. 142 | let inline successTee f result = 143 | eitherTee f ignore result 144 | 145 | /// If the result is a Failure it executes the given function on the messages. 146 | /// Result is propagated unchanged. 147 | let inline failureTee f result = 148 | eitherTee ignore f result 149 | 150 | /// Collects a sequence of Results and accumulates their values. 151 | /// If the sequence contains an error the error will be propagated. 152 | let inline collect xs = 153 | Seq.fold (fun result next -> 154 | match result, next with 155 | | Ok(rs, m1), Ok(r, m2) -> Ok(r :: rs, m1 @ m2) 156 | | Ok(_, m1), Bad(m2) | Bad(m1), Ok(_, m2) -> Bad(m1 @ m2) 157 | | Bad(m1), Bad(m2) -> Bad(m1 @ m2)) (ok []) xs 158 | |> lift List.rev 159 | 160 | /// Converts an option into a Result. 161 | let inline failIfNone message result = 162 | match result with 163 | | Some x -> ok x 164 | | None -> fail message 165 | 166 | /// Converts a Choice into a Result. 167 | let inline ofChoice choice = 168 | match choice with 169 | | Choice1Of2 v -> ok v 170 | | Choice2Of2 v -> fail v 171 | 172 | /// Categorizes a result based on its state and the presence of extra messages 173 | let inline (|Pass|Warn|Fail|) result = 174 | match result with 175 | | Ok (value, [] ) -> Pass value 176 | | Ok (value, msgs) -> Warn (value,msgs) 177 | | Bad msgs -> Fail msgs 178 | 179 | let inline failOnWarnings result = 180 | match result with 181 | | Warn (_,msgs) -> Bad msgs 182 | | _ -> result 183 | 184 | /// Builder type for error handling computation expressions. 185 | type TrialBuilder() = 186 | member __.Zero() = ok() 187 | member __.Bind(m, f) = bind f m 188 | member __.Return(x) = ok x 189 | member __.ReturnFrom(x) = x 190 | member __.Combine (a, b) = bind b a 191 | member __.Delay f = f 192 | member __.Run f = f () 193 | member __.TryWith (body, handler) = 194 | try 195 | body() 196 | with 197 | | e -> handler e 198 | member __.TryFinally (body, compensation) = 199 | try 200 | body() 201 | finally 202 | compensation() 203 | member x.Using(d:#IDisposable, body) = 204 | let result = fun () -> body d 205 | x.TryFinally (result, fun () -> 206 | match d with 207 | | null -> () 208 | | d -> d.Dispose()) 209 | member x.While (guard, body) = 210 | if not <| guard () then 211 | x.Zero() 212 | else 213 | bind (fun () -> x.While(guard, body)) (body()) 214 | member x.For(s:seq<_>, body) = 215 | x.Using(s.GetEnumerator(), fun enum -> 216 | x.While(enum.MoveNext, 217 | x.Delay(fun () -> body enum.Current))) 218 | 219 | /// Wraps computations in an error handling computation expression. 220 | let trial = TrialBuilder() 221 | 222 | /// Represents the result of an async computation 223 | [] 224 | type AsyncResult<'a, 'b> = 225 | | AR of Async> 226 | 227 | /// Useful functions for combining error handling computations with async computations. 228 | [] 229 | module AsyncExtensions = 230 | /// Useful functions for combining error handling computations with async computations. 231 | [] 232 | module Async = 233 | /// Creates an async computation that return the given value 234 | let singleton value = value |> async.Return 235 | 236 | /// Creates an async computation that runs a computation and 237 | /// when it generates a result run a binding function on the said result 238 | let bind f x = async.Bind(x, f) 239 | 240 | /// Creates an async computation that runs a mapping function on the result of an async computation 241 | let map f x = x |> bind (f >> singleton) 242 | 243 | /// Creates an async computation from an asyncTrial computation 244 | let ofAsyncResult (AR x) = x 245 | 246 | /// Basic support for async error handling computation 247 | [] 248 | module AsyncTrial = 249 | /// Builder type for error handling in async computation expressions. 250 | type AsyncTrialBuilder() = 251 | member __.Return value : AsyncResult<'a, 'b> = 252 | value 253 | |> ok 254 | |> Async.singleton 255 | |> AR 256 | 257 | member __.ReturnFrom(asyncResult : AsyncResult<'a, 'b>) = asyncResult 258 | member this.Zero() : AsyncResult = this.Return() 259 | member __.Delay(generator : unit -> AsyncResult<'a, 'b>) : AsyncResult<'a, 'b> = 260 | async.Delay(generator >> Async.ofAsyncResult) |> AR 261 | 262 | member __.Bind(asyncResult : AsyncResult<'a, 'c>, binder : 'a -> AsyncResult<'b, 'c>) : AsyncResult<'b, 'c> = 263 | let fSuccess (value, msgs) = 264 | value |> (binder 265 | >> Async.ofAsyncResult 266 | >> Async.map (mergeMessages msgs)) 267 | 268 | let fFailure errs = 269 | errs 270 | |> Bad 271 | |> Async.singleton 272 | 273 | asyncResult 274 | |> Async.ofAsyncResult 275 | |> Async.bind (either fSuccess fFailure) 276 | |> AR 277 | 278 | member this.Bind(result : Result<'a, 'c>, binder : 'a -> AsyncResult<'b, 'c>) : AsyncResult<'b, 'c> = 279 | this.Bind(result 280 | |> Async.singleton 281 | |> AR, binder) 282 | 283 | member __.Bind(async : Async<'a>, binder : 'a -> AsyncResult<'b, 'c>) : AsyncResult<'b, 'c> = 284 | async 285 | |> Async.bind (binder >> Async.ofAsyncResult) 286 | |> AR 287 | 288 | member __.TryWith(asyncResult : AsyncResult<'a, 'b>, catchHandler : exn -> AsyncResult<'a, 'b>) : AsyncResult<'a, 'b> = 289 | async.TryWith(asyncResult |> Async.ofAsyncResult, (catchHandler >> Async.ofAsyncResult)) |> AR 290 | member __.TryFinally(asyncResult : AsyncResult<'a, 'b>, compensation : unit -> unit) : AsyncResult<'a, 'b> = 291 | async.TryFinally(asyncResult |> Async.ofAsyncResult, compensation) |> AR 292 | member __.Using(resource : 'T when 'T :> System.IDisposable, binder : 'T -> AsyncResult<'a, 'b>) : AsyncResult<'a, 'b> = 293 | async.Using(resource, (binder >> Async.ofAsyncResult)) |> AR 294 | 295 | // Wraps async computations in an error handling computation expression. 296 | let asyncTrial = AsyncTrialBuilder() 297 | 298 | namespace Chessie.ErrorHandling.CSharp 299 | 300 | open System 301 | open System.Runtime.CompilerServices 302 | open Chessie.ErrorHandling 303 | 304 | /// Extensions methods for easier C# usage. 305 | [] 306 | type ResultExtensions () = 307 | /// Allows pattern matching on Results from C#. 308 | [] 309 | static member inline Match(this, ifSuccess:Action<'TSuccess , ('TMessage list)>, ifFailure:Action<'TMessage list>) = 310 | match this with 311 | | Result.Ok(x, msgs) -> ifSuccess.Invoke(x,msgs) 312 | | Result.Bad(msgs) -> ifFailure.Invoke(msgs) 313 | 314 | /// Allows pattern matching on Results from C#. 315 | [] 316 | static member inline Either(this, ifSuccess:Func<'TSuccess , ('TMessage list),'TResult>, ifFailure:Func<'TMessage list,'TResult>) = 317 | match this with 318 | | Result.Ok(x, msgs) -> ifSuccess.Invoke(x,msgs) 319 | | Result.Bad(msgs) -> ifFailure.Invoke(msgs) 320 | 321 | /// Lifts a Func into a Result and applies it on the given result. 322 | [] 323 | static member inline Map(this:Result<'TSuccess, 'TMessage>,func:Func<_,_>) = 324 | lift func.Invoke this 325 | 326 | /// Collects a sequence of Results and accumulates their values. 327 | /// If the sequence contains an error the error will be propagated. 328 | [] 329 | static member inline Collect(values:seq>) = 330 | collect values 331 | 332 | /// Collects a sequence of Results and accumulates their values. 333 | /// If the sequence contains an error the error will be propagated. 334 | [] 335 | static member inline Flatten(this) : Result,'TMessage>= 336 | match this with 337 | | Result.Ok(values:Result<'TSuccess,'TMessage> seq, _msgs:'TMessage list) -> 338 | match collect values with 339 | | Result.Ok(values,msgs) -> Ok(values |> List.toSeq,msgs) 340 | | Result.Bad(msgs:'TMessage list) -> Bad msgs 341 | | Result.Bad(msgs:'TMessage list) -> Bad msgs 342 | 343 | /// If the result is a Success it executes the given Func on the value. 344 | /// Otherwise the exisiting failure is propagated. 345 | [] 346 | static member inline SelectMany (this:Result<'TSuccess, 'TMessage>, func: Func<_,_>) = 347 | bind func.Invoke this 348 | 349 | /// If the result is a Success it executes the given Func on the value. 350 | /// If the result of the Func is a Success it maps it using the given Func. 351 | /// Otherwise the exisiting failure is propagated. 352 | [] 353 | static member inline SelectMany (this:Result<'TSuccess, 'TMessage>, func: Func<_,_>, mapper: Func<_,_,_>) = 354 | bind (fun s -> s |> func.Invoke |> lift (fun v -> mapper.Invoke(s,v))) this 355 | 356 | /// Lifts a Func into a Result and applies it on the given result. 357 | [] 358 | static member inline Select (this:Result<'TSuccess, 'TMessage>, func: Func<_,_>) = lift func.Invoke this 359 | 360 | /// Returns the error messages or fails if the result was a success. 361 | [] 362 | static member inline FailedWith(this:Result<'TSuccess, 'TMessage>) = 363 | match this with 364 | | Result.Ok(v,msgs) -> failwithf "Result was a success: %A - %s" v (String.Join(Environment.NewLine, msgs |> Seq.map (fun x -> x.ToString()))) 365 | | Result.Bad(msgs) -> msgs 366 | 367 | /// Returns the result or fails if the result was an error. 368 | [] 369 | static member inline SucceededWith(this:Result<'TSuccess, 'TMessage>) : 'TSuccess = 370 | match this with 371 | | Result.Ok(v,_msgs) -> v 372 | | Result.Bad(msgs) -> failwithf "Result was an error: %s" (String.Join(Environment.NewLine, msgs |> Seq.map (fun x -> x.ToString()))) 373 | 374 | /// Joins two results. 375 | /// If both are a success the resultSelector Func is applied to the values and the existing success messages are propagated. 376 | /// Otherwise the exisiting error messages are propagated. 377 | [] 378 | static member inline Join (this: Result<'TOuter, 'TMessage>, inner: Result<'TInner, 'TMessage>, _outerKeySelector: Func<'TOuter,'TKey>, _innerKeySelector: Func<'TInner, 'TKey>, resultSelector: Func<'TOuter, 'TInner, 'TResult>) = 379 | let curry func = fun a b -> func (a, b) 380 | curry resultSelector.Invoke 381 | this 382 | <*> inner 383 | 384 | /// Converts an option into a Result. 385 | [] 386 | static member ToResult(this, msg) = 387 | this |> failIfNone msg 388 | 389 | /// Maps a function over the existing error messages in case of failure. In case of success, the message type will be changed and warnings will be discarded. 390 | [] 391 | static member inline MapFailure (this: Result<'TSuccess, 'TMessage>, f: Func<'TMessage list, 'TMessage2 seq>) = 392 | this |> Trial.mapFailure (f.Invoke >> Seq.toList) 393 | -------------------------------------------------------------------------------- /src/Chessie/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core -------------------------------------------------------------------------------- /src/Chessie/paket.template: -------------------------------------------------------------------------------- 1 | type project 2 | owners 3 | Steffen Forkmann, Max Malook, Tomasz Heimowski 4 | authors 5 | Steffen Forkmann, Max Malook, Tomasz Heimowski 6 | projectUrl 7 | http://github.com/fsprojects/Chessie 8 | iconUrl 9 | https://raw.githubusercontent.com/fsprojects/Chessie/master/docs/files/img/logo.png 10 | licenseUrl 11 | http://github.com/fsprojects/Chessie/blob/master/LICENSE.txt 12 | requireLicenseAcceptance 13 | false 14 | copyright 15 | Copyright 2015 16 | tags 17 | rop, fsharp, F# 18 | summary 19 | Railway-oriented programming for .NET 20 | description 21 | Railway-oriented programming for .NET 22 | files 23 | ../../bin/Chessie.* ==> lib/net40 24 | -------------------------------------------------------------------------------- /src/Chessie/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.6.0", 3 | "buildOptions": { 4 | "debugType": "portable", 5 | "compilerName": "fsc", 6 | "define": [], 7 | "xmlDoc": true, 8 | "compile": { 9 | "includeFiles": [ 10 | "ErrorHandling.fs" 11 | ] 12 | } 13 | }, 14 | "dependencies": { 15 | "NETStandard.Library": "1.6.0", 16 | "FSharp.Core": "4.0.1.7-alpha" 17 | }, 18 | "frameworks": { 19 | "netstandard1.6": { 20 | "buildOptions": { 21 | "define": [ 22 | "NETSTANDARD1_5", 23 | "CORE_CLR" 24 | ] 25 | } 26 | } 27 | }, 28 | "tools": { 29 | "dotnet-compile-fsc": { 30 | "version": "1.0.0-preview2-*", 31 | "imports": "dnxcore50" 32 | }, 33 | "dotnet-mergenupkg": { 34 | "target": "package", 35 | "version": "1.0.*" 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /tests/Chessie.CSharp.Test/Chessie.CSharp.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {FC460F0A-F3C6-4314-BD2C-9F4218B902BC} 8 | Library 9 | Properties 10 | Chessie.CSharp.Test 11 | Chessie.CSharp.Test 12 | v4.5 13 | 512 14 | 15 | false 16 | 17 | 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | false 26 | 27 | 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | false 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 62 | 63 | 64 | 65 | 66 | 67 | {5c095a65-1bf4-4c24-82d2-a07e8c258789} 68 | Chessie 69 | 70 | 71 | 72 | 73 | 74 | 75 | ..\..\packages\FSharp.Core\lib\net20\FSharp.Core.dll 76 | True 77 | True 78 | 79 | 80 | 81 | 82 | 83 | 84 | ..\..\packages\FSharp.Core\lib\net40\FSharp.Core.dll 85 | True 86 | True 87 | 88 | 89 | 90 | 91 | 92 | 93 | ..\..\packages\FSharp.Core\lib\portable-net45+monoandroid10+monotouch10+xamarinios10\FSharp.Core.dll 94 | True 95 | True 96 | 97 | 98 | 99 | 100 | 101 | 102 | ..\..\packages\FSharp.Core\lib\portable-net45+netcore45\FSharp.Core.dll 103 | True 104 | True 105 | 106 | 107 | 108 | 109 | 110 | 111 | ..\..\packages\FSharp.Core\lib\portable-net45+netcore45+wp8\FSharp.Core.dll 112 | True 113 | True 114 | 115 | 116 | 117 | 118 | 119 | 120 | ..\..\packages\FSharp.Core\lib\portable-net45+netcore45+wpa81+wp8\FSharp.Core.dll 121 | True 122 | True 123 | 124 | 125 | 126 | 127 | 128 | 129 | ..\..\packages\FSharp.Core\lib\portable-net45+sl5+netcore45\FSharp.Core.dll 130 | True 131 | True 132 | 133 | 134 | 135 | 136 | 137 | 138 | ..\..\packages\test\NUnit\lib\nunit.framework.dll 139 | True 140 | True 141 | 142 | 143 | -------------------------------------------------------------------------------- /tests/Chessie.CSharp.Test/ExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Chessie.ErrorHandling; 5 | using Chessie.ErrorHandling.CSharp; 6 | using Microsoft.FSharp.Core; 7 | using NUnit.Framework; 8 | 9 | namespace Chessie.CSharp.Test 10 | { 11 | [TestFixture] 12 | public class ExtensionsTests 13 | { 14 | [Test] 15 | public void JoinToResultsOfSuccessWorks() 16 | { 17 | var result1 = Result.Succeed(1, "added one"); 18 | var result2 = Result.Succeed(2, "added two"); 19 | 20 | var result3 = result1.Join(result2, _ => 0, _ => -0, (i1, i2) => i1 + i2); 21 | 22 | result3.Match( 23 | ifSuccess: (x, msgs) => 24 | { 25 | Assert.AreEqual(3, x); 26 | Assert.That(msgs, Is.EquivalentTo(new[] { "added one", "added two" })); 27 | }, 28 | ifFailure: errs => Assert.Fail()); 29 | } 30 | 31 | [Test] 32 | public void Test() 33 | { 34 | Func, Result, Result, Result> f = (r1, r2, r3) => 35 | from a in r1 36 | from b in r2 37 | from c in r3 38 | select a + b + c; 39 | 40 | f(Result.Succeed("1"), Result.Succeed("2"), Result.Succeed("3")).Match( 41 | ifSuccess: (s, _) => Assert.That(s, Is.EqualTo("123")), 42 | ifFailure: _ => Assert.Fail("should not fail")); 43 | 44 | f(Result.Succeed("1", "msg1"), Result.Succeed("2", "msg2"), Result.Succeed("3", "msg3")).Match( 45 | ifSuccess: (s, list) => 46 | { 47 | Assert.That(s, Is.EqualTo("123")); 48 | Assert.That(list, Is.EquivalentTo(new[] {"msg1", "msg2", "msg3"})); 49 | }, 50 | ifFailure: list => Assert.Fail("should not fail")); 51 | 52 | f(Result.FailWith("fail"), Result.Succeed("2"), Result.Succeed("3")).Match( 53 | ifSuccess: (s, _) => Assert.Fail("should fail"), 54 | ifFailure: list => Assert.That(list, Is.EquivalentTo(new[] { "fail" }))); 55 | 56 | f(Result.Succeed("1"), Result.FailWith("fail"), Result.Succeed("3")).Match( 57 | ifSuccess: (s, _) => Assert.Fail("should fail"), 58 | ifFailure: list => Assert.That(list, Is.EquivalentTo(new[] { "fail" }))); 59 | 60 | f(Result.Succeed("1"), Result.FailWith("fail1"), Result.FailWith("fail2")).Match( 61 | ifSuccess: (s, _) => Assert.Fail("should fail"), 62 | ifFailure: list => Assert.That(list, Is.EquivalentTo(new[] { "fail1" }))); 63 | } 64 | 65 | [Test] 66 | public void ToResultOnSomeShouldSucceed() 67 | { 68 | var opt = FSharpOption.Some(42); 69 | var result = opt.ToResult("error"); 70 | result.Match( 71 | ifSuccess: (x, msgs) => 72 | { 73 | Assert.AreEqual(42, x); 74 | Assert.That(msgs, Is.Empty); 75 | }, 76 | ifFailure: errs => Assert.Fail()); 77 | } 78 | 79 | [Test] 80 | public void ToResultOnNoneShoulFail() 81 | { 82 | var opt = FSharpOption.None; 83 | var result = opt.ToResult("error"); 84 | result.Match( 85 | ifSuccess: (x, _) => Assert.Fail(), 86 | ifFailure: errs => Assert.That(errs, Is.EquivalentTo(new[] {"error"}))); 87 | } 88 | 89 | [Test] 90 | public void MapFailureOnSuccessShouldReturnSuccess() 91 | { 92 | Result.Succeed(42, "warn1") 93 | .MapFailure(list => new[] { 42 }) 94 | .Match( 95 | ifSuccess: (v, msgs) => 96 | { 97 | Assert.AreEqual(42, v); 98 | Assert.That(msgs, Is.Empty); 99 | }, 100 | ifFailure: errs => Assert.Fail()); 101 | } 102 | 103 | [Test] 104 | public void MapFailureOnFailureShouldMapOverError() 105 | { 106 | Result.FailWith(new[] { "err1", "err2" }) 107 | .MapFailure(_ => new[] { 42 }) 108 | .Match( 109 | ifSuccess: (v, msgs) => Assert.Fail(), 110 | ifFailure: errs => Assert.That(errs, Is.EquivalentTo(new[] { 42 }))); 111 | } 112 | 113 | [Test] 114 | public void MapFailureOnFailureShouldMapOverListOfErrors() 115 | { 116 | Result.FailWith(new[] { "err1", "err2" }) 117 | .MapFailure(errs => errs.Select(err => 118 | { 119 | switch (err) 120 | { 121 | case "err1": return 42; 122 | case "err2": return 43; 123 | default: return 0; 124 | } 125 | })) 126 | .Match( 127 | ifSuccess: (v, msgs) => Assert.Fail(), 128 | ifFailure: errs => Assert.That(errs, Is.EquivalentTo(new[] { 42, 43 }))); 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /tests/Chessie.CSharp.Test/NightClubsValidation.cs: -------------------------------------------------------------------------------- 1 | using Chessie.ErrorHandling; 2 | using Chessie.ErrorHandling.CSharp; 3 | using NUnit.Framework; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace Chessie.CSharp.Test 9 | { 10 | // originally from https://github.com/fsprojects/fsharpx/blob/master/tests/FSharpx.CSharpTests/ValidationExample.cs 11 | 12 | enum Sobriety { Sober, Tipsy, Drunk, Paralytic, Unconscious } 13 | enum Gender { Male, Female } 14 | 15 | class Person 16 | { 17 | public Gender Gender { get; private set; } 18 | public int Age { get; private set; } 19 | public List Clothes { get; private set; } 20 | public Sobriety Sobriety { get; private set; } 21 | 22 | public Person(Gender gender, int age, List clothes, Sobriety sobriety) 23 | { 24 | this.Gender = gender; 25 | this.Age = age; 26 | this.Clothes = clothes; 27 | this.Sobriety = sobriety; 28 | } 29 | } 30 | 31 | class Club 32 | { 33 | public static Result CheckAge(Person p) 34 | { 35 | if (p.Age < 18) 36 | return Result.FailWith("Too young!"); 37 | if (p.Age > 40) 38 | return Result.FailWith("Too old!"); 39 | return Result.Succeed(p); 40 | } 41 | 42 | public static Result CheckClothes(Person p) 43 | { 44 | if (p.Gender == Gender.Male && !p.Clothes.Contains("Tie")) 45 | return Result.FailWith("Smarten up!"); 46 | if (p.Gender == Gender.Female && p.Clothes.Contains("Trainers")) 47 | return Result.FailWith("Wear high heels!"); 48 | return Result.Succeed(p); 49 | } 50 | 51 | public static Result CheckSobriety(Person p) 52 | { 53 | if (new[] { Sobriety.Drunk, Sobriety.Paralytic, Sobriety.Unconscious }.Contains(p.Sobriety)) 54 | return Result.FailWith("Sober up!"); 55 | return Result.Succeed(p); 56 | } 57 | } 58 | 59 | class ClubbedToDeath 60 | { 61 | public static Result CostToEnter(Person p) 62 | { 63 | return from a in Club.CheckAge(p) 64 | from b in Club.CheckClothes(a) 65 | from c in Club.CheckSobriety(b) 66 | select c.Gender == Gender.Female ? 0m : 5m; 67 | } 68 | } 69 | 70 | [TestFixture] 71 | class Test1 72 | { 73 | [Test] 74 | public void Part1() 75 | { 76 | var Dave = new Person(Gender.Male, 41, new List { "Tie", "Jeans" }, Sobriety.Sober); 77 | var costDave = ClubbedToDeath.CostToEnter(Dave); 78 | Assert.AreEqual("Too old!", costDave.FailedWith().First()); 79 | 80 | var Ken = new Person(Gender.Male, 28, new List { "Tie", "Shirt" }, Sobriety.Tipsy); 81 | var costKen = ClubbedToDeath.CostToEnter(Ken); 82 | Assert.AreEqual(5m, costKen.SucceededWith()); 83 | 84 | var Ruby = new Person(Gender.Female, 25, new List { "High heels" }, Sobriety.Tipsy); 85 | var costRuby = ClubbedToDeath.CostToEnter(Ruby); 86 | costRuby.Match( 87 | (x, msgs) => 88 | { 89 | Assert.AreEqual(0m, x); 90 | }, 91 | msgs => 92 | { 93 | Assert.Fail(); 94 | 95 | }); 96 | 97 | var Ruby17 = new Person(Ruby.Gender, 17, Ruby.Clothes, Ruby.Sobriety); 98 | var costRuby17 = ClubbedToDeath.CostToEnter(Ruby17); 99 | Assert.AreEqual("Too young!", costRuby17.FailedWith().First()); 100 | 101 | var KenUnconscious = new Person(Ken.Gender, Ken.Age, Ken.Clothes, Sobriety.Unconscious); 102 | var costKenUnconscious = ClubbedToDeath.CostToEnter(KenUnconscious); 103 | costKenUnconscious.Match( 104 | (x, msgs) => 105 | { 106 | Assert.Fail(); 107 | }, 108 | msgs => 109 | { 110 | Assert.AreEqual("Sober up!", msgs.First()); 111 | }); 112 | } 113 | } 114 | 115 | class ClubTropicana 116 | { 117 | public static Result CostToEnter(Person p) 118 | { 119 | return from c in Club.CheckAge(p) 120 | join x in Club.CheckClothes(p) on 1 equals 1 121 | join y in Club.CheckSobriety(p) on 1 equals 1 122 | select c.Gender == Gender.Female ? 0m : 7.5m; 123 | } 124 | 125 | public static decimal CostByGender(Person p, Person x, Person y) 126 | { 127 | return p.Gender == Gender.Female ? 0m : 7.5m; 128 | } 129 | } 130 | 131 | [TestFixture] 132 | class Test2 133 | { 134 | [Test] 135 | public void Part2() 136 | { 137 | var daveParalytic = new Person( 138 | age: 41, 139 | clothes: new List { "Tie", "Shirt" }, 140 | gender: Gender.Male, 141 | sobriety: Sobriety.Paralytic); 142 | 143 | var costDaveParalytic = ClubTropicana.CostToEnter(daveParalytic); 144 | 145 | costDaveParalytic.Match( 146 | ifSuccess: (x, msgs) => Assert.Fail(), 147 | ifFailure: errs => Assert.That(errs.ToList(), Is.EquivalentTo(new[] { "Too old!", "Sober up!" }))); 148 | } 149 | } 150 | 151 | class GayBar 152 | { 153 | public static Result CheckGender (Person p) 154 | { 155 | if (p.Gender == Gender.Male) 156 | return Result.Succeed(p); 157 | return Result.FailWith("Men only"); 158 | } 159 | 160 | public static Result CostToEnter(Person p) 161 | { 162 | return new List>> { CheckGender, Club.CheckAge, Club.CheckClothes, Club.CheckSobriety } 163 | .Select(check => check(p)) 164 | .Collect() 165 | .Select(x => x[0].Age + 1.5m); 166 | } 167 | } 168 | 169 | [TestFixture] 170 | class Test3 171 | { 172 | [Test] 173 | public void Part3() 174 | { 175 | var person = new Person( 176 | gender: Gender.Male, 177 | age: 59, 178 | clothes: new List { "Jeans" }, 179 | sobriety: Sobriety.Paralytic); 180 | var cost = GayBar.CostToEnter(person); 181 | cost.Match( 182 | ifSuccess: (x, msgs) => Assert.Fail(), 183 | ifFailure: errs => Assert.That(errs, Is.EquivalentTo(new[] { "Too old!", "Smarten up!", "Sober up!" }))); 184 | } 185 | } 186 | } -------------------------------------------------------------------------------- /tests/Chessie.CSharp.Test/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace Chessie.CSharp.TestRunner 5 | { 6 | public static class Program 7 | { 8 | public static int Main(string[] argv) 9 | { 10 | #if NETCOREAPP1_0 11 | return 0; 12 | #else 13 | var run = new NUnitLite.AutoRun(); 14 | return run.Execute(argv); 15 | #endif 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Chessie.CSharp.Test/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Chessie.CSharp.Test")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Chessie.CSharp.Test")] 13 | [assembly: AssemblyCopyright("Copyright © 2015")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("98b876ac-91ea-4e81-83cc-77b6f56ef9b1")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /tests/Chessie.CSharp.Test/SimpleValidation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using NUnit.Framework; 7 | using Chessie.ErrorHandling; 8 | using Chessie.ErrorHandling.CSharp; 9 | 10 | namespace Chessie.CSharp.Test 11 | { 12 | public class Request 13 | { 14 | public string Name { get; set; } 15 | public string EMail { get; set; } 16 | } 17 | 18 | public class Validation 19 | { 20 | public static Result ValidateInput(Request input) 21 | { 22 | if (input.Name == "") 23 | return Result.FailWith("Name must not be blank"); 24 | if (input.EMail == "") 25 | return Result.FailWith("Email must not be blank"); 26 | return Result.Succeed(input); 27 | 28 | } 29 | } 30 | 31 | [TestFixture] 32 | public class TrySpecs 33 | { 34 | [Test] 35 | public void TryWillCatch() 36 | { 37 | var exn = new Exception("Hello World"); 38 | var result = Result.Try(() => { throw exn; }); 39 | Assert.AreEqual(exn, result.FailedWith().First()); 40 | } 41 | 42 | [Test] 43 | public void TryWillReturnValue() 44 | { 45 | var result = Result.Try(() => { return "hello world"; }); 46 | Assert.AreEqual("hello world", result.SucceededWith()); 47 | } 48 | } 49 | 50 | [TestFixture] 51 | public class SimpleValidation 52 | { 53 | [Test] 54 | public void CanCreateSuccess() 55 | { 56 | var request = new Request { Name = "Steffen", EMail = "mail@support.com" }; 57 | var result = Validation.ValidateInput(request); 58 | Assert.AreEqual(request, result.SucceededWith()); 59 | } 60 | } 61 | 62 | [TestFixture] 63 | public class SimplePatternMatching 64 | { 65 | [Test] 66 | public void CanMatchSuccess() 67 | { 68 | var request = new Request { Name = "Steffen", EMail = "mail@support.com" }; 69 | var result = Validation.ValidateInput(request); 70 | result.Match( 71 | (x, msgs) => { Assert.AreEqual(request, x); }, 72 | msgs => { throw new Exception("wrong match case"); }); 73 | } 74 | 75 | [Test] 76 | public void CanMatchFailure() 77 | { 78 | var request = new Request { Name = "Steffen", EMail = "" }; 79 | var result = Validation.ValidateInput(request); 80 | result.Match( 81 | (x, msgs) => { throw new Exception("wrong match case"); }, 82 | msgs => { Assert.AreEqual("Email must not be blank", msgs[0]); }); 83 | } 84 | } 85 | 86 | [TestFixture] 87 | public class SimpleEitherPatternMatching 88 | { 89 | [Test] 90 | public void CanMatchSuccess() 91 | { 92 | var request = new Request { Name = "Steffen", EMail = "mail@support.com" }; 93 | var result = 94 | Validation 95 | .ValidateInput(request) 96 | .Either( 97 | (x, msgs) => { return x; }, 98 | msgs => { throw new Exception("wrong match case"); }); 99 | Assert.AreEqual(request, result); 100 | } 101 | 102 | [Test] 103 | public void CanMatchFailure() 104 | { 105 | var request = new Request { Name = "Steffen", EMail = "" }; 106 | var result = 107 | Validation.ValidateInput(request) 108 | .Either( 109 | (x, msgs) => { throw new Exception("wrong match case"); }, 110 | msgs => { return msgs[0]; }); 111 | 112 | Assert.AreEqual("Email must not be blank", result); 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /tests/Chessie.CSharp.Test/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | group Test 3 | NUnit 4 | NUnit.Runners -------------------------------------------------------------------------------- /tests/Chessie.CSharp.Test/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.6.0", 3 | "dependencies": { 4 | "NUnit": "3.4.1", 5 | "dotnet-test-nunit": "3.4.0-beta-1", 6 | "Chessie": { 7 | "target": "project" 8 | } 9 | }, 10 | "testRunner": "nunit", 11 | "frameworks": { 12 | "netcoreapp1.0": { 13 | "imports": "portable-net45+win8", 14 | "dependencies": { 15 | "Microsoft.NETCore.App": { 16 | "version": "1.0.0-*", 17 | "type": "platform" 18 | } 19 | } 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /tests/Chessie.Tests/BuilderTests.fs: -------------------------------------------------------------------------------- 1 | module Chessie.Builder.Tests 2 | 3 | open Chessie.ErrorHandling 4 | open NUnit.Framework 5 | open FsUnit 6 | open System 7 | 8 | [] 9 | let ``Using CE syntax should be equivilent to bind`` () = 10 | let sut = 11 | trial { 12 | let! bob = ok "bob" 13 | let greeting = sprintf "Hello %s" bob 14 | return greeting 15 | } 16 | sut |> shouldEqual (bind (sprintf "Hello %s" >> ok) (ok "bob")) 17 | 18 | [] 19 | let ``You should be able to "combine" in CE syntax`` () = 20 | let sut = 21 | trial { 22 | if "bob" = "bob" then 23 | do! ok () 24 | return "bob" 25 | } 26 | sut |> shouldEqual (ok "bob") 27 | 28 | [] 29 | let ``Try .. with works in CE syntax`` () = 30 | let sut = 31 | trial { 32 | return 33 | try 34 | failwith "bang" 35 | "not bang" 36 | with 37 | | e -> e.Message 38 | } 39 | sut |> shouldEqual (ok "bang") 40 | 41 | [] 42 | let ``Try .. finally works in CE syntax`` () = 43 | let i = ref 0 44 | try 45 | trial { 46 | try 47 | failwith "bang" 48 | finally 49 | i := 1 50 | } 51 | with 52 | | e -> ok () 53 | |> returnOrFail 54 | !i |> shouldEqual 1 55 | 56 | [] 57 | let ``use! works in CE expressions`` () = 58 | use mem = new IO.MemoryStream() 59 | try 60 | trial { 61 | use! mem = ok <| new IO.StreamReader(mem) 62 | failwith "bang" 63 | } 64 | with 65 | | e -> ok () 66 | |> returnOrFail 67 | (fun () -> mem.ReadByte() |> ignore) |> shouldFail 68 | 69 | [] 70 | let ``While works in CE syntax`` () = 71 | let i = ref 0 72 | trial { 73 | while !i < 3 do 74 | i := !i + 1 75 | } 76 | |> returnOrFail 77 | !i |> shouldEqual 3 78 | 79 | [] 80 | let ``For works in CE syntax`` () = 81 | let i = ref 0 82 | trial { 83 | for x in 0..2 do 84 | i := !i + x 85 | } 86 | |> returnOrFail 87 | !i |> shouldEqual 3 -------------------------------------------------------------------------------- /tests/Chessie.Tests/Chessie.Tests.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {5D8D2601-26EF-42C7-9703-8BDDDA015CB8} 8 | Library 9 | Chessie.Tests 10 | Chessie.Tests 11 | v4.5 12 | 4.3.0.0 13 | Chessie.Tests 14 | 15 | ..\..\ 16 | true 17 | false 18 | 19 | 20 | true 21 | full 22 | false 23 | false 24 | bin\Debug\ 25 | DEBUG;TRACE 26 | 3 27 | bin\Debug\fsharp_project_scaffold_tests.XML 28 | Project 29 | 30 | 31 | 32 | 33 | 34 | 35 | pdbonly 36 | true 37 | true 38 | bin\Release\ 39 | TRACE 40 | 3 41 | bin\Release\Chessie.Tests.xml 42 | 43 | 44 | 11 45 | 46 | 47 | 48 | 49 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets 50 | 51 | 52 | 53 | 54 | $(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\3.1\Framework\v4.0\Microsoft.FSharp.Targets 55 | 56 | 57 | 58 | 59 | $(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\3.0\Framework\v4.0\Microsoft.FSharp.Targets 60 | 61 | 62 | 63 | 64 | 71 | 72 | 73 | 74 | True 75 | FsUnit.fs 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | Chessie 92 | {5c095a65-1bf4-4c24-82d2-a07e8c258789} 93 | True 94 | 95 | 96 | 97 | 98 | 99 | 100 | ..\..\packages\FSharp.Core\lib\net20\FSharp.Core.dll 101 | True 102 | True 103 | 104 | 105 | 106 | 107 | 108 | 109 | ..\..\packages\FSharp.Core\lib\net40\FSharp.Core.dll 110 | True 111 | True 112 | 113 | 114 | 115 | 116 | 117 | 118 | ..\..\packages\FSharp.Core\lib\portable-net45+monoandroid10+monotouch10+xamarinios10\FSharp.Core.dll 119 | True 120 | True 121 | 122 | 123 | 124 | 125 | 126 | 127 | ..\..\packages\FSharp.Core\lib\portable-net45+netcore45\FSharp.Core.dll 128 | True 129 | True 130 | 131 | 132 | 133 | 134 | 135 | 136 | ..\..\packages\FSharp.Core\lib\portable-net45+netcore45+wp8\FSharp.Core.dll 137 | True 138 | True 139 | 140 | 141 | 142 | 143 | 144 | 145 | ..\..\packages\FSharp.Core\lib\portable-net45+netcore45+wpa81+wp8\FSharp.Core.dll 146 | True 147 | True 148 | 149 | 150 | 151 | 152 | 153 | 154 | ..\..\packages\FSharp.Core\lib\portable-net45+sl5+netcore45\FSharp.Core.dll 155 | True 156 | True 157 | 158 | 159 | 160 | 161 | 162 | 163 | ..\..\packages\test\NUnit\lib\nunit.framework.dll 164 | True 165 | True 166 | 167 | 168 | -------------------------------------------------------------------------------- /tests/Chessie.Tests/NightClubs.fs: -------------------------------------------------------------------------------- 1 | module Chessie.Validaton.NightClubs.Tests 2 | 3 | open Chessie.ErrorHandling 4 | open NUnit.Framework 5 | open FsUnit 6 | 7 | type Sobriety = 8 | | Sober 9 | | Tipsy 10 | | Drunk 11 | | Paralytic 12 | | Unconscious 13 | 14 | type Gender = 15 | | Male 16 | | Female 17 | 18 | type Person = 19 | { Gender : Gender 20 | Age : int 21 | Clothes : string Set 22 | Sobriety : Sobriety } 23 | 24 | // Let's define the checks that *all* nightclubs make! 25 | module Club = 26 | let checkAge (p : Person) = 27 | if p.Age < 18 then fail "Too young!" 28 | elif p.Age > 40 then fail "Too old!" 29 | else ok p 30 | 31 | let checkClothes (p : Person) = 32 | if p.Gender = Male && not (p.Clothes.Contains "Tie") then fail "Smarten up!" 33 | elif p.Gender = Female && p.Clothes.Contains "Trainers" then fail "Wear high heels" 34 | else ok p 35 | 36 | let checkSobriety (p : Person) = 37 | match p.Sobriety with 38 | | Drunk | Paralytic | Unconscious -> fail "Sober up!" 39 | | _ -> ok p 40 | 41 | module ClubbedToDeath = 42 | open Club 43 | 44 | let costToEnter p = 45 | trial { 46 | let! a = checkAge p 47 | let! b = checkClothes a 48 | let! c = checkSobriety b 49 | return 50 | match c.Gender with 51 | | Female -> 0m 52 | | Male -> 5m 53 | } 54 | 55 | let Ken = { Person.Gender = Male; Age = 28; Clothes = set ["Tie"; "Shirt"]; Sobriety = Tipsy } 56 | let Dave = { Person.Gender = Male; Age = 41; Clothes = set ["Tie"; "Jeans"]; Sobriety = Sober } 57 | let Ruby = { Person.Gender = Female; Age = 25; Clothes = set ["High heels"]; Sobriety = Tipsy } 58 | 59 | [] 60 | let part1() = 61 | ClubbedToDeath.costToEnter Dave |> shouldEqual (fail "Too old!") 62 | ClubbedToDeath.costToEnter Ken |> shouldEqual (ok 5m) 63 | ClubbedToDeath.costToEnter Ruby |> shouldEqual (ok 0m) 64 | ClubbedToDeath.costToEnter { Ruby with Age = 17 } |> shouldEqual (fail "Too young!") 65 | ClubbedToDeath.costToEnter { Ken with Sobriety = Unconscious } |> shouldEqual (fail "Sober up!") -------------------------------------------------------------------------------- /tests/Chessie.Tests/Program.fs: -------------------------------------------------------------------------------- 1 | module Chessie.TestRunner 2 | 3 | open System 4 | open System.Reflection 5 | 6 | type Program = class end 7 | 8 | [] 9 | let main argv = 10 | 11 | #if NETSTANDARD1_5 12 | let run = typeof.GetTypeInfo().Assembly |> NUnitLite.AutoRun 13 | run.Execute(argv, (new NUnit.Common.ExtendedTextWrapper(Console.Out)), Console.In) 14 | #else 15 | let run = NUnitLite.AutoRun() 16 | run.Execute(argv) 17 | #endif 18 | -------------------------------------------------------------------------------- /tests/Chessie.Tests/SimpleValidation.fs: -------------------------------------------------------------------------------- 1 | module Chessie.Validaton.Tests 2 | 3 | open Chessie.ErrorHandling 4 | open NUnit.Framework 5 | open FsUnit 6 | 7 | type Request = 8 | { Name : string 9 | EMail : string } 10 | 11 | let validateInput input = 12 | if input.Name = "" then fail "Name must not be blank" 13 | elif input.EMail = "" then fail "Email must not be blank" 14 | else ok input // happy path 15 | 16 | let validate1 input = 17 | if input.Name = "" then fail "Name must not be blank" 18 | else ok input 19 | 20 | let validate2 input = 21 | if input.Name.Length > 50 then fail "Name must not be longer than 50 chars" 22 | else ok input 23 | 24 | let validate3 input = 25 | if input.EMail = "" then fail "Email must not be blank" 26 | else ok input 27 | 28 | let combinedValidation = 29 | // connect the two-tracks together 30 | validate1 31 | >> bind validate2 32 | >> bind validate3 33 | 34 | [] 35 | let ``should find empty name``() = 36 | { Name = "" 37 | EMail = "" } 38 | |> combinedValidation 39 | |> shouldEqual (Bad [ "Name must not be blank" ]) 40 | 41 | [] 42 | let ``should find empty mail``() = 43 | { Name = "Scott" 44 | EMail = "" } 45 | |> combinedValidation 46 | |> shouldEqual (Bad [ "Email must not be blank" ]) 47 | 48 | [] 49 | let ``should find long name``() = 50 | { Name = "ScottScottScottScottScottScottScottScottScottScottScottScottScottScottScottScottScottScottScott" 51 | EMail = "" } 52 | |> combinedValidation 53 | |> shouldEqual (Bad [ "Name must not be longer than 50 chars" ]) 54 | 55 | [] 56 | let ``should not complain on valid data``() = 57 | let scott = 58 | { Name = "Scott" 59 | EMail = "scott@chessie.com" } 60 | scott 61 | |> combinedValidation 62 | |> returnOrFail 63 | |> shouldEqual scott 64 | 65 | let canonicalizeEmail input = { input with EMail = input.EMail.Trim().ToLower() } 66 | 67 | let usecase = 68 | combinedValidation 69 | >> (lift canonicalizeEmail) 70 | 71 | [] 72 | let ``should canonicalize valid data``() = 73 | { Name = "Scott" 74 | EMail = "SCOTT@CHESSIE.com" } 75 | |> usecase 76 | |> returnOrFail 77 | |> shouldEqual { Name = "Scott" 78 | EMail = "scott@chessie.com" } 79 | 80 | [] 81 | let ``should not canonicalize invalid data``() = 82 | { Name = "" 83 | EMail = "SCOTT@CHESSIE.com" } 84 | |> usecase 85 | |> shouldEqual (Bad [ "Name must not be blank" ]) 86 | 87 | // a dead-end function 88 | let updateDatabase input = 89 | () // dummy dead-end function for now 90 | 91 | 92 | let log logF twoTrackInput = 93 | let success(x,msgs) = logF "DEBUG. Success so far." 94 | let failure msgs = logF <| sprintf "ERROR. %A" msgs 95 | eitherTee success failure twoTrackInput 96 | 97 | let usecase2 logF = 98 | usecase 99 | >> (successTee updateDatabase) 100 | >> (log logF) 101 | 102 | [] 103 | let ``should log valid data``() = 104 | let logF s = 105 | if s <> "DEBUG. Success so far." then 106 | failwithf "unexpected log: %s" s 107 | 108 | { Name = "Scott" 109 | EMail = "SCOTT@CHESSIE.com" } 110 | |> usecase2 logF 111 | |> returnOrFail 112 | |> shouldEqual { Name = "Scott" 113 | EMail = "scott@chessie.com" } 114 | 115 | 116 | [] 117 | let ``should log invalid data``() = 118 | let logF s = 119 | if s <> """ERROR. ["Email must not be blank"]""" then 120 | failwithf "unexpected log: %s" s 121 | 122 | { Name = "Scott" 123 | EMail = "" } 124 | |> usecase2 logF 125 | |> ignore 126 | -------------------------------------------------------------------------------- /tests/Chessie.Tests/TrialTests.fs: -------------------------------------------------------------------------------- 1 | module Chessie.Trial.Tests 2 | 3 | open Chessie.ErrorHandling 4 | open NUnit.Framework 5 | open FsUnit 6 | 7 | [] 8 | let ``ofChoice if Choice1Of2 it should succeed`` () = 9 | let choice = Choice1Of2 "foo" 10 | let result = choice |> Trial.ofChoice 11 | result |> shouldEqual (ok "foo") 12 | 13 | [] 14 | let ``ofChoice if Choice2Of2 it should fail`` () = 15 | let choice = Choice2Of2 "error" 16 | let result = choice |> Trial.ofChoice 17 | result |> shouldEqual (fail "error") 18 | 19 | [] 20 | let ``ofChoice if Choice2Of2 of list it should fail`` () = 21 | let choice = Choice2Of2 ["error1";"error2"] 22 | let result = choice |> Trial.ofChoice 23 | result |> shouldEqual (fail ["error1";"error2"]) 24 | 25 | [] 26 | let ``mapFailure if success should discard warning`` () = 27 | Ok (42,[1;2;3]) 28 | |> Trial.mapFailure (fun _ -> ["err1"]) 29 | |> shouldEqual (Ok (42,[])) 30 | 31 | [] 32 | let ``mapFailure if failure should map over error`` () = 33 | fail "error" 34 | |> Trial.mapFailure (fun _ -> [42]) 35 | |> shouldEqual (Bad [42]) 36 | 37 | [] 38 | let ``mapFailure if failure should map over list of errors`` () = 39 | Bad ["err1"; "err2"] 40 | |> Trial.mapFailure (fun errs -> errs |> List.map (function "err1" -> 42 | "err2" -> 43 | _ -> 0)) 41 | |> shouldEqual (Bad [42; 43]) 42 | 43 | [] 44 | let ``mapFailure if failure should replace errors with singleton list`` () = 45 | Bad ["err1", "err2"] 46 | |> Trial.mapFailure (fun _ -> [42]) 47 | |> shouldEqual (Bad [42]) 48 | 49 | [] 50 | let ``mapFailure if failure should map over empty list of errors`` () = 51 | Bad [] 52 | |> Trial.mapFailure (fun errs -> errs |> List.map (function "err1" -> 42 | "err2" -> 43 | _ -> 0)) 53 | |> shouldEqual (Bad []) 54 | 55 | [] 56 | let ``tryCatch if failure should return exception`` () = 57 | let ex = exn "error" 58 | 1 59 | |> Trial.Catch (fun x -> raise ex) 60 | |> shouldEqual (Bad[ex]) 61 | 62 | 63 | [] 64 | let ``tryCatch if success should return list`` () = 65 | 1 66 | |> Trial.Catch id 67 | |> shouldEqual (Ok(1,[])) -------------------------------------------------------------------------------- /tests/Chessie.Tests/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | group Test 3 | NUnit 4 | NUnit.Runners 5 | File:FsUnit.fs . -------------------------------------------------------------------------------- /tests/Chessie.Tests/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.6.0", 3 | "buildOptions": { 4 | "debugType": "portable", 5 | "compilerName": "fsc", 6 | "compile": { 7 | "includeFiles": [ 8 | "../../paket-files/test/forki/FsUnit/FsUnit.fs", 9 | "TrialTests.fs", 10 | "BuilderTests.fs", 11 | "SimpleValidation.fs", 12 | "NightClubs.fs" 13 | ] 14 | } 15 | }, 16 | "dependencies": { 17 | "FSharp.Core": "4.0.1.7-alpha", 18 | "NUnit": "3.4.1", 19 | "dotnet-test-nunit": "3.4.0-beta-1", 20 | "Chessie": { 21 | "target": "project" 22 | } 23 | }, 24 | "testRunner": "nunit", 25 | "frameworks": { 26 | "netcoreapp1.0": { 27 | "imports": "portable-net45+win8", 28 | "dependencies": { 29 | "Microsoft.NETCore.App": { 30 | "version": "1.0.0-*", 31 | "type": "platform" 32 | } 33 | } 34 | } 35 | }, 36 | "tools": { 37 | "dotnet-compile-fsc": { 38 | "version": "1.0.0-preview2-*", 39 | "imports": "dnxcore50" 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /tests/Chessie.VisualBasic.Tests/Chessie.VisualBasic.Tests.vbproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | AnyCPU 6 | {3AA8D82D-6F46-42DD-A7DE-7E2F6A633239} 7 | Library 8 | Chessie.VisualBasic.Tests 9 | Chessie.VisualBasic.Tests 10 | 512 11 | Windows 12 | v4.5 13 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{F184B08F-C81C-45F6-A57F-5ABD9991F28F} 14 | 10.0 15 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 16 | $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages 17 | False 18 | UnitTest 19 | 20 | 21 | 22 | true 23 | full 24 | true 25 | true 26 | bin\Debug\ 27 | Chessie.VisualBasic.Test.xml 28 | 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 29 | 30 | 31 | pdbonly 32 | false 33 | true 34 | true 35 | bin\Release\ 36 | Chessie.VisualBasic.Test.xml 37 | 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 38 | 39 | 40 | On 41 | 42 | 43 | Binary 44 | 45 | 46 | Off 47 | 48 | 49 | On 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | True 85 | Application.myapp 86 | 87 | 88 | True 89 | True 90 | Resources.resx 91 | 92 | 93 | True 94 | Settings.settings 95 | True 96 | 97 | 98 | 99 | 100 | VbMyResourcesResXFileCodeGenerator 101 | Resources.Designer.vb 102 | My.Resources 103 | Designer 104 | 105 | 106 | 107 | 108 | MyApplicationCodeGenerator 109 | Application.Designer.vb 110 | 111 | 112 | SettingsSingleFileGenerator 113 | My 114 | Settings.Designer.vb 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | {5c095a65-1bf4-4c24-82d2-a07e8c258789} 127 | Chessie 128 | 129 | 130 | 131 | 132 | 133 | 134 | False 135 | 136 | 137 | False 138 | 139 | 140 | False 141 | 142 | 143 | False 144 | 145 | 146 | 147 | 148 | 149 | 150 | 157 | -------------------------------------------------------------------------------- /tests/Chessie.VisualBasic.Tests/NightClubsValidation.vb: -------------------------------------------------------------------------------- 1 | Imports Chessie.ErrorHandling 2 | Imports Chessie.ErrorHandling.CSharp 3 | Imports NUnit.Framework 4 | 5 | ' originally from https://github.com/fsprojects/fsharpx/blob/master/tests/FSharpx.CSharpTests/ValidationExample.cs 6 | 7 | Enum Sobriety 8 | Sober 9 | Tipsy 10 | Drunk 11 | Paralytic 12 | Unconscious 13 | End Enum 14 | Enum Gender 15 | Male 16 | Female 17 | End Enum 18 | 19 | Class Person 20 | Public Property Gender() As Gender 21 | Public Property Age() As Integer 22 | Public Property Clothes() As List(Of String) 23 | Public Property Sobriety() As Sobriety 24 | 25 | Public Sub New(gender As Gender, age As Integer, clothes As List(Of String), sobriety As Sobriety) 26 | Me.Gender = gender 27 | Me.Age = age 28 | Me.Clothes = clothes 29 | Me.Sobriety = sobriety 30 | End Sub 31 | End Class 32 | 33 | Class Club 34 | Public Shared Function CheckAge(p As Person) As Result(Of Person, String) 35 | If p.Age < 18 Then 36 | Return Result(Of Person, String).FailWith("Too young!") 37 | End If 38 | If p.Age > 40 Then 39 | Return Result(Of Person, String).FailWith("Too old!") 40 | End If 41 | Return Result(Of Person, String).Succeed(p) 42 | End Function 43 | 44 | Public Shared Function CheckClothes(p As Person) As Result(Of Person, String) 45 | If p.Gender = Gender.Male AndAlso Not p.Clothes.Contains("Tie") Then 46 | Return Result(Of Person, String).FailWith("Smarten up!") 47 | End If 48 | If p.Gender = Gender.Female AndAlso p.Clothes.Contains("Trainers") Then 49 | Return Result(Of Person, String).FailWith("Wear high heels!") 50 | End If 51 | Return Result(Of Person, String).Succeed(p) 52 | End Function 53 | 54 | Public Shared Function CheckSobriety(p As Person) As Result(Of Person, String) 55 | If {Sobriety.Drunk, Sobriety.Paralytic, Sobriety.Unconscious}.Contains(p.Sobriety) Then 56 | Return Result(Of Person, String).FailWith("Sober up!") 57 | End If 58 | Return Result(Of Person, String).Succeed(p) 59 | End Function 60 | End Class 61 | 62 | Class ClubbedToDeath 63 | Public Shared Function CostToEnter(p As Person) As Result(Of Decimal, String) 64 | Return From a In Club.CheckAge(p) From b In Club.CheckClothes(a) From c In Club.CheckSobriety(b) Select If(c.Gender = Gender.Female, 0D, 5D) 65 | End Function 66 | End Class 67 | 68 | 69 | Class Test1 70 | 71 | Public Sub Part1() 72 | Dim dave = New Person(Gender.Male, 41, New List(Of String)({"Tie", "Jeans"}), Sobriety.Sober) 73 | Dim costDave = ClubbedToDeath.CostToEnter(dave) 74 | Assert.AreEqual("Too old!", costDave.FailedWith().First()) 75 | 76 | Dim ken = New Person(Gender.Male, 28, New List(Of String)({"Tie", "Shirt"}), Sobriety.Tipsy) 77 | Dim costKen = ClubbedToDeath.CostToEnter(ken) 78 | Assert.AreEqual(5D, costKen.SucceededWith()) 79 | 80 | Dim ruby = New Person(Gender.Female, 25, New List(Of String)({"High heels"}), Sobriety.Tipsy) 81 | Dim costRuby = ClubbedToDeath.CostToEnter(ruby) 82 | costRuby.Match(Sub(x, msgs) Assert.AreEqual(0D, x), 83 | Sub(msgs) Assert.Fail()) 84 | 85 | Dim ruby17 = New Person(ruby.Gender, 17, ruby.Clothes, ruby.Sobriety) 86 | Dim costRuby17 = ClubbedToDeath.CostToEnter(ruby17) 87 | Assert.AreEqual("Too young!", costRuby17.FailedWith().First()) 88 | 89 | Dim kenUnconscious = New Person(ken.Gender, ken.Age, ken.Clothes, Sobriety.Unconscious) 90 | Dim costKenUnconscious = ClubbedToDeath.CostToEnter(kenUnconscious) 91 | costKenUnconscious.Match(Sub(x, msgs) Assert.Fail(), 92 | Sub(msgs) Assert.AreEqual("Sober up!", msgs.First())) 93 | End Sub 94 | End Class 95 | 96 | Class ClubTropicana 97 | Public Shared Function CostToEnter(p As Person) As Result(Of Decimal, String) 98 | Return From c In Club.CheckAge(p) Join x In Club.CheckClothes(p) On x Equals c Join y In Club.CheckSobriety(p) On x Equals y Select If(c.Gender = Gender.Female, 0D, 7.5D) 99 | End Function 100 | 101 | Public Shared Function CostByGender(p As Person, x As Person, y As Person) As Decimal 102 | Return If(p.Gender = Gender.Female, 0D, 7.5D) 103 | End Function 104 | End Class 105 | 106 | 107 | Class Test2 108 | 109 | Public Sub Part2() 110 | Dim daveParalytic = New Person(age:=41, clothes:=New List(Of String)({"Tie", "Shirt"}), gender:=Gender.Male, sobriety:=Sobriety.Paralytic) 111 | 112 | Dim costDaveParalytic = ClubTropicana.CostToEnter(daveParalytic) 113 | 114 | costDaveParalytic.Match(Sub(x, msgs) Assert.Fail(), 115 | Sub(errs) Assert.That(errs.ToList(), [Is].EquivalentTo({"Too old!", "Sober up!"}))) 116 | End Sub 117 | End Class 118 | 119 | Class GayBar 120 | Public Shared Function CheckGender(p As Person) As Result(Of Person, String) 121 | If p.Gender = Gender.Male Then 122 | Return Result(Of Person, String).Succeed(p) 123 | End If 124 | Return Result(Of Person, String).FailWith("Men only") 125 | End Function 126 | 127 | Public Shared Function CostToEnter(p As Person) As Result(Of Decimal, String) 128 | Return New List(Of Func(Of Person, Result(Of Person, String)))({ 129 | AddressOf CheckGender, 130 | AddressOf Club.CheckAge, 131 | AddressOf Club.CheckClothes, 132 | AddressOf Club.CheckSobriety 133 | }).[Select](Function(check) check(p)).Collect().[Select](Function(x) x(0).Age + 1.5D) 134 | End Function 135 | End Class 136 | 137 | 138 | Class Test3 139 | 140 | Public Sub Part3() 141 | Dim person = New Person(gender:=Gender.Male, age:=59, clothes:=New List(Of String)({"Jeans"}), sobriety:=Sobriety.Paralytic) 142 | Dim cost = GayBar.CostToEnter(person) 143 | cost.Match(Sub(x, msgs) Assert.Fail(), 144 | Sub(errs) Assert.That(errs, [Is].EquivalentTo({"Too old!", "Smarten up!", "Sober up!"}))) 145 | End Sub 146 | End Class 147 | -------------------------------------------------------------------------------- /tests/Chessie.VisualBasic.Tests/SimpleValidation.vb: -------------------------------------------------------------------------------- 1 | Imports Chessie.ErrorHandling 2 | Imports Chessie.ErrorHandling.CSharp 3 | Imports NUnit.Framework 4 | 5 | Public Class Request 6 | Public Property Name() As String 7 | Public Property EMail() As String 8 | End Class 9 | 10 | Public Class Validation 11 | Public Shared Function ValidateInput(input As Request) As Result(Of Request, String) 12 | If input.Name = "" Then 13 | Return Result(Of Request, String).FailWith("Name must not be blank") 14 | End If 15 | If input.EMail = "" Then 16 | Return Result(Of Request, String).FailWith("Email must not be blank") 17 | End If 18 | Return Result(Of Request, String).Succeed(input) 19 | 20 | End Function 21 | End Class 22 | 23 | 24 | Public Class TrySpecs 25 | 26 | Public Sub TryWillCatch() 27 | Dim exn = New Exception("Hello World") 28 | Dim result = ErrorHandling.Result(Of String, Exception).[Try](Function() 29 | Throw exn 30 | Return "" 31 | End Function) 32 | Assert.AreEqual(exn, result.FailedWith().First()) 33 | End Sub 34 | 35 | 36 | Public Sub TryWillReturnValue() 37 | Dim result = ErrorHandling.Result(Of String, Exception).[Try](Function() "hello world") 38 | Assert.AreEqual("hello world", result.SucceededWith()) 39 | End Sub 40 | End Class 41 | 42 | 43 | Public Class SimpleValidation 44 | 45 | Public Sub CanCreateSuccess() 46 | Dim request = New Request() With { 47 | .Name = "Steffen", 48 | .EMail = "mail@support.com" 49 | } 50 | Dim result = Validation.ValidateInput(request) 51 | Assert.AreEqual(request, result.SucceededWith()) 52 | End Sub 53 | End Class 54 | 55 | 56 | Public Class SimplePatternMatching 57 | 58 | Public Sub CanMatchSuccess() 59 | Dim request = New Request() With { 60 | .Name = "Steffen", 61 | .EMail = "mail@support.com" 62 | } 63 | Dim result = Validation.ValidateInput(request) 64 | result.Match(Sub(x, msgs) Assert.AreEqual(request, x), 65 | Sub(msgs) Throw New Exception("wrong match case")) 66 | End Sub 67 | 68 | 69 | Public Sub CanMatchFailure() 70 | Dim request = New Request() With { 71 | .Name = "Steffen", 72 | .EMail = "" 73 | } 74 | Dim result = Validation.ValidateInput(request) 75 | result.Match(Sub(x, msgs) Throw New Exception("wrong match case"), 76 | Sub(msgs) Assert.AreEqual("Email must not be blank", msgs(0))) 77 | End Sub 78 | End Class 79 | 80 | 81 | Public Class SimpleEitherPatternMatching 82 | 83 | Public Sub CanMatchSuccess() 84 | Dim request = New Request() With { 85 | .Name = "Steffen", 86 | .EMail = "mail@support.com" 87 | } 88 | Dim result = Validation.ValidateInput(request).Either(Function(x, msgs) x, 89 | Function(msgs) 90 | Throw New Exception("wrong match case") 91 | Return request 92 | End Function) 93 | Assert.AreEqual(request, result) 94 | End Sub 95 | 96 | 97 | Public Sub CanMatchFailure() 98 | Dim request = New Request() With { 99 | .Name = "Steffen", 100 | .EMail = "" 101 | } 102 | Dim result = Validation.ValidateInput(request).Either(Function(x, msgs) 103 | Throw New Exception("wrong match case") 104 | Return "" 105 | End Function, 106 | Function(msgs) msgs(0)) 107 | 108 | Assert.AreEqual("Email must not be blank", result) 109 | End Sub 110 | End Class 111 | --------------------------------------------------------------------------------
PM> Install-Package Chessie