├── .config └── dotnet-tools.json ├── .github └── workflows │ └── dotnetcore.yml ├── .gitignore ├── CHANGELOG.md ├── Directory.build.props ├── GiraffeGenerator.sln ├── LICENSE ├── README.md ├── build.ps1 ├── publish.ps1 ├── src ├── Example │ ├── Example.fsproj │ ├── Generated.fs │ ├── Program.fs │ └── spec.yaml ├── GiraffeGenerator.Core │ ├── AST.fs │ ├── ASTExt.fs │ ├── CodeGen.fs │ ├── CodeGenErrorsDU.fs │ ├── CodeGenValidation.fs │ ├── CodeGenValidation_InstanceGeneration.fs │ ├── CodeGenValidation_LogicGeneration.fs │ ├── CodeGenValidation_TypeGeneration.fs │ ├── CodeGenValidation_Types.fs │ ├── Configuration.fs │ ├── GiraffeGenerator.Core.fsproj │ ├── NuGet.Config │ ├── OpenApi.fs │ ├── OpenApiToAstTypesMatchingAndConversion.fs │ ├── OpenApiValidation.fs │ └── build │ │ └── GiraffeGenerator.Core.props ├── GiraffeGenerator.Sdk │ ├── GiraffeGenerator.Sdk.proj │ ├── build │ │ ├── GiraffeGenerator.Sdk.props │ │ └── GiraffeGenerator.Sdk.targets │ └── buildMultiTargeting │ │ └── GiraffeGenerator.Sdk.targets └── GiraffeGenerator │ ├── GiraffeGenerator.fsproj │ ├── NuGet.Config │ └── Program.fs └── tests ├── GiraffeGenerator.IntegrationTests ├── GiraffeGenerator.IntegrationTests.fsproj ├── NuGet.Config ├── OptionConverter.fs ├── RemoveGenerated.ps1 ├── SpecForNodaTimeDateTimeInstantFormatHandlingTests.fs ├── SpecGeneralForNodaTimeTests.fs ├── SpecSimpleTests.fs ├── SpecWithArgumentsTests.fs ├── SpecWithParametersAndRequestBodyTests.fs ├── SpecWithSchemasTests.fs ├── SpecWithTextXmlTests.fs ├── SpecWithValidationExtensibilityTests.fs ├── SpecWithValidationTests.fs └── specs │ ├── specForNodaTimeDateTimeFormatHandling.yaml │ ├── specGeneralForNodaTime.yaml │ ├── specSimple.yaml │ ├── specWithArguments.yaml │ ├── specWithParametersAndRequestBodySchemas.yaml │ ├── specWithSchemas.yaml │ ├── specWithTextXml.yaml │ ├── specWithValidation.yaml │ └── specWithValidationExtensibility.yaml └── GiraffeGenerator.NugetTests ├── GiraffeGenerator.NugetTests.fsproj └── NuGet.Config /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "dotnet-mergenupkg": { 6 | "version": "3.0.0", 7 | "commands": [ 8 | "dotnet-mergenupkg" 9 | ] 10 | }, 11 | "powershell": { 12 | "version": "7.0.0", 13 | "commands": [ 14 | "pwsh" 15 | ] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /.github/workflows/dotnetcore.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [macos-latest, ubuntu-latest, windows-latest] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Extract version 21 | shell: pwsh 22 | run: | 23 | $matches = Select-String -path Directory.build.props '(\d+\.\d+\.\d+)' 24 | $version_local = $matches.Matches[0].Groups[1].Value 25 | echo "VERSION=$version_local" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append 26 | 27 | - name: Setup .NET Core 28 | uses: actions/setup-dotnet@v1 29 | with: 30 | dotnet-version: 6.0.406 31 | 32 | - name: Setup .NET Core for Example project 33 | uses: actions/setup-dotnet@v1 34 | with: 35 | dotnet-version: 3.1.101 36 | 37 | - name: Install local tools 38 | run: dotnet tool restore 39 | 40 | - name: Dotnet Pack SDK 41 | run: dotnet pack src/GiraffeGenerator.Sdk -c Release -o bin/nupkg /p:Version=${{ env.VERSION }} 42 | 43 | - name: Dotnet Pack Tool 44 | run: dotnet pack src/GiraffeGenerator -c Release -o bin/nupkg /p:Version=${{ env.VERSION }} 45 | 46 | - name: Dotnet Merge Nugets 47 | run: dotnet mergenupkg --source bin/nupkg/GiraffeGenerator.Sdk.${{ env.VERSION }}.nupkg --other bin/nupkg/GiraffeGenerator.${{ env.VERSION }}.nupkg --tools --only-files 48 | 49 | - name: Dotnet Pack Core 50 | run: dotnet pack src/GiraffeGenerator.Core -c Release -o bin/nupkg /p:Version=${{ env.VERSION }} 51 | 52 | - name: Dotnet build Example project 53 | run: dotnet build src/Example -c Release 54 | 55 | - name: Dotnet force restore 56 | run: dotnet restore --force --no-cache 57 | 58 | - name: Run Integration Tests 59 | run: dotnet test -c Release 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | ################### 5 | # compiled source # 6 | ################### 7 | *.com 8 | *.class 9 | *.dll 10 | *.exe 11 | *.pdb 12 | *.dll.config 13 | *.cache 14 | *.suo 15 | # Include dlls if they’re in the NuGet packages directory 16 | !/packages/*/lib/*.dll 17 | # Include dlls if they're in the CommonReferences directory 18 | !*CommonReferences/*.dll 19 | #################### 20 | # VS Upgrade stuff # 21 | #################### 22 | _UpgradeReport_Files/ 23 | ############### 24 | # Directories # 25 | ############### 26 | bin/ 27 | obj/ 28 | TestResults/ 29 | ################### 30 | # Web publish log # 31 | ################### 32 | *.Publish.xml 33 | ############# 34 | # Resharper # 35 | ############# 36 | /_ReSharper.* 37 | *.ReSharper.* 38 | ############ 39 | # Packages # 40 | ############ 41 | # it’s better to unpack these files and commit the raw source 42 | # git has its own built in compression methods 43 | *.7z 44 | *.dmg 45 | *.gz 46 | *.iso 47 | *.jar 48 | *.rar 49 | *.tar 50 | *.zip 51 | ###################### 52 | # Logs and databases # 53 | ###################### 54 | *.log 55 | *.sqlite 56 | # OS generated files # 57 | ###################### 58 | .DS_Store? 59 | ehthumbs.db 60 | Icon? 61 | Thumbs.db 62 | 63 | 64 | # User-specific files 65 | *.user 66 | *.userosscache 67 | *.sln.docstates 68 | 69 | # User-specific files (MonoDevelop/Xamarin Studio) 70 | *.userprefs 71 | 72 | # Build results 73 | [Dd]ebug/ 74 | [Dd]ebugPublic/ 75 | [Rr]elease/ 76 | [Rr]eleases/ 77 | x64/ 78 | x86/ 79 | bld/ 80 | [Bb]in/ 81 | [Oo]bj/ 82 | 83 | # Visual Studo 2015 cache/options directory 84 | .vs/ 85 | 86 | # MSTest test Results 87 | [Tt]est[Rr]esult*/ 88 | [Bb]uild[Ll]og.* 89 | 90 | # NUNIT 91 | *.VisualState.xml 92 | TestResult.xml 93 | 94 | # Build Results of an ATL Project 95 | [Dd]ebugPS/ 96 | [Rr]eleasePS/ 97 | dlldata.c 98 | 99 | # DNX 100 | project.lock.json 101 | artifacts/ 102 | 103 | *_i.c 104 | *_p.c 105 | *_i.h 106 | *.ilk 107 | *.meta 108 | *.obj 109 | *.pch 110 | *.pgc 111 | *.pgd 112 | *.rsp 113 | *.sbr 114 | *.tlb 115 | *.tli 116 | *.tlh 117 | *.tmp 118 | *.tmp_proj 119 | *.vspscc 120 | *.vssscc 121 | .builds 122 | *.pidb 123 | *.svclog 124 | *.scc 125 | 126 | # Chutzpah Test files 127 | _Chutzpah* 128 | 129 | # Visual C++ cache files 130 | ipch/ 131 | *.aps 132 | *.ncb 133 | *.opensdf 134 | *.sdf 135 | *.cachefile 136 | 137 | # Visual Studio profiler 138 | *.psess 139 | *.vsp 140 | *.vspx 141 | 142 | # TFS 2012 Local Workspace 143 | $tf/ 144 | 145 | # Guidance Automation Toolkit 146 | *.gpState 147 | 148 | # ReSharper is a .NET coding add-in 149 | _ReSharper*/ 150 | *.[Rr]e[Ss]harper 151 | *.DotSettings.user 152 | 153 | # JustCode is a .NET coding add-in 154 | .JustCode 155 | 156 | # TeamCity is a build add-in 157 | _TeamCity* 158 | 159 | # DotCover is a Code Coverage Tool 160 | *.dotCover 161 | 162 | # NCrunch 163 | _NCrunch_* 164 | .*crunch*.local.xml 165 | 166 | # MightyMoose 167 | *.mm.* 168 | AutoTest.Net/ 169 | 170 | # Web workbench (sass) 171 | .sass-cache/ 172 | 173 | # Installshield output folder 174 | [Ee]xpress/ 175 | 176 | # DocProject is a documentation generator add-in 177 | DocProject/buildhelp/ 178 | DocProject/Help/*.HxT 179 | DocProject/Help/*.HxC 180 | DocProject/Help/*.hhc 181 | DocProject/Help/*.hhk 182 | DocProject/Help/*.hhp 183 | DocProject/Help/Html2 184 | DocProject/Help/html 185 | 186 | # Click-Once directory 187 | publish/ 188 | 189 | # Publish Web Output 190 | *.[Pp]ublish.xml 191 | *.azurePubxml 192 | # but database connection strings (with potential passwords) will be unencrypted 193 | *.pubxml 194 | *.publishproj 195 | 196 | # NuGet Packages 197 | *.nupkg 198 | # The packages folder can be ignored because of Package Restore 199 | **/packages/* 200 | # except build/, which is used as an MSBuild target. 201 | !**/packages/build/ 202 | # Uncomment if necessary however generally it will be regenerated when needed 203 | #!**/packages/repositories.config 204 | 205 | # Windows Azure Build Output 206 | csx/ 207 | *.build.csdef 208 | 209 | # Windows Store app package directory 210 | AppPackages/ 211 | 212 | # Visual Studio cache files 213 | # files ending in .cache can be ignored 214 | *.[Cc]ache 215 | # but keep track of directories ending in .cache 216 | !*.[Cc]ache/ 217 | 218 | # Others 219 | ClientBin/ 220 | [Ss]tyle[Cc]op.* 221 | ~$* 222 | *~ 223 | *.dbmdl 224 | *.dbproj.schemaview 225 | *.pfx 226 | *.publishsettings 227 | node_modules/ 228 | bower_components/ 229 | orleans.codegen.cs 230 | 231 | # RIA/Silverlight projects 232 | Generated_Code/ 233 | 234 | # Backup & report files from converting an old project file 235 | # to a newer Visual Studio version. Backup files are not needed, 236 | # because we have git ;-) 237 | _UpgradeReport_Files/ 238 | Backup*/ 239 | UpgradeLog*.XML 240 | UpgradeLog*.htm 241 | 242 | # SQL Server files 243 | *.mdf 244 | *.ldf 245 | 246 | # Business Intelligence projects 247 | *.rdl.data 248 | *.bim.layout 249 | *.bim_*.settings 250 | 251 | # Microsoft Fakes 252 | FakesAssemblies/ 253 | 254 | # Node.js Tools for Visual Studio 255 | .ntvs_analysis.dat 256 | 257 | # Visual Studio 6 build log 258 | *.plg 259 | 260 | # Visual Studio 6 workspace options file 261 | *.opt 262 | 263 | # Common IntelliJ Platform excludes 264 | .idea/** 265 | 266 | # Generated file in integration test folder 267 | /tests/GiraffeGenerator.NugetTests/*.fs 268 | /tests/GiraffeGenerator.IntegrationTests/Spec*.fs 269 | !/tests/GiraffeGenerator.IntegrationTests/Spec*Tests.fs 270 | 271 | # scripts 272 | **/*.fsx 273 | 274 | # package lock for SDK project is unnecessary 275 | /src/GiraffeGenerator.Sdk/packages.lock.json 276 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Szer/GiraffeGenerator/fd0fbdd780ce5060e716ebde36e6776cc0e875c8/CHANGELOG.md -------------------------------------------------------------------------------- /Directory.build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $([System.IO.Path]::GetFullPath("$(MSBuildThisFileDirectory)")) 5 | 0.5.0 6 | true 7 | 8 | 9 | 10 | Szer 11 | https://github.com/Szer/GiraffeGenerator/ 12 | fsharp;codegen;generation;giraffe;openapi 13 | git 14 | https://github.com/Szer/GiraffeGenerator.git 15 | Apache-2.0 16 | Ayrat Hudagyulov 17 | see $(PackageProjectUrl)blob/master/CHANGELOG.md#$(Version) 18 | 19 | 20 | -------------------------------------------------------------------------------- /GiraffeGenerator.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "GiraffeGenerator.Core", "src\GiraffeGenerator.Core\GiraffeGenerator.Core.fsproj", "{A16217A1-F6D4-42F1-B7B1-48B89DF9A66E}" 7 | EndProject 8 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "GiraffeGenerator", "src\GiraffeGenerator\GiraffeGenerator.fsproj", "{9CCC0436-A95B-4CF9-9871-8C702CAEE119}" 9 | EndProject 10 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Example", "src\Example\Example.fsproj", "{5EEC249F-404B-4011-B7AE-0ACEE99249C4}" 11 | EndProject 12 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "GiraffeGenerator.IntegrationTests", "tests\GiraffeGenerator.IntegrationTests\GiraffeGenerator.IntegrationTests.fsproj", "{96A7CC71-42B5-4EEA-8C82-5548F649FC0B}" 13 | EndProject 14 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "GiraffeGenerator.NugetTests", "tests\GiraffeGenerator.NugetTests\GiraffeGenerator.NugetTests.fsproj", "{78A9E55F-85D1-435C-B292-3E6693852429}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Debug|x64 = Debug|x64 20 | Debug|x86 = Debug|x86 21 | Release|Any CPU = Release|Any CPU 22 | Release|x64 = Release|x64 23 | Release|x86 = Release|x86 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 29 | {A16217A1-F6D4-42F1-B7B1-48B89DF9A66E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {A16217A1-F6D4-42F1-B7B1-48B89DF9A66E}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {A16217A1-F6D4-42F1-B7B1-48B89DF9A66E}.Debug|x64.ActiveCfg = Debug|Any CPU 32 | {A16217A1-F6D4-42F1-B7B1-48B89DF9A66E}.Debug|x64.Build.0 = Debug|Any CPU 33 | {A16217A1-F6D4-42F1-B7B1-48B89DF9A66E}.Debug|x86.ActiveCfg = Debug|Any CPU 34 | {A16217A1-F6D4-42F1-B7B1-48B89DF9A66E}.Debug|x86.Build.0 = Debug|Any CPU 35 | {A16217A1-F6D4-42F1-B7B1-48B89DF9A66E}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {A16217A1-F6D4-42F1-B7B1-48B89DF9A66E}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {A16217A1-F6D4-42F1-B7B1-48B89DF9A66E}.Release|x64.ActiveCfg = Release|Any CPU 38 | {A16217A1-F6D4-42F1-B7B1-48B89DF9A66E}.Release|x64.Build.0 = Release|Any CPU 39 | {A16217A1-F6D4-42F1-B7B1-48B89DF9A66E}.Release|x86.ActiveCfg = Release|Any CPU 40 | {A16217A1-F6D4-42F1-B7B1-48B89DF9A66E}.Release|x86.Build.0 = Release|Any CPU 41 | {9CCC0436-A95B-4CF9-9871-8C702CAEE119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {9CCC0436-A95B-4CF9-9871-8C702CAEE119}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {9CCC0436-A95B-4CF9-9871-8C702CAEE119}.Debug|x64.ActiveCfg = Debug|Any CPU 44 | {9CCC0436-A95B-4CF9-9871-8C702CAEE119}.Debug|x64.Build.0 = Debug|Any CPU 45 | {9CCC0436-A95B-4CF9-9871-8C702CAEE119}.Debug|x86.ActiveCfg = Debug|Any CPU 46 | {9CCC0436-A95B-4CF9-9871-8C702CAEE119}.Debug|x86.Build.0 = Debug|Any CPU 47 | {9CCC0436-A95B-4CF9-9871-8C702CAEE119}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {9CCC0436-A95B-4CF9-9871-8C702CAEE119}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {9CCC0436-A95B-4CF9-9871-8C702CAEE119}.Release|x64.ActiveCfg = Release|Any CPU 50 | {9CCC0436-A95B-4CF9-9871-8C702CAEE119}.Release|x64.Build.0 = Release|Any CPU 51 | {9CCC0436-A95B-4CF9-9871-8C702CAEE119}.Release|x86.ActiveCfg = Release|Any CPU 52 | {9CCC0436-A95B-4CF9-9871-8C702CAEE119}.Release|x86.Build.0 = Release|Any CPU 53 | {5EEC249F-404B-4011-B7AE-0ACEE99249C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {5EEC249F-404B-4011-B7AE-0ACEE99249C4}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {5EEC249F-404B-4011-B7AE-0ACEE99249C4}.Debug|x64.ActiveCfg = Debug|Any CPU 56 | {5EEC249F-404B-4011-B7AE-0ACEE99249C4}.Debug|x64.Build.0 = Debug|Any CPU 57 | {5EEC249F-404B-4011-B7AE-0ACEE99249C4}.Debug|x86.ActiveCfg = Debug|Any CPU 58 | {5EEC249F-404B-4011-B7AE-0ACEE99249C4}.Debug|x86.Build.0 = Debug|Any CPU 59 | {5EEC249F-404B-4011-B7AE-0ACEE99249C4}.Release|Any CPU.ActiveCfg = Release|Any CPU 60 | {5EEC249F-404B-4011-B7AE-0ACEE99249C4}.Release|Any CPU.Build.0 = Release|Any CPU 61 | {5EEC249F-404B-4011-B7AE-0ACEE99249C4}.Release|x64.ActiveCfg = Release|Any CPU 62 | {5EEC249F-404B-4011-B7AE-0ACEE99249C4}.Release|x64.Build.0 = Release|Any CPU 63 | {5EEC249F-404B-4011-B7AE-0ACEE99249C4}.Release|x86.ActiveCfg = Release|Any CPU 64 | {5EEC249F-404B-4011-B7AE-0ACEE99249C4}.Release|x86.Build.0 = Release|Any CPU 65 | {96A7CC71-42B5-4EEA-8C82-5548F649FC0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 66 | {96A7CC71-42B5-4EEA-8C82-5548F649FC0B}.Debug|Any CPU.Build.0 = Debug|Any CPU 67 | {96A7CC71-42B5-4EEA-8C82-5548F649FC0B}.Debug|x64.ActiveCfg = Debug|Any CPU 68 | {96A7CC71-42B5-4EEA-8C82-5548F649FC0B}.Debug|x64.Build.0 = Debug|Any CPU 69 | {96A7CC71-42B5-4EEA-8C82-5548F649FC0B}.Debug|x86.ActiveCfg = Debug|Any CPU 70 | {96A7CC71-42B5-4EEA-8C82-5548F649FC0B}.Debug|x86.Build.0 = Debug|Any CPU 71 | {96A7CC71-42B5-4EEA-8C82-5548F649FC0B}.Release|Any CPU.ActiveCfg = Release|Any CPU 72 | {96A7CC71-42B5-4EEA-8C82-5548F649FC0B}.Release|Any CPU.Build.0 = Release|Any CPU 73 | {96A7CC71-42B5-4EEA-8C82-5548F649FC0B}.Release|x64.ActiveCfg = Release|Any CPU 74 | {96A7CC71-42B5-4EEA-8C82-5548F649FC0B}.Release|x64.Build.0 = Release|Any CPU 75 | {96A7CC71-42B5-4EEA-8C82-5548F649FC0B}.Release|x86.ActiveCfg = Release|Any CPU 76 | {96A7CC71-42B5-4EEA-8C82-5548F649FC0B}.Release|x86.Build.0 = Release|Any CPU 77 | {78A9E55F-85D1-435C-B292-3E6693852429}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 78 | {78A9E55F-85D1-435C-B292-3E6693852429}.Debug|Any CPU.Build.0 = Debug|Any CPU 79 | {78A9E55F-85D1-435C-B292-3E6693852429}.Debug|x64.ActiveCfg = Debug|Any CPU 80 | {78A9E55F-85D1-435C-B292-3E6693852429}.Debug|x64.Build.0 = Debug|Any CPU 81 | {78A9E55F-85D1-435C-B292-3E6693852429}.Debug|x86.ActiveCfg = Debug|Any CPU 82 | {78A9E55F-85D1-435C-B292-3E6693852429}.Debug|x86.Build.0 = Debug|Any CPU 83 | {78A9E55F-85D1-435C-B292-3E6693852429}.Release|Any CPU.ActiveCfg = Release|Any CPU 84 | {78A9E55F-85D1-435C-B292-3E6693852429}.Release|Any CPU.Build.0 = Release|Any CPU 85 | {78A9E55F-85D1-435C-B292-3E6693852429}.Release|x64.ActiveCfg = Release|Any CPU 86 | {78A9E55F-85D1-435C-B292-3E6693852429}.Release|x64.Build.0 = Release|Any CPU 87 | {78A9E55F-85D1-435C-B292-3E6693852429}.Release|x86.ActiveCfg = Release|Any CPU 88 | {78A9E55F-85D1-435C-B292-3E6693852429}.Release|x86.Build.0 = Release|Any CPU 89 | EndGlobalSection 90 | EndGlobal 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Giraffe Generator 2 | [![.NET Core CI](https://github.com/Szer/GiraffeGenerator/workflows/.NET%20Core/badge.svg?branch=master)](https://github.com/Szer/GiraffeGenerator/actions?query=workflow%3A%22.NET+Core%22) 3 | 4 | 5 | This is first naive version of Giraffe server generator from OpenAPI specification 6 | 7 | I believe in "contract-first" approach, and your OpenAPI spec is basically contract for your API 8 | 9 | Backend code is just an implementation of this contract and client doesn't really want to know about it 10 | 11 | Neither should you, so this library will help you with that 12 | 13 | It follows [Myriad](https://github.com/MoiraeSoftware/myriad) approach and defines MSBuild target to generate code based on input 14 | 15 | ## Example project is available to check [here](https://github.com/Szer/GiraffeGenerator/tree/master/src/Example) 16 | 17 | ## Future feature list (basically TODO list): 18 | 19 | - [ ] Creating models from OpenAPI schemas 20 | - [x] records generated from schema definitions with all data types from spec 21 | - [x] handlers in generated webApp should support these types 22 | - [x] default values support for primitive types 23 | - [ ] `oneOf` support 24 | - [ ] `anyOf` support 25 | - [ ] `allOf` support 26 | - [ ] `discriminator` support 27 | - [x] `not` *won't be supported* 28 | - [x] validation support (#33) 29 | - [x] NodaTime support opt-in (#32) 30 | - [x] Multiple responses from one endpoint 31 | - [ ] Creating endpoints with support for bindings 32 | - [x] path 33 | - [x] query 34 | - [x] body 35 | - [ ] header (#35) 36 | - [ ] cookie (#35) 37 | - [ ] content-type negotiated body (#36) 38 | - [x] binding error handling 39 | - [x] Add XML comments on top of endpoint from OpenAPI descriptions 40 | - [ ] Support authentication (best efforts) 41 | - [ ] Support JSON/XML (de-)serialization 42 | - [x] JSON 43 | - [ ] XML 44 | 45 | ## How to use 46 | 47 | - Add nuget `GiraffeGenerator.Sdk` 48 | - Create OpenAPI spec file 49 | - Add generated file to your project file: 50 | ``` 51 | 52 | spec.yaml 53 | 54 | ``` 55 | - Build project to generate the file 56 | - Implement interface defined in this file and register your implementation in AspNetCore DI 57 | - If you want to customize validation for some or all of your models, you have two extension points: 58 | - `IGiraffeValidator<'model>` - replaces all generated validation for the `'model` type. 59 | Note: doesn't replace the validation for nested complex types (objects, arrays, options). 60 | Provide an implementation for them explicitely if you want to replace validation for them too. 61 | - `IGiraffeAdditionalValidator<'model>` - adds more validation 62 | to either `IGiraffeValidator<'model>` or generated validation 63 | - Note for people migrating from/to *nix: `System.ComponentModel.DataAnnotations.RangeAttribute` used 64 | by validation produces a different text representation for Double.*Infinity on *nix: 65 | "Infinity" instead of infinity unicode symbol (ÝE;) 66 | - May require serializer configuration to support mapping of absent and null values from/to Optionon<_> 67 | - May require serializer configuration to throw on absent required properties 68 | 69 | ## Codegen configuration 70 | 71 | All configuration is done by adding more child tags to Compile object. 72 | There are two types of configuration: parameterless (flags) and parameterfull (parameters). 73 | 74 | `flag` is specified like `true`: absense of tag or any content except for `true` is treated as `false` 75 | 76 | `parameter` is passed as tag content like `your value` 77 | 78 | Example of both: 79 | ``` 80 | 81 | spec.yaml 82 | true 83 | 84 | ``` 85 | 86 | ### Generated module name customization 87 | Defined as parameter `OpenApiModuleName` 88 | 89 | ### Allowing non-qualified access 90 | Defined as flag `OpenApiAllowUnqualifiedAccess` 91 | 92 | ### NodaTime support 93 | Enabled by `OpenApiUseNodaTime` flag. 94 | 95 | Has optional parameter `OpenApiMapDateTimeInto` which controls generated type for `date-time` OpenAPI string format. 96 | Has four possible values: 97 | - `instant` 98 | - `local-date-time` 99 | - `offset-date-time` (default) 100 | - `zoned-date-time` 101 | 102 | Adds support for the following custom string formats: 103 | | Format | NodaTime type | Supports default values (format) | 104 | | ---------------- | -------------------------------------- | -------------------------------- | 105 | | local-date | LocalDate | [x] `uuuu'-'MM'-'dd (c)` 106 | | date | LocalDate | [x] (as above) 107 | | date-time | Differs (see `OpenApiMapDateTimeInto`) | [~] By configured format 108 | | instant | Instant | [x] `uuuu'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFFF'Z'` 109 | | local-time | LocalTime | [x] `HH':'mm':'ss.FFFFFFFFF` 110 | | time | LocalTime | [x] (as above) 111 | | local-date-time | LocalDateTime | [x] `uuuu'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFFF (c)` 112 | | offset-date-time | OffsetDateTime | [x] `uuuu'-'MM'-'dd'T'HH':'mm':'ss;FFFFFFFFFo (c)` 113 | | zoned-date-time | ZonedDateTime | [ ] No 114 | | offset | Offset | [x] general pattern, e.g. +05 or -03:30 115 | | time-offset | Offset | [x] (as above) 116 | | duration | Duration | [x] -H:mm:ss.FFFFFFFFF 117 | | period | Interval | [x] ISO8601 Duration (round-trip) 118 | | time-zone | DateTimeZone | [x] IANA Tzdb identifier 119 | | date-time-zone | DateTimeZone | [x] (as above) 120 | 121 | Usage requires installation of NodaTime 3+ nuget at least. 122 | For some content types of bodies containing NodaTime types [additional packages may be needed](https://nodatime.org/3.0.x/userguide/serialization). 123 | 124 | Note that zoned-date-time cannot be passed in query string or path parameters by default. 125 | Also note that (de)serialization duration format differs for query string binding and 126 | Json.NET serialization by default. See [this test](https://github.com/bessgeor/GiraffeGenerator/blob/feature/noda-time-support__%2332/tests/GiraffeGenerator.IntegrationTests/SpecGeneralForNodaTimeTests.fs#L115) for more details. 127 | 128 | ## How it works internally 129 | 130 | - It parses OpenAPI spec with package `Microsoft.OpenApi.Readers` to intermediate representation (IR) 131 | - Then it creates F# AST based on that IR 132 | - Finally it produces source code file with help of `Fantomas` 133 | 134 | ## How to build and test 135 | 136 | 1. Restore tools: `dotnet tool restore` 137 | 1. `dotnet pwsh build.ps1` 138 | 139 | At this stage there is no NuGet package publishing and packages are being published locally 140 | 141 | To consume them in `Example` project there is local `NuGet.Config` with local repo added 142 | 143 | After first full `build&pack` you could delete `Generated.fs` file from `Example` project and build it again to see that it actually generates on build 144 | 145 | ## How to publish 146 | 147 | 1. Make sure you have nuget API key set in `GIRAFFE_GENERATOR_NUGET` env 148 | 1. Update version in `Directory.build.props` 149 | 1. Put whatever you haven't put into `Unreleased` section of CHANGELOG 150 | 1. Run `./publish.ps1` 151 | - It will ask you for random number (as protection from accidental runs) 152 | - It will ask you whether you changed the version and updated CHANGELOG 153 | - It will parse new version from the `Directory.build.props` 154 | - Hard reset with git clean to latest master (stashing and poping props and CHANGELOG) 155 | - Run `./build.ps1` (compilation + test) 156 | - Check that tag with that version doesn't exist 157 | - Check that last version in changelog is lower 158 | - Will update CHANGELOG on major and minor (not patch) updates 159 | - It will replace `Unreleased` section with new version and will put a date on it 160 | - Put link to the bottom section called Changes (as git diff) 161 | - Will commit "release vX.Y.Z" with a tag 162 | - Will push artifacts to nuget at the end 163 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | 3 | $matches = Select-String -path Directory.build.props '(\d+\.\d+\.\d+)' 4 | $version = $matches.Matches[0].Groups[1].Value 5 | 6 | dotnet tool restore 7 | dotnet pack src/GiraffeGenerator.Sdk -c Release -o bin/nupkg /p:Version=$version 8 | dotnet pack src/GiraffeGenerator -c Release -o bin/nupkg /p:Version=$version 9 | dotnet mergenupkg --source bin/nupkg/GiraffeGenerator.Sdk.$version.nupkg --other bin/nupkg/GiraffeGenerator.$version.nupkg --tools --only-files 10 | dotnet pack src/GiraffeGenerator.Core -c Release -o bin/nupkg /p:Version=$version 11 | dotnet restore --force --no-cache 12 | 13 | dotnet build src/Example -c Release 14 | dotnet build tests/GiraffeGenerator.NugetTests -c Release 15 | dotnet test 16 | -------------------------------------------------------------------------------- /publish.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | 3 | # protection from accident runs 4 | $rndNumber = Get-Random -Maximum 999 -Minimum 100 5 | Write-Host "Please enter number $rndNumber to continue: " 6 | $inputNumber = Read-Host 7 | if ($inputNumber -ne $rndNumber) { exit } 8 | 9 | Write-Host "Have you bumped version and added all changes to changelog? (y/n)" 10 | $inputCh = Read-Host 11 | if ($inputCh -ne "y") { exit } 12 | 13 | #read new version from dir.build.props 14 | $matches = Select-String -path Directory.build.props '(\d+)\.(\d+)\.(\d+)' 15 | $major = $matches.Matches[0].Groups[1].Value 16 | $minor = $matches.Matches[0].Groups[2].Value 17 | $patch = $matches.Matches[0].Groups[3].Value 18 | $version = "$major.$minor.$patch" 19 | 20 | #save dir.build.props and changelog for later 21 | git stash clear 22 | git stash push Directory.build.props 23 | git stash push CHANGELOG.md 24 | 25 | #fetch tags and hard reset current master to origin master 26 | git fetch --all --tags 27 | git checkout master 28 | git reset --hard origin/master 29 | git clean -xfd 30 | 31 | #unstash dir.build.props and changelog 32 | git stash pop 33 | git stash pop 34 | 35 | #run build script early to make sure everything works 36 | ./build.ps1 37 | if( $LASTEXITCODE -ne 0 ) { 38 | throw "errors on build" 39 | } 40 | 41 | #check that this tag hasn't been released yet 42 | $tag_exists = git tag -l $version 43 | if ($null -ne $tag_exists) { 44 | throw "Tag already exists" 45 | } 46 | 47 | #check that last version in changelog is lower 48 | $old_matches = Select-String -path CHANGELOG.md '## \[(\d+)\.(\d+)\.(\d+)\]' 49 | $old_major = 0 50 | $old_minor = 0 51 | $old_patch = 0 52 | 53 | if ($null -ne $old_matches) { 54 | $old_major = $old_matches.Matches[0].Groups[1].Value 55 | $old_minor = $old_matches.Matches[0].Groups[2].Value 56 | $old_patch = $old_matches.Matches[0].Groups[3].Value 57 | } 58 | 59 | if ($major -lt $old_major) { 60 | throw "major version can't be lower than current one" 61 | } elseif ($major -eq $old_major) { 62 | if($minor -lt $old_minor) { 63 | throw "minor version can't be lower than current one" 64 | } elseif ($minor -eq $old_minor) { 65 | if($patch -le $old_patch) { 66 | throw "patch version can't be lower or equal than current one" 67 | } 68 | } 69 | } 70 | 71 | # we don't want to change changelog on patch updates 72 | if ($major -ne $old_major -or $minor -ne $old_minor) { 73 | 74 | #put version and current date in changelog 75 | $date = Get-Date -UFormat "%Y-%m-%d" 76 | (Get-Content CHANGELOG.md) ` 77 | -replace '## \[Unreleased\]$', "## [Unreleased]`n`n## [$version] - $date" | 78 | Out-File CHANGELOG.md 79 | 80 | #put link to changes at bottom 81 | $repo_link = "https://github.com/Szer/GiraffeGenerator/compare" 82 | $compareString = Select-String -path CHANGELOG.md "\[Unreleased\]: $repo_link/(.*)\.\.\.(.*)$" 83 | if($null -eq $compareString) {throw "can't find unreleased link at the bottom of CHANGELOG"} 84 | 85 | $from = $compareString.Matches[0].Groups[1].Value 86 | 87 | $newLinks = "[Unreleased]: https://github.com/Szer/GiraffeGenerator/compare/v$version...master`n[$version]: https://github.com/Szer/GiraffeGenerator/compare/$from...v$version" 88 | (Get-Content CHANGELOG.md) ` 89 | -replace "\[Unreleased\]: $repo_link/.*\.\.\..*$", $newLinks | 90 | Out-File CHANGELOG.md 91 | } 92 | 93 | #commit dir.build.props with changelog with message "release {version}" 94 | git commit -m "release v$version" -a --allow-empty 95 | git tag -a "v$version" -m "release v$version" 96 | 97 | #push to master with tags 98 | git push --follow-tags 99 | 100 | #push to master#publish two packages to nuget 101 | $apikey = Get-ChildItem Env:GIRAFFE_GENERATOR_NUGET 102 | dotnet nuget push "bin/nupkg/GiraffeGenerator.Core.$version.nupkg" --api-key $apikey.Value -s https://api.nuget.org/v3/index.json 103 | dotnet nuget push "bin/nupkg/GiraffeGenerator.Sdk.$version.nupkg" --api-key $apikey.Value -s https://api.nuget.org/v3/index.json 104 | -------------------------------------------------------------------------------- /src/Example/Example.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | exe 6 | 7 | 8 | 9 | 10 | spec.yaml 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Example/Generated.fs: -------------------------------------------------------------------------------- 1 | /// 2 | ///This is very simple API 3 | /// 4 | [] 5 | module SimpleAPIoverview 6 | 7 | open System.ComponentModel.DataAnnotations 8 | open FSharp.Control.Tasks.V2.ContextInsensitive 9 | open Giraffe 10 | open System.Threading.Tasks 11 | open Microsoft.AspNetCore.Http 12 | open Microsoft.Extensions.DependencyInjection 13 | 14 | ///replaces any generated validation rules for type 15 | [] 16 | type 'model IGiraffeValidator = 17 | abstract Validate: ('model * ValidationContext) -> ValidationResult array 18 | 19 | /// 20 | ///This is apis 21 | /// 22 | [] 23 | type apis = 24 | { apiKey: string option 25 | apiVersionNumber: string option 26 | apiUrl: System.Uri option 27 | apiCount: int64 option 28 | apiAvg: double option 29 | isInternal: bool option 30 | start: System.DateTime option 31 | apiHash: byte array option } 32 | 33 | /// 34 | ///This is data set list 35 | /// 36 | and [] dataSetList = 37 | { total: int option 38 | apis: apis array option } 39 | 40 | /// 41 | ///Input binding error 42 | /// 43 | and ArgumentError = /// 44 | ///Bound argument is not valid 45 | /// 46 | ArgumentValidationError of ValidationResult array 47 | 48 | /// 49 | ///Location argument error 50 | /// 51 | and ArgumentLocationedError = 52 | /// 53 | ///Body error 54 | /// 55 | | BodyBindingError of ArgumentError 56 | /// 57 | ///Query error 58 | /// 59 | | QueryBindingError of ArgumentError 60 | /// 61 | ///Path error 62 | /// 63 | | PathBindingError of ArgumentError 64 | /// 65 | ///Multiple locations errors 66 | /// 67 | | CombinedArgumentLocationError of ArgumentLocationedError array 68 | 69 | let rec argErrorToString level value = 70 | let sep = System.String(' ', level * 2) 71 | match value with 72 | | ArgumentValidationError err -> 73 | let errStrings = 74 | Option.ofObj err 75 | |> Option.defaultValue Array.empty 76 | |> Array.map (fun v -> 77 | let path = 78 | Option.ofObj v.MemberNames |> Option.map (String.concat ".") 79 | 80 | let error = Option.ofObj v.ErrorMessage 81 | Option.map2 (sprintf "%s (at %s)") error path 82 | |> Option.orElse error 83 | |> Option.orElse path 84 | |> Option.defaultValue "unknown validation error") 85 | 86 | if errStrings |> Array.length = 0 then 87 | sprintf "%sUnknown validation error" sep 88 | else if errStrings |> Array.length = 1 then 89 | errStrings 90 | |> Array.head 91 | |> sprintf "%sValidation error: %s" sep 92 | else 93 | let sepInner = sprintf "\n%s " sep 94 | errStrings 95 | |> String.concat sepInner 96 | |> sprintf "%sValidation errors:%s%s" sep sepInner 97 | 98 | let rec argLocationErrorToString level value = 99 | let sep = System.String(' ', level * 2) 100 | match value with 101 | | BodyBindingError body -> sprintf "%sBody binding error:\n%s" sep (argErrorToString (level + 1) body) 102 | | PathBindingError path -> sprintf "%sPath binding error:\n%s" sep (argErrorToString (level + 1) path) 103 | | QueryBindingError query -> sprintf "%sQuery binding error:\n%s" sep (argErrorToString (level + 1) query) 104 | | CombinedArgumentLocationError err -> 105 | sprintf 106 | "%sMultiple binding errors:\n%s" 107 | sep 108 | (String.concat "\n\n" (Seq.map (argLocationErrorToString (level + 1)) err)) 109 | 110 | let tryExtractError value = 111 | match value with 112 | | Ok _ -> None 113 | | Error err -> Some err 114 | 115 | let isObjectValid boxed errors validationContext = 116 | Validator.TryValidateObject(boxed, validationContext, errors, true) 117 | 118 | let isValueValid validationAttributes boxed errors validationContext = 119 | Validator.TryValidateValue(boxed, validationContext, errors, validationAttributes) 120 | 121 | let validateInner isValid (ctx: HttpContext) validationContext (value: 'model) = 122 | let customValidator = 123 | ctx.RequestServices.GetService>() 124 | 125 | let errs = 126 | if System.Object.ReferenceEquals(customValidator, null) then 127 | let errs = System.Collections.Generic.List() 128 | if isValid value errs validationContext then Array.empty else errs |> Seq.toArray 129 | else 130 | customValidator.Validate(value, validationContext) 131 | 132 | errs 133 | 134 | let withValue (validationContext: ValidationContext) value = 135 | let ctx = 136 | ValidationContext(value, validationContext.Items) 137 | 138 | ctx.InitializeServiceProvider(fun t -> validationContext.GetService t) 139 | ctx.MemberName <- null 140 | ctx 141 | 142 | 143 | let withMemberAndValue (validationContext: ValidationContext) name value = 144 | let ctx = withValue validationContext value 145 | ctx.MemberName <- name 146 | ctx 147 | 148 | 149 | let rec validate ctx (validationContext: ValidationContext) = 150 | let instance = validationContext.ObjectInstance 151 | [| match instance with 152 | | v -> failwithf "Unknown type came to validation: %A" (v.GetType()) |] 153 | 154 | let bindValidation (ctx: HttpContext) location (value: 'model) = 155 | let validationContext = 156 | ValidationContext(value, ctx.RequestServices, ctx.Items) 157 | 158 | let errs = validate ctx validationContext 159 | if (errs |> Array.length) = 0 then 160 | Ok value 161 | else 162 | errs 163 | |> ArgumentValidationError 164 | |> location 165 | |> Error 166 | 167 | [] 168 | type Service() = 169 | /// 170 | ///This is very cool API for list API versions 171 | /// 172 | /// 173 | ///List API versions 174 | /// 175 | abstract ListVersionsv2: HttpHandler 176 | 177 | override this.ListVersionsv2 = 178 | fun next ctx -> 179 | task { 180 | let! logicOutput = this.ListVersionsv2Input ctx 181 | return! this.ListVersionsv2Output logicOutput next ctx 182 | } 183 | 184 | abstract ListVersionsv2Input: HttpContext -> Task> 185 | abstract ListVersionsv2Output: Choice -> HttpHandler 186 | 187 | override this.ListVersionsv2Output input = 188 | match input with 189 | | Choice1Of2 responseOn200 -> json responseOn200 190 | | Choice2Of2 responseOn300 -> setStatusCode 300 >=> json responseOn300 191 | 192 | /// 193 | ///List API version details 194 | /// 195 | abstract GetVersionDetailsv2: HttpHandler 196 | 197 | override this.GetVersionDetailsv2 = 198 | fun next ctx -> 199 | task { 200 | let! logicOutput = this.GetVersionDetailsv2Input ctx 201 | return! this.GetVersionDetailsv2Output logicOutput next ctx 202 | } 203 | 204 | abstract GetVersionDetailsv2Input: HttpContext -> Task> 205 | abstract GetVersionDetailsv2Output: Choice<{| subscriptionId: string option |}, bool> -> HttpHandler 206 | 207 | override this.GetVersionDetailsv2Output input = 208 | match input with 209 | | Choice1Of2 responseOn200 -> json responseOn200 210 | | Choice2Of2 responseOn203 -> setStatusCode 203 >=> json responseOn203 211 | 212 | let webApp: HttpHandler = 213 | fun next ctx -> 214 | task { 215 | let service = ctx.GetService() 216 | return! choose 217 | [ (GET >=> route "/" >=> service.ListVersionsv2) 218 | (GET 219 | >=> route "/v2" 220 | >=> service.GetVersionDetailsv2) 221 | ] 222 | next 223 | ctx 224 | } 225 | -------------------------------------------------------------------------------- /src/Example/Program.fs: -------------------------------------------------------------------------------- 1 | module Program 2 | 3 | open FSharp.Control.Tasks.V2.ContextInsensitive 4 | open System 5 | open Giraffe 6 | open Microsoft.AspNetCore.Builder 7 | open Microsoft.AspNetCore.Hosting 8 | open Microsoft.Extensions.DependencyInjection 9 | open Microsoft.Extensions.Hosting 10 | open Microsoft.Extensions.Logging 11 | 12 | let exampleService = 13 | { new SimpleAPIoverview.Service() with 14 | member _.ListVersionsv2Input ctx = task { 15 | return 16 | if DateTime.Now.Ticks / 10L % 2L = 0L then 17 | Choice1Of2 { SimpleAPIoverview.dataSetList.apis = Some [||]; total = Some 123 } 18 | else 19 | Choice2Of2 true 20 | } 21 | 22 | member _.GetVersionDetailsv2Input ctx = task { 23 | return 24 | if DateTime.Now.Ticks / 10L % 2L = 0L then 25 | Choice1Of2 {| subscriptionId = Some "hello" |} 26 | else 27 | Choice2Of2 false 28 | } 29 | } 30 | 31 | let configureApp (app : IApplicationBuilder) = 32 | app.UseGiraffe SimpleAPIoverview.webApp 33 | 34 | let configureServices (services : IServiceCollection) = 35 | services 36 | .AddGiraffe() 37 | .AddSingleton(exampleService) 38 | |> ignore 39 | 40 | let configureLogging (loggerBuilder : ILoggingBuilder) = 41 | loggerBuilder.AddConsole() 42 | .AddDebug() |> ignore 43 | 44 | [] 45 | let main _ = 46 | Host.CreateDefaultBuilder() 47 | .ConfigureWebHostDefaults( 48 | fun webHostBuilder -> 49 | webHostBuilder 50 | .Configure(configureApp) 51 | .ConfigureServices(configureServices) 52 | .ConfigureLogging(configureLogging) 53 | .UseUrls("http://*:5005") 54 | |> ignore) 55 | .Build() 56 | .Run() 57 | 0 58 | -------------------------------------------------------------------------------- /src/Example/spec.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | title: Simple API overview 4 | description: This is very simple API 5 | version: 2.0.0 6 | paths: 7 | /: 8 | get: 9 | operationId: listVersionsv2 10 | summary: List API versions 11 | description: This is very cool API for list API versions 12 | responses: 13 | '200': 14 | description: |- 15 | 200 response 16 | content: 17 | application/json: 18 | schema: 19 | $ref: '#/components/schemas/dataSetList' 20 | '300': 21 | description: A bool response 22 | content: 23 | application/json: 24 | schema: 25 | type: boolean 26 | /v2: 27 | get: 28 | operationId: getVersionDetailsv2 29 | summary: List API version details 30 | responses: 31 | '200': 32 | description: This is even cooler API for listing detail versions 33 | content: 34 | application/json: 35 | schema: 36 | properties: 37 | subscriptionId: 38 | type: string 39 | '203': 40 | description: This is even cooler API for listing detail versions 41 | content: 42 | application/json: 43 | schema: 44 | type: boolean 45 | components: 46 | schemas: 47 | dataSetList: 48 | description: This is data set list 49 | type: object 50 | properties: 51 | total: 52 | type: integer 53 | apis: 54 | type: array 55 | items: 56 | type: object 57 | description: This is apis 58 | properties: 59 | apiKey: 60 | type: string 61 | apiVersionNumber: 62 | type: string 63 | apiUrl: 64 | type: string 65 | format: uriref 66 | apiCount: 67 | type: integer 68 | format: int64 69 | apiAvg: 70 | type: number 71 | isInternal: 72 | type: boolean 73 | start: 74 | type: string 75 | format: date 76 | apiHash: 77 | type: string 78 | format: byte -------------------------------------------------------------------------------- /src/GiraffeGenerator.Core/ASTExt.fs: -------------------------------------------------------------------------------- 1 | module ASTExt 2 | 3 | open AST 4 | 5 | let _id = identExpr "id" 6 | let _not = identExpr "not" 7 | let seqChooseId = app (longIdentExpr "Seq.choose") _id 8 | 9 | module Array = 10 | let mapExpr = longIdentExpr "Array.map" |> app 11 | let lengthExpr = longIdentExpr "Array.length" 12 | let headExpr = longIdentExpr "Array.head" 13 | 14 | module Seq = 15 | let collectExpr = longIdentExpr "Seq.collect" |> app 16 | let chooseExpr = longIdentExpr "Seq.choose" |> app 17 | let mapExpr = longIdentExpr "Seq.map" |> app 18 | let filterExpr = longIdentExpr "Seq.filter" |> app 19 | let reduceExpr reducer initial = app (app (longIdentExpr "Seq.reduce") reducer) initial 20 | let containsExpr = longIdentExpr "Seq.contains" |> app 21 | 22 | module Option = 23 | let bindExpr = longIdentExpr "Option.bind" |> app 24 | let mapExpr = longIdentExpr "Option.map" |> app 25 | let map2Expr f a b = app (app (app (longIdentExpr "Option.map2") f) a) b 26 | let orElseExpr = longIdentExpr "Option.orElse" |> app 27 | let defaultValueExpr = longIdentExpr "Option.defaultValue" |> app 28 | let ofObjExpr = longIdentExpr "Option.ofObj" |> app 29 | let isSomeExpr = longIdentExpr "Option.isSome" 30 | 31 | module Result = 32 | let mapExpr = longIdentExpr "Result.map" |> app 33 | let bindExpr = longIdentExpr "Result.bind" |> app 34 | let mapErrorExpr = longIdentExpr "Result.mapError" |> app 35 | 36 | module String = 37 | let concatExprComplex delimiterExpr = app (longIdentExpr "String.concat") delimiterExpr 38 | let concatExpr delimiter = concatExprComplex (strExpr delimiter) -------------------------------------------------------------------------------- /src/GiraffeGenerator.Core/CodeGenErrorsDU.fs: -------------------------------------------------------------------------------- 1 | module CodeGenErrorsDU 2 | open AST 3 | open ASTExt 4 | open FSharp.Compiler.SyntaxTree 5 | open OpenApi 6 | 7 | // generate binding errors DU and stringifiers 8 | let errInnerTypeName = "ArgumentError" 9 | let errInnerGiraffeBinding = "GiraffeBindingError" 10 | let errInnerFormatterBindingExn = "FormatterBindingException" 11 | let errInnerValidationError = "ArgumentValidationError" 12 | let errInnerCombined = "CombinedArgumentErrors" 13 | let private summary v = Some { Summary = Some [v]; Example = None; Remarks = None; } 14 | let private errInnerType shouldGenerateNonValidation = 15 | DU { 16 | Name = errInnerTypeName 17 | Docs = summary "Input binding error" 18 | Cases = 19 | [ 20 | { 21 | CaseName = Some errInnerValidationError 22 | Docs = summary "Bound argument is not valid" 23 | Kind = TypeKind.Array (BuiltIn "ValidationResult", None, None) 24 | } 25 | if shouldGenerateNonValidation then 26 | { 27 | CaseName = Some errInnerGiraffeBinding 28 | Docs = summary "Giraffe returned a Result.Error from tryBindXXX" 29 | Kind = Prim <| PrimTypeKind.String (StringFormat.String None) 30 | } 31 | { 32 | CaseName = Some errInnerFormatterBindingExn 33 | Docs = summary "Exception occurred during IFormatter bind" 34 | Kind = BuiltIn "exn" 35 | } 36 | { 37 | CaseName = Some errInnerCombined 38 | Docs = summary "Multiple errors occurred in one location" 39 | Kind = TypeKind.Array (DU { Name = errInnerTypeName; Docs = None; Cases = [] }, None, None) 40 | } 41 | ] 42 | } 43 | let errOuterTypeName = "ArgumentLocationedError" 44 | let errOuterBody = "BodyBindingError" 45 | let errOuterQuery = "QueryBindingError" 46 | let errOuterPath = "PathBindingError" 47 | let errOuterCombined = "CombinedArgumentLocationError" 48 | let private errOuterType errInnerType = 49 | DU { 50 | Name = errOuterTypeName 51 | Docs = summary "Location argument error" 52 | Cases = 53 | [ 54 | { 55 | CaseName = Some errOuterBody 56 | Docs = summary "Body error" 57 | Kind = errInnerType 58 | } 59 | { 60 | CaseName = Some errOuterQuery 61 | Docs = summary "Query error" 62 | Kind = errInnerType 63 | } 64 | { 65 | CaseName = Some errOuterPath 66 | Docs = summary "Path error" 67 | Kind = errInnerType 68 | } 69 | { 70 | CaseName = Some errOuterCombined 71 | Docs = summary "Multiple locations errors" 72 | Kind = TypeKind.Array (DU { Name = errOuterTypeName; Docs = None; Cases = [] }, None, None) 73 | } 74 | ] 75 | } 76 | 77 | let innerErrToStringName = "argErrorToString" 78 | let private levelParam = "level" 79 | let private valueParam = "value" 80 | let private sepVar = "sep" 81 | let private err = "err" 82 | let private nextLevel = app (appI (identExpr "op_Addition") (identExpr levelParam)) (SynConst.Int32 1 |> constExpr) 83 | let private letSep = 84 | letExpr 85 | sepVar 86 | [] 87 | ( 88 | app 89 | (longIdentExpr "System.String") 90 | ( 91 | tupleComplexExpr 92 | [ 93 | SynConst.Char ' ' |> constExpr 94 | app (appI (identExpr "op_Multiply") (identExpr levelParam)) (SynConst.Int32 2 |> constExpr) 95 | ] 96 | ) 97 | ) 98 | 99 | let private innerErrToStringDecl shouldGenerateNonValidation = 100 | letDecl true innerErrToStringName [levelParam; valueParam] None 101 | ^ letSep 102 | ^ simpleValueMatching valueParam 103 | [ 104 | if shouldGenerateNonValidation then 105 | errInnerGiraffeBinding, err, sprintfExpr "%sGiraffe binding error: %s" [identExpr sepVar; identExpr err] 106 | errInnerFormatterBindingExn, err, longIdentExpr (sprintf "%s.Message" err) 107 | errInnerCombined, err, 108 | sprintfExpr "%sMultiple errors:\\n%s" 109 | ^ [identExpr sepVar 110 | app 111 | (String.concatExpr "\\n") 112 | ( 113 | app // Seq.map (recCall (level + 1)) 114 | (app (longIdentExpr "Seq.map") (paren(app (identExpr innerErrToStringName) (paren(nextLevel))))) 115 | (identExpr err) 116 | |> paren 117 | ) 118 | |> paren 119 | ] 120 | errInnerValidationError, err, 121 | letExpr "errStrings" [] 122 | ( 123 | Option.ofObjExpr (identExpr err) 124 | ^|> Option.defaultValueExpr (longIdentExpr "Array.empty") 125 | ^|> Array.mapExpr 126 | ( 127 | lambda (singleSimplePat "v") 128 | ( 129 | let letPath = 130 | letExpr "path" [] 131 | ( 132 | Option.ofObjExpr ^ longIdentExpr "v.MemberNames" 133 | ^|> Option.mapExpr ^ paren (String.concatExpr ".") 134 | ) 135 | let letError = 136 | letExpr "error" [] 137 | ( 138 | Option.ofObjExpr ^ longIdentExpr "v.ErrorMessage" 139 | ) 140 | let error = identExpr "error" 141 | let path = identExpr "path" 142 | let body = 143 | Option.map2Expr (sprintfExpr "%s (at %s)" [] |> paren) error path 144 | ^|> Option.orElseExpr error 145 | ^|> Option.orElseExpr path 146 | ^|> Option.defaultValueExpr (strExpr "unknown validation error") 147 | letPath ^ letError ^ body 148 | ) 149 | ) 150 | ) 151 | ( 152 | let errStrings = identExpr "errStrings" 153 | ifElseExpr (errStrings ^|> Array.lengthExpr ^= (intExpr 0)) 154 | (sprintfExpr "%sUnknown validation error" [identExpr sepVar]) 155 | ^ ifElseExpr (errStrings ^|> Array.lengthExpr ^= (intExpr 1)) 156 | (errStrings ^|> Array.headExpr ^|> sprintfExpr "%sValidation error: %s" [identExpr sepVar]) 157 | ^ letExpr "sepInner" [] (sprintfExpr "\\n%s " [identExpr sepVar]) 158 | ( 159 | errStrings 160 | ^|> String.concatExprComplex (identExpr "sepInner") 161 | ^|> sprintfExpr "%sValidation errors:%s%s" [identExpr sepVar; identExpr "sepInner"] 162 | ) 163 | ) 164 | ] 165 | 166 | let private callInnerWithFormat format var = 167 | sprintfExpr format [ identExpr sepVar; paren ( app (app (identExpr innerErrToStringName) (paren nextLevel)) (identExpr var)) ] 168 | 169 | let outerErrToStringName = "argLocationErrorToString" 170 | let private outerErrToStringDecl = 171 | letDecl true outerErrToStringName [levelParam; valueParam] None 172 | ^ letSep 173 | ^ simpleValueMatching valueParam 174 | [ 175 | let common = 176 | [ 177 | errOuterBody, "body" 178 | errOuterPath, "path" 179 | errOuterQuery, "query" 180 | ] 181 | for (case, var) in common do 182 | let uppercase = System.String([| System.Char.ToUpperInvariant var.[0]; yield! var |> Seq.skip 1 |]) 183 | let format = sprintf "%%s%s binding error:\\n%%s" uppercase 184 | case, var, (callInnerWithFormat format var) 185 | 186 | errOuterCombined, err, 187 | sprintfExpr "%sMultiple binding errors:\\n%s" 188 | ^ [identExpr sepVar 189 | app 190 | (String.concatExpr "\\n\\n") 191 | ( 192 | app // Seq.map (recCall (level + 1)) 193 | (Seq.mapExpr (paren(app (identExpr outerErrToStringName) (paren(nextLevel))))) 194 | (identExpr err) 195 | |> paren 196 | ) 197 | |> paren 198 | ] 199 | ] 200 | 201 | let tryExtractErrorName = "tryExtractError" 202 | let private tryExtractErrorDecl = 203 | let value = "value" 204 | letDecl false tryExtractErrorName [value] None 205 | ^ matchExpr value 206 | [ 207 | clause (SynPat.LongIdent(longIdentWithDots "Ok", None, None, [SynPat.Wild r] |> Pats, None, r)) (identExpr "None") 208 | clause (SynPat.LongIdent(longIdentWithDots "Error", None, None, [SynPat.Named(SynPat.Wild(r), ident "err", false, None, r)] |> Pats, None, r)) 209 | ^ app (identExpr "Some") (identExpr "err") 210 | ] 211 | 212 | let typeSchemas shouldGenerateNonValidation = 213 | let inner = errInnerType shouldGenerateNonValidation 214 | [ errInnerTypeName, inner 215 | errOuterTypeName, errOuterType inner ] 216 | |> List.map (fun (name, kind) -> { DefaultValue = None; Name = name; Kind = kind; Docs = None }) 217 | 218 | let generateHelperFunctions shouldGenerateNonValidation = 219 | [ 220 | innerErrToStringDecl shouldGenerateNonValidation 221 | outerErrToStringDecl 222 | tryExtractErrorDecl 223 | ] -------------------------------------------------------------------------------- /src/GiraffeGenerator.Core/CodeGenValidation.fs: -------------------------------------------------------------------------------- 1 | module CodeGenValidation 2 | 3 | open OpenApi 4 | open CodeGenValidation_Types 5 | open CodeGenValidation_Types.Enumeration 6 | open CodeGenValidation_TypeGeneration 7 | open CodeGenValidation_TypeGeneration.ExtensionPoints 8 | open CodeGenValidation_InstanceGeneration 9 | open CodeGenValidation_LogicGeneration 10 | 11 | /// gets all validation attributes to be used in API 12 | let private decideWhichValidationIsBeingUsed api = 13 | seq { 14 | for path in api.Paths do 15 | for method in path.Methods do 16 | for parameter in method.AllParameters do 17 | yield! enumerateKindValidation true parameter.Kind 18 | } 19 | 20 | let generateModuleLevelDeclarations api = 21 | let usedValidation = decideWhichValidationIsBeingUsed api |> Seq.toArray 22 | let usedValidationTypes = 23 | usedValidation 24 | |> Seq.distinctBy identifyCustomValidationAttributeType 25 | |> Seq.sortBy string 26 | |> Seq.toArray 27 | let hasGeneratedValidationRules = usedValidationTypes.Length > 0 28 | [ 29 | for validationType in usedValidationTypes do 30 | let attrOption = generateAttributeDefinitionFor validationType 31 | if attrOption.IsSome then 32 | attrOption.Value 33 | generateValidationReplacerInterface() 34 | if hasGeneratedValidationRules then 35 | generateValidationAugmenterInterface() 36 | ], [ 37 | let replacerInterface = validationReplacerInterface 38 | let augmenterInterface = 39 | Some validationAugmenterInterface 40 | |> Option.filter (fun _ -> hasGeneratedValidationRules) 41 | yield! generateValidationBinder api replacerInterface augmenterInterface 42 | ] 43 | 44 | 45 | /// Applies validation to the resultExpr: 46 | /// {resultExpr} |> Result.bind (bindValidation isObjectValid ctx {location}) 47 | /// for objects and DUs or 48 | /// {resultExpr} |> Result.bind (bindValidation (isValueValid [|{attribute instances for this value}|]) ctx {location}) 49 | /// for any other kind of value 50 | let bindValidationIntoResult = bindValidationIntoResult 51 | 52 | let getValidationAttributesForProperty = getValidationAttributesForProperty -------------------------------------------------------------------------------- /src/GiraffeGenerator.Core/CodeGenValidation_InstanceGeneration.fs: -------------------------------------------------------------------------------- 1 | module CodeGenValidation_InstanceGeneration 2 | 3 | open CodeGenValidation_Types 4 | open CodeGenValidation_Types.Enumeration 5 | open AST 6 | open FSharp.Compiler.SyntaxTree 7 | 8 | // get the name and argument expression list for the attribute passed 9 | let getTypeMin (typeName: string) = 10 | let suffix = 11 | if typeName.EndsWith("Double") then 12 | ".NegativeInfinity" 13 | else ".MinValue" 14 | typeName + suffix |> longIdentExpr 15 | let getTypeMax (typeName: string) = 16 | let suffix = 17 | if typeName.EndsWith("Double") then 18 | ".PositiveInfinity" 19 | else ".MaxValue" 20 | typeName + suffix |> longIdentExpr 21 | let private rangeToExpressions typeName valueToConst r = 22 | let valueToExpr = valueToConst >> constExpr 23 | match r with 24 | | Min min -> [valueToExpr min; getTypeMax typeName] 25 | | Max max -> [getTypeMin typeName; valueToExpr max] 26 | | Both (min, max) -> [valueToExpr min; valueToExpr max] 27 | let private getAttributeUsageDefinitionForBuiltInAttribute = 28 | function 29 | | Required -> "Required", [] 30 | | FloatRange r -> "Range", rangeToExpressions "System.Double" SynConst.Double r 31 | | IntRange r -> "Range", rangeToExpressions "System.Int32" SynConst.Int32 r 32 | | MaxLength len -> "MaxLength", [intExpr len] 33 | | MinLength len -> "MinLength", [intExpr len] 34 | let private getEnumAttributeUsageDefinition = 35 | let convertArgs toConst values = 36 | values 37 | |> Seq.map toConst 38 | |> Seq.map constExpr 39 | |> Seq.toList 40 | function 41 | | IntEnum values -> "IntEnumValues", convertArgs SynConst.Int32 values 42 | | LongEnum values -> "LongEnumValues", convertArgs SynConst.Int64 values 43 | | FloatEnum values -> "FloatEnumValues", convertArgs SynConst.Double values 44 | | StringEnum values -> "StringEnumValues", convertArgs (fun v -> SynConst.String(v, r)) values 45 | let private getMultipleOfAttributeUsageDefinition = 46 | let convertArg toConst value = 47 | [toConst value |> constExpr] 48 | function 49 | | IntMultiple divisor -> "IntMultipleOf", convertArg SynConst.Int32 divisor 50 | | LongMultiple divisor -> "LongMultipleOf", convertArg SynConst.Int64 divisor 51 | let private getAttributeUsageDefinitionForSpecialCasedAttribute expr = 52 | function 53 | | BuiltInAttribute _ 54 | | CustomAttribute _ -> None 55 | | SpecialCasedCustomValidationAttribute specialCased -> 56 | match specialCased with 57 | | UniqueItems -> 58 | let exprLength = 59 | SynExpr.DotGet(expr, r, longIdentWithDots "Length", r) 60 | let set = 61 | app (identExpr "Set") (expr) |> paren 62 | let setCount = 63 | SynExpr.DotGet(set, r, longIdentWithDots "Count", r) 64 | Some ("UniqueItems", [exprLength ^= setCount]) 65 | let private getAttributeUsageDefinitionForCustomAttribute = 66 | function 67 | | LongRange r -> "LongRange", rangeToExpressions "System.Int64" SynConst.Int64 r 68 | | RegexPattern pattern -> "RegexPattern", [strExpr pattern] 69 | | EnumAttribute enum -> getEnumAttributeUsageDefinition enum 70 | | MultipleOf target -> getMultipleOfAttributeUsageDefinition target 71 | let rec private getAttributeUsageDefinitionForAttribute attr = 72 | match attr with 73 | | BuiltInAttribute builtIn -> getAttributeUsageDefinitionForBuiltInAttribute builtIn |> Some 74 | | CustomAttribute custom -> getAttributeUsageDefinitionForCustomAttribute custom |> Some 75 | | SpecialCasedCustomValidationAttribute _ -> None 76 | 77 | /// generates an array of isSpecialCased * array expr of attributeTypeName(attributeConstructorParameters) for a kind 78 | let getValidationAttributeConstructionForKind expr kind = 79 | [ 80 | for attr in enumerateKindValidation false kind do 81 | let commonDefinition = 82 | getAttributeUsageDefinitionForAttribute attr 83 | |> Option.map (fun x -> false, x) 84 | let specialCasedDefinition = 85 | getAttributeUsageDefinitionForSpecialCasedAttribute expr attr 86 | |> Option.map (fun x -> true, x) 87 | let definition = Option.orElse specialCasedDefinition commonDefinition 88 | 89 | if definition.IsSome then 90 | let isSpecialCased, (attrName, args) = definition.Value 91 | let arg = tupleComplexExpr args 92 | isSpecialCased, app (identExpr (attrName + "Attribute")) arg 93 | ] 94 | |> List.groupBy fst 95 | |> List.choose ^ fun (isSpecialCased, expressions) -> 96 | let attrConstruction = List.map snd expressions 97 | if attrConstruction.IsEmpty then None 98 | else (isSpecialCased, SynExpr.ArrayOrList(true, attrConstruction, r)) |> Some 99 | 100 | let getValidationAttributesForProperty kind = 101 | [ 102 | for attr in enumeratePropertyValidation false kind do 103 | let def = getAttributeUsageDefinitionForAttribute attr 104 | if def.IsSome then 105 | let attrName, args = def.Value 106 | attrComplex attrName (if args.IsEmpty then unitExpr else tupleComplexExpr args) 107 | ] 108 | -------------------------------------------------------------------------------- /src/GiraffeGenerator.Core/CodeGenValidation_TypeGeneration.fs: -------------------------------------------------------------------------------- 1 | module CodeGenValidation_TypeGeneration 2 | 3 | open FSharp.Compiler.SyntaxTree 4 | open CodeGenValidation_Types 5 | open AST 6 | open ASTExt 7 | 8 | /// codegen custom attributes for 9 | /// - multiplyOf 10 | /// - enum 11 | /// - uniqueItems 12 | /// - pattern (built-in one doesn't follow the OpenAPI spec: not ECMAScript and always tries to match the whole string) 13 | /// - minValue and maxValue for Int64 (as a Range attribute) 14 | /// Example: 15 | (* 16 | type IntEnumAttribute([]values: int array) = 17 | inherit ValidationAttribute(sprintf "must be a one of %s" (String.concat ", " (Seq.map string values))) 18 | override this.FormatErrorMessage name = sprintf "The field %s %s" name base.ErrorMessage 19 | override _.IsValid(value: obj) = 20 | let value = value :?> int 21 | values |> Array.contains value 22 | 23 | type StringEnumAttribute([]values: string array) = 24 | inherit ValidationAttribute(sprintf "must be a one of %s" (String.concat ", " values)) 25 | override this.FormatErrorMessage name = sprintf "The field %s %s" name base.ErrorMessage 26 | override _.IsValid(value: obj) = 27 | let value = value :?> string 28 | values |> Array.contains value 29 | 30 | type IntMultiplyOfAttribute(divisor: int) = 31 | inherit ValidationAttribute(sprintf "must be a multiply of %d" divisor) 32 | override this.FormatErrorMessage name = sprintf "The field %s %s" name base.ErrorMessage 33 | override _.IsValid(value: obj) = 34 | let value = value :?> int 35 | value % divisor = 0 36 | 37 | // special case, as described in the according DU 38 | type UniqueItemsAttribute(isValid) = 39 | inherit ValidationAttribute("must contain only unique values") 40 | override this.FormatErrorMessage name = sprintf "The field %s %s" name base.ErrorMessage 41 | override _.IsValid(value: obj) = isValid 42 | *) 43 | /// generates custom ValidationAttribute. Body should assume single parameter "value" of type obj 44 | let private generateAttributeDefinitionASTComplex additionalMembers (attributeName, pats, messageExpr, bodyExpr) = 45 | let componentInfo = 46 | SynComponentInfo.ComponentInfo 47 | ([], [], [], longIdent attributeName, xmlEmpty, false, None, r) 48 | 49 | let implicitCtor = SynMemberDefn.ImplicitCtor(None, [], simplePats pats, None, r) 50 | 51 | let inheritance = 52 | SynMemberDefn.ImplicitInherit(synType "ValidationAttribute", messageExpr, None, r) 53 | 54 | let methodOverride = 55 | implDefn MemberKind.Member true "IsValid" ["value"] bodyExpr 56 | 57 | let messageOverride = 58 | implDefn MemberKind.Member true "FormatErrorMessage" ["name"] 59 | (sprintfExpr "The field %s %s." [identExpr "name"; longIdentExpr "base.ErrorMessageString"]) 60 | 61 | let members = 62 | [ 63 | implicitCtor 64 | inheritance 65 | yield! additionalMembers 66 | messageOverride 67 | methodOverride 68 | ] 69 | 70 | let objModel = 71 | SynTypeDefnRepr.ObjectModel(SynTypeDefnKind.TyconUnspecified, members, r) 72 | 73 | TypeDefn(componentInfo, objModel, [], r) 74 | 75 | let private rangeBody name synType = 76 | let pats = [simpleTypedPat "min" synType; simpleTypedPat "max" synType] 77 | let min = identExpr "min" 78 | let max = identExpr "max" 79 | let value = identExpr "value" 80 | let lte = appBinaryOpExpr "op_LessThanOrEqual" 81 | let checkMin = lte min value 82 | let checkMax = lte value max 83 | let body = appBinaryOpExpr "op_BooleanAnd" checkMin checkMax 84 | let message = "must be between %d and %d", [min; max] 85 | name + "RangeAttribute", pats, message, body 86 | 87 | let private generateSpecialCasedAttributeBody attrName attrMessage = 88 | let pats = [simpleTypedPat "isValid" boolType] 89 | let message = strExpr attrMessage 90 | attrName, pats, message, identExpr "isValid" 91 | 92 | let private generateCustomAttributeValidationBodyForTypedValue value = 93 | match value with 94 | | LongRange _ -> [], rangeBody "Long" int64Type 95 | | RegexPattern _ -> 96 | let pattern = simpleTypedPat "pattern" (stringType) 97 | let pats = [pattern] 98 | let pattern = identExpr "pattern" 99 | let regexMember = 100 | [ 101 | pattern 102 | appBinaryOpExpr 103 | "op_BitwiseOr" 104 | (longIdentExpr "System.Text.RegularExpressions.RegexOptions.ECMAScript") 105 | (longIdentExpr "System.Text.RegularExpressions.RegexOptions.Compiled") 106 | ] 107 | |> tupleComplexExpr 108 | |> app (longIdentExpr "System.Text.RegularExpressions.Regex") 109 | let regexMember = 110 | SynMemberDefn.LetBindings 111 | ([ 112 | SynBinding.Binding 113 | (None, 114 | SynBindingKind.NormalBinding, 115 | false, 116 | false, 117 | [], 118 | xmlEmpty, 119 | SynValData (None, SynValInfo([], SynArgInfo([], false, None)), None), 120 | synPat "regex", 121 | None, 122 | regexMember, 123 | r, 124 | DebugPointForBinding.DebugPointAtBinding r) 125 | ], false, false, r) 126 | let body = app (longIdentExpr "regex.IsMatch") (identExpr "value") 127 | let message = "must match the regular expression '%s'", [pattern] 128 | [regexMember], ("RegexPattern", pats, message, body) 129 | | MultipleOf target -> 130 | let name, argType, zero = 131 | match target with 132 | | IntMultiple _ -> "Int", intType, intExpr 0 133 | | LongMultiple _ -> "Long", int64Type, constExpr(SynConst.Int64 0L) 134 | let pats = [simpleTypedPat "divisor" argType] 135 | let divisor = identExpr "divisor" 136 | let value = identExpr "value" 137 | let body = appBinaryOpExpr "op_Modulus" value divisor ^= zero 138 | let name = sprintf "%sMultipleOfAttribute" name 139 | let message = "must be a multiple of %d", [divisor] 140 | [], (name, pats, message, body) 141 | | EnumAttribute target -> 142 | let name, argType, mapper = 143 | match target with 144 | | IntEnum _ -> "Int", intType, identExpr "string" 145 | | LongEnum _ -> "Long", int64Type, identExpr "string" 146 | | FloatEnum _ -> "Float", doubleType, identExpr "string" 147 | | StringEnum _ -> "String", stringType, _id 148 | let pats = [SynSimplePat.Attrib(simpleTypedPat "values" (genericType true "array" [argType]), [attr "System.ParamArray"], r)] 149 | let values = identExpr "values" 150 | let value = identExpr "value" 151 | let body = values ^|> app (longIdentExpr "Array.contains") value 152 | let name = sprintf "%sEnumValuesAttribute" name 153 | let message = "must be a one of ['%s']", [values ^|> Seq.mapExpr mapper ^|> String.concatExpr "', '" |> paren] 154 | [], (name, pats, message, body) 155 | 156 | let private messageToExpr (pattern, args) = 157 | if args |> List.tryHead |> Option.isSome then 158 | sprintfExpr pattern args |> paren 159 | else strExpr pattern 160 | 161 | let private generateAttributeForSpecialCasedValidationType value = 162 | match value with 163 | | UniqueItems -> 164 | generateSpecialCasedAttributeBody "UniqueItemsAttribute" "must contain only unique items" 165 | |> generateAttributeDefinitionASTComplex [] 166 | 167 | let private generateAttributeForCustomValidationType value = 168 | let additionalMembers, (name, pats, message, rawBody) = generateCustomAttributeValidationBodyForTypedValue value 169 | let message = messageToExpr message 170 | match value with 171 | | LongRange _ -> 172 | let body = letExpr "value" [] (SynExpr.Downcast(identExpr "value", int64Type, r)) rawBody 173 | name, pats, message, body 174 | | RegexPattern _ -> 175 | let body = letExpr "value" [] (SynExpr.Downcast(identExpr "value", stringType, r)) rawBody 176 | name, pats, message, body 177 | | MultipleOf target -> 178 | let argType = 179 | match target with 180 | | IntMultiple _ -> intType 181 | | LongMultiple _ -> int64Type 182 | let body = letExpr "value" [] (SynExpr.Downcast(identExpr "value", argType, r)) rawBody 183 | name, pats, message, body 184 | | EnumAttribute target -> 185 | let argType = 186 | match target with 187 | | IntEnum _ -> intType 188 | | LongEnum _ -> int64Type 189 | | FloatEnum _ -> doubleType 190 | | StringEnum _ -> stringType 191 | let body = letExpr "value" [] (SynExpr.Downcast(identExpr "value", argType, r)) rawBody 192 | name, pats, message, body 193 | |> generateAttributeDefinitionASTComplex additionalMembers 194 | 195 | let generateAttributeDefinitionFor value = 196 | match value with 197 | | CustomAttribute custom -> 198 | generateAttributeForCustomValidationType custom 199 | |> Some 200 | | SpecialCasedCustomValidationAttribute specialCased -> 201 | generateAttributeForSpecialCasedValidationType specialCased 202 | |> Some 203 | | BuiltInAttribute _ -> None 204 | 205 | 206 | /// Validation extensibility is accomplished by the interfaces below. 207 | /// To customize validation, user has to implement any of those interfaces for bound model types 208 | /// E.g. for types resulted by default application before type combining 209 | module ExtensionPoints = 210 | let private generateValidationInterface name summary generic methodName methodType = 211 | let docs = xmlDocs [[sprintf "%s" summary]] 212 | 213 | let componentInfo = 214 | SynComponentInfo.ComponentInfo 215 | ([attr "Interface"], [TyparDecl([], generic)], [], longIdent name, docs, false, None, r) 216 | 217 | let methodDefn = abstractMemberDfn xmlEmpty methodName methodType 218 | 219 | let objModel = 220 | SynTypeDefnRepr.ObjectModel(SynTypeDefnKind.TyconUnspecified, [methodDefn], r) 221 | 222 | TypeDefn(componentInfo, objModel, [], r) 223 | 224 | let validationReplacerInterface = "IGiraffeValidator" 225 | /// [] 226 | /// /// replaces any generated validation rules for type 227 | /// type IGiraffeValidator<'model> = 228 | /// abstract member Validate: 'model * ValidationContext -> ValidationResult array 229 | let generateValidationReplacerInterface () = 230 | let generic = Typar(ident "model", NoStaticReq, false) 231 | let sign = tuple [SynType.Var(generic, r); synType "ValidationContext"] ^-> genericType true "array" [synType "ValidationResult"] 232 | generateValidationInterface validationReplacerInterface "replaces any generated validation rules for type" generic "Validate" sign 233 | 234 | let validationAugmenterInterface = "IGiraffeAdditionalValidator" 235 | /// [] 236 | /// /// augments generated validation rules 237 | /// type IGiraffeAdditionalValidator<'model> = 238 | /// abstract member Validate: 'model * ValidationContext -> ValidationResult array 239 | let generateValidationAugmenterInterface () = 240 | let generic = Typar(ident "model", NoStaticReq, false) 241 | let sign = tuple [SynType.Var(generic, r); synType "ValidationContext"] ^-> genericType true "array" [synType "ValidationResult"] 242 | generateValidationInterface validationAugmenterInterface "replaces any generated validation rules for type" generic "Validate" sign 243 | 244 | // maybe later: 245 | (* 246 | [] 247 | /// augments any validation rules with model verification 248 | type IGiraffeVerifier<'model> = 249 | abstract member Verify: 'model * ValidationContext -> Task 250 | *) -------------------------------------------------------------------------------- /src/GiraffeGenerator.Core/CodeGenValidation_Types.fs: -------------------------------------------------------------------------------- 1 | module CodeGenValidation_Types 2 | 3 | open OpenApi 4 | open OpenApiValidation 5 | 6 | // convert OpenApi representation to something more useful for codegen 7 | 8 | type EnumTargets = 9 | | IntEnum of int array 10 | | LongEnum of int64 array 11 | | FloatEnum of float array 12 | | StringEnum of string array 13 | with 14 | member s.TypeEquals v = 15 | match s,v with 16 | | IntEnum _, IntEnum _ 17 | | LongEnum _, LongEnum _ 18 | | FloatEnum _, FloatEnum _ 19 | | StringEnum _, StringEnum _ -> true 20 | | _ -> false 21 | 22 | type MultipleOfTargets = 23 | | IntMultiple of int 24 | | LongMultiple of int64 25 | with 26 | member s.TypeEquals v = 27 | match s,v with 28 | | IntMultiple _, IntMultiple _ 29 | | LongMultiple _, LongMultiple _ -> true 30 | | _ -> false 31 | 32 | type RangeValues<'a> = 33 | | Min of 'a 34 | | Max of 'a 35 | | Both of 'a*'a 36 | 37 | let inline private toExclusive operator (range: ^v NumberBoundary) = 38 | if not range.Exclusive then range.Value 39 | elif not (box range.Value :? float) then operator range.Value LanguagePrimitives.GenericOne 40 | else ((box operator :?> float -> float -> float) ((box range.Value) :?> float) 1e-8 (*Fabulous doesn't generate values with a really small exponents*) |> box) :?> ^v 41 | 42 | let inline private rangeValuesFromRanges a b = 43 | let a = a |> Option.map (toExclusive (+)) 44 | let b = b |> Option.map (toExclusive (-)) 45 | Option.map2 (fun a b -> Both (a, b)) a b 46 | |> Option.orElse (a |> Option.map Min) 47 | |> Option.orElse (b |> Option.map Max) 48 | 49 | type BuiltInAttribute = 50 | | IntRange of int RangeValues 51 | | FloatRange of float RangeValues 52 | | MinLength of int 53 | | MaxLength of int 54 | | Required 55 | with 56 | member s.TypeEquals v = 57 | match s,v with 58 | | IntRange _, IntRange _ 59 | | FloatRange _, FloatRange _ 60 | | MinLength _, MinLength _ 61 | | MaxLength _, MaxLength _ 62 | | Required, Required -> true 63 | | _ -> false 64 | 65 | /// as we've got no way to define attribute which depends on the type to which it's applied 66 | /// without code-generating a reflection 67 | /// this attribute should be special-cased to be created with validity as parameter 68 | type SpecialCasedCustomValidationAttribute = 69 | | UniqueItems 70 | with 71 | member s.TypeEquals v = 72 | match s,v with 73 | | UniqueItems, UniqueItems -> true 74 | 75 | type CustomBasicValidationAttribute = 76 | | EnumAttribute of EnumTargets 77 | | MultipleOf of MultipleOfTargets 78 | | LongRange of int64 RangeValues 79 | // built-in doesn't work for some reason 80 | | RegexPattern of string 81 | with 82 | member s.TypeEquals v = 83 | match s,v with 84 | | EnumAttribute _, EnumAttribute _ 85 | | MultipleOf _, MultipleOf _ 86 | | RegexPattern _, RegexPattern _ 87 | | LongRange _, LongRange _ 88 | | _ -> false 89 | 90 | type ValidationAttribute = 91 | | CustomAttribute of CustomBasicValidationAttribute 92 | | BuiltInAttribute of BuiltInAttribute 93 | | SpecialCasedCustomValidationAttribute of SpecialCasedCustomValidationAttribute 94 | // no type recursion for arrays and options 95 | // because they should be handled via 96 | // recursive validator function 97 | // instead of generating a stuff like 98 | // ArrayItemOptionValueArrayItemMinLengthAttribute 99 | with 100 | member s.TypeEquals v = 101 | match s,v with 102 | | CustomAttribute a, CustomAttribute b -> a.TypeEquals b 103 | | BuiltInAttribute a, BuiltInAttribute b -> a.TypeEquals b 104 | | SpecialCasedCustomValidationAttribute a, SpecialCasedCustomValidationAttribute b -> a.TypeEquals b 105 | | _ -> false 106 | 107 | module rec Enumeration = 108 | let enumeratePropertyValidation recurse kind = 109 | seq { 110 | let isRequired = match kind with | TypeKind.Option _ -> false | _ -> true 111 | if isRequired then 112 | Required |> BuiltInAttribute 113 | yield! enumerateKindValidation recurse kind 114 | } 115 | 116 | /// Enumerates every used attribute for a kind (including attribute parameters). 117 | /// Includes a flag parameter controlling should the enumeration be recursive. 118 | /// Arrays yield only the validation for arrays themselves (like MinLength/MaxLength). 119 | /// Arrays, Options and Objects do not yield theirs underlying value validation by themselves. 120 | /// Underlying values validation is still yielded if recurse: true 121 | let rec enumerateKindValidation recurse kind = 122 | seq { 123 | match kind with 124 | | TypeKind.Array (kind,_,validation) -> 125 | if validation.IsSome then 126 | let validation = validation.Value 127 | if validation.UniqueItems then 128 | UniqueItems |> SpecialCasedCustomValidationAttribute |> Some 129 | validation.MinItems |> Option.map (MinLength >> BuiltInAttribute) 130 | validation.MaxItems |> Option.map (MaxLength >> BuiltInAttribute) 131 | if recurse then 132 | for child in enumerateKindValidation recurse kind do 133 | Some child 134 | | TypeKind.Option kind -> 135 | if recurse then 136 | for child in enumerateKindValidation recurse kind do 137 | Some child 138 | | TypeKind.Object obj -> 139 | if recurse then 140 | for (_, propertyKind, _) in obj.Properties do 141 | for child in enumeratePropertyValidation recurse propertyKind do 142 | Some child 143 | | TypeKind.DU _ -> failwith "OneOf validation is not supported yet" 144 | | TypeKind.Prim prim -> 145 | match prim with 146 | | PrimTypeKind.Int (Some validation) -> 147 | validation.MultipleOf |> Option.map (IntMultiple >> MultipleOf >> CustomAttribute) 148 | validation.EnumValues |> Option.map (IntEnum >> EnumAttribute >> CustomAttribute) 149 | rangeValuesFromRanges validation.Minimum validation.Maximum 150 | |> Option.map (IntRange >> BuiltInAttribute) 151 | | PrimTypeKind.Long (Some validation) -> 152 | validation.MultipleOf |> Option.map (LongMultiple >> MultipleOf >> CustomAttribute) 153 | validation.EnumValues |> Option.map (LongEnum >> EnumAttribute >> CustomAttribute) 154 | rangeValuesFromRanges validation.Minimum validation.Maximum 155 | |> Option.map (LongRange >> CustomAttribute) 156 | | PrimTypeKind.Double (Some validation) -> 157 | validation.EnumValues |> Option.map (FloatEnum >> EnumAttribute >> CustomAttribute) 158 | rangeValuesFromRanges validation.Minimum validation.Maximum 159 | |> Option.map (FloatRange >> BuiltInAttribute) 160 | | PrimTypeKind.String (StringFormat.String (Some validation)) -> 161 | validation.MinLength |> Option.map (MinLength >> BuiltInAttribute) 162 | validation.MaxLength |> Option.map (MaxLength >> BuiltInAttribute) 163 | validation.EnumValues |> Option.map (StringEnum >> EnumAttribute >> CustomAttribute) 164 | validation.Pattern |> Option.map (RegexPattern >> CustomAttribute) 165 | | _ -> () 166 | | TypeKind.NoType 167 | | TypeKind.BuiltIn _ -> () 168 | } 169 | |> Seq.choose id 170 | 171 | /// gets a unique identifier of a custom attribute type ignoring any parameters for a type's instance 172 | /// basically converts DU case value to a bitflag 173 | /// 0 indicates a non-custom type which needs no generation 174 | let rec identifyCustomValidationAttributeType value = 175 | match value with 176 | | BuiltInAttribute _ -> 0 177 | | SpecialCasedCustomValidationAttribute specialCased -> 178 | match specialCased with 179 | | UniqueItems -> 1 180 | | CustomAttribute custom -> 181 | match custom with 182 | | LongRange _ -> 2 183 | | RegexPattern _ -> 4 184 | | MultipleOf target -> 185 | match target with 186 | | IntMultiple _ -> 8 187 | | LongMultiple _ -> 16 188 | | EnumAttribute enum -> 189 | match enum with 190 | | IntEnum _ -> 32 191 | | LongEnum _ -> 64 192 | | FloatEnum _ -> 128 193 | | StringEnum _ -> 256 194 | -------------------------------------------------------------------------------- /src/GiraffeGenerator.Core/Configuration.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module Configuration 3 | 4 | type DateTimeGeneratedType = 5 | | ZonedDateTime 6 | | OffsetDateTime 7 | | LocalDateTime 8 | | Instant 9 | 10 | type Configuration = 11 | { 12 | UseNodaTime: bool 13 | AllowUnqualifiedAccess: bool 14 | MapDateTimeInto: DateTimeGeneratedType 15 | ModuleName: string option 16 | } 17 | 18 | let mutable value = 19 | { UseNodaTime = false 20 | AllowUnqualifiedAccess = false 21 | MapDateTimeInto = OffsetDateTime 22 | ModuleName = None } -------------------------------------------------------------------------------- /src/GiraffeGenerator.Core/GiraffeGenerator.Core.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | true 27 | %(Identity) 28 | true 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/GiraffeGenerator.Core/NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/GiraffeGenerator.Core/OpenApiValidation.fs: -------------------------------------------------------------------------------- 1 | module OpenApiValidation 2 | open System.Text.RegularExpressions 3 | open AST 4 | open Microsoft.OpenApi.Any 5 | open Microsoft.OpenApi.Models 6 | 7 | type NumberBoundary<'n> = 8 | { 9 | Value: 'n 10 | Exclusive: bool 11 | } 12 | // the following block contains functions used for design-time values (e.g. defaults) validation 13 | 14 | // numerics validation 15 | let private checkBoundary op message value boundary = 16 | let isValid = not boundary.Exclusive && boundary.Value = value || op boundary.Value value 17 | if isValid then None 18 | else 19 | let subMessage = sprintf "%s %A" (if not boundary.Exclusive then "or equal to" else "") boundary.Value 20 | message subMessage |> Some 21 | let inline private checkMinBoundary value = checkBoundary (<) (sprintf "should be greater than %s") value 22 | let inline private checkMaxBoundary value = checkBoundary (>) (sprintf "should be lesser than %s") value 23 | 24 | let inline private checkMultipleOf value divisor = 25 | if value % divisor = LanguagePrimitives.GenericZero then None 26 | else sprintf "should be a multiple of %d" divisor |> Some 27 | 28 | // numerics or string validation 29 | 30 | let inline private checkEnum value enum = 31 | if enum |> Array.contains value then None 32 | else 33 | enum 34 | |> Array.map string 35 | |> String.concat "; " 36 | |> sprintf "should be one of [%s]" 37 | |> Some 38 | 39 | // array or string length validation 40 | let inline private getLength value = (^v: (member Length: int) value) 41 | let inline private checkLength boundaryChecker value measure length = 42 | { 43 | Value = length 44 | Exclusive = false 45 | } 46 | |> boundaryChecker (getLength value) 47 | |> Option.map ^ sprintf "%s count %s" measure 48 | let inline private checkMinLength value = checkLength checkMinBoundary value 49 | let inline private checkMaxLength value = checkLength checkMaxBoundary value 50 | 51 | let inline private failIfInvalidImpl name value errs = 52 | let errs = 53 | errs 54 | |> List.choose id 55 | |> String.concat "; " 56 | if errs.Length > 0 then 57 | failwithf "%s %A is invalid: %s" name value errs 58 | 59 | // utilities for validation rules validation 60 | 61 | let inline private failIfMultipleOfInvalidOrReturnIt (schema: OpenApiSchema) converter = 62 | let multipleOf = 63 | schema.MultipleOf 64 | |> Option.ofNullable 65 | |> Option.map converter 66 | |> Option.filter ((<>) LanguagePrimitives.GenericOne) 67 | do 68 | multipleOf 69 | |> Option.filter ((=) LanguagePrimitives.GenericZero) 70 | |> Option.map (failwithf "multipleOf must not be equal to %d") 71 | |> Option.defaultValue () 72 | multipleOf 73 | 74 | let validateLengths min max = 75 | do 76 | min 77 | |> Option.filter ((>) 0) 78 | |> Option.map (failwithf "minItems must be greater than or equal to 0. %d is not") 79 | |> Option.defaultValue () 80 | do 81 | max 82 | |> Option.filter ((>) 0) 83 | |> Option.map (failwithf "maxItems must be greater than or equal to 0. %d is not") 84 | |> Option.defaultValue () 85 | do 86 | Option.map2 (fun min max -> min, max) min max 87 | |> Option.filter (fun (min, max) -> min > max) 88 | |> Option.map (fun (min, max) -> failwithf "maxItems (%d) should be greater than minItems (%d)" max min) 89 | |> Option.defaultValue () 90 | 91 | // and here come the validation rule models themselves 92 | 93 | type IntValidation = 94 | { 95 | MultipleOf: int option 96 | Minimum: NumberBoundary option 97 | Maximum: NumberBoundary option 98 | EnumValues: int array option 99 | } 100 | with 101 | member s.FailIfInvalid (name, value) = 102 | failIfInvalidImpl name value 103 | [ 104 | s.MultipleOf |> Option.bind (checkMultipleOf value) 105 | s.Minimum |> Option.bind (checkMinBoundary value) 106 | s.Maximum |> Option.bind (checkMaxBoundary value) 107 | s.EnumValues |> Option.bind (checkEnum value) 108 | ] 109 | static member TryParse (schema: OpenApiSchema): IntValidation option = 110 | let multipleOf = failIfMultipleOfInvalidOrReturnIt schema int 111 | { 112 | MultipleOf = multipleOf 113 | Minimum = 114 | schema.Minimum 115 | |> Option.ofNullable 116 | |> Option.map int 117 | |> Option.map ^ fun v -> 118 | { Value = v; Exclusive = schema.ExclusiveMinimum |> Option.ofNullable |> Option.defaultValue false } 119 | Maximum = 120 | schema.Maximum 121 | |> Option.ofNullable 122 | |> Option.map int 123 | |> Option.map ^ fun v -> 124 | { Value = v; Exclusive = schema.ExclusiveMaximum |> Option.ofNullable |> Option.defaultValue false } 125 | EnumValues = 126 | schema.Enum 127 | |> Option.ofObj 128 | |> Option.map (Seq.map (fun x -> (x :?> OpenApiInteger).Value) >> Seq.toArray) 129 | |> Option.filter (Array.length >> (<>) 0) 130 | } 131 | |> Some 132 | |> Option.filter ^ fun x -> x.MultipleOf.IsSome || x.Minimum.IsSome || x.Maximum.IsSome || x.EnumValues.IsSome 133 | 134 | type LongValidation = 135 | { 136 | MultipleOf: int64 option 137 | Minimum: NumberBoundary option 138 | Maximum: NumberBoundary option 139 | EnumValues: int64 array option 140 | } 141 | with 142 | member s.FailIfInvalid (name, value) = 143 | failIfInvalidImpl name value 144 | [ 145 | s.MultipleOf |> Option.bind (checkMultipleOf value) 146 | s.Minimum |> Option.bind (checkMinBoundary value) 147 | s.Maximum |> Option.bind (checkMaxBoundary value) 148 | s.EnumValues |> Option.bind (checkEnum value) 149 | ] 150 | static member TryParse (schema: OpenApiSchema): LongValidation option = 151 | let multipleOf = failIfMultipleOfInvalidOrReturnIt schema int64 152 | { 153 | MultipleOf = multipleOf 154 | Minimum = 155 | schema.Minimum 156 | |> Option.ofNullable 157 | |> Option.map int64 158 | |> Option.map ^ fun v -> 159 | { Value = v; Exclusive = schema.ExclusiveMinimum |> Option.ofNullable |> Option.defaultValue false } 160 | Maximum = 161 | schema.Maximum 162 | |> Option.ofNullable 163 | |> Option.map int64 164 | |> Option.map ^ fun v -> 165 | { Value = v; Exclusive = schema.ExclusiveMaximum |> Option.ofNullable |> Option.defaultValue false } 166 | EnumValues = 167 | schema.Enum 168 | |> Option.ofObj 169 | |> Option.map (Seq.map (fun x -> (x :?> OpenApiLong).Value) >> Seq.toArray) 170 | |> Option.filter (Array.length >> (<>) 0) 171 | } 172 | |> Some 173 | |> Option.filter ^ fun x -> x.MultipleOf.IsSome || x.Minimum.IsSome || x.Maximum.IsSome || x.EnumValues.IsSome 174 | 175 | type FloatValidation = 176 | { 177 | Minimum: NumberBoundary option 178 | Maximum: NumberBoundary option 179 | EnumValues: float array option 180 | } 181 | with 182 | member s.FailIfInvalid (name, value) = 183 | failIfInvalidImpl name value 184 | [ 185 | s.Minimum |> Option.bind (checkMinBoundary value) 186 | s.Maximum |> Option.bind (checkMaxBoundary value) 187 | s.EnumValues |> Option.bind (checkEnum value) 188 | ] 189 | static member TryParse (schema: OpenApiSchema): FloatValidation option = 190 | { 191 | Minimum = 192 | schema.Minimum 193 | |> Option.ofNullable 194 | |> Option.map float 195 | |> Option.map ^ fun v -> 196 | { Value = v; Exclusive = schema.ExclusiveMinimum |> Option.ofNullable |> Option.defaultValue false } 197 | Maximum = 198 | schema.Maximum 199 | |> Option.ofNullable 200 | |> Option.map float 201 | |> Option.map ^ fun v -> 202 | { Value = v; Exclusive = schema.ExclusiveMaximum |> Option.ofNullable |> Option.defaultValue false } 203 | EnumValues = 204 | schema.Enum 205 | |> Option.ofObj 206 | |> Option.map (Seq.map (fun x -> (x :?> OpenApiDouble).Value) >> Seq.toArray) 207 | |> Option.filter (Array.length >> (<>) 0) 208 | } 209 | |> Some 210 | |> Option.filter ^ fun x -> x.Minimum.IsSome || x.Maximum.IsSome || x.EnumValues.IsSome 211 | 212 | type StringValidation = 213 | { 214 | /// inclusive: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.7 215 | MinLength: int option 216 | /// inclusive: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.6 217 | MaxLength: int option 218 | Pattern: string option 219 | EnumValues: string array option 220 | } 221 | with 222 | member s.FailIfInvalid (name, value) = 223 | failIfInvalidImpl name value 224 | [ 225 | s.MinLength |> Option.bind (checkMinLength value "characters") 226 | s.MaxLength |> Option.bind (checkMaxLength value "characters") 227 | s.EnumValues |> Option.bind (checkEnum value) 228 | s.Pattern 229 | |> Option.filter (fun pattern -> Regex(pattern, RegexOptions.ECMAScript).IsMatch value) 230 | |> Option.map ^ sprintf "should match pattern /%s/" 231 | ] 232 | static member TryParse (schema: OpenApiSchema): StringValidation option = 233 | let minLength = schema.MinLength |> Option.ofNullable 234 | let maxLength = schema.MaxLength |> Option.ofNullable 235 | let pattern = schema.Pattern |> Option.ofObj 236 | do validateLengths minLength maxLength 237 | do 238 | pattern 239 | |> Option.filter (fun pattern -> 240 | try 241 | do Regex(pattern, RegexOptions.ECMAScript) |> ignore 242 | false 243 | with | _ -> true) 244 | |> Option.map (failwithf "pattern /%s/ should be a valid ECMA regexp") 245 | |> Option.defaultValue () 246 | { 247 | MinLength = minLength 248 | MaxLength = maxLength 249 | Pattern = pattern 250 | EnumValues = 251 | schema.Enum 252 | |> Option.ofObj 253 | |> Option.map (Seq.map (fun x -> (x :?> OpenApiString).Value) >> Seq.toArray) 254 | |> Option.filter (Array.length >> (<>) 0) 255 | } 256 | |> Some 257 | |> Option.filter ^ fun x -> x.MinLength.IsSome || x.MaxLength.IsSome || x.Pattern.IsSome || x.EnumValues.IsSome 258 | 259 | type ArrayValidation = 260 | { 261 | /// inclusive: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.11 262 | MinItems: int option 263 | /// inclusive: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.10 264 | MaxItems: int option 265 | UniqueItems: bool 266 | } 267 | with 268 | member s.FailIfInvalid (name, value) = 269 | failIfInvalidImpl name value 270 | [ 271 | s.MinItems |> Option.bind (checkMinLength value "items") 272 | s.MaxItems |> Option.bind (checkMaxLength value "items") 273 | if s.UniqueItems && Array.length value <> (Set value).Count then 274 | Some "should contain only unique values" 275 | ] 276 | static member TryParse (schema: OpenApiSchema): ArrayValidation option = 277 | let minItems = schema.MinItems |> Option.ofNullable 278 | let maxItems = schema.MaxItems |> Option.ofNullable 279 | do validateLengths minItems maxItems 280 | { 281 | MinItems = minItems 282 | MaxItems = maxItems 283 | UniqueItems = schema.UniqueItems |> Option.ofNullable |> Option.defaultValue false 284 | } 285 | |> Some 286 | |> Option.filter ^ fun x -> x.MinItems.IsSome || x.MaxItems.IsSome || x.UniqueItems 287 | 288 | let inline failIfInvalid name validation value = 289 | validation 290 | |> Option.map(fun validation -> 291 | (^validation:(member FailIfInvalid: string -> ^a -> unit) validation, name, value)) 292 | |> Option.defaultValue () -------------------------------------------------------------------------------- /src/GiraffeGenerator.Core/build/GiraffeGenerator.Core.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/GiraffeGenerator.Sdk/GiraffeGenerator.Sdk.proj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | false 6 | true 7 | $(MSBuildProjectFullPath) 8 | true 9 | GiraffeGenerator.Sdk 10 | MSBuild integration between GiraffeGenerator and .NET Sdk projects 11 | true 12 | 13 | 14 | 15 | 16 | true 17 | %(Identity) 18 | true 19 | 20 | 21 | true 22 | %(Identity) 23 | true 24 | 25 | 26 | true 27 | %(Identity) 28 | true 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/GiraffeGenerator.Sdk/build/GiraffeGenerator.Sdk.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | dotnet 5 | $(MSBuildThisFileDirectory)../tools/net6.0/any/GiraffeGenerator.dll 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/GiraffeGenerator.Sdk/build/GiraffeGenerator.Sdk.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | $([System.IO.Path]::GetFullPath('%(Compile.FullPath)')) 8 | true 9 | true 10 | %(Compile.OpenApiModuleName) 11 | %(Compile.OpenApiMapDateTimeInto) 12 | 13 | 14 | 15 | 16 | 17 | $([System.IO.Path]::GetFullPath('%(GiraffeGeneratorSource.OutputPath)')) 18 | %(GiraffeGeneratorSource.FullPath).fs 19 | %(GiraffeGeneratorSource.ModuleName) 20 | true 21 | true 22 | %(GiraffeGeneratorSource.MapDateTimeInto) 23 | 24 | 25 | 26 | 27 | <_GiraffeGeneratorSdkCodeGenInputCache>$(IntermediateOutputPath)$(MSBuildProjectFile).GiraffeGeneratorSdkCodeGenInputs.cache 28 | 29 | 30 | 31 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | $(GiraffeGeneratorSdkGenerateCodeDependsOn);ResolveReferences;GiraffeGeneratorSdkGenerateInputCache 50 | 51 | 52 | 58 | 59 | 60 | <_GiraffeGeneratorSdk_InputFileName>%(GiraffeGeneratorCodegen.Identity) 61 | <_GiraffeGeneratorSdk_OutputFileName>%(GiraffeGeneratorCodegen.OutputPath) 62 | <_GiraffeGeneratorSdk_ModuleName>%(GiraffeGeneratorCodegen.ModuleName) 63 | <_GiraffeGeneratorSdk_AllowUnqualifiedAccess Condition=" '%(GiraffeGeneratorCodegen.AllowUnqualifiedAccess)' == 'true' ">true 64 | <_GiraffeGeneratorSdk_UseNodaTime Condition=" '%(GiraffeGeneratorCodegen.UseNodaTime)' == 'true' ">true 65 | <_GiraffeGeneratorSdk_MapDateTimeInto>%(GiraffeGeneratorCodegen.MapDateTimeInto) 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/GiraffeGenerator.Sdk/buildMultiTargeting/GiraffeGenerator.Sdk.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/GiraffeGenerator/GiraffeGenerator.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net6.0 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/GiraffeGenerator/NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/GiraffeGenerator/Program.fs: -------------------------------------------------------------------------------- 1 | module Program 2 | 3 | open OpenApi 4 | open CodeGen 5 | open System.IO 6 | open Argu 7 | 8 | type DateTimeGeneratedType = 9 | | Zoned_Date_Time 10 | | Offset_Date_Time 11 | | Local_Date_Time 12 | | Instant 13 | member this.ToConfigType() = 14 | match this with 15 | | Zoned_Date_Time -> Configuration.DateTimeGeneratedType.ZonedDateTime 16 | | Offset_Date_Time -> Configuration.DateTimeGeneratedType.OffsetDateTime 17 | | Local_Date_Time -> Configuration.DateTimeGeneratedType.LocalDateTime 18 | | Instant -> Configuration.DateTimeGeneratedType.Instant 19 | 20 | type Config = 21 | | []Inputfile of string 22 | | []Outputfile of string 23 | | Module_Name of string 24 | | Allow_Unqualified_Access 25 | | Use_Noda_Time 26 | | Map_Date_Time_Into of DateTimeGeneratedType 27 | 28 | interface IArgParserTemplate with 29 | member this.Usage = 30 | match this with 31 | | Inputfile _ -> "Path to OpenAPI spec file to generate server implementation by" 32 | | Outputfile _ -> "Path to .fs file to write the source code generated" 33 | | Module_Name _ -> "Override module name for the server generated. Default is taken from the spec description" 34 | | Allow_Unqualified_Access -> "Opts-out [] generation for the module being generated" 35 | | Use_Noda_Time -> "Opts-in usage of NodaTime types" 36 | | Map_Date_Time_Into _ -> "Specifies NodaTime type used for date-time OpenAPI format" 37 | 38 | let parser = ArgumentParser.Create("GiraffeGenerator") 39 | 40 | [] 41 | let main argv = 42 | let parsed = parser.Parse argv 43 | let parsed = parsed.GetAllResults() 44 | let mutable inputFile = "" 45 | let mutable outputFile = "" 46 | 47 | for option in parsed do 48 | match option with 49 | | Inputfile file -> inputFile <- file 50 | | Outputfile file -> outputFile <- file 51 | | Module_Name name -> Configuration.value <- { Configuration.value with ModuleName = Some name } 52 | | Allow_Unqualified_Access -> Configuration.value <- { Configuration.value with AllowUnqualifiedAccess = true } 53 | | Use_Noda_Time -> Configuration.value <- { Configuration.value with UseNodaTime = true } 54 | | Map_Date_Time_Into kind -> Configuration.value <- { Configuration.value with MapDateTimeInto = kind.ToConfigType() } 55 | 56 | let doc, errors = read inputFile 57 | if errors <> null && errors.Errors <> null && errors.Errors.Count > 0 then 58 | errors.Errors 59 | |> Seq.map (fun err -> sprintf "%s (at %s)" err.Message err.Pointer) 60 | |> String.concat "\n" 61 | |> failwith 62 | let api = parse doc 63 | 64 | let resultSource = 65 | giraffeAst api 66 | |> sourceCode 67 | File.WriteAllText(outputFile, resultSource) 68 | 69 | 0 70 | -------------------------------------------------------------------------------- /tests/GiraffeGenerator.IntegrationTests/GiraffeGenerator.IntegrationTests.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net6.0 6 | 7 | 8 | 9 | 10 | 11 | specs/specSimple.yaml 12 | 13 | 14 | specs/specWithSchemas.yaml 15 | 16 | 17 | specs/specWithTextXml.yaml 18 | 19 | 20 | specs/specWithArguments.yaml 21 | 22 | 23 | specs/specWithParametersAndRequestBodySchemas.yaml 24 | 25 | 26 | true 27 | specs/specGeneralForNodaTime.yaml 28 | 29 | 30 | SpecForNodaTimeDateTimeInstantFormatHandling 31 | true 32 | instant 33 | specs/specForNodaTimeDateTimeFormatHandling.yaml 34 | 35 | 36 | SpecForNodaTimeDateTimeLocalDateTimeFormatHandling 37 | true 38 | local-date-time 39 | specs/specForNodaTimeDateTimeFormatHandling.yaml 40 | 41 | 42 | SpecForNodaTimeDateTimeOffsetDateTimeFormatHandling 43 | true 44 | offset-date-time 45 | specs/specForNodaTimeDateTimeFormatHandling.yaml 46 | 47 | 48 | SpecForNodaTimeDateTimeZonedDateTimeFormatHandling 49 | true 50 | zoned-date-time 51 | specs/specForNodaTimeDateTimeFormatHandling.yaml 52 | 53 | 54 | SpecForNodaTimeDateTimeDefaultFormatHandling 55 | true 56 | specs/specForNodaTimeDateTimeFormatHandling.yaml 57 | 58 | 59 | SpecWithValidation 60 | specs/specWithValidation.yaml 61 | 62 | 63 | SpecWithValidationExtensibility 64 | specs/specWithValidationExtensibility.yaml 65 | true 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | runtime; build; native; contentfiles; analyzers; buildtransitive 98 | all 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | dotnet 109 | ../../src/GiraffeGenerator/bin/$(Configuration)/net6.0/GiraffeGenerator.dll 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /tests/GiraffeGenerator.IntegrationTests/NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/GiraffeGenerator.IntegrationTests/OptionConverter.fs: -------------------------------------------------------------------------------- 1 | namespace GiraffeGenerator.IntegrationTests 2 | 3 | [] 4 | module OptionConverter = 5 | 6 | open Newtonsoft.Json 7 | open Microsoft.FSharp.Reflection 8 | open System 9 | 10 | /// see `optionConverter` 11 | type OptionConverter() = 12 | inherit JsonConverter() 13 | 14 | override __.CanConvert t = 15 | t.IsGenericType 16 | && typedefof>.Equals (t.GetGenericTypeDefinition()) 17 | 18 | override __.WriteJson(writer, value, serializer) = 19 | let value = 20 | if isNull value then 21 | null 22 | else 23 | let _,fields = FSharpValue.GetUnionFields(value, value.GetType()) 24 | fields.[0] 25 | serializer.Serialize(writer, value) 26 | 27 | override __.ReadJson(reader, t, _, serializer) = 28 | let innerType = t.GetGenericArguments().[0] 29 | 30 | let innerType = 31 | if innerType.IsValueType then 32 | typedefof>.MakeGenericType([| innerType |]) 33 | else 34 | innerType 35 | 36 | let value = serializer.Deserialize(reader, innerType) 37 | let cases = FSharpType.GetUnionCases t 38 | 39 | if isNull value then 40 | FSharpValue.MakeUnion(cases.[0], [||]) 41 | else 42 | FSharpValue.MakeUnion(cases.[1], [|value|]) 43 | 44 | /// `Newtonsoft.Json` converter which could (de-)serialize `Option<'a>` values: 45 | /// 46 | /// from: 47 | /// ```fsharp 48 | /// type Record = 49 | /// { Id : string 50 | /// Name: string option } 51 | /// let withName = { Id = "1"; Name = Some "A" } 52 | /// let woName = { Id = "2"; Name = None } 53 | /// ``` 54 | /// 55 | /// to jsons: 56 | /// 57 | /// ```json 58 | /// { 59 | /// "Id": "1", 60 | /// "Name": "A" 61 | /// } 62 | /// ``` 63 | /// 64 | /// ```json 65 | /// { 66 | /// "Id": "2", 67 | /// "Name": null 68 | /// } 69 | /// ``` 70 | let optionConverter = OptionConverter() :> JsonConverter -------------------------------------------------------------------------------- /tests/GiraffeGenerator.IntegrationTests/RemoveGenerated.ps1: -------------------------------------------------------------------------------- 1 | ls Spec*.fs | ?{ -not $_.Name.EndsWith("Tests.fs") } | rm -------------------------------------------------------------------------------- /tests/GiraffeGenerator.IntegrationTests/SpecForNodaTimeDateTimeInstantFormatHandlingTests.fs: -------------------------------------------------------------------------------- 1 | namespace GiraffeGenerator.IntegrationTests 2 | 3 | open System.Net 4 | open System.Net.Http 5 | open System.Text 6 | open FSharp.Control.Tasks.V2.ContextInsensitive 7 | open System 8 | open Giraffe 9 | open Giraffe.Serialization.Json 10 | open Microsoft.AspNetCore 11 | open Microsoft.AspNetCore.Builder 12 | open Microsoft.AspNetCore.Hosting 13 | open Microsoft.AspNetCore.TestHost 14 | open Microsoft.Extensions.DependencyInjection 15 | open Microsoft.Extensions.Logging 16 | open Newtonsoft.Json 17 | open NodaTime 18 | open NodaTime.Serialization.JsonNet 19 | open Xunit 20 | 21 | type SpecForNodaTimeDateTimeFormatHandlingTests() = 22 | 23 | let serviceInstant = 24 | { 25 | new SpecForNodaTimeDateTimeInstantFormatHandling.Service() with 26 | member _.PostIdInput ((body, ctx)) = 27 | task { 28 | return body 29 | } 30 | 31 | } 32 | let serviceLocalDateTime = 33 | { 34 | new SpecForNodaTimeDateTimeLocalDateTimeFormatHandling.Service() with 35 | member _.PostIdInput ((body, ctx)) = 36 | task { 37 | return body 38 | } 39 | 40 | } 41 | let serviceOffsetDateTime = 42 | { 43 | new SpecForNodaTimeDateTimeOffsetDateTimeFormatHandling.Service() with 44 | member _.PostIdInput ((body, ctx)) = 45 | task { 46 | return body 47 | } 48 | 49 | } 50 | let serviceZonedDateTime = 51 | { 52 | new SpecForNodaTimeDateTimeZonedDateTimeFormatHandling.Service() with 53 | member _.PostIdInput ((body, ctx)) = 54 | task { 55 | return body 56 | } 57 | 58 | } 59 | let serviceDefaultDateTime = 60 | { 61 | new SpecForNodaTimeDateTimeDefaultFormatHandling.Service() with 62 | member _.PostIdInput ((body, ctx)) = 63 | task { 64 | return body 65 | } 66 | 67 | } 68 | 69 | let configureApp (app : IApplicationBuilder) = 70 | app.UseGiraffe 71 | <| 72 | choose [ 73 | subRoute "/instant" SpecForNodaTimeDateTimeInstantFormatHandling.webApp 74 | subRoute "/local-date-time" SpecForNodaTimeDateTimeLocalDateTimeFormatHandling.webApp 75 | subRoute "/offset-date-time" SpecForNodaTimeDateTimeOffsetDateTimeFormatHandling.webApp 76 | subRoute "/zoned-date-time" SpecForNodaTimeDateTimeZonedDateTimeFormatHandling.webApp 77 | subRoute "/default" SpecForNodaTimeDateTimeDefaultFormatHandling.webApp 78 | ] 79 | 80 | let jsonSettings = 81 | let s = JsonSerializerSettings(Converters=System.Collections.Generic.List()) 82 | s.Converters.Add optionConverter 83 | s.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb) 84 | let configureServices (services : IServiceCollection) = 85 | services 86 | .AddGiraffe() 87 | .AddSingleton(NewtonsoftJsonSerializer(jsonSettings)) 88 | .AddSingleton(serviceInstant) 89 | .AddSingleton(serviceLocalDateTime) 90 | .AddSingleton(serviceOffsetDateTime) 91 | .AddSingleton(serviceZonedDateTime) 92 | .AddSingleton(serviceDefaultDateTime) 93 | |> ignore 94 | 95 | let configureLogging (loggerBuilder : ILoggingBuilder) = 96 | loggerBuilder.AddFilter(fun lvl -> lvl.Equals LogLevel.Error) 97 | .AddConsole() 98 | .AddDebug() |> ignore 99 | 100 | let webHostBuilder = 101 | WebHost.CreateDefaultBuilder() 102 | .Configure(Action configureApp) 103 | .ConfigureServices(configureServices) 104 | .ConfigureLogging(configureLogging) 105 | 106 | let server = new TestServer(webHostBuilder) 107 | let client = server.CreateClient() 108 | 109 | let testBody prefix value = task { 110 | let expected = value 111 | let jsonInputString = JsonConvert.SerializeObject(value, jsonSettings) 112 | use jsonContent = new StringContent(jsonInputString, Encoding.UTF8, "text/json") 113 | let url = sprintf "/%s/id" prefix 114 | let! response = client.PostAsync(url, jsonContent) 115 | let! text = response.Content.ReadAsStringAsync() 116 | let deserialized = JsonConvert.DeserializeObject(text, value.GetType(), jsonSettings) 117 | Assert.Equal(expected, deserialized) 118 | Assert.Equal(HttpStatusCode.OK, response.StatusCode) 119 | } 120 | 121 | let instant = Instant.FromUtc(2020, 10, 22, 14, 20) 122 | let zone = DateTimeZoneProviders.Tzdb.Item "Europe/Moscow" 123 | let zoned = instant.InZone zone 124 | 125 | [] 126 | let ``API method accepts and returns date time in the instant format``() = 127 | let value: SpecForNodaTimeDateTimeInstantFormatHandling.bodyModel = 128 | { dateTime = instant } 129 | testBody "instant" value 130 | 131 | [] 132 | let ``API method accepts and returns date time in the local date time format``() = 133 | let value: SpecForNodaTimeDateTimeLocalDateTimeFormatHandling.bodyModel = 134 | { dateTime = zoned.LocalDateTime } 135 | testBody "local-date-time" value 136 | 137 | [] 138 | let ``API method accepts and returns date time in the offset date time format``() = 139 | let value: SpecForNodaTimeDateTimeOffsetDateTimeFormatHandling.bodyModel = 140 | { dateTime = zoned.ToOffsetDateTime() } 141 | testBody "offset-date-time" value 142 | 143 | [] 144 | let ``API method accepts and returns date time in the zoned date time format``() = 145 | let value: SpecForNodaTimeDateTimeZonedDateTimeFormatHandling.bodyModel = 146 | { dateTime = zoned } 147 | testBody "zoned-date-time" value 148 | 149 | 150 | [] 151 | let ``API method accepts and returns date time in the default format``() = 152 | let value: SpecForNodaTimeDateTimeOffsetDateTimeFormatHandling.bodyModel = 153 | { dateTime = zoned.ToOffsetDateTime() } 154 | testBody "default" value 155 | 156 | interface IDisposable with 157 | member _.Dispose() = server.Dispose() 158 | 159 | -------------------------------------------------------------------------------- /tests/GiraffeGenerator.IntegrationTests/SpecGeneralForNodaTimeTests.fs: -------------------------------------------------------------------------------- 1 | namespace GiraffeGenerator.IntegrationTests 2 | 3 | open System.Net 4 | open System.Net.Http 5 | open System.Text 6 | open FSharp.Control.Tasks.V2.ContextInsensitive 7 | open System 8 | open Giraffe 9 | open Giraffe.Serialization.Json 10 | open Microsoft.AspNetCore 11 | open Microsoft.AspNetCore.Builder 12 | open Microsoft.AspNetCore.Hosting 13 | open Microsoft.AspNetCore.TestHost 14 | open Microsoft.Extensions.DependencyInjection 15 | open Microsoft.Extensions.Logging 16 | open Newtonsoft.Json 17 | open NodaTime 18 | open NodaTime.Serialization.JsonNet 19 | open Xunit 20 | 21 | type SpecGeneralForNodaTimeTests() = 22 | 23 | let specGeneralForNodaTimeService = 24 | { 25 | new Specforgeneralnodatimetesting.Service() with 26 | member _.PostIdInput ((input, body, ctx)) = 27 | task { 28 | return 29 | { 30 | dateParamFromQuery = input.dateParam 31 | timeParamFromQuery = input.timeParam 32 | offsetParamFromQuery = input.offsetParam 33 | durationParamFromQuery = input.durationParam 34 | instantParamFromQuery = input.instantParam 35 | localDateTimeParamFromQuery = input.localDateTimeParam 36 | offsetDateTimeParamFromQuery = input.offsetDateTimeParam 37 | forDefaultsTesting = body.forDefaultsTesting 38 | zonedDateTime = body.zonedDateTime 39 | } 40 | } 41 | 42 | } 43 | 44 | let configureApp (app : IApplicationBuilder) = 45 | app.UseGiraffe Specforgeneralnodatimetesting.webApp 46 | 47 | let jsonSettings = 48 | let s = JsonSerializerSettings(Converters=System.Collections.Generic.List()) 49 | s.Converters.Add optionConverter 50 | s.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb) 51 | let configureServices (services : IServiceCollection) = 52 | services 53 | .AddGiraffe() 54 | .AddSingleton(NewtonsoftJsonSerializer(jsonSettings)) 55 | .AddSingleton(specGeneralForNodaTimeService) 56 | |> ignore 57 | 58 | let configureLogging (loggerBuilder : ILoggingBuilder) = 59 | loggerBuilder.AddFilter(fun lvl -> lvl.Equals LogLevel.Error) 60 | .AddConsole() 61 | .AddDebug() |> ignore 62 | 63 | let webHostBuilder = 64 | WebHost.CreateDefaultBuilder() 65 | .Configure(Action configureApp) 66 | .ConfigureServices(configureServices) 67 | .ConfigureLogging(configureLogging) 68 | 69 | let server = new TestServer(webHostBuilder) 70 | let client = server.CreateClient() 71 | 72 | [] 73 | let ``Fully specified input parameters returned as output as is``() = task { 74 | let date = LocalDate(1995, 10, 9) 75 | let time = LocalTime(01, 30, 00) 76 | let offset = Offset.FromHours 1 77 | let duration = Duration.FromMinutes 15. 78 | let zone = DateTimeZoneProviders.Tzdb.[ "Europe/London" ] 79 | let dateTime = date + time 80 | let offsetDateTime = OffsetDateTime(dateTime, offset) 81 | let zonedDateTime = dateTime.InZoneLeniently zone 82 | let instant = zonedDateTime.ToInstant() 83 | let expected: Specforgeneralnodatimetesting.dataSetListOutput = 84 | { 85 | dateParamFromQuery = date |> Some 86 | timeParamFromQuery = time |> Some 87 | offsetParamFromQuery = offset |> Some 88 | durationParamFromQuery = duration |> Some 89 | instantParamFromQuery = instant |> Some 90 | localDateTimeParamFromQuery = dateTime |> Some 91 | offsetDateTimeParamFromQuery = offsetDateTime |> Some 92 | zonedDateTime = zonedDateTime 93 | forDefaultsTesting = 94 | { 95 | dateParamFromBody = date 96 | timeParamFromBody = time 97 | offsetParamFromBody = offset 98 | timeZoneParamFromBody = zone 99 | periodParamFromBody = 100 | let v = PeriodBuilder() 101 | v.Days <- 8 102 | v.Hours <- 115L 103 | v.Minutes <- 78L 104 | v.Build() 105 | durationParamFromBody = duration 106 | instantParamFromBody = instant 107 | localDateTimeParamFromBody = dateTime 108 | offsetDateTimeParamFromBody = offsetDateTime 109 | } 110 | } 111 | // usage of codegen requires custom converter for options and noda time types 112 | // as we don't know which json framework user is going to use. 113 | // But we want tests to be nice and to respect the spec, so here is OptionConverter.fs (by @Szer) and NodaTime.Serialization.JsonNet 114 | // Also, json by hand for the success story this test is checking to ensure that spec is respected 115 | let jsonInputString = """{ 116 | "forDefaultsTesting": { 117 | "dateParamFromBody": "1995-10-09", 118 | "timeParamFromBody": "01:30:00", 119 | "offsetParamFromBody": "+01:00", 120 | "timeZoneParamFromBody": "Europe/London", 121 | "periodParamFromBody": "P8DT115H78M", 122 | "durationParamFromBody": "00:15:00", 123 | "instantParamFromBody": "1995-10-09T00:30:00Z", 124 | "localDateTimeParamFromBody": "1995-10-09T01:30:00", 125 | "offsetDateTimeParamFromBody": "1995-10-09T01:30:00+01:00" 126 | }, 127 | "zonedDateTime": "1995-10-09T01:30:00.000+01 Europe/London" 128 | } 129 | """ 130 | use jsonContent = new StringContent(jsonInputString, Encoding.UTF8, "text/json") 131 | let query = // note the difference in duration formats. Also, default zoned date time format for query is still unknown 132 | [ 133 | "dateParam", "1995-10-09" 134 | "timeParam", "01:30:00" 135 | "offsetParam", "+01:00" 136 | "durationParam", "00:00:15:00" 137 | "instantParam", "1995-10-09T00:30:00Z" 138 | "localDateTimeParam", "1995-10-09T01:30:00" 139 | "offsetDateTimeParam", "1995-10-09T01:30:00+01:00" 140 | ] 141 | |> Seq.map (fun (n,v) -> n, Uri.EscapeDataString v) 142 | |> Seq.map (fun (n,v) -> n + "=" + v) 143 | |> String.concat "&" 144 | let url = sprintf "/id?%s" query 145 | let! response = client.PostAsync(url, jsonContent) 146 | let! text = response.Content.ReadAsStringAsync() 147 | let deserialized = JsonConvert.DeserializeObject<_>(text, jsonSettings) 148 | Assert.Equal(expected, deserialized) 149 | Assert.Equal(HttpStatusCode.OK, response.StatusCode) 150 | } 151 | 152 | [] 153 | let ``Unspecified input returns expected defaults``() = task { 154 | let zonedDateTime = ZonedDateTime(Instant.FromUtc(1995, 10, 09, 00, 30), DateTimeZoneProviders.Tzdb.[ "Europe/London" ]) 155 | let expected: Specforgeneralnodatimetesting.dataSetListOutput = 156 | { 157 | dateParamFromQuery = None 158 | timeParamFromQuery = None 159 | offsetParamFromQuery = None 160 | durationParamFromQuery = None 161 | instantParamFromQuery = None 162 | localDateTimeParamFromQuery = None 163 | offsetDateTimeParamFromQuery = None 164 | zonedDateTime = zonedDateTime 165 | forDefaultsTesting = 166 | { 167 | dateParamFromBody = LocalDate(2020,10,21) 168 | timeParamFromBody = LocalTime(19,58,12) 169 | offsetParamFromBody = Offset.FromHoursAndMinutes(-14, -55) + Offset.FromSeconds -1 170 | timeZoneParamFromBody = DateTimeZoneProviders.Tzdb.[ "Europe/Moscow" ] 171 | periodParamFromBody = 172 | let v = PeriodBuilder() 173 | v.Days <- 13 174 | v.Hours <- 49L 175 | v.Minutes <- 156L 176 | v.Build() 177 | durationParamFromBody = Duration.FromHours(-364) + Duration.FromMinutes -36L 178 | instantParamFromBody = Instant.FromUtc(2020, 10, 21, 16, 59, 12) + Duration.FromTicks 1L 179 | localDateTimeParamFromBody = LocalDateTime(2020, 10, 21, 19, 59, 12) + Period.FromTicks 1L 180 | offsetDateTimeParamFromBody = OffsetDateTime(LocalDateTime(2020, 10, 21, 19, 59, 12) + Period.FromTicks 1L, Offset.FromHours 3) 181 | } 182 | } 183 | // usage of codegen requires custom converter for options and noda time types 184 | // as we don't know which json framework user is going to use. 185 | // But we want tests to be nice and to respect the spec, so here is OptionConverter.fs (by @Szer) and NodaTime.Serialization.JsonNet 186 | // Also, json by hand for the success story this test is checking to ensure that spec is respected 187 | let jsonInputString = """{ 188 | "forDefaultsTesting": {}, 189 | "zonedDateTime": "1995-10-09T01:30:00.000+01 Europe/London" 190 | } 191 | """ 192 | use jsonContent = new StringContent(jsonInputString, Encoding.UTF8, "text/json") 193 | let! response = client.PostAsync("/id", jsonContent) 194 | let! text = response.Content.ReadAsStringAsync() 195 | let deserialized = JsonConvert.DeserializeObject<_>(text, jsonSettings) 196 | Assert.Equal(expected, deserialized) 197 | Assert.Equal(HttpStatusCode.OK, response.StatusCode) 198 | } 199 | 200 | interface IDisposable with 201 | member _.Dispose() = server.Dispose() 202 | 203 | -------------------------------------------------------------------------------- /tests/GiraffeGenerator.IntegrationTests/SpecSimpleTests.fs: -------------------------------------------------------------------------------- 1 | namespace GiraffeGenerator.IntegrationTests 2 | 3 | open System.Net 4 | open System.Threading.Tasks 5 | open FSharp.Control.Tasks.V2.ContextInsensitive 6 | open System 7 | open Giraffe 8 | open Microsoft.AspNetCore 9 | open Microsoft.AspNetCore.Builder 10 | open Microsoft.AspNetCore.Hosting 11 | open Microsoft.AspNetCore.TestHost 12 | open Microsoft.Extensions.DependencyInjection 13 | open Microsoft.Extensions.Logging 14 | open Xunit 15 | 16 | type SpecSimpleTests() = 17 | 18 | let simpleSpecService = 19 | { new SpecSimpleAPI.Service() with 20 | member _.ListVersionsv2Input _ = Task.FromResult "123" 21 | member _.GetVersionDetailsv2Input _ = Task.FromResult "234" 22 | member _.PostVersionDetailsv2Input _ = Task.FromResult "345" } 23 | 24 | let configureApp (app : IApplicationBuilder) = 25 | app.UseGiraffe SpecSimpleAPI.webApp 26 | 27 | let configureServices (services : IServiceCollection) = 28 | services 29 | .AddGiraffe() 30 | .AddSingleton(simpleSpecService) 31 | |> ignore 32 | 33 | let configureLogging (loggerBuilder : ILoggingBuilder) = 34 | loggerBuilder.AddFilter(fun lvl -> lvl.Equals LogLevel.Error) 35 | .AddConsole() 36 | .AddDebug() |> ignore 37 | 38 | let webHostBuilder = 39 | WebHost.CreateDefaultBuilder() 40 | .Configure(Action configureApp) 41 | .ConfigureServices(configureServices) 42 | .ConfigureLogging(configureLogging) 43 | 44 | let server = new TestServer(webHostBuilder) 45 | let client = server.CreateClient() 46 | 47 | [] 48 | let ``GET / -> OK "123"``() = task { 49 | let! response = client.GetAsync("/") 50 | let! text = response.Content.ReadAsStringAsync() 51 | Assert.Equal("\"123\"",text) 52 | Assert.Equal(HttpStatusCode.OK ,response.StatusCode) 53 | } 54 | 55 | [] 56 | let ``GET /v2 -> OK "234"``() = task { 57 | let! response = client.GetAsync("/v2") 58 | let! text = response.Content.ReadAsStringAsync() 59 | Assert.Equal("\"234\"",text) 60 | Assert.Equal(HttpStatusCode.OK ,response.StatusCode) 61 | } 62 | 63 | [] 64 | let ``POST /v2 -> OK "345"``() = task { 65 | let! response = client.PostAsync("/v2", null) 66 | let! text = response.Content.ReadAsStringAsync() 67 | Assert.Equal("\"345\"",text) 68 | Assert.Equal(HttpStatusCode.OK ,response.StatusCode) 69 | } 70 | 71 | interface IDisposable with 72 | member _.Dispose() = server.Dispose() 73 | 74 | -------------------------------------------------------------------------------- /tests/GiraffeGenerator.IntegrationTests/SpecWithArgumentsTests.fs: -------------------------------------------------------------------------------- 1 | namespace GiraffeGenerator.IntegrationTests 2 | 3 | open System.Collections.Generic 4 | open System.Net 5 | open System.Net.Http 6 | open System.Text 7 | open FSharp.Control.Tasks.V2.ContextInsensitive 8 | open System 9 | open Giraffe 10 | open Microsoft.AspNetCore 11 | open Microsoft.AspNetCore.Builder 12 | open Microsoft.AspNetCore.Hosting 13 | open Microsoft.AspNetCore.TestHost 14 | open Microsoft.Extensions.DependencyInjection 15 | open Microsoft.Extensions.Logging 16 | open Xunit 17 | 18 | type SpecWithArgumentsTests() = 19 | 20 | let specWithArgumentsService= 21 | { new SpecwithargumentsAPI.Service() with 22 | 23 | member _.ListSearchableFieldsInput ((args,ctx)) = task { 24 | return 25 | if args.version = "v1" then 26 | Choice1Of2 "ok" 27 | elif args.dataset = "foo" then 28 | Choice1Of2 "good" 29 | else 30 | Choice2Of2 "not_ok" 31 | } 32 | 33 | member _.PerformSearchInput ((args,body,ctx)) = task { 34 | return 35 | if args.version = "v1" then 36 | Choice1Of2 [| box "abc" |] 37 | elif args.dataset = "foo" then 38 | Choice1Of2 [| box "good" |] 39 | else 40 | Choice2Of2 () 41 | } 42 | 43 | member _.OnlyPathParametersInput ((args, ctx)) = task { 44 | return [|args.dataset; args.version|] 45 | } 46 | member _.OnlyQueryParametersInput ((args, ctx)) = task { 47 | return [|args.dataset; args.version|] 48 | } 49 | member _.OnlyFormParametersInput ((args, ctx)) = task { 50 | return [|args.dataset; args.version|] 51 | } 52 | member _.OnlyJsonParametersInput ((args, ctx)) = task { 53 | return [|args.dataset; args.version|] 54 | } 55 | } 56 | 57 | let configureApp (app : IApplicationBuilder) = 58 | app.UseGiraffe SpecwithargumentsAPI.webApp 59 | 60 | let configureServices (services : IServiceCollection) = 61 | services 62 | .AddGiraffe() 63 | .AddSingleton(specWithArgumentsService) 64 | |> ignore 65 | 66 | let configureLogging (loggerBuilder : ILoggingBuilder) = 67 | loggerBuilder.AddFilter(fun lvl -> lvl.Equals LogLevel.Error) 68 | .AddConsole() 69 | .AddDebug() |> ignore 70 | 71 | let webHostBuilder = 72 | WebHost.CreateDefaultBuilder() 73 | .Configure(Action configureApp) 74 | .ConfigureServices(configureServices) 75 | .ConfigureLogging(configureLogging) 76 | 77 | let server = new TestServer(webHostBuilder) 78 | let client = server.CreateClient() 79 | 80 | [] 81 | let ``GET /abc/v1/fields -> OK "ok"``() = task { 82 | let! response = client.GetAsync("/abc/v1/fields") 83 | let! text = response.Content.ReadAsStringAsync() 84 | Assert.Equal("\"ok\"",text) 85 | Assert.Equal(HttpStatusCode.OK, response.StatusCode) 86 | } 87 | 88 | [] 89 | let ``GET /foo/v2/fields -> OK "good"``() = task { 90 | let! response = client.GetAsync("/foo/v2/fields") 91 | let! text = response.Content.ReadAsStringAsync() 92 | Assert.Equal("\"good\"",text) 93 | Assert.Equal(HttpStatusCode.OK, response.StatusCode) 94 | } 95 | 96 | [] 97 | let ``GET /abc/v3/fields -> NOT_FOUND``() = task { 98 | let! response = client.GetAsync("/abc/v3/fields") 99 | let! text = response.Content.ReadAsStringAsync() 100 | Assert.Equal(HttpStatusCode.NotFound, response.StatusCode) 101 | } 102 | 103 | [] 104 | let ``POST /abc/v1/records -> OK ["abc"]``() = task { 105 | use content = new FormUrlEncodedContent(Seq.empty) 106 | let! response = client.PostAsync("/abc/v1/records", content) 107 | let! text = response.Content.ReadAsStringAsync() 108 | Assert.Equal("[\"abc\"]",text) 109 | Assert.Equal(HttpStatusCode.OK, response.StatusCode) 110 | } 111 | 112 | [] 113 | let ``POST /foo/v2/records -> OK ["good"]``() = task { 114 | use content = new FormUrlEncodedContent(Seq.empty) 115 | let! response = client.PostAsync("/foo/v2/records", content) 116 | let! text = response.Content.ReadAsStringAsync() 117 | Assert.Equal("[\"good\"]",text) 118 | Assert.Equal(HttpStatusCode.OK, response.StatusCode) 119 | } 120 | 121 | [] 122 | let ``POST /abc/v3/records -> NOT_FOUND``() = task { 123 | use content = new FormUrlEncodedContent(Seq.empty) 124 | let! response = client.PostAsync("/abc/v3/records", content) 125 | let! text = response.Content.ReadAsStringAsync() 126 | Assert.Equal(HttpStatusCode.NotFound, response.StatusCode) 127 | } 128 | 129 | [] 130 | let ``POST /foo/v2/only-path-parameters -> OK ["foo", "v2"]``() = task { 131 | let! response = client.PostAsync("/foo/v2/only-path-parameters", null) 132 | let! text = response.Content.ReadAsStringAsync() 133 | Assert.Equal("[\"foo\",\"v2\"]",text) 134 | Assert.Equal(HttpStatusCode.OK, response.StatusCode) 135 | } 136 | 137 | [] 138 | let ``POST /only-query-parameters?dataset=foo&version=v2 -> OK ["foo", "v2"]``() = task { 139 | let! response = client.PostAsync("/only-query-parameters?dataset=foo&version=v2", null) 140 | let! text = response.Content.ReadAsStringAsync() 141 | Assert.Equal("[\"foo\",\"v2\"]",text) 142 | Assert.Equal(HttpStatusCode.OK, response.StatusCode) 143 | } 144 | 145 | [] 146 | let ``POST /only-form-parameters (FormUrlEncoded dataset=foo&version=v2) -> OK ["foo", "v2"]``() = task { 147 | let content = 148 | seq { 149 | "dataset", "foo" 150 | "version", "v2" 151 | } 152 | |> Seq.map KeyValuePair 153 | use content = new FormUrlEncodedContent(content) 154 | let! response = client.PostAsync("/only-form-parameters", content) 155 | let! text = response.Content.ReadAsStringAsync() 156 | Assert.Equal("[\"foo\",\"v2\"]",text) 157 | Assert.Equal(HttpStatusCode.OK, response.StatusCode) 158 | } 159 | 160 | [] 161 | let ``POST /only-json-parameters (JSON {"dataset":"foo","version":"v2"}) -> OK ["foo", "v2"]``() = task { 162 | let content = """{"dataset":"foo","version":"v2"}""" 163 | use content = new StringContent(content, Encoding.UTF8, "application/json") 164 | let! response = client.PostAsync("/only-json-parameters", content) 165 | let! text = response.Content.ReadAsStringAsync() 166 | Assert.Equal("[\"foo\",\"v2\"]",text) 167 | Assert.Equal(HttpStatusCode.OK, response.StatusCode) 168 | } 169 | 170 | interface IDisposable with 171 | member _.Dispose() = server.Dispose() 172 | 173 | -------------------------------------------------------------------------------- /tests/GiraffeGenerator.IntegrationTests/SpecWithParametersAndRequestBodyTests.fs: -------------------------------------------------------------------------------- 1 | namespace GiraffeGenerator.IntegrationTests 2 | 3 | open System.Net 4 | open System.Net.Http 5 | open System.Text 6 | open FSharp.Control.Tasks.V2.ContextInsensitive 7 | open System 8 | open Giraffe 9 | open Giraffe.Serialization.Json 10 | open Microsoft.AspNetCore 11 | open Microsoft.AspNetCore.Builder 12 | open Microsoft.AspNetCore.Hosting 13 | open Microsoft.AspNetCore.TestHost 14 | open Microsoft.Extensions.DependencyInjection 15 | open Microsoft.Extensions.Logging 16 | open Newtonsoft.Json 17 | open Xunit 18 | 19 | type SpecWithParametersAndRequestBodyTests() = 20 | 21 | let specWithParametersAndRequestBodyService= 22 | { 23 | new SpecwithparametersandrequestbodyAPI.Service() with 24 | member _.PostIdInput ((input, body, ctx)) = 25 | task { 26 | return 27 | { 28 | pathParam = input.paramFromPath 29 | queryParam = input.paramFromQuery 30 | total = body.total 31 | defaultsTest = body.defaultsTest 32 | } 33 | } 34 | 35 | } 36 | 37 | let configureApp (app : IApplicationBuilder) = 38 | app.UseGiraffe SpecwithparametersandrequestbodyAPI.webApp 39 | 40 | let jsonSettings = 41 | JsonSerializerSettings(Converters=[|optionConverter|]) 42 | let configureServices (services : IServiceCollection) = 43 | services 44 | .AddGiraffe() 45 | .AddSingleton(NewtonsoftJsonSerializer(jsonSettings)) 46 | .AddSingleton(specWithParametersAndRequestBodyService) 47 | |> ignore 48 | 49 | let configureLogging (loggerBuilder : ILoggingBuilder) = 50 | loggerBuilder.AddFilter(fun lvl -> lvl.Equals LogLevel.Error) 51 | .AddConsole() 52 | .AddDebug() |> ignore 53 | 54 | let webHostBuilder = 55 | WebHost.CreateDefaultBuilder() 56 | .Configure(Action configureApp) 57 | .ConfigureServices(configureServices) 58 | .ConfigureLogging(configureLogging) 59 | 60 | let server = new TestServer(webHostBuilder) 61 | let client = server.CreateClient() 62 | 63 | [] 64 | let ``Fully specified input parameters returned as output as is``() = task { 65 | let expected: SpecwithparametersandrequestbodyAPI.dataSetListOutput = 66 | { 67 | pathParam = "valueOfThePathParam" 68 | queryParam = Some 1123 69 | total = Some 165443 70 | defaultsTest = 71 | { 72 | optionalArrayWithDefaultItems = Some [| 1;2;3;4 |] 73 | requiredArrayWithDefaultItems = [| 5;6;7;8 |] 74 | apiKey = "passw0rd" 75 | apiVersionNumber = "48" 76 | apiUrl = Uri("https://microsoft.com") 77 | apiCount = 1L 78 | apiAvg = 9 79 | isInternal = false 80 | start = DateTime(2019, 1, 21) 81 | someDateTime = DateTimeOffset(DateTime(2019,1,21,23,59,16,343), TimeSpan(3,0,0)) 82 | pi = 3.141592654 83 | someUid = Guid.Parse("6662cbfd-f323-4b7d-bcc0-28f127c2b365") 84 | } 85 | } 86 | // usage of codegen requires custom converter for options 87 | // as we don't know which json framework user is going to use. 88 | // But we want tests to be nice and to respect the spec, so here is OptionConverter.fs (by @Szer) 89 | // Also, json by hand for the success story this test is checking to ensure that spec is respected 90 | let jsonInputString = """{ 91 | "total": 165443, 92 | "defaultsTest": { 93 | "optionalArrayWithDefaultItems": [1,2,3,4], 94 | "requiredArrayWithDefaultItems": [5,6,7,8], 95 | "apiKey": "passw0rd", 96 | "apiVersionNumber": "48", 97 | "apiUrl": "https://microsoft.com", 98 | "apiCount": 1, 99 | "apiAvg": 9, 100 | "isInternal": false, 101 | "start": "2019-1-21", 102 | "someDateTime": "2019-1-21T23:59:16.343+03:00", 103 | "pi": 3.141592654, 104 | "someUid": "6662cbfd-f323-4b7d-bcc0-28f127c2b365" 105 | } 106 | } 107 | """ 108 | use jsonContent = new StringContent(jsonInputString, Encoding.UTF8, "text/json") 109 | let! response = client.PostAsync("/id/valueOfThePathParam?param=1123", jsonContent) 110 | let! text = response.Content.ReadAsStringAsync() 111 | let deserialized = JsonConvert.DeserializeObject<_>(text, jsonSettings) 112 | Assert.Equal(expected, deserialized) 113 | Assert.Equal(HttpStatusCode.OK, response.StatusCode) 114 | } 115 | 116 | [] 117 | let ``All defaults are applied``() = task { 118 | let input: SpecwithparametersandrequestbodyAPI.dataSetListInputForBinding = 119 | { 120 | total = None 121 | defaultsTest = 122 | { 123 | optionalArrayWithDefaultItems = Some [| None;None;None |] 124 | requiredArrayWithDefaultItems = [| None;None |] 125 | apiKey = None 126 | apiVersionNumber = None 127 | apiUrl = None 128 | apiCount = None 129 | apiAvg = None 130 | isInternal = None 131 | start = None 132 | someDateTime = None 133 | pi = None 134 | someUid = None 135 | } 136 | } 137 | let expected: SpecwithparametersandrequestbodyAPI.dataSetListOutput = 138 | { 139 | pathParam = "valueOfThePathParam" 140 | queryParam = None 141 | total = None 142 | defaultsTest = 143 | { 144 | optionalArrayWithDefaultItems = Some [| 42;42;42 |] // three non-int items, all defaulted to 42 145 | requiredArrayWithDefaultItems = [| 48;48 |] // two non-int items, all defaulted to 48 146 | apiKey = "pa$$word" 147 | apiVersionNumber = "1" 148 | apiUrl = Uri("http://localhost:8080/api") 149 | apiCount = 123456789123456L 150 | apiAvg = 1234567890 151 | isInternal = true 152 | start = DateTime(2020, 10, 8) 153 | someDateTime = DateTimeOffset(DateTime(2020, 10, 8, 00, 55, 00, 0), TimeSpan(3,0,0)) 154 | pi = 3.14 155 | someUid = Guid.Parse("8282cbfd-f323-4b7d-bcc0-28f127c2b365") 156 | } 157 | } 158 | let jsonInputString = JsonConvert.SerializeObject(input, jsonSettings) 159 | use jsonContent = new StringContent(jsonInputString, Encoding.UTF8, "text/json") 160 | let! response = client.PostAsync("/id/valueOfThePathParam", jsonContent) 161 | let! text = response.Content.ReadAsStringAsync() 162 | let deserialized = JsonConvert.DeserializeObject<_>(text, jsonSettings) 163 | Assert.Equal(expected, deserialized) 164 | Assert.Equal(HttpStatusCode.OK, response.StatusCode) 165 | } 166 | 167 | [] 168 | let ``No defaults are applied to an array items which has no default value itself and passed as empty or none``() = task { 169 | let input: SpecwithparametersandrequestbodyAPI.dataSetListInputForBinding = 170 | { 171 | total = None 172 | defaultsTest = 173 | { 174 | optionalArrayWithDefaultItems = None 175 | requiredArrayWithDefaultItems = [||] 176 | apiKey = Some <| "pa$$word" 177 | apiVersionNumber = Some <| "1" 178 | apiUrl = Some <| Uri("http://localhost:8080/api") 179 | apiCount = Some <| 123456789123456L 180 | apiAvg = Some <| 1234567890 181 | isInternal = Some <| true 182 | start = Some <| DateTime(2020, 10, 8) 183 | someDateTime = Some <| DateTimeOffset(DateTime(2020, 10, 8, 00, 55, 00, 0), TimeSpan(3,0,0)) 184 | pi = Some <| 3.14 185 | someUid = Some <| Guid.Parse("8282cbfd-f323-4b7d-bcc0-28f127c2b365") 186 | } 187 | } 188 | let expected: SpecwithparametersandrequestbodyAPI.dataSetListOutput = 189 | { 190 | pathParam = "valueOfThePathParam" 191 | queryParam = None 192 | total = None 193 | defaultsTest = 194 | { 195 | optionalArrayWithDefaultItems = None // no defaults for no array 196 | requiredArrayWithDefaultItems = [||] // no default values for absent items 197 | apiKey = "pa$$word" 198 | apiVersionNumber = "1" 199 | apiUrl = Uri("http://localhost:8080/api") 200 | apiCount = 123456789123456L 201 | apiAvg = 1234567890 202 | isInternal = true 203 | start = DateTime(2020, 10, 8) 204 | someDateTime = DateTimeOffset(DateTime(2020, 10, 8, 00, 55, 00, 0), TimeSpan(3,0,0)) 205 | pi = 3.14 206 | someUid = Guid.Parse("8282cbfd-f323-4b7d-bcc0-28f127c2b365") 207 | } 208 | } 209 | let jsonInputString = JsonConvert.SerializeObject(input, jsonSettings) 210 | use jsonContent = new StringContent(jsonInputString, Encoding.UTF8, "text/json") 211 | let! response = client.PostAsync("/id/valueOfThePathParam", jsonContent) 212 | let! text = response.Content.ReadAsStringAsync() 213 | let deserialized = JsonConvert.DeserializeObject<_>(text, jsonSettings) 214 | Assert.Equal(expected, deserialized) 215 | Assert.Equal(HttpStatusCode.OK, response.StatusCode) 216 | } 217 | 218 | interface IDisposable with 219 | member _.Dispose() = server.Dispose() 220 | 221 | -------------------------------------------------------------------------------- /tests/GiraffeGenerator.IntegrationTests/SpecWithSchemasTests.fs: -------------------------------------------------------------------------------- 1 | namespace GiraffeGenerator.IntegrationTests 2 | 3 | open System.Net 4 | open System.Threading.Tasks 5 | open FSharp.Control.Tasks.V2.ContextInsensitive 6 | open System 7 | open Giraffe 8 | open Microsoft.AspNetCore 9 | open Microsoft.AspNetCore.Builder 10 | open Microsoft.AspNetCore.Hosting 11 | open Microsoft.AspNetCore.TestHost 12 | open Microsoft.Extensions.DependencyInjection 13 | open Microsoft.Extensions.Logging 14 | open Xunit 15 | 16 | type SpecWithSchemasTests() = 17 | 18 | let specWithSchemasService= 19 | { new SpecwithschemasAPI.Service() with 20 | 21 | member _.ListVersionsv2Input _ = Task.FromResult "123" 22 | 23 | member _.GetVersionDetailsv2Input ctx = task { 24 | return { SpecwithschemasAPI.dataSetList.apis = [||]; total = 123 } 25 | } 26 | member _.PostVersionDetailsv2Input _ = Task.FromResult "345"} 27 | 28 | let configureApp (app : IApplicationBuilder) = 29 | app.UseGiraffe SpecwithschemasAPI.webApp 30 | 31 | let configureServices (services : IServiceCollection) = 32 | services 33 | .AddGiraffe() 34 | .AddSingleton(specWithSchemasService) 35 | |> ignore 36 | 37 | let configureLogging (loggerBuilder : ILoggingBuilder) = 38 | loggerBuilder.AddFilter(fun lvl -> lvl.Equals LogLevel.Error) 39 | .AddConsole() 40 | .AddDebug() |> ignore 41 | 42 | let webHostBuilder = 43 | WebHost.CreateDefaultBuilder() 44 | .Configure(Action configureApp) 45 | .ConfigureServices(configureServices) 46 | .ConfigureLogging(configureLogging) 47 | 48 | let server = new TestServer(webHostBuilder) 49 | let client = server.CreateClient() 50 | 51 | [] 52 | let ``GET / -> OK "123"``() = task { 53 | let! response = client.GetAsync("/") 54 | let! text = response.Content.ReadAsStringAsync() 55 | Assert.Equal("\"123\"",text) 56 | Assert.Equal(HttpStatusCode.OK ,response.StatusCode) 57 | } 58 | 59 | [] 60 | let ``GET /v2 -> OK "{total:123,apis:[]}"``() = task { 61 | let! response = client.GetAsync("/v2") 62 | let! text = response.Content.ReadAsStringAsync() 63 | Assert.Equal("{\"total\":123,\"apis\":[]}",text) 64 | Assert.Equal(HttpStatusCode.OK ,response.StatusCode) 65 | } 66 | 67 | [] 68 | let ``POST /v2 -> OK "345"``() = task { 69 | let! response = client.PostAsync("/v2", null) 70 | let! text = response.Content.ReadAsStringAsync() 71 | Assert.Equal("\"345\"",text) 72 | Assert.Equal(HttpStatusCode.OK ,response.StatusCode) 73 | } 74 | 75 | interface IDisposable with 76 | member _.Dispose() = server.Dispose() 77 | 78 | -------------------------------------------------------------------------------- /tests/GiraffeGenerator.IntegrationTests/SpecWithTextXmlTests.fs: -------------------------------------------------------------------------------- 1 | namespace GiraffeGenerator.IntegrationTests 2 | 3 | open System.Net 4 | open System.Net.Http 5 | open System.Threading.Tasks 6 | open FSharp.Control.Tasks.V2.ContextInsensitive 7 | open System 8 | open Giraffe 9 | open Microsoft.AspNetCore 10 | open Microsoft.AspNetCore.Builder 11 | open Microsoft.AspNetCore.Hosting 12 | open Microsoft.AspNetCore.TestHost 13 | open Microsoft.Extensions.DependencyInjection 14 | open Microsoft.Extensions.Logging 15 | open Xunit 16 | 17 | type SpecWithTextXmlTests() = 18 | 19 | let service = 20 | { new SpecwithxmlschemasAPI.Service() with 21 | member _.PostXmlInput ((i,_)) = Task.FromResult i 22 | member _.ApplicationJsonInput _ = Task.FromResult "1" 23 | member _.ApplicationXmlInput _ = Task.FromResult "2" 24 | member _.TextJsonInput _ = Task.FromResult "3" 25 | member _.TextXmlInput _ = Task.FromResult "4" } 26 | 27 | let configureApp (app : IApplicationBuilder) = 28 | app.UseGiraffe SpecwithxmlschemasAPI.webApp 29 | 30 | let configureServices (services : IServiceCollection) = 31 | services 32 | .AddGiraffe() 33 | .AddSingleton(service) 34 | |> ignore 35 | 36 | let configureLogging (loggerBuilder : ILoggingBuilder) = 37 | loggerBuilder.AddFilter(fun lvl -> lvl.Equals LogLevel.Error) 38 | .AddConsole() 39 | .AddDebug() |> ignore 40 | 41 | let webHostBuilder = 42 | WebHost.CreateDefaultBuilder() 43 | .Configure(Action configureApp) 44 | .ConfigureServices(configureServices) 45 | .ConfigureLogging(configureLogging) 46 | 47 | let server = new TestServer(webHostBuilder) 48 | let client = server.CreateClient() 49 | 50 | [] 51 | let ``GET /applicationJson -> OK "1"``() = task { 52 | let! response = client.GetAsync("/applicationJson") 53 | let! text = response.Content.ReadAsStringAsync() 54 | Assert.Equal("\"1\"",text) 55 | Assert.Equal(HttpStatusCode.OK ,response.StatusCode) 56 | } 57 | 58 | [] 59 | let ``GET /applicationXml -> OK "2"``() = task { 60 | let! response = client.GetAsync("/applicationXml") 61 | let! text = response.Content.ReadAsStringAsync() 62 | Assert.Equal(""" 63 | 2""",text) 64 | Assert.Equal(HttpStatusCode.OK ,response.StatusCode) 65 | } 66 | 67 | [] 68 | let ``GET /textJson -> OK "3"``() = task { 69 | let! response = client.GetAsync("/textJson") 70 | let! text = response.Content.ReadAsStringAsync() 71 | Assert.Equal("\"3\"",text) 72 | Assert.Equal(HttpStatusCode.OK ,response.StatusCode) 73 | } 74 | 75 | [] 76 | let ``GET /textXml -> OK "4"``() = task { 77 | let! response = client.GetAsync("/textXml") 78 | let! text = response.Content.ReadAsStringAsync() 79 | Assert.Equal(""" 80 | 4""",text) 81 | Assert.Equal(HttpStatusCode.OK ,response.StatusCode) 82 | } 83 | 84 | [] 85 | let ``POST /postXml -> OK "42"``() = task { 86 | use xmlContent = new StringContent(""" 87 | 42""", Text.Encoding.UTF8, "application/xml") 88 | let! response = client.PostAsync("/postXml", xmlContent) 89 | let! text = response.Content.ReadAsStringAsync() 90 | Assert.Equal(""" 91 | 42""",text) 92 | Assert.Equal(HttpStatusCode.OK ,response.StatusCode) 93 | } 94 | 95 | interface IDisposable with 96 | member _.Dispose() = server.Dispose() 97 | -------------------------------------------------------------------------------- /tests/GiraffeGenerator.IntegrationTests/SpecWithValidationExtensibilityTests.fs: -------------------------------------------------------------------------------- 1 | namespace GiraffeGenerator.IntegrationTests 2 | 3 | open System.ComponentModel.DataAnnotations 4 | open System.Net 5 | open System.Net.Http 6 | open System.Text 7 | open FSharp.Control.Tasks.V2.ContextInsensitive 8 | open System 9 | open Giraffe 10 | open Giraffe.Serialization.Json 11 | open Microsoft.AspNetCore 12 | open Microsoft.AspNetCore.Builder 13 | open Microsoft.AspNetCore.Hosting 14 | open Microsoft.AspNetCore.TestHost 15 | open Microsoft.Extensions.DependencyInjection 16 | open Microsoft.Extensions.Logging 17 | open Newtonsoft.Json 18 | open SpecWithValidationExtensibility 19 | open Xunit 20 | 21 | type AlwaysFailValidationReplacer() = 22 | interface IGiraffeValidator with 23 | member _.Validate((_,_)) = [| 24 | ValidationResult("Never valid") 25 | |] 26 | 27 | type AlwaysFailValidationAugmenter() = 28 | interface IGiraffeAdditionalValidator with 29 | member _.Validate((_,_)) = [| 30 | ValidationResult("Never valid") 31 | |] 32 | 33 | type NeverFailValidationReplacer() = 34 | interface IGiraffeValidator with 35 | member _.Validate((_,_)) = [||] 36 | 37 | type NeverFailValidationAugmenter() = 38 | interface IGiraffeAdditionalValidator with 39 | member _.Validate((_,_)) = [||] 40 | 41 | type SpecWithValidationExtensibilityTests() = 42 | let specWithValidationService= 43 | { 44 | new Service() with 45 | member _.TestValidationInput ((_, _)) = 46 | task { 47 | return Choice1Of2 "ok" 48 | } 49 | member _.TestValidationInputError ((err, _)) = 50 | task { 51 | return Choice2Of2 (argLocationErrorToString 0 err) 52 | } 53 | 54 | } 55 | 56 | let configureApp (app : IApplicationBuilder) = 57 | app.UseGiraffe webApp 58 | 59 | let jsonSettings = 60 | JsonSerializerSettings(Converters=[|optionConverter|]) 61 | 62 | let configureServices (services : IServiceCollection) = 63 | services 64 | .AddGiraffe() 65 | .AddSingleton(NewtonsoftJsonSerializer(jsonSettings)) 66 | .AddSingleton(specWithValidationService) 67 | |> ignore 68 | 69 | let configureLogging (loggerBuilder : ILoggingBuilder) = 70 | loggerBuilder.AddFilter(fun lvl -> lvl.Equals LogLevel.Error) 71 | .AddConsole() 72 | .AddDebug() |> ignore 73 | 74 | let webHostBuilder addServices = 75 | WebHost.CreateDefaultBuilder() 76 | .Configure(Action configureApp) 77 | .ConfigureServices(addServices >> configureServices) 78 | .ConfigureLogging(configureLogging) 79 | 80 | let server addServices = new TestServer(webHostBuilder addServices) 81 | let client addServices = (server addServices).CreateClient() 82 | 83 | [] 84 | let ``[no extensions (baseline)] /test-validation?maxLengthRestrictedTo8String=12345678 -> 200 "ok"`` () = task { 85 | let client = client id 86 | let! response = client.GetAsync("/test-validation?maxLengthRestrictedTo8String=12345678") 87 | let! responseText = response.Content.ReadAsStringAsync() 88 | do Assert.Equal("\"ok\"", responseText) 89 | do Assert.Equal(HttpStatusCode.OK, response.StatusCode) 90 | } 91 | 92 | [] 93 | let ``[no extensions (baseline)] /test-validation?maxLengthRestrictedTo8String=123456789 -> 400 "must be a string or array type with a maximum length of '8'"`` () = task { 94 | let client = client id 95 | let! response = client.GetAsync("/test-validation?maxLengthRestrictedTo8String=123456789") 96 | let! responseText = response.Content.ReadAsStringAsync() 97 | do Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode) 98 | do Assert.Contains("must be a string or array type with a maximum length of '8'", responseText) 99 | } 100 | 101 | [] 102 | let ``[replace validation: always fail] /test-validation?maxLengthRestrictedTo8String=12345678 -> 400 "Never valid"`` () = task { 103 | let client = client (fun s -> s.AddSingleton, AlwaysFailValidationReplacer>()) 104 | let! response = client.GetAsync("/test-validation?maxLengthRestrictedTo8String=12345678") 105 | let! responseText = response.Content.ReadAsStringAsync() 106 | do Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode) 107 | do Assert.Contains("Never valid", responseText) 108 | } 109 | 110 | [] 111 | let ``[replace validation: always fail] /test-validation?maxLengthRestrictedTo8String=123456789 -> 400 "Never valid"`` () = task { 112 | let client = client (fun s -> s.AddSingleton, AlwaysFailValidationReplacer>()) 113 | let! response = client.GetAsync("/test-validation?maxLengthRestrictedTo8String=123456789") 114 | let! responseText = response.Content.ReadAsStringAsync() 115 | do Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode) 116 | do Assert.Contains("Never valid", responseText) 117 | } 118 | 119 | [] 120 | let ``[augment validation: always fail] /test-validation?maxLengthRestrictedTo8String=12345678 -> 400 "Never valid"`` () = task { 121 | let client = client (fun s -> s.AddSingleton, AlwaysFailValidationAugmenter>()) 122 | let! response = client.GetAsync("/test-validation?maxLengthRestrictedTo8String=12345678") 123 | let! responseText = response.Content.ReadAsStringAsync() 124 | do Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode) 125 | do Assert.Contains("Never valid", responseText) 126 | } 127 | 128 | [] 129 | let ``[augment validation: always fail] /test-validation?maxLengthRestrictedTo8String=123456789 -> 400 "Never valid, must be a string or array type with a maximum length of '8'"`` () = task { 130 | let client = client (fun s -> s.AddSingleton, AlwaysFailValidationAugmenter>()) 131 | let! response = client.GetAsync("/test-validation?maxLengthRestrictedTo8String=123456789") 132 | let! responseText = response.Content.ReadAsStringAsync() 133 | do Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode) 134 | do Assert.Contains("Never valid", responseText) 135 | do Assert.Contains("must be a string or array type with a maximum length of '8'", responseText) 136 | } 137 | 138 | [] 139 | let ``[replace&augment validation: always fail] /test-validation?maxLengthRestrictedTo8String=12345678 -> 400 "Never valid, Never valid"`` () = task { 140 | let client = client (fun s -> 141 | s.AddSingleton, AlwaysFailValidationReplacer>() 142 | .AddSingleton, AlwaysFailValidationAugmenter>()) 143 | let! response = client.GetAsync("/test-validation?maxLengthRestrictedTo8String=12345678") 144 | let! responseText = response.Content.ReadAsStringAsync() 145 | do Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode) 146 | do Assert.Contains("Never valid", responseText) 147 | let idx1 = responseText.IndexOf "Never valid" 148 | let idx2 = responseText.LastIndexOf "Never valid" 149 | Assert.NotEqual(idx1, idx2) 150 | } 151 | 152 | [] 153 | let ``[replace&augment validation: always fail] /test-validation?maxLengthRestrictedTo8String=123456789 -> 400 "Never valid, Never valid'"`` () = task { 154 | let client = client (fun s -> 155 | s.AddSingleton, AlwaysFailValidationReplacer>() 156 | .AddSingleton, AlwaysFailValidationAugmenter>()) 157 | let! response = client.GetAsync("/test-validation?maxLengthRestrictedTo8String=123456789") 158 | let! responseText = response.Content.ReadAsStringAsync() 159 | do Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode) 160 | do Assert.Contains("Never valid", responseText) 161 | let idx1 = responseText.IndexOf "Never valid" 162 | let idx2 = responseText.LastIndexOf "Never valid" 163 | Assert.NotEqual(idx1, idx2) 164 | } 165 | 166 | [] 167 | let ``[replace validation: never fail] /test-validation?maxLengthRestrictedTo8String=12345678 -> 200 "ok"`` () = task { 168 | let client = client (fun s -> s.AddSingleton, NeverFailValidationReplacer>()) 169 | let! response = client.GetAsync("/test-validation?maxLengthRestrictedTo8String=12345678") 170 | let! responseText = response.Content.ReadAsStringAsync() 171 | do Assert.Equal("\"ok\"", responseText) 172 | do Assert.Equal(HttpStatusCode.OK, response.StatusCode) 173 | } 174 | 175 | [] 176 | let ``[replace validation: never fail] /test-validation?maxLengthRestrictedTo8String=123456789 -> 200 "ok"`` () = task { 177 | let client = client (fun s -> s.AddSingleton, NeverFailValidationReplacer>()) 178 | let! response = client.GetAsync("/test-validation?maxLengthRestrictedTo8String=123456789") 179 | let! responseText = response.Content.ReadAsStringAsync() 180 | do Assert.Equal("\"ok\"", responseText) 181 | do Assert.Equal(HttpStatusCode.OK, response.StatusCode) 182 | } 183 | 184 | [] 185 | let ``[augment validation: never fail] /test-validation?maxLengthRestrictedTo8String=12345678 -> 200 "ok"`` () = task { 186 | let client = client (fun s -> s.AddSingleton, NeverFailValidationAugmenter>()) 187 | let! response = client.GetAsync("/test-validation?maxLengthRestrictedTo8String=12345678") 188 | let! responseText = response.Content.ReadAsStringAsync() 189 | do Assert.Equal("\"ok\"", responseText) 190 | do Assert.Equal(HttpStatusCode.OK, response.StatusCode) 191 | } 192 | 193 | [] 194 | let ``[augment validation: never fail] /test-validation?maxLengthRestrictedTo8String=123456789 -> 400 "must be a string or array type with a maximum length of '8'"`` () = task { 195 | let client = client (fun s -> s.AddSingleton, NeverFailValidationAugmenter>()) 196 | let! response = client.GetAsync("/test-validation?maxLengthRestrictedTo8String=123456789") 197 | let! responseText = response.Content.ReadAsStringAsync() 198 | do Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode) 199 | do Assert.Contains("must be a string or array type with a maximum length of '8'", responseText) 200 | } -------------------------------------------------------------------------------- /tests/GiraffeGenerator.IntegrationTests/specs/specForNodaTimeDateTimeFormatHandling.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | title: Spec for date-time format noda time handling testing 4 | version: 1 5 | paths: 6 | /id: 7 | post: 8 | operationId: postId 9 | requestBody: 10 | content: 11 | application/json: 12 | schema: 13 | $ref: '#/components/schemas/bodyModel' 14 | responses: 15 | '200': 16 | description: returns input 17 | content: 18 | application/json: 19 | schema: 20 | $ref: '#/components/schemas/bodyModel' 21 | components: 22 | schemas: 23 | bodyModel: 24 | type: object 25 | properties: 26 | dateTime: 27 | type: string 28 | format: date-time 29 | required: 30 | - dateTime -------------------------------------------------------------------------------- /tests/GiraffeGenerator.IntegrationTests/specs/specGeneralForNodaTime.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | title: Spec for general noda time testing 4 | version: 1 5 | paths: 6 | /id: 7 | post: 8 | operationId: postId 9 | parameters: 10 | - schema: 11 | type: string 12 | format: date 13 | name: dateParam 14 | in: query 15 | required: false 16 | - schema: 17 | type: string 18 | format: time 19 | name: timeParam 20 | in: query 21 | required: false 22 | - schema: 23 | type: string 24 | format: offset 25 | name: offsetParam 26 | in: query 27 | required: false 28 | - schema: 29 | type: string 30 | format: duration 31 | name: durationParam 32 | in: query 33 | required: false 34 | - schema: 35 | type: string 36 | format: instant 37 | name: instantParam 38 | in: query 39 | required: false 40 | - schema: 41 | type: string 42 | format: local-date-time 43 | name: localDateTimeParam 44 | in: query 45 | required: false 46 | - schema: 47 | type: string 48 | format: offset-date-time 49 | name: offsetDateTimeParam 50 | in: query 51 | required: false 52 | requestBody: 53 | content: 54 | application/json: 55 | schema: 56 | $ref: '#/components/schemas/dataSetListInput' 57 | responses: 58 | '200': 59 | description: returns input combined into a single object 60 | content: 61 | application/json: 62 | schema: 63 | $ref: '#/components/schemas/dataSetListOutput' 64 | components: 65 | schemas: 66 | forDefaultsTesting: 67 | type: object 68 | properties: 69 | dateParamFromBody: 70 | type: string 71 | format: date 72 | default: '2020-10-21' 73 | timeParamFromBody: 74 | type: string 75 | format: time 76 | default: '19:58:12' 77 | offsetParamFromBody: 78 | type: string 79 | format: offset 80 | default: '-14:55:01' 81 | timeZoneParamFromBody: 82 | type: string 83 | format: time-zone 84 | default: 'Europe/Moscow' 85 | periodParamFromBody: 86 | type: string 87 | format: period 88 | default: 'P13DT49H156M' 89 | durationParamFromBody: 90 | type: string 91 | format: duration 92 | default: '-364:36:00' 93 | instantParamFromBody: 94 | type: string 95 | format: instant 96 | default: '2020-10-21T16:59:12.0000001Z' 97 | localDateTimeParamFromBody: 98 | type: string 99 | format: local-date-time 100 | default: '2020-10-21T19:59:12.0000001' 101 | offsetDateTimeParamFromBody: 102 | type: string 103 | format: offset-date-time 104 | default: '2020-10-21T19:59:12.0000001+03:00' 105 | dataSetListInput: 106 | type: object 107 | properties: 108 | forDefaultsTesting: 109 | $ref: '#/components/schemas/forDefaultsTesting' 110 | zonedDateTime: 111 | type: string 112 | format: zoned-date-time 113 | required: 114 | - forDefaultsTesting 115 | - zonedDateTime 116 | dataSetListOutput: 117 | type: object 118 | properties: 119 | dateParamFromQuery: 120 | type: string 121 | format: date 122 | timeParamFromQuery: 123 | type: string 124 | format: time 125 | offsetParamFromQuery: 126 | type: string 127 | format: offset 128 | durationParamFromQuery: 129 | type: string 130 | format: duration 131 | instantParamFromQuery: 132 | type: string 133 | format: instant 134 | localDateTimeParamFromQuery: 135 | type: string 136 | format: local-date-time 137 | offsetDateTimeParamFromQuery: 138 | type: string 139 | format: offset-date-time 140 | forDefaultsTesting: 141 | $ref: '#/components/schemas/forDefaultsTesting' 142 | zonedDateTime: 143 | type: string 144 | format: zoned-date-time 145 | required: 146 | - forDefaultsTesting 147 | - zonedDateTime -------------------------------------------------------------------------------- /tests/GiraffeGenerator.IntegrationTests/specs/specSimple.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | title: Spec Simple API 4 | version: 1 5 | paths: 6 | /: 7 | get: 8 | operationId: listVersionsv2 9 | responses: 10 | '200': 11 | description: returns string 12 | content: 13 | application/json: 14 | schema: 15 | type: string 16 | /v2: 17 | get: 18 | operationId: getVersionDetailsv2 19 | responses: 20 | '200': 21 | description: returns string 22 | content: 23 | application/json: 24 | schema: 25 | type: string 26 | post: 27 | operationId: postVersionDetailsv2 28 | responses: 29 | '200': 30 | description: returns string 31 | content: 32 | application/json: 33 | schema: 34 | type: string -------------------------------------------------------------------------------- /tests/GiraffeGenerator.IntegrationTests/specs/specWithArguments.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | title: Spec with arguments API 4 | version: 1 5 | paths: 6 | /{dataset}/{version}/fields: 7 | get: 8 | summary: >- 9 | Provides the general information about the API and the list of fields 10 | that can be used to query the dataset. 11 | description: >- 12 | This GET API returns the list of all the searchable field names that are 13 | in the oa_citations. Please see the 'fields' attribute which returns an 14 | array of field names. Each field or a combination of fields can be 15 | searched using the syntax options shown below. 16 | operationId: list-searchable-fields 17 | parameters: 18 | - name: dataset 19 | in: path 20 | description: 'Name of the dataset.' 21 | required: true 22 | example: "oa_citations" 23 | schema: 24 | type: string 25 | - name: version 26 | in: path 27 | description: Version of the dataset. 28 | required: true 29 | example: "v1" 30 | schema: 31 | type: string 32 | responses: 33 | '200': 34 | description: >- 35 | The dataset API for the given version is found and it is accessible 36 | to consume. 37 | content: 38 | application/json: 39 | schema: 40 | type: string 41 | '404': 42 | description: >- 43 | The combination of dataset name and version is not found in the 44 | system or it is not published yet to be consumed by public. 45 | content: 46 | application/json: 47 | schema: 48 | type: string 49 | /{dataset}/{version}/only-path-parameters: 50 | parameters: 51 | - name: dataset 52 | in: path 53 | required: true 54 | schema: 55 | type: string 56 | post: 57 | tags: 58 | - search 59 | summary: >- 60 | Used to test path-only parameters case 61 | operationId: only-path-parameters 62 | parameters: 63 | - name: version 64 | in: path 65 | description: Version of the dataset. 66 | required: true 67 | schema: 68 | type: string 69 | responses: 70 | '200': 71 | description: successful operation 72 | content: 73 | application/json: 74 | schema: 75 | type: array 76 | items: 77 | type: string 78 | /only-query-parameters: 79 | post: 80 | tags: 81 | - search 82 | summary: >- 83 | Used to test path-only parameters case 84 | operationId: only-query-parameters 85 | parameters: 86 | - name: dataset 87 | schema: 88 | type: string 89 | in: query 90 | required: true 91 | - name: version 92 | in: query 93 | description: Version of the dataset. 94 | required: true 95 | schema: 96 | type: string 97 | responses: 98 | '200': 99 | description: successful operation 100 | content: 101 | application/json: 102 | schema: 103 | type: array 104 | items: 105 | type: string 106 | /only-form-parameters: 107 | post: 108 | tags: 109 | - search 110 | summary: >- 111 | Used to test path-only parameters case 112 | operationId: only-form-parameters 113 | requestBody: 114 | content: 115 | application/x-www-form-urlencoded: 116 | schema: 117 | type: object 118 | properties: 119 | dataset: 120 | type: string 121 | version: 122 | description: Version of the dataset. 123 | type: string 124 | required: 125 | - dataset 126 | - version 127 | responses: 128 | '200': 129 | description: successful operation 130 | content: 131 | application/json: 132 | schema: 133 | type: array 134 | items: 135 | type: string 136 | /only-json-parameters: 137 | post: 138 | tags: 139 | - search 140 | summary: >- 141 | Used to test json-only parameters case 142 | operationId: only-json-parameters 143 | requestBody: 144 | content: 145 | application/json: 146 | schema: 147 | type: object 148 | properties: 149 | dataset: 150 | type: string 151 | version: 152 | description: Version of the dataset. 153 | type: string 154 | required: 155 | - dataset 156 | - version 157 | responses: 158 | '200': 159 | description: successful operation 160 | content: 161 | application/json: 162 | schema: 163 | type: array 164 | items: 165 | type: string 166 | /{dataset}/{version}/records: 167 | post: 168 | tags: 169 | - search 170 | summary: >- 171 | Provides search capability for the data set with the given search 172 | criteria. 173 | description: >- 174 | This API is based on Solr/Lucense Search. The data is indexed using 175 | SOLR. This GET API returns the list of all the searchable field names 176 | that are in the Solr Index. Please see the 'fields' attribute which 177 | returns an array of field names. Each field or a combination of fields 178 | can be searched using the Solr/Lucene Syntax. Please refer 179 | https://lucene.apache.org/core/3_6_2/queryparsersyntax.html#Overview for 180 | the query syntax. List of field names that are searchable can be 181 | determined using above GET api. 182 | operationId: perform-search 183 | parameters: 184 | - name: start 185 | description: Starting record number. Default value is 0. 186 | in: query 187 | schema: 188 | type: integer 189 | default: 0 190 | - name: rows 191 | description: >- 192 | Specify number of rows to be returned. If you run the search 193 | with default values, in the response you will see 'numFound' 194 | attribute which will tell the number of records available in 195 | the dataset. 196 | in: query 197 | schema: 198 | type: integer 199 | default: 100 200 | - name: version 201 | in: path 202 | description: Version of the dataset. 203 | required: true 204 | schema: 205 | type: string 206 | default: v1 207 | - name: dataset 208 | in: path 209 | description: 'Name of the dataset. In this case, the default value is oa_citations' 210 | required: true 211 | schema: 212 | type: string 213 | default: oa_citations 214 | responses: 215 | '200': 216 | description: successful operation 217 | content: 218 | application/json: 219 | schema: 220 | type: array 221 | items: 222 | type: object 223 | additionalProperties: 224 | type: object 225 | '404': 226 | description: No matching record found for the given criteria. 227 | requestBody: 228 | content: 229 | application/x-www-form-urlencoded: 230 | schema: 231 | type: object 232 | properties: 233 | criteria: 234 | description: >- 235 | Uses Lucene Query Syntax in the format of 236 | propertyName:value, propertyName:[num1 TO num2] and date 237 | range format: propertyName:[yyyyMMdd TO yyyyMMdd]. In the 238 | response please see the 'docs' element which has the list of 239 | record objects. Each record structure would consist of all 240 | the fields and their corresponding values. 241 | type: string 242 | default: '*:*' 243 | start: 244 | description: Starting record number. Default value is 0. 245 | type: integer 246 | default: 0 247 | rows: 248 | description: >- 249 | Specify number of rows to be returned. If you run the search 250 | with default values, in the response you will see 'numFound' 251 | attribute which will tell the number of records available in 252 | the dataset. 253 | type: integer 254 | default: 100 255 | -------------------------------------------------------------------------------- /tests/GiraffeGenerator.IntegrationTests/specs/specWithParametersAndRequestBodySchemas.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | title: Spec with parameters and request body API 4 | version: 1 5 | paths: 6 | /id/{param}: 7 | parameters: 8 | - schema: 9 | type: string 10 | name: param 11 | in: path 12 | required: true 13 | post: 14 | operationId: postId 15 | parameters: 16 | - schema: 17 | type: integer 18 | name: param 19 | in: query 20 | required: false 21 | requestBody: 22 | content: 23 | application/json: 24 | schema: 25 | $ref: '#/components/schemas/dataSetListInput' 26 | responses: 27 | '200': 28 | description: returns all input parameters in one object 29 | content: 30 | application/json: 31 | schema: 32 | $ref: '#/components/schemas/dataSetListOutput' 33 | components: 34 | schemas: 35 | forDefaultsTesting: 36 | type: object 37 | properties: 38 | optionalArrayWithDefaultItems: 39 | type: array 40 | items: 41 | type: integer 42 | default: 42 43 | requiredArrayWithDefaultItems: 44 | type: array 45 | items: 46 | type: integer 47 | default: 48 48 | apiKey: 49 | type: string 50 | format: password 51 | default: pa$$word 52 | apiVersionNumber: 53 | type: string 54 | default: 1 55 | apiUrl: 56 | type: string 57 | format: uriref 58 | default: http://localhost:8080/api 59 | apiCount: 60 | type: integer 61 | format: int64 62 | default: 123456789123456 63 | apiAvg: 64 | type: integer 65 | default: 1234567890 66 | isInternal: 67 | type: boolean 68 | default: true 69 | start: 70 | type: string 71 | format: date 72 | default: 2020-10-8 73 | someDateTime: 74 | type: string 75 | format: date-time 76 | default: 2020-10-8T00:55:00+03:00 77 | pi: 78 | type: number 79 | default: 3.14 80 | someUid: 81 | type: string 82 | format: uid 83 | default: 8282cbfd-f323-4b7d-bcc0-28f127c2b365 84 | required: 85 | - requiredArrayWithDefaultItems 86 | dataSetListInput: 87 | type: object 88 | properties: 89 | total: 90 | type: integer 91 | defaultsTest: 92 | $ref: '#/components/schemas/forDefaultsTesting' 93 | required: 94 | - defaultsTest 95 | dataSetListOutput: 96 | type: object 97 | properties: 98 | pathParam: 99 | type: string 100 | queryParam: 101 | type: integer 102 | total: 103 | type: integer 104 | defaultsTest: 105 | $ref: '#/components/schemas/forDefaultsTesting' 106 | required: 107 | - pathParam 108 | - defaultsTest -------------------------------------------------------------------------------- /tests/GiraffeGenerator.IntegrationTests/specs/specWithSchemas.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | title: Spec with schemas API 4 | version: 1 5 | paths: 6 | /: 7 | get: 8 | operationId: listVersionsv2 9 | responses: 10 | '200': 11 | description: returns string 12 | content: 13 | application/json: 14 | schema: 15 | type: string 16 | /v2: 17 | get: 18 | operationId: getVersionDetailsv2 19 | responses: 20 | '200': 21 | description: returns dataset 22 | content: 23 | application/json: 24 | schema: 25 | $ref: '#/components/schemas/dataSetList' 26 | post: 27 | operationId: postVersionDetailsv2 28 | responses: 29 | '200': 30 | description: returns string 31 | content: 32 | application/json: 33 | schema: 34 | type: string 35 | components: 36 | schemas: 37 | dataSetList: 38 | type: object 39 | properties: 40 | total: 41 | type: integer 42 | apis: 43 | type: array 44 | items: 45 | type: object 46 | properties: 47 | apiKey: 48 | type: string 49 | apiVersionNumber: 50 | type: string 51 | apiUrl: 52 | type: string 53 | format: uriref 54 | apiCount: 55 | type: integer 56 | format: int64 57 | apiAvg: 58 | type: number 59 | isInternal: 60 | type: boolean 61 | start: 62 | type: string 63 | format: date 64 | apiHash: 65 | type: string 66 | format: byte 67 | required: 68 | - apiKey 69 | - apiVersionNumber 70 | - apiUrl 71 | - apiCount 72 | - apiAvg 73 | - isInternal 74 | - start 75 | - apiHash 76 | required: 77 | - total 78 | - apis -------------------------------------------------------------------------------- /tests/GiraffeGenerator.IntegrationTests/specs/specWithTextXml.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | title: Spec with xml schemas API 4 | version: 1 5 | paths: 6 | /postXml: 7 | post: 8 | operationId: postXml 9 | requestBody: 10 | content: 11 | application/xml: 12 | schema: 13 | type: integer 14 | responses: 15 | '200': 16 | description: returns integer 17 | content: 18 | application/xml: 19 | schema: 20 | type: integer 21 | /textJson: 22 | get: 23 | operationId: textJson 24 | responses: 25 | '200': 26 | description: returns string 27 | content: 28 | text/json: 29 | schema: 30 | type: string 31 | /textXml: 32 | get: 33 | operationId: textXml 34 | responses: 35 | '200': 36 | description: returns string 37 | content: 38 | text/xml: 39 | schema: 40 | type: string 41 | /applicationXml: 42 | get: 43 | operationId: applicationXml 44 | responses: 45 | '200': 46 | description: returns string 47 | content: 48 | application/xml: 49 | schema: 50 | type: string 51 | /applicationJson: 52 | get: 53 | operationId: applicationJson 54 | responses: 55 | '200': 56 | description: returns string 57 | content: 58 | application/json: 59 | schema: 60 | type: string 61 | -------------------------------------------------------------------------------- /tests/GiraffeGenerator.IntegrationTests/specs/specWithValidation.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | title: Spec with validated arguments 4 | version: 1 5 | paths: 6 | /test-validation/{versionEnum}/{stringOfRestrictedToBeBetween3And8Length}: 7 | parameters: 8 | - name: versionEnum 9 | in: path 10 | required: true 11 | schema: 12 | type: integer 13 | enum: 14 | - 1 15 | - 2 16 | - 3 17 | - name: stringOfRestrictedToBeBetween3And8Length 18 | in: path 19 | required: true 20 | schema: 21 | type: string 22 | minLength: 3 23 | maxLength: 8 24 | post: 25 | tags: 26 | - search 27 | summary: >- 28 | Used to test validation 29 | operationId: TestValidation 30 | parameters: 31 | - name: maxLengthRestrictedTo8String 32 | schema: 33 | type: string 34 | maxLength: 8 35 | in: query 36 | required: true 37 | - name: minLengthRestrictedTo3String 38 | schema: 39 | type: string 40 | minLength: 3 41 | in: query 42 | required: true 43 | - name: stringOfDDMMMPattern 44 | schema: 45 | type: string 46 | pattern: ^((3[0-1])|([0-2]?[0-9]))(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov)$ 47 | in: query 48 | required: true 49 | - name: stringOfLengthBetween8And64ContainingAGiraffe 50 | schema: 51 | type: string 52 | pattern: giraffe 53 | minLength: 8 54 | maxLength: 64 55 | in: query 56 | required: true 57 | - name: integerDivisibleBy2 58 | schema: 59 | type: integer 60 | multipleOf: 2 61 | in: query 62 | required: true 63 | - name: integerBetween8And64 64 | schema: 65 | type: integer 66 | minimum: 8 67 | maximum: 64 68 | in: query 69 | required: true 70 | - name: integerBetween8And64Exclusive 71 | schema: 72 | type: integer 73 | minimum: 8 74 | maximum: 64 75 | exclusiveMaximum: true 76 | in: query 77 | required: true 78 | - name: integerBetween8ExclusiveAnd64 79 | schema: 80 | type: integer 81 | minimum: 8 82 | exclusiveMinimum: true 83 | maximum: 64 84 | in: query 85 | required: true 86 | - name: integerBetween8ExclusiveAnd64Exclusive 87 | schema: 88 | type: integer 89 | minimum: 8 90 | exclusiveMinimum: true 91 | maximum: 64 92 | exclusiveMaximum: true 93 | in: query 94 | required: true 95 | - name: integerGTE8 96 | schema: 97 | type: integer 98 | minimum: 8 99 | in: query 100 | required: true 101 | - name: integerGT8 102 | schema: 103 | type: integer 104 | minimum: 8 105 | exclusiveMinimum: true 106 | in: query 107 | required: true 108 | - name: integerLTE64 109 | schema: 110 | type: integer 111 | maximum: 64 112 | in: query 113 | required: true 114 | - name: integerLT64 115 | schema: 116 | type: integer 117 | maximum: 64 118 | exclusiveMaximum: true 119 | in: query 120 | required: true 121 | - name: integerEQ64 122 | schema: 123 | type: integer 124 | minimum: 64 125 | maximum: 64 126 | in: query 127 | required: true 128 | - name: longEnum 129 | in: query 130 | required: true 131 | schema: 132 | type: integer 133 | format: int64 134 | enum: 135 | - -9223372036854775808 136 | - 0 137 | - 9223372036854775807 138 | - name: longDivisibleBy2 139 | schema: 140 | type: integer 141 | format: int64 142 | multipleOf: 2 143 | in: query 144 | required: true 145 | - name: longBetween8And64 146 | schema: 147 | type: integer 148 | format: int64 149 | minimum: 8 150 | maximum: 64 151 | in: query 152 | required: true 153 | - name: longBetween8And64Exclusive 154 | schema: 155 | type: integer 156 | format: int64 157 | minimum: 8 158 | maximum: 64 159 | exclusiveMaximum: true 160 | in: query 161 | required: true 162 | - name: longBetween8ExclusiveAnd64 163 | schema: 164 | type: integer 165 | format: int64 166 | minimum: 8 167 | exclusiveMinimum: true 168 | maximum: 64 169 | in: query 170 | required: true 171 | - name: longBetween8ExclusiveAnd64Exclusive 172 | schema: 173 | type: integer 174 | format: int64 175 | minimum: 8 176 | exclusiveMinimum: true 177 | maximum: 64 178 | exclusiveMaximum: true 179 | in: query 180 | required: true 181 | - name: longGTE8 182 | schema: 183 | type: integer 184 | format: int64 185 | minimum: 8 186 | in: query 187 | required: true 188 | - name: longGT8 189 | schema: 190 | type: integer 191 | format: int64 192 | minimum: 8 193 | exclusiveMinimum: true 194 | in: query 195 | required: true 196 | - name: longLTE64 197 | schema: 198 | type: integer 199 | format: int64 200 | maximum: 64 201 | in: query 202 | required: true 203 | - name: longLT64 204 | schema: 205 | type: integer 206 | format: int64 207 | maximum: 64 208 | exclusiveMaximum: true 209 | in: query 210 | required: true 211 | - name: longEQ64 212 | schema: 213 | type: integer 214 | format: int64 215 | minimum: 64 216 | maximum: 64 217 | in: query 218 | required: true 219 | - name: floatEnum 220 | in: query 221 | required: true 222 | schema: 223 | type: number 224 | enum: 225 | - 3.14 226 | - 3.141 227 | - 3.1415 228 | - 3.14159 229 | - 3.141592 230 | - name: floatBetween8And64 231 | schema: 232 | type: number 233 | minimum: 8 234 | maximum: 64 235 | in: query 236 | required: true 237 | - name: floatBetween8And64Exclusive 238 | schema: 239 | type: number 240 | minimum: 8 241 | maximum: 64 242 | exclusiveMaximum: true 243 | in: query 244 | required: true 245 | - name: floatBetween8ExclusiveAnd64 246 | schema: 247 | type: number 248 | minimum: 8 249 | exclusiveMinimum: true 250 | maximum: 64 251 | in: query 252 | required: true 253 | - name: floatBetween8ExclusiveAnd64Exclusive 254 | schema: 255 | type: number 256 | minimum: 8 257 | exclusiveMinimum: true 258 | maximum: 64 259 | exclusiveMaximum: true 260 | in: query 261 | required: true 262 | - name: floatGTE8 263 | schema: 264 | type: number 265 | minimum: 8 266 | in: query 267 | required: true 268 | - name: floatGT8 269 | schema: 270 | type: number 271 | minimum: 8 272 | exclusiveMinimum: true 273 | in: query 274 | required: true 275 | - name: floatLTE64 276 | schema: 277 | type: number 278 | maximum: 64 279 | in: query 280 | required: true 281 | - name: floatLT64 282 | schema: 283 | type: number 284 | maximum: 64 285 | exclusiveMaximum: true 286 | in: query 287 | required: true 288 | - name: floatEQ64 289 | schema: 290 | type: number 291 | minimum: 64 292 | maximum: 64 293 | in: query 294 | required: true 295 | requestBody: 296 | content: 297 | application/json: 298 | schema: 299 | type: array 300 | uniqueItems: true 301 | items: 302 | type: object 303 | properties: 304 | arrayOf2To4UniqueItems: 305 | type: array 306 | uniqueItems: true 307 | minItems: 2 308 | maxItems: 4 309 | items: 310 | type: integer 311 | arrayOf2To4Items: 312 | type: array 313 | minItems: 2 314 | maxItems: 4 315 | items: 316 | type: integer 317 | arrayOptionOfMinimum2Items: 318 | type: array 319 | minItems: 2 320 | items: 321 | type: integer 322 | arrayOfMaximum4Items: 323 | type: array 324 | maxItems: 4 325 | items: 326 | type: integer 327 | nestedValidationArrayOfIntGTE2: 328 | type: array 329 | items: 330 | type: integer 331 | minimum: 2 332 | nestedValidationObjectOfIntGTE2: 333 | type: object 334 | properties: 335 | nestedIntGT2: 336 | type: integer 337 | minimum: 2 338 | required: 339 | - nestedIntGT2 340 | nestedValidationOptionOfIntGTE2: 341 | type: integer 342 | minimum: 2 343 | deeplyNestedValidation: 344 | type: array 345 | items: 346 | type: array 347 | uniqueItems: true 348 | maxItems: 2 349 | items: 350 | type: object 351 | properties: 352 | intGTE2ObjectOption: 353 | type: object 354 | properties: 355 | intGTE2Option: 356 | type: integer 357 | minimum: 2 358 | required: 359 | - arrayOf2To4UniqueItems 360 | - arrayOf2To4Items 361 | - arrayOfMaximum4Items 362 | - nestedValidationArrayOfIntGTE2 363 | - nestedValidationObjectOfIntGTE2 364 | responses: 365 | '200': 366 | description: object is valid 367 | content: 368 | application/json: 369 | schema: 370 | type: string 371 | '400': 372 | description: object is invalid 373 | content: 374 | application/json: 375 | schema: 376 | $ref: '#/components/schemas/validationErrorResponse' 377 | components: 378 | schemas: 379 | validationErrorResponse: 380 | type: object 381 | properties: 382 | validationErrors: 383 | type: array 384 | items: 385 | type: object 386 | properties: 387 | location: 388 | type: string 389 | propertyPath: 390 | type: array 391 | items: 392 | type: string 393 | message: 394 | type: string 395 | required: 396 | - location 397 | - propertyPath 398 | - message 399 | otherErrors: 400 | type: string 401 | required: 402 | - validationErrors -------------------------------------------------------------------------------- /tests/GiraffeGenerator.IntegrationTests/specs/specWithValidationExtensibility.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | title: Spec with a simple validated argument for extensibility testing 4 | version: 1 5 | paths: 6 | /test-validation: 7 | get: 8 | tags: 9 | - search 10 | summary: >- 11 | Used to test validation 12 | operationId: TestValidation 13 | parameters: 14 | - name: maxLengthRestrictedTo8String 15 | schema: 16 | type: string 17 | maxLength: 8 18 | in: query 19 | required: true 20 | responses: 21 | '200': 22 | description: object is valid 23 | content: 24 | application/json: 25 | schema: 26 | type: string 27 | '400': 28 | description: object is invalid 29 | content: 30 | application/json: 31 | schema: 32 | type: string -------------------------------------------------------------------------------- /tests/GiraffeGenerator.NugetTests/GiraffeGenerator.NugetTests.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Library 5 | net6.0 6 | 7 | 8 | 9 | 10 | ../GiraffeGenerator.IntegrationTests/specs/specSimple.yaml 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/GiraffeGenerator.NugetTests/NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | --------------------------------------------------------------------------------