├── .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 | [](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