├── .config └── dotnet-tools.json ├── .devcontainer ├── Dockerfile ├── devcontainer.json └── settings.vscode.json ├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── build.yml ├── .gitignore ├── .paket └── Paket.Restore.targets ├── .travis.yml ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── BinaryDefense.JsonWrapper.sln ├── CHANGELOG.md ├── Directory.Build.props ├── LICENSE.md ├── README.md ├── appveyor.yml ├── build.cmd ├── build.fsx ├── build.sh ├── docsSrc ├── Explanations │ └── Background.md ├── How_Tos │ ├── Doing_A_Thing.md │ └── Doing_Another_Thing.md ├── Tutorials │ └── Getting_Started.md ├── content │ ├── cleanups.js │ ├── hotload.js │ ├── style.css │ ├── submenu.js │ ├── themes.js │ ├── tips.js │ ├── toggle-bootstrap-dark.min.css │ └── toggle-bootstrap.min.css ├── files │ └── placeholder.md └── index.md ├── docsTool ├── CLI.fs ├── Prelude.fs ├── Program.fs ├── README.md ├── docsTool.fsproj ├── paket.references └── templates │ ├── helpers.fs │ ├── master.fs │ ├── modules.fs │ ├── namespaces.fs │ ├── nav.fs │ ├── partMembers.fs │ ├── partNested.fs │ └── types.fs ├── paket.dependencies ├── paket.lock ├── src ├── BinaryDefense.JsonWrapper.Core │ ├── AssemblyInfo.fs │ ├── BinaryDefense.JsonWrapper.Core.fs │ ├── BinaryDefense.JsonWrapper.Core.fsproj │ └── paket.references ├── BinaryDefense.Myriad.Plugins.JsonWrapper │ ├── AssemblyInfo.fs │ ├── BinaryDefense.Myriad.Plugins.JsonWrapper.fs │ ├── BinaryDefense.Myriad.Plugins.JsonWrapper.fsproj │ ├── build │ │ ├── BinaryDefense.Myriad.Plugins.JsonWrapper.InTest.props │ │ └── BinaryDefense.Myriad.Plugins.JsonWrapper.props │ └── paket.references └── Directory.Build.props └── tests ├── Directory.Build.props └── JsonWrapperPlugin.Tests ├── AssemblyInfo.fs ├── Generated.fs ├── JsonWrapperPlugin.Tests.fsproj ├── Main.fs ├── Tests.fs ├── Types.fs ├── coverage.netcoreapp3.1.xml.acv └── paket.references /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "sourcelink": { 6 | "version": "3.1.1", 7 | "commands": [ 8 | "sourcelink" 9 | ] 10 | }, 11 | "dotnet-reportgenerator-globaltool": { 12 | "version": "4.2.15", 13 | "commands": [ 14 | "reportgenerator" 15 | ] 16 | }, 17 | "fake-cli": { 18 | "version": "5.20.0", 19 | "commands": [ 20 | "fake" 21 | ] 22 | }, 23 | "paket": { 24 | "version": "5.245.1", 25 | "commands": [ 26 | "paket" 27 | ] 28 | }, 29 | "fcswatch-cli": { 30 | "version": "0.7.14", 31 | "commands": [ 32 | "fcswatch" 33 | ] 34 | }, 35 | "fsharp-analyzers": { 36 | "version": "0.4.0", 37 | "commands": [ 38 | "fsharp-analyzers" 39 | ] 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM fsharp:netcore 2 | 3 | # Copy endpoint specific user settings into container to specify 4 | # .NET Core should be used as the runtime. 5 | COPY settings.vscode.json /root/.vscode-remote/data/Machine/settings.json 6 | 7 | # Install git, process tools 8 | RUN apt-get update && apt-get -y install git procps 9 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MiniScaffold", 3 | "dockerFile": "Dockerfile", 4 | "appPort": [8080], 5 | "extensions": [ 6 | "ionide.ionide-fsharp", 7 | "ms-dotnettools.csharp", 8 | "editorconfig.editorconfig", 9 | "ionide.ionide-paket", 10 | "ionide.ionide-fake" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.devcontainer/settings.vscode.json: -------------------------------------------------------------------------------- 1 | { 2 | "FSharp.fsacRuntime":"netcore", 3 | "FSharp.enableAnalyzers": true, 4 | "FSharp.analyzersPath": [ 5 | "./packages/analyzers" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: 2 | http://EditorConfig.org 3 | 4 | # top-most EditorConfig file 5 | root = true 6 | 7 | # Default settings: 8 | # A newline ending every file 9 | # Use 4 spaces as indentation 10 | [*] 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.{fs,fsi,fsx,config}] 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | 19 | [paket.*] 20 | trim_trailing_whitespace = true 21 | indent_size = 2 22 | 23 | [*.paket.references] 24 | trim_trailing_whitespace = true 25 | indent_size = 2 26 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp text=auto eol=lf 6 | *.vb diff=csharp text=auto eol=lf 7 | *.fs diff=csharp text=auto eol=lf 8 | *.fsi diff=csharp text=auto eol=lf 9 | *.fsx diff=csharp text=auto eol=lf 10 | *.sln text eol=crlf merge=union 11 | *.csproj merge=union 12 | *.vbproj merge=union 13 | *.fsproj merge=union 14 | *.dbproj merge=union 15 | *.sh text eol=lf 16 | 17 | # Standard to msysgit 18 | *.doc diff=astextplain 19 | *.DOC diff=astextplain 20 | *.docx diff=astextplain 21 | *.DOCX diff=astextplain 22 | *.dot diff=astextplain 23 | *.DOT diff=astextplain 24 | *.pdf diff=astextplain 25 | *.PDF diff=astextplain 26 | *.rtf diff=astextplain 27 | *.RTF diff=astextplain 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please insert a description of your problem or question. 4 | 5 | ## Error messages, screenshots 6 | 7 | Please add any error logs or screenshots if available. 8 | 9 | ## Failing test, failing GitHub repo, or reproduction steps 10 | 11 | Please add either a failing test, a GitHub repo of the problem or detailed reproduction steps. 12 | 13 | ## Expected Behavior 14 | 15 | Please define what you would expect the behavior to be like. 16 | 17 | ## Known workarounds 18 | 19 | Please provide a description of any known workarounds. 20 | 21 | ## Other information 22 | 23 | * Operating System: 24 | - [ ] windows [insert version here] 25 | - [ ] macOs [insert version] 26 | - [ ] linux [insert flavor/version here] 27 | * Platform 28 | - [ ] dotnet core 29 | - [ ] dotnet full 30 | - [ ] mono 31 | * Branch or release version: 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Proposed Changes 2 | 3 | Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. 4 | 5 | ## Types of changes 6 | 7 | What types of changes does your code introduce to BinaryDefense.JsonWrapper? 8 | _Put an `x` in the boxes that apply_ 9 | 10 | - [ ] Bugfix (non-breaking change which fixes an issue) 11 | - [ ] New feature (non-breaking change which adds functionality) 12 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 13 | 14 | 15 | ## Checklist 16 | 17 | _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ 18 | 19 | - [ ] Build and tests pass locally 20 | - [ ] I have added tests that prove my fix is effective or that my feature works (if appropriate) 21 | - [ ] I have added necessary documentation (if appropriate) 22 | 23 | ## Further comments 24 | 25 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... 26 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build master 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest, windows-latest, macOS-latest] 11 | dotnet: [3.1.100] 12 | runs-on: ${{ matrix.os }} 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Setup .NET Core 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: ${{ matrix.dotnet }} 20 | - name: Build 21 | if: runner.os != 'Windows' 22 | run: | 23 | chmod +x ./build.sh 24 | ./build.sh 25 | env: 26 | # Work around https://github.com/actions/setup-dotnet/issues/29 27 | DOTNET_ROOT: ${{ runner.tool_cache }}/dncs/${{ matrix.dotnet }}/x64 28 | CI: true 29 | - name: Build 30 | if: runner.os == 'Windows' 31 | run: ./build.cmd 32 | env: 33 | # Work around https://github.com/actions/setup-dotnet/issues/29 34 | DOTNET_ROOT: ${{ runner.tool_cache }}/dncs/${{ matrix.dotnet }}/x64 35 | CI: true 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | packages/ 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.pfx 193 | *.publishsettings 194 | node_modules/ 195 | orleans.codegen.cs 196 | 197 | # Since there are multiple workflows, uncomment next line to ignore bower_components 198 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 199 | #bower_components/ 200 | 201 | # RIA/Silverlight projects 202 | Generated_Code/ 203 | 204 | # Backup & report files from converting an old project file 205 | # to a newer Visual Studio version. Backup files are not needed, 206 | # because we have git ;-) 207 | _UpgradeReport_Files/ 208 | Backup*/ 209 | UpgradeLog*.XML 210 | UpgradeLog*.htm 211 | 212 | # SQL Server files 213 | *.mdf 214 | *.ldf 215 | 216 | # Business Intelligence projects 217 | *.rdl.data 218 | *.bim.layout 219 | *.bim_*.settings 220 | 221 | # Microsoft Fakes 222 | FakesAssemblies/ 223 | 224 | # GhostDoc plugin setting file 225 | *.GhostDoc.xml 226 | 227 | # Node.js Tools for Visual Studio 228 | .ntvs_analysis.dat 229 | 230 | # Visual Studio 6 build log 231 | *.plg 232 | 233 | # Visual Studio 6 workspace options file 234 | *.opt 235 | 236 | # Visual Studio LightSwitch build output 237 | **/*.HTMLClient/GeneratedArtifacts 238 | **/*.DesktopClient/GeneratedArtifacts 239 | **/*.DesktopClient/ModelManifest.xml 240 | **/*.Server/GeneratedArtifacts 241 | **/*.Server/ModelManifest.xml 242 | _Pvt_Extensions 243 | 244 | # Paket dependency manager 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml 253 | 254 | TestResults.xml 255 | 256 | # NuGet packages distributables 257 | dist/ 258 | 259 | # Ionide cache 260 | .ionide/ 261 | 262 | # Test coverage files 263 | coverage.xml 264 | coverage.*.xml 265 | 266 | # Paket tool store 267 | .paket/.store 268 | .paket/paket 269 | 270 | .fake 271 | .ionide -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | sudo: required 3 | dist: xenial 4 | 5 | dotnet: 3.0.100 6 | mono: 7 | - latest # => "stable release" 8 | - alpha 9 | - beta 10 | - weekly # => "latest commits" 11 | os: 12 | - linux 13 | 14 | script: 15 | - ./build.sh 16 | 17 | matrix: 18 | fast_finish: true 19 | allow_failures: 20 | - mono: alpha 21 | - mono: beta 22 | - mono: weekly 23 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ionide.ionide-paket", 4 | "ionide.ionide-fsharp", 5 | "ionide.ionide-fake", 6 | "ms-dotnettools.csharp", 7 | "editorConfig.editorConfig" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Attach to running process", 9 | "type": "coreclr", 10 | "request": "attach", 11 | "processId": "${command:pickProcess}", 12 | "justMyCode": false, 13 | "enableStepFiltering": false, 14 | "symbolOptions": { 15 | "searchPaths": [ 16 | "http://srv.symbolsource.org/pdb/Public", 17 | "http://srv.symbolsource.org/pdb/MyGet" 18 | ], 19 | "searchMicrosoftSymbolServer": true 20 | }, 21 | "sourceLinkOptions": { 22 | "*": { 23 | "enabled": true 24 | } 25 | } 26 | }] 27 | } 28 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "FSharp.fsacRuntime":"netcore", 3 | "FSharp.enableAnalyzers": true, 4 | "FSharp.analyzersPath": [ 5 | "./packages/analyzers" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /BinaryDefense.JsonWrapper.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("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C397A34C-84F1-49E7-AEBC-2F9F2B196216}" 7 | EndProject 8 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BinaryDefense.Myriad.Plugins.JsonWrapper", "src\BinaryDefense.Myriad.Plugins.JsonWrapper\BinaryDefense.Myriad.Plugins.JsonWrapper.fsproj", "{5D30E174-2538-47AC-8443-318C8C5DC2C9}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{ACBEE43C-7A88-4FB1-9B06-DB064D22B29F}" 11 | EndProject 12 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "JsonWrapperPlugin.Tests", "tests\JsonWrapperPlugin.Tests\JsonWrapperPlugin.Tests.fsproj", "{1CA2E092-2320-451D-A4F0-9ED7C7C528CA}" 13 | EndProject 14 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "docsTool", "docsTool\docsTool.fsproj", "{8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}" 15 | EndProject 16 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BinaryDefense.JsonWrapper.Core", "src\BinaryDefense.JsonWrapper.Core\BinaryDefense.JsonWrapper.Core.fsproj", "{C3089DDF-96EA-49DD-888A-0E50DDC74F58}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Debug|x64 = Debug|x64 22 | Debug|x86 = Debug|x86 23 | Release|Any CPU = Release|Any CPU 24 | Release|x64 = Release|x64 25 | Release|x86 = Release|x86 26 | EndGlobalSection 27 | GlobalSection(SolutionProperties) = preSolution 28 | HideSolutionNode = FALSE 29 | EndGlobalSection 30 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 31 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|x64.ActiveCfg = Debug|x64 34 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|x64.Build.0 = Debug|x64 35 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|x86.ActiveCfg = Debug|x86 36 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|x86.Build.0 = Debug|x86 37 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|x64.ActiveCfg = Release|x64 40 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|x64.Build.0 = Release|x64 41 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|x86.ActiveCfg = Release|x86 42 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|x86.Build.0 = Release|x86 43 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x64.ActiveCfg = Debug|x64 46 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x64.Build.0 = Debug|x64 47 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x86.ActiveCfg = Debug|x86 48 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x86.Build.0 = Debug|x86 49 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x64.ActiveCfg = Release|x64 52 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x64.Build.0 = Release|x64 53 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x86.ActiveCfg = Release|x86 54 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x86.Build.0 = Release|x86 55 | {8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 56 | {8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Debug|Any CPU.Build.0 = Debug|Any CPU 57 | {8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Debug|x64.ActiveCfg = Debug|Any CPU 58 | {8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Debug|x64.Build.0 = Debug|Any CPU 59 | {8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Debug|x86.ActiveCfg = Debug|Any CPU 60 | {8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Debug|x86.Build.0 = Debug|Any CPU 61 | {8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Release|Any CPU.ActiveCfg = Release|Any CPU 62 | {8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Release|Any CPU.Build.0 = Release|Any CPU 63 | {8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Release|x64.ActiveCfg = Release|Any CPU 64 | {8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Release|x64.Build.0 = Release|Any CPU 65 | {8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Release|x86.ActiveCfg = Release|Any CPU 66 | {8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Release|x86.Build.0 = Release|Any CPU 67 | {C3089DDF-96EA-49DD-888A-0E50DDC74F58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 68 | {C3089DDF-96EA-49DD-888A-0E50DDC74F58}.Debug|Any CPU.Build.0 = Debug|Any CPU 69 | {C3089DDF-96EA-49DD-888A-0E50DDC74F58}.Debug|x64.ActiveCfg = Debug|Any CPU 70 | {C3089DDF-96EA-49DD-888A-0E50DDC74F58}.Debug|x64.Build.0 = Debug|Any CPU 71 | {C3089DDF-96EA-49DD-888A-0E50DDC74F58}.Debug|x86.ActiveCfg = Debug|Any CPU 72 | {C3089DDF-96EA-49DD-888A-0E50DDC74F58}.Debug|x86.Build.0 = Debug|Any CPU 73 | {C3089DDF-96EA-49DD-888A-0E50DDC74F58}.Release|Any CPU.ActiveCfg = Release|Any CPU 74 | {C3089DDF-96EA-49DD-888A-0E50DDC74F58}.Release|Any CPU.Build.0 = Release|Any CPU 75 | {C3089DDF-96EA-49DD-888A-0E50DDC74F58}.Release|x64.ActiveCfg = Release|Any CPU 76 | {C3089DDF-96EA-49DD-888A-0E50DDC74F58}.Release|x64.Build.0 = Release|Any CPU 77 | {C3089DDF-96EA-49DD-888A-0E50DDC74F58}.Release|x86.ActiveCfg = Release|Any CPU 78 | {C3089DDF-96EA-49DD-888A-0E50DDC74F58}.Release|x86.Build.0 = Release|Any CPU 79 | EndGlobalSection 80 | GlobalSection(NestedProjects) = preSolution 81 | {5D30E174-2538-47AC-8443-318C8C5DC2C9} = {C397A34C-84F1-49E7-AEBC-2F9F2B196216} 82 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA} = {ACBEE43C-7A88-4FB1-9B06-DB064D22B29F} 83 | {C3089DDF-96EA-49DD-888A-0E50DDC74F58} = {C397A34C-84F1-49E7-AEBC-2F9F2B196216} 84 | EndGlobalSection 85 | EndGlobal 86 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.1.1] - 2020-06-05 9 | 10 | ### Added 11 | - Initial release 12 | 13 | ## [0.1.1-beta009] - 2020-06-05 14 | 15 | ### Added 16 | - Initial release 17 | 18 | ## [0.1.0] - 2020-05-26 19 | 20 | ### Added 21 | - Initial release 22 | 23 | [Unreleased]: https://github.com/BinaryDefense/JsonWrapper/compare/v0.1.1...HEAD 24 | [0.1.1]: https://github.com/BinaryDefense/JsonWrapper/compare/v0.1.0...v0.1.1 25 | [0.1.1-beta009]: https://github.com/BinaryDefense/JsonWrapper/compare/v0.1.0...v0.1.1-beta009 26 | [0.1.1-beta008]: https://github.com/BinaryDefense/JsonWrapper/compare/v0.1.0...v0.1.1-beta008 27 | [0.1.1-beta007]: https://github.com/BinaryDefense/JsonWrapper/compare/v0.1.0...v0.1.1-beta007 28 | [0.1.1-beta006]: https://github.com/BinaryDefense/JsonWrapper/compare/v0.1.0...v0.1.1-beta006 29 | [0.1.0-beta005]: https://github.com/BinaryDefense/JsonWrapper/releases/tag/v0.1.0-beta005 30 | [0.1.0-beta004]: https://github.com/BinaryDefense/JsonWrapper/releases/tag/v0.1.0-beta004 31 | [0.1.0-beta003]: https://github.com/BinaryDefense/JsonWrapper/releases/tag/v0.1.0-beta003 32 | [0.1.0-beta002]: https://github.com/BinaryDefense/JsonWrapper/releases/tag/v0.1.0-beta002 33 | [0.1.0-beta001]: https://github.com/BinaryDefense/JsonWrapper/releases/tag/v0.1.0-beta001 34 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | f#, fsharp 5 | https://github.com/BinaryDefense/BinaryDefense.JsonWrapper 6 | https://github.com/BinaryDefense/BinaryDefense.JsonWrapper/blob/master/LICENSE.md 7 | false 8 | git 9 | BinaryDefense 10 | https://github.com/BinaryDefense/BinaryDefense.JsonWrapper 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BinaryDefense.JsonWrapper 2 | 3 | ## What 4 | 5 | A plugin for [Myriad](https://github.com/MoiraeSoftware/myriad) for generating statically typed lossless wrappers around JToken given a schema. 6 | 7 | ## Why 8 | 9 | When serializing JSON directly into known record types, this can cause the underlying dataset to be lost. Given a JSON object: 10 | 11 | ```json 12 | { 13 | "Name" : "James Kirk", 14 | "Age" : 32 15 | } 16 | ``` 17 | 18 | and a [record type](https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/records): 19 | 20 | ```fsharp 21 | 22 | type Person = { 23 | Name : string 24 | Age : uint32 25 | } 26 | ``` 27 | 28 | serializing is fine. However, if the underlying json adds more information in it's payload such as: 29 | 30 | ```json 31 | { 32 | "Name" : "James Kirk", 33 | "Age" : 32, 34 | "UniformColor": "Gold" 35 | } 36 | ``` 37 | 38 | If that type is then serialized and saved somewhere, our record type will lose the `UniformColor` field, causing this to be a lossy conversion. 39 | 40 | This Myriad plugin will instead generate [a class](https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/classes) given a record type as a schema. 41 | 42 | ## How 43 | 44 | Add `BinaryDefense.Myriad.Plugins.JsonWrapper` to your project. 45 | 46 | Given our record above as a schema, we need to add the attribute `[]` to it. 47 | 48 | ```fsharp 49 | [] 50 | type Person = { 51 | Name : string 52 | Age : uint32 53 | } 54 | ``` 55 | 56 | Additionally, an entry to the `fsproj` file must be added: 57 | 58 | ```xml 59 | 60 | MyTypes.fs 61 | GeneratedNamespace 62 | 63 | ``` 64 | 65 | `MyTypes.fs` is where the `Person` record type lives. `MyGeneratedFile.fs` will be the file generated with the output. `GeneratedNamespace` will be the namespace the generated code lives in. On build, Myriad [will generate the code](https://github.com/MoiraeSoftware/myriad#msbuild-usage.) An example of it's output: 66 | 67 | ```fsharp 68 | type Person(jtoken: JToken, serializer: JsonSerializer) = 69 | member this.Name 70 | with get () = 71 | let selectedToken = jtoken.["Name"] 72 | selectedToken.ToObject serializer 73 | and set (newValue: string) = 74 | jtoken.["Name"] <- JToken.FromObject(newValue, serializer) 75 | 76 | member this.Age 77 | with get () = 78 | let selectedToken = jtoken.["Age"] 79 | selectedToken.ToObject serializer 80 | and set (newValue: uint32) = 81 | jtoken.["Age"] <- JToken.FromObject(newValue, serializer) 82 | 83 | override this.GetHashCode () = jtoken.GetHashCode() 84 | 85 | override this.Equals(objToCompare: obj) = 86 | match objToCompare with 87 | | :? IHaveJToken as jTokenToCompare -> JToken.DeepEquals(jTokenToCompare.InnerData, jtoken) 88 | | _ -> false 89 | 90 | ///This allows the class to be pattern matched against 91 | member this.Deconstruct(Name: outref, Age: outref) = 92 | Name <- this.Name 93 | Age <- this.Age 94 | 95 | interface IHaveJToken with 96 | override this.InnerData = jtoken 97 | ``` 98 | 99 | When using this type, you'll need to also add [custom converters](https://www.newtonsoft.com/json/help/html/CustomJsonConverter.htm) to the JsonSerializer you are using throughout your application. 100 | 101 | ```fsharp 102 | let converters = Converters.recommendedConverters 103 | 104 | let serializationSettings = 105 | let s = JsonSerializerSettings() 106 | scrubDefaultDUConverter s.Converters 107 | for c in converters do s.Converters.Add c 108 | s 109 | 110 | let jsonSettings = serializationSettings 111 | let jsonSerializer =JsonSerializer.CreateDefault jsonSettings 112 | 113 | ``` 114 | 115 | This will be using the [IHaveJTokenConverter](src/BinaryDefense.JsonWrapper.Core/BinaryDefense.JsonWrapper.Core.fs) to ensure the serialization is lossless. 116 | 117 | 118 | Additional reading on this topic: 119 | 120 | - [Event Sourcing Versioning](https://leanpub.com/esversioning/read#leanpub-auto-wrapper) 121 | - [Serialization is lossy](https://web.archive.org/web/20160913235956/http://kellabyte.com/2013/05/02/serialization-is-lossy/) 122 | 123 | ### Features 124 | 125 | - Lossless 126 | - Override backing field name 127 | - Enforce Required fields 128 | - Destructuring 129 | - Structural equals 130 | 131 | --- 132 | 133 | ## Builds 134 | 135 | GitHub Actions | 136 | :---: | 137 | [![GitHub Actions](https://github.com/BinaryDefense/JsonWrapper/workflows/Build%20master/badge.svg)](https://github.com/BinaryDefense/JsonWrapper/actions?query=branch%3Amaster) | 138 | [![Build History](https://buildstats.info/github/chart/BinaryDefense/JsonWrapper)](https://github.com/BinaryDefense/JsonWrapper/actions?query=branch%3Amaster) | 139 | 140 | 141 | 142 | ## NuGet 143 | 144 | Package | Stable | Prerelease 145 | --- | --- | --- 146 | BinaryDefense.JsonWrapper.Core | [![NuGet Badge](https://buildstats.info/nuget/BinaryDefense.JsonWrapper.Core)](https://www.nuget.org/packages/BinaryDefense.JsonWrapper.Core/) | [![NuGet Badge](https://buildstats.info/nuget/BinaryDefense.JsonWrapper.Core?includePreReleases=true)](https://www.nuget.org/packages/BinaryDefense.JsonWrapper.Core/) 147 | BinaryDefense.Myriad.Plugins.JsonWrapper | [![NuGet Badge](https://buildstats.info/nuget/BinaryDefense.Myriad.Plugins.JsonWrapper)](https://www.nuget.org/packages/BinaryDefense.Myriad.Plugins.JsonWrapper/) | [![NuGet Badge](https://buildstats.info/nuget/BinaryDefense.Myriad.Plugins.JsonWrapper?includePreReleases=true)](https://www.nuget.org/packages/BinaryDefense.Myriad.Plugins.JsonWrapper/) 148 | 149 | --- 150 | 151 | ### Developing 152 | 153 | Make sure the following **requirements** are installed on your system: 154 | 155 | - [dotnet SDK](https://www.microsoft.com/net/download/core) 3.0 or higher 156 | - [Mono](http://www.mono-project.com/) if you're on Linux or macOS. 157 | 158 | or 159 | 160 | - [VSCode Dev Container](https://code.visualstudio.com/docs/remote/containers) 161 | 162 | 163 | --- 164 | 165 | ### Environment Variables 166 | 167 | - `CONFIGURATION` will set the [configuration](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-build?tabs=netcore2x#options) of the dotnet commands. If not set, it will default to Release. 168 | - `CONFIGURATION=Debug ./build.sh` will result in `-c` additions to commands such as in `dotnet build -c Debug` 169 | - `GITHUB_TOKEN` will be used to upload release notes and Nuget packages to GitHub. 170 | - Be sure to set this before releasing 171 | - `DISABLE_COVERAGE` Will disable running code coverage metrics. AltCover can have [severe performance degradation](https://github.com/SteveGilham/altcover/issues/57) so it's worth disabling when looking to do a quicker feedback loop. 172 | - `DISABLE_COVERAGE=1 ./build.sh` 173 | 174 | 175 | --- 176 | 177 | ### Building 178 | 179 | 180 | ```sh 181 | > build.cmd // on windows 182 | $ ./build.sh // on unix 183 | ``` 184 | 185 | The bin of your library should look similar to: 186 | 187 | ``` 188 | $ tree src/MyCoolNewLib/bin/ 189 | src/MyCoolNewLib/bin/ 190 | └── Debug 191 | ├── net461 192 | │   ├── FSharp.Core.dll 193 | │   ├── MyCoolNewLib.dll 194 | │   ├── MyCoolNewLib.pdb 195 | │   ├── MyCoolNewLib.xml 196 | └── netstandard2.1 197 | ├── MyCoolNewLib.deps.json 198 | ├── MyCoolNewLib.dll 199 | ├── MyCoolNewLib.pdb 200 | └── MyCoolNewLib.xml 201 | 202 | ``` 203 | 204 | --- 205 | 206 | ### Build Targets 207 | 208 | - `Clean` - Cleans artifact and temp directories. 209 | - `DotnetRestore` - Runs [dotnet restore](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-restore?tabs=netcore2x) on the [solution file](https://docs.microsoft.com/en-us/visualstudio/extensibility/internals/solution-dot-sln-file?view=vs-2019). 210 | - [`DotnetBuild`](#Building) - Runs [dotnet build](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-build?tabs=netcore2x) on the [solution file](https://docs.microsoft.com/en-us/visualstudio/extensibility/internals/solution-dot-sln-file?view=vs-2019). 211 | - `DotnetTest` - Runs [dotnet test](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-test?tabs=netcore21) on the [solution file](https://docs.microsoft.com/en-us/visualstudio/extensibility/internals/solution-dot-sln-file?view=vs-2019). 212 | - `GenerateCoverageReport` - Code coverage is run during `DotnetTest` and this generates a report via [ReportGenerator](https://github.com/danielpalme/ReportGenerator). 213 | - `WatchTests` - Runs [dotnet watch](https://docs.microsoft.com/en-us/aspnet/core/tutorials/dotnet-watch?view=aspnetcore-3.0) with the test projects. Useful for rapid feedback loops. 214 | - `GenerateAssemblyInfo` - Generates [AssemblyInfo](https://docs.microsoft.com/en-us/dotnet/api/microsoft.visualbasic.applicationservices.assemblyinfo?view=netframework-4.8) for libraries. 215 | - `DotnetPack` - Runs [dotnet pack](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-pack). This includes running [Source Link](https://github.com/dotnet/sourcelink). 216 | - `SourceLinkTest` - Runs a Source Link test tool to verify Source Links were properly generated. 217 | - `PublishToNuGet` - Publishes the NuGet packages generated in `DotnetPack` to NuGet via [paket push](https://fsprojects.github.io/Paket/paket-push.html). 218 | - `GitRelease` - Creates a commit message with the [Release Notes](https://fake.build/apidocs/v5/fake-core-releasenotes.html) and a git tag via the version in the `Release Notes`. 219 | - `GitHubRelease` - Publishes a [GitHub Release](https://help.github.com/en/articles/creating-releases) with the Release Notes and any NuGet packages. 220 | - `FormatCode` - Runs [Fantomas](https://github.com/fsprojects/fantomas) on the solution file. 221 | - `BuildDocs` - Generates Documentation from `docsSrc` and the [XML Documentation Comments](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/xmldoc/) from your libraries in `src`. 222 | - `WatchDocs` - Generates documentation and starts a webserver locally. It will rebuild and hot reload if it detects any changes made to `docsSrc` files, libraries in `src`, or the `docsTool` itself. 223 | - `ReleaseDocs` - Will stage, commit, and push docs generated in the `BuildDocs` target. 224 | - [`Release`](#Releasing) - Task that runs all release type tasks such as `PublishToNuGet`, `GitRelease`, `ReleaseDocs`, and `GitHubRelease`. Make sure to read [Releasing](#Releasing) to setup your environment correctly for releases. 225 | --- 226 | 227 | 228 | ### Releasing 229 | 230 | - [Start a git repo with a remote](https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/) 231 | 232 | ```sh 233 | git add . 234 | git commit -m "Scaffold" 235 | git remote add origin https://github.com/user/MyCoolNewLib.git 236 | git push -u origin master 237 | ``` 238 | 239 | - [Create your NuGeT API key](https://docs.microsoft.com/en-us/nuget/nuget-org/publish-a-package#create-api-keys) 240 | - [Add your NuGet API key to paket](https://fsprojects.github.io/Paket/paket-config.html#Adding-a-NuGet-API-key) 241 | 242 | ```sh 243 | paket config add-token "https://www.nuget.org" 4003d786-cc37-4004-bfdf-c4f3e8ef9b3a 244 | ``` 245 | 246 | - or set the environment variable `NUGET_TOKEN` to your key 247 | 248 | 249 | - [Create a GitHub OAuth Token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/) 250 | - You can then set the environment variable `GITHUB_TOKEN` to upload release notes and artifacts to github 251 | - Otherwise it will fallback to username/password 252 | 253 | - Then update the `CHANGELOG.md` with an "Unreleased" section containing release notes for this version, in [KeepAChangelog](https://keepachangelog.com/en/1.1.0/) format. 254 | 255 | NOTE: Its highly recommend to add a link to the Pull Request next to the release note that it affects. The reason for this is when the `RELEASE` target is run, it will add these new notes into the body of git commit. GitHub will notice the links and will update the Pull Request with what commit referenced it saying ["added a commit that referenced this pull request"](https://github.com/TheAngryByrd/MiniScaffold/pull/179#ref-commit-837ad59). Since the build script automates the commit message, it will say "Bump Version to x.y.z". The benefit of this is when users goto a Pull Request, it will be clear when and which version those code changes released. Also when reading the `CHANGELOG`, if someone is curious about how or why those changes were made, they can easily discover the work and discussions. 256 | 257 | Here's an example of adding an "Unreleased" section to a `CHANGELOG.md` with a `0.1.0` section already released. 258 | 259 | ```markdown 260 | ## [Unreleased] 261 | 262 | ### Added 263 | - Does cool stuff! 264 | 265 | ### Fixed 266 | - Fixes that silly oversight 267 | 268 | ## [0.1.0] - 2017-03-17 269 | First release 270 | 271 | ### Added 272 | - This release already has lots of features 273 | 274 | [Unreleased]: https://github.com/user/MyCoolNewLib.git/compare/v0.1.0...HEAD 275 | [0.1.0]: https://github.com/user/MyCoolNewLib.git/releases/tag/v0.1.0 276 | ``` 277 | 278 | - You can then use the `Release` target, specifying the version number either in the `RELEASE_VERSION` environment 279 | variable, or else as a parameter after the target name. This will: 280 | - update `CHANGELOG.md`, moving changes from the `Unreleased` section into a new `0.2.0` section 281 | - if there were any prerelease versions of 0.2.0 in the changelog, it will also collect their changes into the final 0.2.0 entry 282 | - make a commit bumping the version: `Bump version to 0.2.0` and adds the new changelog section to the commit's body 283 | - publish the package to NuGet 284 | - push a git tag 285 | - create a GitHub release for that git tag 286 | 287 | macOS/Linux Parameter: 288 | 289 | ```sh 290 | ./build.sh Release 0.2.0 291 | ``` 292 | 293 | macOS/Linux Environment Variable: 294 | 295 | ```sh 296 | RELEASE_VERSION=0.2.0 ./build.sh Release 297 | ``` 298 | 299 | 300 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | init: 2 | - git config --global core.autocrlf input 3 | build_script: 4 | - cmd: build.cmd 5 | test: off 6 | version: 0.0.1.{build} 7 | image: Visual Studio 2019 8 | install: 9 | - cmd: choco install dotnetcore-sdk -y 10 | artifacts: 11 | - path: bin 12 | name: bin 13 | - path: dist 14 | name: dist 15 | -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | echo Restoring dotnet tools... 2 | dotnet tool restore 3 | 4 | dotnet fake build -t %* 5 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | set -o pipefail 5 | 6 | echo "Restoring dotnet tools..." 7 | dotnet tool restore 8 | 9 | PAKET_SKIP_RESTORE_TARGETS=true FAKE_DETAILED_ERRORS=true dotnet fake build -t "$@" 10 | -------------------------------------------------------------------------------- /docsSrc/Explanations/Background.md: -------------------------------------------------------------------------------- 1 | # Background 2 | 3 | Here's a core concept and reasons why this exists at a deeper level. 4 | -------------------------------------------------------------------------------- /docsSrc/How_Tos/Doing_A_Thing.md: -------------------------------------------------------------------------------- 1 | # How To do this specific thing 2 | 3 | -------------------------------------------------------------------------------- /docsSrc/How_Tos/Doing_Another_Thing.md: -------------------------------------------------------------------------------- 1 | # How To do this specific thing 2 | 3 | -------------------------------------------------------------------------------- /docsSrc/Tutorials/Getting_Started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ```fsharp 4 | let foo = () 5 | let myAge = 21 6 | ``` 7 | 8 | ## Here is the path to downloading 9 | 10 | [lang=bash] 11 | paket install BinaryDefense.JsonWrapper 12 | 13 | 14 | -------------------------------------------------------------------------------- /docsSrc/content/cleanups.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | // Makes code snippets responsive 3 | $("table").addClass("table-responsive"); 4 | }) 5 | 6 | -------------------------------------------------------------------------------- /docsSrc/content/hotload.js: -------------------------------------------------------------------------------- 1 | var refreshSocket = new WebSocket('ws://' + window.location.host) 2 | .onmessage = () => { 3 | location.reload(); 4 | } 5 | -------------------------------------------------------------------------------- /docsSrc/content/style.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Droid+Sans|Droid+Sans+Mono|Open+Sans:400,600,700); 2 | 3 | /*-------------------------------------------------------------------------- 4 | Formatting for F# code snippets 5 | /*--------------------------------------------------------------------------*/ 6 | 7 | /* strings --- and styles for other string related formats */ 8 | span.s { color:#E0E268; } 9 | /* printf formatters */ 10 | span.pf { color:#E0C57F; } 11 | /* escaped chars */ 12 | span.e { color:#EA8675; } 13 | 14 | /* identifiers --- and styles for more specific identifier types */ 15 | span.i { color:#d1d1d1; } 16 | /* type or module */ 17 | span.t { color:#43AEC6; } 18 | /* function */ 19 | span.f { color:#e1e1e1; } 20 | /* DU case or active pattern */ 21 | span.p { color:#4ec9b0; } 22 | 23 | /* keywords */ 24 | span.k { color:#FAB11D; } 25 | /* comment */ 26 | span.c { color:#808080; } 27 | /* operators */ 28 | span.o { color:#af75c1; } 29 | /* numbers */ 30 | span.n { color:#96C71D; } 31 | /* line number */ 32 | span.l { color:#80b0b0; } 33 | /* mutable var or ref cell */ 34 | span.v { color:#d1d1d1; font-weight: bold; } 35 | /* inactive code */ 36 | span.inactive { color:#808080; } 37 | /* preprocessor */ 38 | span.prep { color:#af75c1; } 39 | /* fsi output */ 40 | span.fsi { color:#808080; } 41 | 42 | /* omitted */ 43 | span.omitted { 44 | background:#3c4e52; 45 | border-radius:5px; 46 | color:#808080; 47 | padding:0px 0px 1px 0px; 48 | } 49 | /* tool tip */ 50 | div.tip { 51 | background:#475b5f; 52 | border-radius:4px; 53 | font:11pt 'Droid Sans', arial, sans-serif; 54 | padding:6px 8px 6px 8px; 55 | display:none; 56 | color:#d1d1d1; 57 | pointer-events:none; 58 | } 59 | table.pre pre { 60 | padding:0px; 61 | margin:0px; 62 | border:none; 63 | } 64 | table.pre, pre.fssnip, pre { 65 | line-height:13pt; 66 | /*border:1px solid #d8d8d8;*/ 67 | border:1px solid #000; 68 | /* border: none; */ 69 | border-collapse:separate; 70 | white-space:pre-wrap; 71 | font: 9pt 'Droid Sans Mono',consolas,monospace; 72 | width:90%; 73 | margin:10px 20px 20px 20px; 74 | background-color:#212d30; 75 | padding:10px; 76 | /*border-radius:5px;*/ 77 | color:#d1d1d1; 78 | max-width: none; 79 | } 80 | pre.fssnip code { 81 | font: 9pt 'Droid Sans Mono',consolas,monospace; 82 | } 83 | table.pre pre { 84 | padding:0px; 85 | margin:0px; 86 | border-radius:0px; 87 | width: 100%; 88 | } 89 | table.pre td { 90 | padding:0px; 91 | white-space:normal; 92 | margin:0px; 93 | } 94 | table.pre td.lines { 95 | width:30px; 96 | } 97 | 98 | .table thead td.fit, 99 | .table th.fit { 100 | white-space: nowrap; 101 | width: 1%; 102 | } 103 | /*-------------------------------------------------------------------------- 104 | Formatting for page & standard document content 105 | /*--------------------------------------------------------------------------*/ 106 | 107 | body { 108 | font-family: 'Open Sans', serif; 109 | } 110 | 111 | pre { 112 | word-wrap: inherit; 113 | } 114 | 115 | /* Format the heading - nicer spacing etc. */ 116 | .masthead { 117 | overflow: hidden; 118 | } 119 | .masthead .muted a { 120 | text-decoration:none; 121 | color:#999999; 122 | } 123 | .masthead ul, .masthead li { 124 | margin-bottom:0px; 125 | } 126 | .masthead .nav li { 127 | margin-top: 15px; 128 | font-size:110%; 129 | } 130 | .masthead h3 { 131 | margin-bottom:5px; 132 | font-size:170%; 133 | } 134 | hr { 135 | margin:0px 0px 20px 0px; 136 | } 137 | 138 | /* Make table headings and td.title bold */ 139 | td.title, thead { 140 | font-weight:bold; 141 | } 142 | 143 | /* Format the right-side menu */ 144 | #menu { 145 | margin-top:50px; 146 | font-size:11pt; 147 | padding-left:20px; 148 | } 149 | 150 | #menu .nav-header { 151 | font-size:12pt; 152 | color:#606060; 153 | margin-top:20px; 154 | } 155 | 156 | #menu li { 157 | line-height:25px; 158 | } 159 | 160 | .wrapper { 161 | margin-top: -56px; 162 | padding-top: 56px; 163 | } 164 | 165 | /* Change font sizes for headings etc. */ 166 | #main h1 { font-size: 26pt; margin:10px 0px 15px 0px; font-weight:400; } 167 | #main h2 { font-size: 20pt; margin:20px 0px 0px 0px; font-weight:400; } 168 | #main h3 { font-size: 14pt; margin:15px 0px 0px 0px; font-weight:600; } 169 | #main p { font-size: 11pt; margin:5px 0px 15px 0px; } 170 | #main ul { font-size: 11pt; margin-top:10px; } 171 | #main li { font-size: 11pt; margin: 5px 0px 5px 0px; } 172 | #main strong { font-weight:700; } 173 | 174 | /*-------------------------------------------------------------------------- 175 | Formatting for API reference 176 | /*--------------------------------------------------------------------------*/ 177 | 178 | .type-list .type-name, .module-list .module-name { 179 | width:25%; 180 | font-weight:bold; 181 | } 182 | .member-list .member-name { 183 | width:35%; 184 | } 185 | #main .xmldoc h2 { 186 | font-size:14pt; 187 | margin:10px 0px 0px 0px; 188 | } 189 | #main .xmldoc h3 { 190 | font-size:12pt; 191 | margin:10px 0px 0px 0px; 192 | } 193 | .github-link { 194 | float:right; 195 | text-decoration:none; 196 | } 197 | .github-link img { 198 | border-style:none; 199 | margin-left:10px; 200 | } 201 | .github-link .hover { display:none; } 202 | .github-link:hover .hover { display:block; } 203 | .github-link .normal { display: block; } 204 | .github-link:hover .normal { display: none; } 205 | 206 | /*-------------------------------------------------------------------------- 207 | Links 208 | /*--------------------------------------------------------------------------*/ 209 | 210 | .bootstrap h1 a, .bootstrap h1 a:hover, .bootstrap h1 a:focus, 211 | .bootstrap h2 a, .bootstrap h2 a:hover, .bootstrap h2 a:focus, 212 | .bootstrap h3 a, .bootstrap h3 a:hover, .bootstrap h3 a:focus, 213 | .bootstrap h4 a, .bootstrap h4 a:hover, .bootstrap h4 a:focus, 214 | .bootstrap h5 a, .bootstrap h5 a:hover, .bootstrap h5 a:focus, 215 | .bootstrap h6 a, .bootstrap h6 a:hover, .bootstrap h6 a:focus { color : inherit; text-decoration : inherit; outline:none } 216 | 217 | /*-------------------------------------------------------------------------- 218 | Additional formatting for the homepage 219 | /*--------------------------------------------------------------------------*/ 220 | 221 | #nuget { 222 | margin-top:20px; 223 | font-size: 11pt; 224 | padding:20px; 225 | } 226 | 227 | #nuget pre { 228 | font-size:11pt; 229 | -moz-border-radius: 0px; 230 | -webkit-border-radius: 0px; 231 | border-radius: 0px; 232 | background: #404040; 233 | border-style:none; 234 | color: #e0e0e0; 235 | margin-top:15px; 236 | } 237 | 238 | .date { 239 | font-style: italic; 240 | margin-bottom: 15px; 241 | } 242 | 243 | h1.header { 244 | color: green; 245 | } 246 | 247 | h1.header:hover { 248 | color: green; 249 | } 250 | 251 | h1.header:visited { 252 | color: green; 253 | } 254 | 255 | .categories, .category, .recent-posts { 256 | font-family: 'Droid Sans', arial, sans-serif; 257 | } 258 | 259 | .categories ul, 260 | .recent-posts ul { 261 | margin-left: 0; 262 | } 263 | .categories li, 264 | .category li, 265 | .recent-posts li 266 | { 267 | list-style-type: none; 268 | white-space: nowrap; 269 | } 270 | 271 | .links { 272 | text-align: center; 273 | margin-bottom: 8px; 274 | } 275 | 276 | .copyright { 277 | text-align: center; 278 | color: lightslategray; 279 | margin-bottom: 25px; 280 | } 281 | 282 | .social { 283 | margin-bottom: 30px; 284 | } 285 | 286 | /* Fixes page anchors with bootstrap navbar */ 287 | :target::before { 288 | display: block; 289 | height: 59px; 290 | margin-top: -59px; 291 | content: ""; 292 | } 293 | 294 | /* Hides first br from FSharp.Literate xml-doc rendering */ 295 | .comment-block > br:first-child, 296 | .xmldoc > br:first-child { 297 | display: none; 298 | } 299 | 300 | .main h1 { 301 | padding: .5em 0em 302 | } 303 | 304 | .main h2 { 305 | padding: .5em 0em 306 | } 307 | 308 | .dropdown-submenu { 309 | position: relative; 310 | } 311 | 312 | .dropdown-submenu>a:after { 313 | content: "\f0da"; 314 | padding-left: 5px; 315 | vertical-align: middle; 316 | border: none; 317 | font-weight: 900; 318 | font-family: 'Font Awesome 5 Free'; 319 | } 320 | 321 | .dropdown-submenu>.dropdown-menu { 322 | top: 0; 323 | left: 100%; 324 | margin-top: 0px; 325 | margin-left: 0px; 326 | } 327 | 328 | .fsharp-footer-logo { 329 | width: 20px; 330 | margin-top: -2px; 331 | -webkit-filter: grayscale(100%) brightness(0) invert(1); /* Safari 6.0 - 9.0 */ 332 | filter: grayscale(100%) brightness(0) invert(1); 333 | } 334 | -------------------------------------------------------------------------------- /docsSrc/content/submenu.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | // ------------------------------------------------------- // 3 | // Multi Level dropdowns 4 | // ------------------------------------------------------ // 5 | $("ul.dropdown-menu [data-toggle='dropdown']").on("click", function(event) { 6 | event.preventDefault(); 7 | event.stopPropagation(); 8 | 9 | $(this).siblings().toggleClass("show"); 10 | 11 | 12 | if (!$(this).next().hasClass('show')) { 13 | $(this).parents('.dropdown-menu').first().find('.show').removeClass("show"); 14 | } 15 | $(this).parents('li.nav-item.dropdown.show').on('hidden.bs.dropdown', function(e) { 16 | $('.dropdown-submenu .show').removeClass("show"); 17 | }); 18 | 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /docsSrc/content/themes.js: -------------------------------------------------------------------------------- 1 | 2 | var themes = { 3 | "light" : { 4 | "button-text" : "Swap to Dark", 5 | "button-classes" : "btn btn-dark border-light", 6 | "next-theme" : "dark", 7 | "body-class" : "bootstrap" 8 | }, 9 | "dark" : { 10 | "button-text" : "Swap to Light", 11 | "button-classes" : "btn btn-light", 12 | "next-theme" : "light", 13 | "body-class" : "bootstrap-dark" 14 | } 15 | }; 16 | 17 | var themeStorageKey = 'theme'; 18 | 19 | function swapThemeInDom(theme) { 20 | var newTheme = themes[theme]; 21 | var bootstrapCSS = document.getElementsByTagName('body')[0]; 22 | bootstrapCSS.setAttribute('class', newTheme['body-class']) 23 | } 24 | 25 | function persistNewTheme(theme) { 26 | window.localStorage.setItem(themeStorageKey, theme); 27 | } 28 | 29 | function setToggleButton(theme) { 30 | var newTheme = themes[theme]; 31 | var themeToggleButton = document.getElementById('theme-toggle'); 32 | themeToggleButton.textContent = newTheme['button-text']; 33 | themeToggleButton.className = newTheme['button-classes']; 34 | themeToggleButton.onclick = function() { 35 | setTheme(newTheme['next-theme']); 36 | } 37 | } 38 | 39 | function setTheme(theme) { 40 | try { 41 | swapThemeInDom(theme); 42 | } 43 | catch(e){ 44 | } 45 | try { 46 | persistNewTheme(theme); 47 | } 48 | catch(e) { 49 | } 50 | try { 51 | setToggleButton(theme); 52 | } 53 | catch (e) { 54 | } 55 | } 56 | 57 | function getThemeFromStorage() { 58 | return window.localStorage.getItem(themeStorageKey); 59 | } 60 | 61 | function getThemeFromScheme() { 62 | try { 63 | if (window.matchMedia("(prefers-color-scheme: dark)").matches){ 64 | return 'dark'; 65 | } 66 | else { 67 | return 'light'; 68 | } 69 | } 70 | catch(e) { 71 | return null; 72 | } 73 | } 74 | 75 | function loadTheme() { 76 | var theme = getThemeFromStorage() || getThemeFromScheme() || 'light'; 77 | setTheme(theme); 78 | } 79 | 80 | document.addEventListener('readystatechange', (event) => { 81 | loadTheme() 82 | }); 83 | -------------------------------------------------------------------------------- /docsSrc/content/tips.js: -------------------------------------------------------------------------------- 1 | var currentTip = null; 2 | var currentTipElement = null; 3 | 4 | function hideTip(evt, name, unique) { 5 | var el = document.getElementById(name); 6 | el.style.display = "none"; 7 | currentTip = null; 8 | } 9 | 10 | function findPos(obj) { 11 | // no idea why, but it behaves differently in webbrowser component 12 | if (window.location.search == "?inapp") 13 | return [obj.offsetLeft + 10, obj.offsetTop + 30]; 14 | 15 | var curleft = 0; 16 | var curtop = obj.offsetHeight; 17 | while (obj) { 18 | curleft += obj.offsetLeft; 19 | curtop += obj.offsetTop; 20 | obj = obj.offsetParent; 21 | }; 22 | return [curleft, curtop]; 23 | } 24 | 25 | function hideUsingEsc(e) { 26 | if (!e) { e = event; } 27 | hideTip(e, currentTipElement, currentTip); 28 | } 29 | 30 | function showTip(evt, name, unique, owner) { 31 | document.onkeydown = hideUsingEsc; 32 | if (currentTip == unique) return; 33 | currentTip = unique; 34 | currentTipElement = name; 35 | 36 | var pos = findPos(owner ? owner : (evt.srcElement ? evt.srcElement : evt.target)); 37 | var posx = pos[0]; 38 | var posy = pos[1]; 39 | 40 | var el = document.getElementById(name); 41 | var parent = (document.documentElement == null) ? document.body : document.documentElement; 42 | el.style.position = "absolute"; 43 | el.style.left = posx + "px"; 44 | el.style.top = posy + "px"; 45 | el.style.display = "block"; 46 | } 47 | -------------------------------------------------------------------------------- /docsSrc/files/placeholder.md: -------------------------------------------------------------------------------- 1 | place images or other files here 2 | -------------------------------------------------------------------------------- /docsSrc/index.md: -------------------------------------------------------------------------------- 1 | # BinaryDefense.JsonWrapper 2 | 3 | --- 4 | 5 | ## What is BinaryDefense.JsonWrapper? 6 | 7 | BinaryDefense.JsonWrapper is a library that does this specific thing. 8 | 9 | ## Why use BinaryDefense.JsonWrapper? 10 | 11 | I created it because I had to solve an issue with this other thing. 12 | 13 | --- 14 | 15 |
16 |
17 |
18 |
19 |
Tutorials
20 |

Takes you by the hand through a series of steps to create your first thing.

21 |
22 | 25 |
26 |
27 |
28 |
29 |
30 |
How-To Guides
31 |

Guides you through the steps involved in addressing key problems and use-cases.

32 |
33 | 36 |
37 |
38 |
39 |
40 |
41 |
Explanations
42 |

Discusses key topics and concepts at a fairly high level and provide useful background information and explanation..

43 |
44 | 47 |
48 |
49 |
50 |
51 |
52 |
Api Reference
53 |

Contain technical reference for APIs.

54 |
55 | 58 |
59 |
60 |
61 | -------------------------------------------------------------------------------- /docsTool/CLI.fs: -------------------------------------------------------------------------------- 1 | namespace DocsTool 2 | 3 | module CLIArgs = 4 | open Argu 5 | open Fake.IO.Globbing.Operators 6 | 7 | type WatchArgs = 8 | | ProjectGlob of string 9 | | DocsSourceDirectory of string 10 | | GitHubRepoUrl of string 11 | | ProjectName of string 12 | | ReleaseVersion of string 13 | with 14 | interface IArgParserTemplate with 15 | member this.Usage = 16 | match this with 17 | | ProjectGlob _ -> "The glob for the dlls to generate API documentation." 18 | | DocsSourceDirectory _ -> "The docs source directory." 19 | | GitHubRepoUrl _ -> "The GitHub repository url." 20 | | ProjectName _ -> "The project name." 21 | | ReleaseVersion _ -> "The project's Release Version name." 22 | 23 | type BuildArgs = 24 | | SiteBaseUrl of string 25 | | ProjectGlob of string 26 | | DocsOutputDirectory of string 27 | | DocsSourceDirectory of string 28 | | GitHubRepoUrl of string 29 | | ProjectName of string 30 | | ReleaseVersion of string 31 | with 32 | interface IArgParserTemplate with 33 | member this.Usage = 34 | match this with 35 | | SiteBaseUrl _ -> "The public site's base url." 36 | | ProjectGlob _ -> "The glob for the dlls to generate API documentation" 37 | | DocsOutputDirectory _ -> "The docs output directory." 38 | | DocsSourceDirectory _ -> "The docs source directory." 39 | | GitHubRepoUrl _ -> "The GitHub repository url." 40 | | ProjectName _ -> "The project name." 41 | | ReleaseVersion _ -> "The project's Release Version name." 42 | 43 | type CLIArguments = 44 | | [] Watch of ParseResults 45 | | [] Build of ParseResults 46 | with 47 | interface IArgParserTemplate with 48 | member this.Usage = 49 | match this with 50 | | Watch _ -> "Builds the docs, serves the content, and watches for changes to the content." 51 | | Build _ -> "Builds the docs" 52 | -------------------------------------------------------------------------------- /docsTool/Prelude.fs: -------------------------------------------------------------------------------- 1 | namespace DocsTool 2 | 3 | module Uri = 4 | open System 5 | let simpleCombine (slug : string) (baseUri : Uri) = 6 | sprintf "%s/%s" (baseUri.AbsoluteUri.TrimEnd('/')) (slug.TrimStart('/')) 7 | 8 | let create (url : string) = 9 | match Uri.TryCreate(url, UriKind.Absolute) with 10 | | (true, v) -> v 11 | | _ -> failwithf "Bad url %s" url 12 | -------------------------------------------------------------------------------- /docsTool/Program.fs: -------------------------------------------------------------------------------- 1 | // Learn more about F# at http://fsharp.org 2 | 3 | 4 | open System 5 | open Fake.IO.FileSystemOperators 6 | open Fake.IO 7 | open Fake.Core 8 | 9 | let dispose (d : #IDisposable) = d.Dispose() 10 | type DisposableDirectory (directory : string) = 11 | do 12 | Trace.tracefn "Created disposable directory %s" directory 13 | static member Create() = 14 | let tempPath = IO.Path.Combine(IO.Path.GetTempPath(), Guid.NewGuid().ToString("n")) 15 | IO.Directory.CreateDirectory tempPath |> ignore 16 | 17 | new DisposableDirectory(tempPath) 18 | member x.Directory = directory 19 | member x.DirectoryInfo = IO.DirectoryInfo(directory) 20 | 21 | interface IDisposable with 22 | member x.Dispose() = 23 | Trace.tracefn "Deleting directory %s" directory 24 | IO.Directory.Delete(x.Directory,true) 25 | 26 | 27 | let refreshWebpageEvent = new Event() 28 | 29 | type Configuration = { 30 | SiteBaseUrl : Uri 31 | GitHubRepoUrl : Uri 32 | RepositoryRoot : IO.DirectoryInfo 33 | DocsOutputDirectory : IO.DirectoryInfo 34 | DocsSourceDirectory : IO.DirectoryInfo 35 | ProjectName : string 36 | ProjectFilesGlob : IGlobbingPattern 37 | ReleaseVersion : string 38 | } 39 | 40 | let docsApiDir docsDir = docsDir @@ "Api_Reference" 41 | 42 | type DisposableList = 43 | { 44 | disposables : IDisposable list 45 | } interface IDisposable with 46 | member x.Dispose () = 47 | x.disposables |> List.iter(fun s -> s.Dispose()) 48 | 49 | 50 | module ProjInfo = 51 | open System.IO 52 | 53 | type References = FileInfo [] 54 | type TargetPath = FileInfo 55 | 56 | type ProjInfo = { 57 | References : References 58 | TargetPath : TargetPath 59 | } 60 | 61 | open Dotnet.ProjInfo.Workspace 62 | open Dotnet.ProjInfo.Workspace.FCS 63 | let createFCS () = 64 | let checker = 65 | FCS_Checker.Create( 66 | projectCacheSize = 200, 67 | keepAllBackgroundResolutions = true, 68 | keepAssemblyContents = true) 69 | checker.ImplicitlyStartBackgroundWork <- true 70 | checker 71 | 72 | let createLoader () = 73 | let msbuildLocator = MSBuildLocator() 74 | let config = LoaderConfig.Default msbuildLocator 75 | let loader = Loader.Create(config) 76 | let netFwconfig = NetFWInfoConfig.Default msbuildLocator 77 | let netFwInfo = NetFWInfo.Create(netFwconfig) 78 | 79 | loader, netFwInfo 80 | 81 | let [] RefPrefix = "-r:" 82 | 83 | let findTargetPath targetPath = 84 | if File.exists targetPath then 85 | FileInfo targetPath 86 | else 87 | //HACK: Need to get dotnet-proj-info to handle configurations when extracting data 88 | let debugFolder = sprintf "%cDebug%c" Path.DirectorySeparatorChar Path.DirectorySeparatorChar 89 | let releaseFolder = sprintf "%cRelease%c" Path.DirectorySeparatorChar Path.DirectorySeparatorChar 90 | let debugFolderAlt = sprintf "%cDebug%c" Path.DirectorySeparatorChar Path.AltDirectorySeparatorChar 91 | let releaseFolderAlt = sprintf "%cRelease%c" Path.DirectorySeparatorChar Path.AltDirectorySeparatorChar 92 | 93 | let releasePath = targetPath.Replace(debugFolder, releaseFolder).Replace(debugFolderAlt, releaseFolderAlt) 94 | if releasePath |> File.exists then 95 | releasePath |> FileInfo 96 | else 97 | failwithf "Couldn't find a dll to generate documentationfrom %s or %s" targetPath releasePath 98 | 99 | let findReferences projPath : ProjInfo= 100 | let fcs = createFCS () 101 | let loader, netFwInfo = createLoader () 102 | loader.LoadProjects [ projPath ] 103 | let fcsBinder = FCSBinder(netFwInfo, loader, fcs) 104 | match fcsBinder.GetProjectOptions(projPath) with 105 | | Ok options -> 106 | // printfn "OtherOptions -> %A" options 107 | let references = 108 | options.OtherOptions 109 | |> Array.filter(fun s -> 110 | s.StartsWith(RefPrefix) 111 | ) 112 | |> Array.map(fun s -> 113 | // removes "-r:" from beginning of reference path 114 | s.Remove(0,RefPrefix.Length) 115 | |> FileInfo 116 | ) 117 | let dpwPo = 118 | match options.ExtraProjectInfo with 119 | | Some (:? ProjectOptions as dpwPo) -> dpwPo 120 | | x -> failwithf "invalid project info %A" x 121 | let targetPath = findTargetPath dpwPo.ExtraProjectInfo.TargetPath 122 | { References = references ; TargetPath = targetPath} 123 | 124 | | Error e -> 125 | failwithf "Couldn't read project %s - %A" projPath e 126 | 127 | 128 | module GenerateDocs = 129 | open DocsTool 130 | open Fake.Core 131 | open Fake.IO.Globbing.Operators 132 | open Fake.IO 133 | open Fable.React 134 | open Fable.React.Helpers 135 | open FSharp.Literate 136 | open System.IO 137 | open FSharp.MetadataFormat 138 | 139 | 140 | type GeneratedDoc = { 141 | SourcePath : FileInfo option 142 | OutputPath : FileInfo 143 | Content : ReactElement list 144 | Title : string 145 | } 146 | 147 | 148 | let docsFileGlob docsSrcDir = 149 | !! (docsSrcDir @@ "**/*.fsx") 150 | ++ (docsSrcDir @@ "**/*.md") 151 | 152 | let render html = 153 | fragment [] [ 154 | RawText "" 155 | RawText "\n" 156 | html ] 157 | |> Fable.ReactServer.renderToString 158 | 159 | let renderWithMasterTemplate masterCfg navBar titletext bodytext pageSource = 160 | Master.masterTemplate masterCfg navBar titletext bodytext pageSource 161 | |> render 162 | 163 | let renderWithMasterAndWrite masterCfg (outPath : FileInfo) navBar titletext bodytext pageSource = 164 | let contents = renderWithMasterTemplate masterCfg navBar titletext bodytext pageSource 165 | IO.Directory.CreateDirectory(outPath.DirectoryName) |> ignore 166 | 167 | IO.File.WriteAllText(outPath.FullName, contents) 168 | Fake.Core.Trace.tracefn "Rendered to %s" outPath.FullName 169 | 170 | let generateNav (cfg : Configuration) (generatedDocs : GeneratedDoc list) = 171 | let docsDir = cfg.DocsOutputDirectory.FullName 172 | let pages = 173 | generatedDocs 174 | |> List.map(fun gd -> gd.OutputPath) 175 | |> List.filter(fun f -> f.FullName.StartsWith(docsDir "content") |> not) 176 | |> List.filter(fun f -> f.FullName.StartsWith(docsDir "files") |> not) 177 | |> List.filter(fun f -> f.FullName.StartsWith(docsDir "index.html") |> not) 178 | 179 | let topLevelNavs : Nav.TopLevelNav = { 180 | DocsRoot = IO.DirectoryInfo docsDir 181 | DocsPages = pages 182 | } 183 | 184 | let navCfg : Nav.NavConfig = { 185 | SiteBaseUrl = cfg.SiteBaseUrl 186 | GitHubRepoUrl = cfg.GitHubRepoUrl 187 | ProjectName = cfg.ProjectName 188 | TopLevelNav = topLevelNavs 189 | } 190 | 191 | Nav.generateNav navCfg 192 | 193 | let renderGeneratedDocs isWatchMode (cfg : Configuration) (generatedDocs : GeneratedDoc list) = 194 | let nav = generateNav cfg generatedDocs 195 | let masterCfg : Master.MasterTemplateConfig = { 196 | SiteBaseUrl = cfg.SiteBaseUrl 197 | GitHubRepoUrl = cfg.GitHubRepoUrl 198 | ProjectName = cfg.ProjectName 199 | ReleaseVersion = cfg.ReleaseVersion 200 | ReleaseDate = DateTimeOffset.Now 201 | RepositoryRoot = cfg.RepositoryRoot 202 | IsWatchMode = isWatchMode 203 | } 204 | generatedDocs 205 | |> Seq.iter(fun gd -> 206 | let pageSource = 207 | gd.SourcePath 208 | |> Option.map(fun sp -> 209 | sp.FullName.Replace(cfg.RepositoryRoot.FullName, "").Replace("\\", "/") 210 | ) 211 | renderWithMasterAndWrite masterCfg gd.OutputPath nav gd.Title gd.Content pageSource 212 | ) 213 | 214 | 215 | let copyAssets (cfg : Configuration) = 216 | Shell.copyDir (cfg.DocsOutputDirectory.FullName "content") ( cfg.DocsSourceDirectory.FullName "content") (fun _ -> true) 217 | Shell.copyDir (cfg.DocsOutputDirectory.FullName "files") ( cfg.DocsSourceDirectory.FullName "files") (fun _ -> true) 218 | 219 | 220 | let regexReplace (cfg : Configuration) source = 221 | let replacements = 222 | [ 223 | "{{siteBaseUrl}}", (cfg.SiteBaseUrl.ToString().TrimEnd('/')) 224 | ] 225 | (source, replacements) 226 | ||> List.fold(fun state (pattern, replacement) -> 227 | Text.RegularExpressions.Regex.Replace(state, pattern, replacement) 228 | ) 229 | 230 | let stringContainsInsenstive (filter : string) (textToSearch : string) = 231 | textToSearch.IndexOf(filter, StringComparison.CurrentCultureIgnoreCase) >= 0 232 | 233 | let generateDocs (libDirs : ProjInfo.References) (docSourcePaths : IGlobbingPattern) (cfg : Configuration) = 234 | let parse (fileName : string) source = 235 | let doc = 236 | let rref = 237 | libDirs 238 | |> Array.map(fun fi -> fi.FullName) 239 | |> Array.distinct 240 | |> Array.map(sprintf "-r:%s") 241 | 242 | let iref = 243 | libDirs 244 | |> Array.map(fun fi -> fi.DirectoryName) 245 | |> Array.distinct 246 | |> Array.map(sprintf "-I:\"%s\"") 247 | 248 | let fsiArgs = 249 | [| 250 | yield "--noframework" // error FS1222: When mscorlib.dll or FSharp.Core.dll is explicitly referenced the --noframework option must also be passed 251 | yield! iref 252 | |] 253 | let compilerOptions = 254 | [| 255 | yield "--targetprofile:netstandard" 256 | yield "-r:System.Net.WebClient" // FSharp.Formatting on Windows requires this to render fsharp sections in markdown for some reason 257 | yield! 258 | rref 259 | |> Seq.filter(stringContainsInsenstive "fsharp.core.dll" >> not) 260 | |> Seq.filter(stringContainsInsenstive "NETStandard.Library.Ref" >> not) // --targetprofile:netstandard will find the "BCL" libraries 261 | |] 262 | let fsiEvaluator = FSharp.Literate.FsiEvaluator(fsiArgs) 263 | match Path.GetExtension fileName with 264 | | ".fsx" -> 265 | Literate.ParseScriptString( 266 | source, 267 | path = fileName, 268 | compilerOptions = (compilerOptions |> String.concat " "), 269 | fsiEvaluator = fsiEvaluator) 270 | | ".md" -> 271 | let source = regexReplace cfg source 272 | Literate.ParseMarkdownString( 273 | source, 274 | path = fileName, 275 | compilerOptions = (compilerOptions |> String.concat " "), 276 | fsiEvaluator = fsiEvaluator 277 | ) 278 | | others -> failwithf "FSharp.Literal does not support %s file extensions" others 279 | FSharp.Literate.Literate.FormatLiterateNodes(doc, OutputKind.Html, "", true, true) 280 | 281 | let format (doc: LiterateDocument) = 282 | if not <| Seq.isEmpty doc.Errors 283 | then 284 | failwithf "error while formatting file %s. Errors are:\n%A" doc.SourceFile doc.Errors 285 | else 286 | Formatting.format doc.MarkdownDocument true OutputKind.Html 287 | + doc.FormattedTips 288 | 289 | 290 | 291 | docSourcePaths 292 | |> Array.ofSeq 293 | |> Seq.map(fun filePath -> 294 | 295 | Fake.Core.Trace.tracefn "Rendering %s" filePath 296 | let file = IO.File.ReadAllText filePath 297 | let outPath = 298 | let changeExtension ext path = IO.Path.ChangeExtension(path,ext) 299 | filePath.Replace(cfg.DocsSourceDirectory.FullName, cfg.DocsOutputDirectory.FullName) 300 | |> changeExtension ".html" 301 | |> FileInfo 302 | let fs = 303 | file 304 | |> parse filePath 305 | |> format 306 | let contents = 307 | [div [] [ 308 | fs 309 | |> RawText 310 | ]] 311 | 312 | { 313 | SourcePath = FileInfo filePath |> Some 314 | OutputPath = outPath 315 | Content = contents 316 | Title = sprintf "%s-%s" outPath.Name cfg.ProjectName 317 | } 318 | ) 319 | |> Seq.toList 320 | 321 | 322 | let generateAPI (projInfos : ProjInfo.ProjInfo array) (cfg : Configuration) = 323 | let generate (projInfo : ProjInfo.ProjInfo) = 324 | Trace.tracefn "Generating API Docs for %s" projInfo.TargetPath.FullName 325 | let references = 326 | projInfo.References 327 | |> Array.toList 328 | |> List.map(fun fi -> fi.DirectoryName) 329 | |> List.distinct 330 | let libDirs = references 331 | let targetApiDir = docsApiDir cfg.DocsOutputDirectory.FullName @@ IO.Path.GetFileNameWithoutExtension(projInfo.TargetPath.Name) 332 | let generatorOutput = 333 | MetadataFormat.Generate( 334 | projInfo.TargetPath.FullName, 335 | libDirs = libDirs, 336 | sourceFolder = cfg.RepositoryRoot.FullName, 337 | sourceRepo = (cfg.GitHubRepoUrl |> Uri.simpleCombine "tree/master" |> string), 338 | markDownComments = false 339 | ) 340 | 341 | let fi = FileInfo <| targetApiDir @@ (sprintf "%s.html" generatorOutput.AssemblyGroup.Name) 342 | let indexDoc = { 343 | SourcePath = None 344 | OutputPath = fi 345 | Content = [Namespaces.generateNamespaceDocs generatorOutput.AssemblyGroup generatorOutput.Properties] 346 | Title = sprintf "%s-%s" fi.Name cfg.ProjectName 347 | } 348 | 349 | let moduleDocs = 350 | generatorOutput.ModuleInfos 351 | |> List.map (fun m -> 352 | let fi = FileInfo <| targetApiDir @@ (sprintf "%s.html" m.Module.UrlName) 353 | let content = Modules.generateModuleDocs m generatorOutput.Properties 354 | { 355 | SourcePath = None 356 | OutputPath = fi 357 | Content = content 358 | Title = sprintf "%s-%s" m.Module.Name cfg.ProjectName 359 | } 360 | ) 361 | let typeDocs = 362 | generatorOutput.TypesInfos 363 | |> List.map (fun m -> 364 | let fi = FileInfo <| targetApiDir @@ (sprintf "%s.html" m.Type.UrlName) 365 | let content = Types.generateTypeDocs m generatorOutput.Properties 366 | { 367 | SourcePath = None 368 | OutputPath = fi 369 | Content = content 370 | Title = sprintf "%s-%s" m.Type.Name cfg.ProjectName 371 | } 372 | ) 373 | [ indexDoc ] @ moduleDocs @ typeDocs 374 | projInfos 375 | |> Seq.collect(generate) 376 | |> Seq.toList 377 | 378 | let buildDocs (projInfos : ProjInfo.ProjInfo array) (cfg : Configuration) = 379 | let refs = 380 | [| 381 | yield! projInfos |> Array.collect (fun p -> p.References) |> Array.distinct 382 | yield! projInfos |> Array.map(fun p -> p.TargetPath) 383 | |] 384 | copyAssets cfg 385 | let generateDocs = 386 | async { 387 | try 388 | return generateDocs refs (docsFileGlob cfg.DocsSourceDirectory.FullName) cfg 389 | with e -> 390 | eprintfn "generateDocs failure %A" e 391 | return raise e 392 | } 393 | let generateAPI = 394 | async { 395 | return (generateAPI projInfos cfg) 396 | } 397 | Async.Parallel [generateDocs; generateAPI] 398 | |> Async.RunSynchronously 399 | |> Array.toList 400 | |> List.collect id 401 | 402 | let renderDocs (cfg : Configuration) = 403 | let projInfos = cfg.ProjectFilesGlob |> Seq.map(ProjInfo.findReferences) |> Seq.toArray 404 | buildDocs projInfos cfg 405 | |> renderGeneratedDocs false cfg 406 | 407 | let watchDocs (cfg : Configuration) = 408 | let projInfos = cfg.ProjectFilesGlob |> Seq.map(ProjInfo.findReferences) |> Seq.toArray 409 | let initialDocs = buildDocs projInfos cfg 410 | let renderGeneratedDocs = renderGeneratedDocs true 411 | initialDocs |> renderGeneratedDocs cfg 412 | 413 | let refs = 414 | [| 415 | yield! projInfos |> Array.collect (fun p -> p.References) |> Array.distinct 416 | yield! projInfos |> Array.map(fun p -> p.TargetPath) 417 | |] 418 | 419 | let d1 = 420 | docsFileGlob cfg.DocsSourceDirectory.FullName 421 | |> ChangeWatcher.run (fun changes -> 422 | printfn "changes %A" changes 423 | changes 424 | |> Seq.iter (fun m -> 425 | printfn "watching %s" m.FullPath 426 | let generated = generateDocs refs (!! m.FullPath) cfg 427 | initialDocs 428 | |> List.filter(fun x -> generated |> List.exists(fun y -> y.OutputPath = x.OutputPath) |> not ) 429 | |> List.append generated 430 | |> List.distinctBy(fun gd -> gd.OutputPath.FullName) 431 | |> renderGeneratedDocs cfg 432 | ) 433 | refreshWebpageEvent.Trigger "m.FullPath" 434 | ) 435 | let d2 = 436 | !! (cfg.DocsSourceDirectory.FullName "content" "**/*") 437 | ++ (cfg.DocsSourceDirectory.FullName "files" "**/*") 438 | |> ChangeWatcher.run(fun changes -> 439 | printfn "changes %A" changes 440 | copyAssets cfg 441 | refreshWebpageEvent.Trigger "Assets" 442 | ) 443 | 444 | 445 | let d3 = 446 | projInfos 447 | |> Seq.map(fun p -> p.TargetPath.FullName) 448 | |> Seq.fold ((++)) (!! "") 449 | 450 | |> ChangeWatcher.run(fun changes -> 451 | changes 452 | |> Seq.iter(fun c -> Trace.logf "Regenerating API docs due to %s" c.FullPath ) 453 | let generated = generateAPI projInfos cfg 454 | initialDocs 455 | |> List.filter(fun x -> generated |> List.exists(fun y -> y.OutputPath = x.OutputPath) |> not ) 456 | |> List.append generated 457 | |> List.distinctBy(fun gd -> gd.OutputPath.FullName) 458 | |> renderGeneratedDocs cfg 459 | refreshWebpageEvent.Trigger "Api" 460 | ) 461 | { disposables = [d1; d2; d3] } :> IDisposable 462 | 463 | 464 | module WebServer = 465 | open Microsoft.AspNetCore.Hosting 466 | open Microsoft.AspNetCore.Builder 467 | open Microsoft.Extensions.FileProviders 468 | open Microsoft.AspNetCore.Http 469 | open System.Net.WebSockets 470 | open System.Diagnostics 471 | open System.Runtime.InteropServices 472 | 473 | let hostname = "localhost" 474 | let port = 5000 475 | 476 | /// Helper to determine if port is in use 477 | let waitForPortInUse (hostname : string) port = 478 | let mutable portInUse = false 479 | while not portInUse do 480 | Async.Sleep(10) |> Async.RunSynchronously 481 | use client = new Net.Sockets.TcpClient() 482 | try 483 | client.Connect(hostname,port) 484 | portInUse <- client.Connected 485 | client.Close() 486 | with e -> 487 | client.Close() 488 | 489 | /// Async version of IApplicationBuilder.Use 490 | let useAsync (middlware : HttpContext -> (unit -> Async) -> Async) (app:IApplicationBuilder) = 491 | app.Use(fun env next -> 492 | middlware env (next.Invoke >> Async.AwaitTask) 493 | |> Async.StartAsTask 494 | :> System.Threading.Tasks.Task 495 | ) 496 | 497 | let createWebsocketForLiveReload (httpContext : HttpContext) (next : unit -> Async) = async { 498 | if httpContext.WebSockets.IsWebSocketRequest then 499 | let! websocket = httpContext.WebSockets.AcceptWebSocketAsync() |> Async.AwaitTask 500 | use d = 501 | refreshWebpageEvent.Publish 502 | |> Observable.subscribe (fun m -> 503 | let segment = ArraySegment(m |> Text.Encoding.UTF8.GetBytes) 504 | websocket.SendAsync(segment, WebSocketMessageType.Text, true, httpContext.RequestAborted) 505 | |> Async.AwaitTask 506 | |> Async.Start 507 | 508 | ) 509 | while websocket.State <> WebSocketState.Closed do 510 | do! Async.Sleep(1000) 511 | else 512 | do! next () 513 | } 514 | 515 | let configureWebsocket (appBuilder : IApplicationBuilder) = 516 | appBuilder.UseWebSockets() 517 | |> useAsync (createWebsocketForLiveReload) 518 | |> ignore 519 | 520 | let startWebserver docsDir (url : string) = 521 | WebHostBuilder() 522 | .UseKestrel() 523 | .UseUrls(url) 524 | .Configure(fun app -> 525 | let opts = 526 | StaticFileOptions( 527 | FileProvider = new PhysicalFileProvider(docsDir) 528 | ) 529 | app.UseStaticFiles(opts) |> ignore 530 | configureWebsocket app 531 | ) 532 | .Build() 533 | .Run() 534 | 535 | let openBrowser url = 536 | let waitForExit (proc : Process) = 537 | proc.WaitForExit() 538 | if proc.ExitCode <> 0 then eprintf "opening browser failed, open your browser and navigate to url to see the docs site." 539 | try 540 | let psi = ProcessStartInfo(FileName = url, UseShellExecute = true) 541 | Process.Start psi 542 | |> waitForExit 543 | with e -> 544 | //https://github.com/dotnet/corefx/issues/10361 545 | if RuntimeInformation.IsOSPlatform(OSPlatform.Windows) then 546 | let url = url.Replace("&", "&^") 547 | let psi = ProcessStartInfo("cmd", (sprintf "/c %s" url), CreateNoWindow=true) 548 | Process.Start psi 549 | |> waitForExit 550 | elif RuntimeInformation.IsOSPlatform(OSPlatform.Linux) then 551 | Process.Start("xdg-open", url) 552 | |> waitForExit 553 | elif RuntimeInformation.IsOSPlatform(OSPlatform.OSX) then 554 | Process.Start("open", url) 555 | |> waitForExit 556 | else 557 | failwithf "failed to open browser on current OS" 558 | 559 | let serveDocs docsDir = 560 | async { 561 | waitForPortInUse hostname port 562 | sprintf "http://%s:%d/index.html" hostname port |> openBrowser 563 | } |> Async.Start 564 | startWebserver docsDir (sprintf "http://%s:%d" hostname port) 565 | 566 | 567 | open FSharp.Formatting.Common 568 | open System.Diagnostics 569 | 570 | let setupFsharpFormattingLogging () = 571 | let setupListener listener = 572 | [ 573 | FSharp.Formatting.Common.Log.source 574 | Yaaf.FSharp.Scripting.Log.source 575 | ] 576 | |> Seq.iter (fun source -> 577 | source.Switch.Level <- System.Diagnostics.SourceLevels.All 578 | Log.AddListener listener source) 579 | let noTraceOptions = TraceOptions.None 580 | Log.ConsoleListener() 581 | |> Log.SetupListener noTraceOptions System.Diagnostics.SourceLevels.Verbose 582 | |> setupListener 583 | 584 | open Argu 585 | open Fake.IO.Globbing.Operators 586 | open DocsTool.CLIArgs 587 | [] 588 | let main argv = 589 | try 590 | use tempDocsOutDir = DisposableDirectory.Create() 591 | use __ = AppDomain.CurrentDomain.ProcessExit.Subscribe(fun _ -> 592 | dispose tempDocsOutDir 593 | ) 594 | use __ = Console.CancelKeyPress.Subscribe(fun _ -> 595 | dispose tempDocsOutDir 596 | ) 597 | let defaultConfig = { 598 | SiteBaseUrl = Uri(sprintf "http://%s:%d/" WebServer.hostname WebServer.port ) 599 | GitHubRepoUrl = Uri "https://github.com" 600 | RepositoryRoot = IO.DirectoryInfo (__SOURCE_DIRECTORY__ @@ "..") 601 | DocsOutputDirectory = tempDocsOutDir.DirectoryInfo 602 | DocsSourceDirectory = IO.DirectoryInfo "docsSrc" 603 | ProjectName = "" 604 | ProjectFilesGlob = !! "" 605 | ReleaseVersion = "0.1.0" 606 | } 607 | 608 | let errorHandler = ProcessExiter(colorizer = function ErrorCode.HelpText -> None | _ -> Some ConsoleColor.Red) 609 | let programName = 610 | let name = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name 611 | if Fake.Core.Environment.isWindows then 612 | sprintf "%s.exe" name 613 | else 614 | name 615 | 616 | let parser = ArgumentParser.Create(programName = programName, errorHandler = errorHandler) 617 | let parsedArgs = parser.Parse argv 618 | match parsedArgs.GetSubCommand() with 619 | | Build args -> 620 | let config = 621 | (defaultConfig, args.GetAllResults()) 622 | ||> List.fold(fun state next -> 623 | match next with 624 | | BuildArgs.SiteBaseUrl url -> { state with SiteBaseUrl = Uri url } 625 | | BuildArgs.ProjectGlob glob -> { state with ProjectFilesGlob = !! glob} 626 | | BuildArgs.DocsOutputDirectory outdir -> { state with DocsOutputDirectory = IO.DirectoryInfo outdir} 627 | | BuildArgs.DocsSourceDirectory srcdir -> { state with DocsSourceDirectory = IO.DirectoryInfo srcdir} 628 | | BuildArgs.GitHubRepoUrl url -> { state with GitHubRepoUrl = Uri url} 629 | | BuildArgs.ProjectName repo -> { state with ProjectName = repo} 630 | | BuildArgs.ReleaseVersion version -> { state with ReleaseVersion = version} 631 | ) 632 | GenerateDocs.renderDocs config 633 | | Watch args -> 634 | let config = 635 | (defaultConfig, args.GetAllResults()) 636 | ||> List.fold(fun state next -> 637 | match next with 638 | | WatchArgs.ProjectGlob glob -> {state with ProjectFilesGlob = !! glob} 639 | | WatchArgs.DocsSourceDirectory srcdir -> { state with DocsSourceDirectory = IO.DirectoryInfo srcdir} 640 | | WatchArgs.GitHubRepoUrl url -> { state with GitHubRepoUrl = Uri url} 641 | | WatchArgs.ProjectName repo -> { state with ProjectName = repo} 642 | | WatchArgs.ReleaseVersion version -> { state with ReleaseVersion = version} 643 | ) 644 | use ds = GenerateDocs.watchDocs config 645 | WebServer.serveDocs config.DocsOutputDirectory.FullName 646 | 0 647 | with e -> 648 | eprintfn "Fatal error: %A" e 649 | 1 650 | -------------------------------------------------------------------------------- /docsTool/README.md: -------------------------------------------------------------------------------- 1 | # Docs Tool 2 | 3 | ## Example 4 | [MiniScaffold docs example](https://www.jimmybyrd.me/miniscaffold-docs-test/) 5 | 6 | ## Docs High Level Design 7 | 8 | This template is based heavily on [What nobody tells you about documentation](https://www.divio.com/blog/documentation/). In `docsSrc` folder you'll see a similar structure to what is described below: 9 | 10 | - **Tutorials** 11 | - is learning-oriented 12 | - allows the newcomer to get started 13 | - is a lesson 14 | - Analogy: teaching a small child how to cook 15 | - **How-To Guides** 16 | - is goal-oriented 17 | - shows how to solve a specific problem 18 | - is a series of steps 19 | - Analogy: a recipe in a cookery book 20 | - **Explanation** 21 | - is understanding-oriented 22 | - explains 23 | - provides background and context 24 | - Analogy: an article on culinary social history 25 | - **Reference** 26 | - is information-oriented 27 | - describes the machinery 28 | - is accurate and complete 29 | - Analogy: a reference encyclopedia article 30 | 31 | 32 | The folders in `docsSrc` are: 33 | 34 | - `content` - custom css, javascript, and similar go here. 35 | - `Explanations` - A content section as defined above. 36 | - `files` - extra files like screenshots, images, videos. 37 | - `How_Tos` - A content section as defined above. 38 | - `Tutorials` - A content section as defined above. 39 | - `index.md` - The entry page to your documentation 40 | 41 | The navbar is generated by the folders in docsSrc, excluding `content` and `files` folders. Looking at the [example](https://www.jimmybyrd.me/miniscaffold-docs-test/) we can the navbar containing: 42 | 43 | - `Api References` 44 | - `Explanations` 45 | - `How Tos` 46 | - `Tutorials` 47 | 48 | The odd one not generated from the convention of your folders in docsSrc is Api References. This is generated by the [XML Doc Comments](https://docs.microsoft.com/en-us/dotnet/csharp/codedoc) in your libraries under the `src` folder. 49 | 50 | 51 | ## Running docs tool 52 | 53 | ``` 54 | USAGE: docsTool [--help] [ []] 55 | 56 | SUBCOMMANDS: 57 | 58 | watch Builds the docs, serves the content, and watches for changes to the content. 59 | build Builds the docs 60 | 61 | Use 'docsTool --help' for additional information. 62 | ``` 63 | 64 | ### build 65 | 66 | Builds the docs 67 | 68 | ``` 69 | USAGE: docsTool build [--help] [--sitebaseurl ] [--projectglob ] [--docsoutputdirectory ] [--docssourcedirectory ] [--githubrepourl ] [--projectname ] 70 | [--releaseversion ] 71 | 72 | OPTIONS: 73 | 74 | --sitebaseurl 75 | The public site's base url. 76 | --projectglob 77 | The glob for the dlls to generate API documentation 78 | --docsoutputdirectory 79 | The docs output directory. 80 | --docssourcedirectory 81 | The docs source directory. 82 | --githubrepourl 83 | The GitHub repository url. 84 | --projectname 85 | The project name. 86 | --releaseversion 87 | The project's Release Version name. 88 | --help display this list of options. 89 | 90 | ``` 91 | 92 | 93 | ### watch 94 | 95 | Builds the docs, serves the content, and watches for changes to the content. 96 | 97 | ``` 98 | 99 | USAGE: docsTool watch [--help] [--projectglob ] [--docsoutputdirectory ] [--docssourcedirectory ] [--githubrepourl ] [--projectname ] [--releaseversion ] 100 | 101 | OPTIONS: 102 | 103 | --projectglob 104 | The glob for the dlls to generate API documentation. 105 | --docsoutputdirectory 106 | The docs output directory. 107 | --docssourcedirectory 108 | The docs source directory. 109 | --githubrepourl 110 | The GitHub repository url. 111 | --projectname 112 | The project name. 113 | --releaseversion 114 | The project's Release Version name. 115 | --help display this list of options. 116 | ``` 117 | 118 | -------------------------------------------------------------------------------- /docsTool/docsTool.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docsTool/paket.references: -------------------------------------------------------------------------------- 1 | group Docs 2 | Argu 3 | FSharp.Core 4 | Fake.IO.FileSystem 5 | Fake.DotNet.Cli 6 | FSharp.Formatting 7 | FSharp.Literate 8 | Fable.React 9 | Dotnet.ProjInfo.Workspace.FCS 10 | -------------------------------------------------------------------------------- /docsTool/templates/helpers.fs: -------------------------------------------------------------------------------- 1 | module Helpers 2 | open System 3 | open Fable.React 4 | open Fable.React.Props 5 | open FSharp.MetadataFormat 6 | 7 | 8 | let createAnchorIcon name = 9 | let normalized = name 10 | let href = sprintf "#%s" normalized 11 | a [Href href; Id normalized] [ 12 | str "#" 13 | ] 14 | 15 | let createAnchor fullName name = 16 | let fullNameNormalize = fullName 17 | a [ 18 | Name fullNameNormalize 19 | Href (sprintf "#%s" fullNameNormalize) 20 | Class "anchor" 21 | ] [ 22 | str name 23 | ] 24 | 25 | let renderNamespace (ns: Namespace) = [ 26 | h3 [] [ str "Namespace" ] 27 | str ns.Name 28 | ] 29 | 30 | let inline isObsolete< ^t when ^t : (member IsObsolete: bool)> t = 31 | (^t : (member IsObsolete: bool) (t)) 32 | 33 | let inline obsoleteMessage< ^t when ^t : (member ObsoleteMessage: string)> t = 34 | (^t : (member ObsoleteMessage:string) (t)) 35 | 36 | let inline renderObsoleteMessage item = 37 | if isObsolete item 38 | then 39 | let text = match obsoleteMessage item with | "" | null -> "This member is obsolete" | s -> s 40 | [ 41 | div [Class "alert alert-warning"] [ 42 | strong [] [ str "OBSOLETE: "] 43 | str text 44 | ] 45 | ] 46 | else 47 | [] 48 | -------------------------------------------------------------------------------- /docsTool/templates/master.fs: -------------------------------------------------------------------------------- 1 | module Master 2 | 3 | open System 4 | open Fable.React 5 | open Fable.React.Props 6 | open DocsTool 7 | 8 | type MasterTemplateConfig = { 9 | SiteBaseUrl : Uri 10 | GitHubRepoUrl : Uri 11 | ProjectName : string 12 | ReleaseVersion : string 13 | ReleaseDate : DateTimeOffset 14 | RepositoryRoot: IO.DirectoryInfo 15 | IsWatchMode : bool 16 | } 17 | 18 | type FAIcon = 19 | | Solid of name: string 20 | | Brand of name: string 21 | 22 | let footerLink uri image linkText = 23 | let faClass, img = 24 | match image with 25 | | Solid name -> "fas", name 26 | | Brand name -> "fab", name 27 | a [Href uri; Class "text-white"] [ 28 | i [Class (sprintf "%s fa-%s fa-fw mr-2" faClass img)] [] 29 | str linkText 30 | ] 31 | 32 | let repoFileLink repoUrl filePathFromRepoRoot = 33 | let link = repoUrl |> Uri.simpleCombine (sprintf "blob/master/%s" filePathFromRepoRoot) 34 | footerLink link 35 | 36 | let linkColumn headerTitle items = 37 | div [Class "col-12 col-md-4 mb-4 mb-md-0"] [ 38 | div [Class "text-light"] [ 39 | h2 [Class "h5"] [ str headerTitle ] 40 | ul [Class "list-group list-group-flush"] 41 | (items |> List.choose (function | [] -> None 42 | | items -> Some(li [Class "list-group-item bg-dark ml-0 pl-0"] items))) 43 | ] 44 | ] 45 | 46 | let renderFooter (cfg : MasterTemplateConfig) (pageSource : string option) = 47 | let hasFile relPath = 48 | match cfg.RepositoryRoot.GetFiles(relPath) with 49 | | [||] -> false 50 | | [|file|] -> true 51 | | files -> false 52 | 53 | let repoFileLink relPath image title = 54 | if hasFile relPath 55 | then [ repoFileLink cfg.GitHubRepoUrl relPath image title ] 56 | else [] 57 | 58 | footer [Class "footer font-small m-0 py-4 bg-dark"] [ 59 | div [Class "container"] [ 60 | div [Class "row"] [ 61 | linkColumn "Project Resources" [ 62 | repoFileLink "README.md" (Solid "book-reader") "README" 63 | repoFileLink "CHANGELOG.md" (Solid "sticky-note") "Release Notes / Changelog" 64 | repoFileLink "LICENSE.md" (Solid "id-card") "License" 65 | repoFileLink "CONTRIBUTING.md" (Solid "directions") "Contributing" 66 | repoFileLink "CODE_OF_CONDUCT.md" (Solid "users") "Code of Conduct" 67 | ] 68 | linkColumn "Other Links" [ 69 | [footerLink "https://docs.microsoft.com/en-us/dotnet/fsharp/" (Brand "microsoft") "F# Documentation"] 70 | [footerLink "https://fsharp.org/guides/slack/" (Brand "slack") "F# Slack"] 71 | [a [Href "http://foundation.fsharp.org/"; Class "text-white"] [ 72 | img [Class "fsharp-footer-logo mr-2"; Src "https://fsharp.org/img/logo/fsharp.svg"; Alt "FSharp Logo"] 73 | str "F# Software Foundation" 74 | ]] 75 | ] 76 | linkColumn "Metadata" [ 77 | [str "Generated for version " 78 | a [Class "text-white"; Href (cfg.GitHubRepoUrl |> Uri.simpleCombine (sprintf "releases/tag/%s" cfg.ReleaseVersion))] [str cfg.ReleaseVersion] 79 | str (sprintf " on %s" (cfg.ReleaseDate.ToString("yyyy/MM/dd")))] 80 | match pageSource with 81 | | Some p -> 82 | let page = cfg.GitHubRepoUrl |> Uri.simpleCombine "edit/master" |> Uri |> Uri.simpleCombine p 83 | [ str "Found an issue? " 84 | a [Class "text-white"; Href (page |> string)] [ str "Edit this page." ] ] 85 | | None -> 86 | () 87 | ] 88 | ] 89 | div [Class "row"] [ 90 | div [Class "col text-center"] [ 91 | small [Class "text-light"] [ 92 | i [Class "fas fa-copyright mr-1"] [] 93 | str (sprintf "%s BinaryDefense, All rights reserved" (DateTimeOffset.UtcNow.ToString("yyyy"))) 94 | ] 95 | ] 96 | ] 97 | ] 98 | ] 99 | 100 | let masterTemplate (cfg : MasterTemplateConfig) navBar titletext bodyText pageSource = 101 | html [Lang "en"] [ 102 | head [] [ 103 | title [] [ str (sprintf "%s docs / %s" cfg.ProjectName titletext) ] 104 | meta [Name "viewport"; HTMLAttr.Content "width=device-width, initial-scale=1" ] 105 | link [ 106 | Href (cfg.SiteBaseUrl |> Uri.simpleCombine (sprintf "/content/toggle-bootstrap.min.css?version=%i" cfg.ReleaseDate.Ticks) ) 107 | Type "text/css" 108 | Rel "stylesheet" 109 | ] 110 | link [ 111 | Href (cfg.SiteBaseUrl |> Uri.simpleCombine (sprintf "/content/toggle-bootstrap-dark.min.css?version=%i" cfg.ReleaseDate.Ticks) ) 112 | Type "text/css" 113 | Rel "stylesheet" 114 | ] 115 | link [ 116 | Href "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/all.min.css" 117 | Rel "stylesheet" 118 | Integrity "sha384-KA6wR/X5RY4zFAHpv/CnoG2UW1uogYfdnP67Uv7eULvTveboZJg0qUpmJZb5VqzN" 119 | CrossOrigin "anonymous" 120 | ] 121 | link [ 122 | Href (cfg.SiteBaseUrl |> Uri.simpleCombine (sprintf "/content/style.css?version=%i" cfg.ReleaseDate.Ticks) ) 123 | Type "text/css" 124 | Rel "stylesheet" 125 | ] 126 | 127 | ] 128 | body [] [ 129 | yield navBar 130 | yield div [Class "wrapper d-flex flex-column justify-content-between min-vh-100"] [ 131 | main [Class "container main mb-4"] bodyText 132 | renderFooter cfg pageSource 133 | ] 134 | yield script [Src (cfg.SiteBaseUrl |> Uri.simpleCombine (sprintf "/content/themes.js?version=%i" cfg.ReleaseDate.Ticks)) ] [] 135 | yield script [ 136 | Src "https://code.jquery.com/jquery-3.4.1.slim.min.js" 137 | Integrity "sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" 138 | CrossOrigin "anonymous" 139 | ] [] 140 | yield script [ 141 | Src "https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" 142 | Integrity "sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" 143 | CrossOrigin "anonymous" 144 | ] [] 145 | yield script [ 146 | Src "https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" 147 | Integrity "sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" 148 | CrossOrigin "anonymous" 149 | ] [] 150 | yield script [Src (cfg.SiteBaseUrl |> Uri.simpleCombine (sprintf "/content/tips.js?version=%i" cfg.ReleaseDate.Ticks)) ] [] 151 | if cfg.IsWatchMode then 152 | yield script [Src (cfg.SiteBaseUrl |> Uri.simpleCombine (sprintf "/content/hotload.js?version=%i" cfg.ReleaseDate.Ticks)) ] [] 153 | yield script [Src (cfg.SiteBaseUrl |> Uri.simpleCombine (sprintf "/content/submenu.js?version=%i" cfg.ReleaseDate.Ticks)) ] [] 154 | yield script [Src (cfg.SiteBaseUrl |> Uri.simpleCombine (sprintf "/content/cleanups.js?version=%i" cfg.ReleaseDate.Ticks)) ] [] 155 | ] 156 | ] 157 | -------------------------------------------------------------------------------- /docsTool/templates/modules.fs: -------------------------------------------------------------------------------- 1 | module Modules 2 | open System 3 | open Fable.React 4 | open Fable.React.Props 5 | open FSharp.MetadataFormat 6 | open PartNested 7 | open PartMembers 8 | open Helpers 9 | 10 | 11 | let generateModuleDocs (moduleInfo : ModuleInfo) (props) = 12 | let members = moduleInfo.Module.AllMembers 13 | let comment = moduleInfo.Module.Comment 14 | 15 | let byCategory = 16 | members 17 | |> List.groupBy(fun m -> m.Category) 18 | |> List.sortBy(fun (g,v) -> if String.IsNullOrEmpty g then "ZZZ" else g) 19 | |> List.mapi(fun i (key, value) -> { 20 | Index = i 21 | GroupKey = key 22 | Members = value |> List.sortBy(fun m -> m.Name) 23 | Name = if String.IsNullOrEmpty key then "Other module members" else key 24 | }) 25 | let nestModules = moduleInfo.Module.NestedModules 26 | let nestTypes = moduleInfo.Module.NestedTypes 27 | [ 28 | yield div [ Class "container-fluid py-3" ] [ 29 | yield div [ Class "row" ] [ 30 | yield div [ Class "col-12" ] [ 31 | yield h1 [] [ 32 | str moduleInfo.Module.Name 33 | ] 34 | yield! renderObsoleteMessage moduleInfo.Module 35 | yield! renderNamespace moduleInfo.Namespace 36 | yield dl [] [ 37 | if moduleInfo.ParentModule.IsSome then 38 | yield dt [] [ 39 | str "Parent Module" 40 | ] 41 | yield dd [] [ 42 | a [ 43 | Href (sprintf "%s.html" moduleInfo.ParentModule.Value.UrlName) 44 | ] [ 45 | str moduleInfo.ParentModule.Value.Name 46 | ] 47 | ] 48 | if moduleInfo.Module.Attributes |> Seq.isEmpty |> not then 49 | yield dt [] [ 50 | str "Attributes" 51 | ] 52 | yield dd [] [ 53 | for attr in moduleInfo.Module.Attributes do 54 | yield str (attr.Format()) 55 | yield br [] 56 | ] 57 | ] 58 | 59 | yield div [ 60 | Class "xmldoc" 61 | ] [ 62 | for sec in comment.Sections do 63 | if byCategory |> Seq.exists (fun g -> g.GroupKey = sec.Key) |> not then 64 | if sec.Key <> "" then 65 | yield h2 [] [ 66 | RawText sec.Key 67 | ] 68 | yield RawText sec.Value 69 | ] 70 | 71 | 72 | if byCategory |> Seq.length > 1 then 73 | yield h2 [] [ 74 | str "Table of contents" 75 | ] 76 | 77 | yield ul [] [ 78 | for g in byCategory do 79 | yield li [] [ 80 | a [ 81 | Href (g.Index.ToString() |> sprintf "#section%s") 82 | ] [ 83 | str g.Name 84 | ] 85 | ] 86 | ] 87 | 88 | if (nestTypes |> Seq.length) + (nestModules |> Seq.length) > 0 then 89 | yield h2 [] [ 90 | str "Nested types and modules" 91 | ] 92 | 93 | yield! (partNested (nestTypes |> Seq.toArray) (nestModules |> Seq.toArray)) 94 | 95 | for g in byCategory do 96 | if byCategory |> Seq.length > 1 then 97 | yield h2 [] [ 98 | str g.Name 99 | a [ 100 | Name (sprintf "section%d" g.Index) 101 | ] [ 102 | str " " 103 | ] 104 | ] 105 | 106 | let info = comment.Sections |> Seq.tryFind(fun kvp -> kvp.Key = g.GroupKey) 107 | 108 | match info with 109 | | Some info -> 110 | yield div [ 111 | Class "xmldoc" 112 | ] [ 113 | str info.Value 114 | ] 115 | | None -> 116 | yield nothing 117 | 118 | yield! partMembers "Functions and values" "Function or value" (g.Members |> Seq.filter(fun m -> m.Kind = MemberKind.ValueOrFunction)) 119 | 120 | yield! partMembers "Type extensions" "Type extension" (g.Members |> Seq.filter(fun m -> m.Kind = MemberKind.TypeExtension)) 121 | 122 | yield! partMembers "Active patterns" "Active pattern" (g.Members |> Seq.filter(fun m -> m.Kind = MemberKind.ActivePattern)) 123 | ] 124 | ] 125 | ] 126 | ] 127 | -------------------------------------------------------------------------------- /docsTool/templates/namespaces.fs: -------------------------------------------------------------------------------- 1 | module Namespaces 2 | 3 | open System 4 | open Fable.React 5 | open Fable.React.Props 6 | open FSharp.MetadataFormat 7 | 8 | 9 | type ByCategory = { 10 | Name : string 11 | Index : string 12 | Types : Type array 13 | Modules : Module array 14 | } 15 | 16 | let generateNamespaceDocs (asm : AssemblyGroup) (props) = 17 | let parts = 18 | asm.Namespaces 19 | |> Seq.mapi(fun nsi ns -> 20 | let allByCategories = 21 | ns.Types 22 | |> Seq.map(fun t -> t.Category) 23 | |> Seq.append (ns.Modules |> Seq.map(fun m -> m.Category)) 24 | |> Seq.distinct 25 | |> Seq.sortBy(fun s -> 26 | if String.IsNullOrEmpty(s) then "ZZZ" 27 | else s) 28 | |> Seq.mapi(fun ci c -> 29 | { 30 | Name = if String.IsNullOrEmpty(c) then "Other namespace members" else c 31 | Index = sprintf "%d_%d" nsi ci 32 | Types = ns.Types |> Seq.filter(fun t -> t.Category = c) |> Seq.toArray 33 | Modules = ns.Modules |> Seq.filter(fun m -> m.Category = c) |> Seq.toArray 34 | }) 35 | |> Seq.filter(fun c -> c.Types.Length + c.Modules.Length > 0) 36 | |> Seq.toArray 37 | [ 38 | yield h2 [] [ 39 | Helpers.createAnchor ns.Name ns.Name 40 | ] 41 | if allByCategories.Length > 1 then 42 | yield ul [] [ 43 | for c in allByCategories do 44 | yield 45 | li [] [ 46 | a [Href (sprintf "#section%s" c.Index)] [ 47 | str c.Name 48 | ] 49 | ] 50 | ] 51 | 52 | 53 | for c in allByCategories do 54 | if allByCategories.Length > 1 then 55 | yield h3 [] [ 56 | a [Class "anchor"; Name (sprintf "section%s" c.Index); Href (sprintf "#section%s" c.Index)] [ 57 | str c.Name 58 | ] 59 | ] 60 | yield! PartNested.partNested c.Types c.Modules 61 | ] 62 | ) 63 | |> Seq.collect id 64 | div [ Class "container-fluid py-3" ] [ 65 | div [ Class "row" ] [ 66 | div [ Class "col-12" ] [ 67 | yield h1 [] [ 68 | Helpers.createAnchor asm.Name asm.Name 69 | ] 70 | yield! parts 71 | ] 72 | ] 73 | ] 74 | -------------------------------------------------------------------------------- /docsTool/templates/nav.fs: -------------------------------------------------------------------------------- 1 | module Nav 2 | 3 | open System 4 | open DocsTool 5 | open Fable.React 6 | open Fable.React.Props 7 | 8 | type NameOfArticle = string 9 | type UrlPath = string 10 | 11 | type TopLevelNav = { 12 | DocsRoot : IO.DirectoryInfo 13 | DocsPages : IO.FileInfo list 14 | } 15 | 16 | type NavConfig = { 17 | SiteBaseUrl : Uri 18 | GitHubRepoUrl : Uri 19 | ProjectName : string 20 | TopLevelNav : TopLevelNav 21 | } 22 | 23 | let normalizeText text = 24 | System.Text.RegularExpressions.Regex.Replace(text, @"[^0-9a-zA-Z\.]+", " ") 25 | 26 | let normalizeStr = normalizeText >> str 27 | 28 | let navItem link inner = 29 | li [ 30 | Class "nav-item" 31 | ] [ 32 | a [ 33 | Class "nav-link" 34 | Href link 35 | ] inner 36 | ] 37 | 38 | let navItemText text link = 39 | navItem link [ normalizeStr text ] 40 | 41 | let navItemIconOnly link ariaLabel inner = 42 | li [Class "nav-item"] [ 43 | a [ 44 | Class "nav-link" 45 | HTMLAttr.Custom("aria-label", ariaLabel) 46 | Href link 47 | ] inner 48 | ] 49 | 50 | let dropDownNavMenu text items = 51 | li [ Class "nav-item dropdown" ][ 52 | a [ 53 | Id (sprintf "navbarDropdown-%s" text) 54 | Href "#" 55 | DataToggle "dropdown" 56 | AriaHasPopup true 57 | AriaExpanded false 58 | Class "nav-link dropdown-toggle" ] 59 | [ normalizeStr text ] 60 | ul [ HTMLAttr.Custom ("aria-labelledby", "dropdownMenu1") 61 | Class "dropdown-menu border-0 shadow" ] items ] 62 | 63 | let dropDownNavItem text link = 64 | li [ 65 | Class "nav-item" 66 | ] [ 67 | a [ 68 | Class "dropdown-item" 69 | Href link 70 | ] [ 71 | normalizeStr text 72 | ] 73 | ] 74 | let dropdownSubMenu text items = 75 | li [ Class "dropdown-submenu" ] [ 76 | a [ Id (sprintf "navbarDropdown-%s" text) 77 | Href "#" 78 | Role "button" 79 | DataToggle "dropdown" 80 | AriaHasPopup true 81 | AriaExpanded false 82 | Class "dropdown-item dropdown-toggle" ] [ 83 | normalizeStr text ] 84 | ul [ 85 | HTMLAttr.Custom ("aria-labelledby", "dropdownMenu2") 86 | Class "dropdown-menu border-0 shadow" ] items 87 | ] 88 | 89 | type NavTree = 90 | | File of title:string * link:string 91 | | Folder of title: string * NavTree list 92 | 93 | let rec sortNavTree (navtree : NavTree list) = 94 | navtree 95 | |> List.map(fun navTree -> 96 | match navTree with 97 | | File (t,l) -> File (t,l) 98 | | Folder(title, nodes) -> Folder(title, sortNavTree nodes) 99 | ) 100 | |> List.sortBy(fun navtree -> 101 | match navtree with 102 | | File(title,_) -> title 103 | | Folder(title, _) -> title 104 | ) 105 | 106 | let navTreeFromPaths (rootPath : IO.DirectoryInfo) (files : IO.FileInfo list) = 107 | let rec addPath subFilePath parts nodes = 108 | match parts with 109 | | [] -> nodes 110 | | hp :: tp -> 111 | addHeadPath subFilePath hp tp nodes 112 | and addHeadPath subFilePath (part : string) remainingParts (nodes : NavTree list)= 113 | match nodes with 114 | | [] -> 115 | if part.EndsWith("html") then 116 | File(IO.Path.GetFileNameWithoutExtension part, subFilePath) 117 | else 118 | Folder(part, addPath subFilePath remainingParts []) 119 | |> List.singleton 120 | | Folder(title, subnodes) :: nodes when title = part -> Folder(title, addPath subFilePath remainingParts subnodes ) :: nodes 121 | | hn :: tn -> hn :: addHeadPath subFilePath part remainingParts tn 122 | 123 | ([], files) 124 | ||> List.fold(fun state file -> 125 | let subFilePath = file.FullName.Replace(rootPath.FullName, "") 126 | let pathParts = subFilePath.Split(IO.Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries) |> Array.toList 127 | addPath subFilePath pathParts state 128 | ) 129 | 130 | 131 | 132 | let generateNavMenus siteBaseUrl (navTree : NavTree list) = 133 | let rec innerDo depth (navTree : NavTree list) = 134 | navTree 135 | |> List.map(fun nav -> 136 | match nav with 137 | | File (title, link) when depth = 0 -> navItemText title (siteBaseUrl |> Uri.simpleCombine link) 138 | | File (title, link) -> dropDownNavItem title (siteBaseUrl |> Uri.simpleCombine link) 139 | | Folder (title, subtree) when depth = 0 -> 140 | innerDo (depth + 1) subtree 141 | |> dropDownNavMenu title 142 | | Folder (title, subtree) -> 143 | innerDo (depth + 1) subtree 144 | |> dropdownSubMenu title 145 | ) 146 | innerDo 0 navTree 147 | 148 | 149 | 150 | let generateNav (navCfg : NavConfig) = 151 | nav [ 152 | Class "navbar navbar-expand-md sticky-top navbar-dark bg-dark" 153 | ] [ 154 | a [ 155 | Class "navbar-brand" 156 | Href (navCfg.SiteBaseUrl |> Uri.simpleCombine "/index.html") 157 | ] [ 158 | i [ Class "fa fa-car text-white mr-2"] [] 159 | str (navCfg.ProjectName) 160 | ] 161 | button [ 162 | Class "navbar-toggler" 163 | Type "button" 164 | DataToggle "collapse" 165 | HTMLAttr.Custom("data-target","#navbarNav" ) 166 | HTMLAttr.Custom("aria-controls","navbarNav" ) 167 | HTMLAttr.Custom("aria-expanded","false" ) 168 | HTMLAttr.Custom("aria-label","Toggle navigation" ) 169 | ] [ 170 | span [Class "navbar-toggler-icon"] [] 171 | ] 172 | div [ Class "collapse navbar-collapse" 173 | Id "navbarNav" ] [ 174 | ul [ Class "navbar-nav mr-auto" ] [ 175 | yield! navTreeFromPaths navCfg.TopLevelNav.DocsRoot navCfg.TopLevelNav.DocsPages |> sortNavTree |> generateNavMenus navCfg.SiteBaseUrl 176 | ] 177 | ul [ Class "navbar-nav"] [ 178 | button [Id "theme-toggle"; Class ""] [ 179 | str "" 180 | ] 181 | navItemIconOnly (string navCfg.GitHubRepoUrl) (sprintf "%s Repository on Github" navCfg.ProjectName) [ 182 | i [ Class "fab fa-github fa-lg fa-fw text-light"] [] 183 | ] 184 | ] 185 | ] 186 | ] 187 | 188 | 189 | 190 | -------------------------------------------------------------------------------- /docsTool/templates/partMembers.fs: -------------------------------------------------------------------------------- 1 | module PartMembers 2 | 3 | open System 4 | open Fable.React 5 | open Fable.React.Props 6 | open FSharp.MetadataFormat 7 | open System.Collections.Generic 8 | open Helpers 9 | 10 | type ModuleByCategory = { 11 | Index : int 12 | GroupKey : string 13 | Members : list 14 | Name : string 15 | } 16 | 17 | 18 | let signature (m : Member) = seq { 19 | if m.Details.Signature |> String.IsNullOrEmpty |> not then 20 | yield 21 | code [ Class "function-or-value"] [ 22 | str m.Details.Signature 23 | ] 24 | } 25 | 26 | let repoSourceLink (m: Member) = seq { 27 | if m.Details.FormatSourceLocation |> String.IsNullOrEmpty |> not then 28 | yield a [ 29 | Href m.Details.FormatSourceLocation 30 | Class "float-right" 31 | HTMLAttr.Custom("aria-label", "View source on GitHub") 32 | ] [ 33 | yield i [ 34 | Class "fab fa-github text-dark" 35 | ] [] 36 | ] 37 | } 38 | 39 | let replaceh2withh5 (content : string) = 40 | content.Replace("

", "

") 41 | 42 | 43 | let normalize (content : string) = 44 | content 45 | |> replaceh2withh5 46 | 47 | 48 | 49 | let commentBlock (c: Comment) = 50 | let (|EmptyDefaultBlock|NonEmptyDefaultBlock|Section|) (KeyValue(section, content)) = 51 | match section, content with 52 | | "", c when String.IsNullOrEmpty c -> EmptyDefaultBlock 53 | | "", c -> NonEmptyDefaultBlock c 54 | | section, content -> Section (section, content) 55 | 56 | let renderSection (s : KeyValuePair): Fable.React.ReactElement list = 57 | match s with 58 | | EmptyDefaultBlock -> [] 59 | | NonEmptyDefaultBlock content -> [ div [ Class "comment-block" ] [ RawText (normalize content) ] ] 60 | | Section(name, content) -> [ h5 [] [ str name ] // h2 is obnoxiously large for this context, go with the smaller h5 61 | RawText (normalize content) ] 62 | c.Sections 63 | |> List.collect renderSection 64 | 65 | let compiledName (m: Member) = seq { 66 | if m.Details.FormatCompiledName |> String.IsNullOrEmpty |> not then 67 | yield p [] [ 68 | strong [] [ str "CompiledName:" ] 69 | code [] [ str m.Details.FormatCompiledName ] 70 | ] 71 | } 72 | 73 | let partMembers (header : string) (tableHeader : string) (members : #seq) = [ 74 | if members |> Seq.length > 0 then 75 | yield h3 [] [ 76 | str header 77 | ] 78 | 79 | yield table [ 80 | Class "table" 81 | ] [ 82 | thead [] [ 83 | 84 | tr [] [ 85 | th [Class "fit"] [ 86 | 87 | ] 88 | th [] [ 89 | str tableHeader 90 | ] 91 | 92 | th [] [ 93 | str "Signature" 94 | ] 95 | 96 | th [] [ 97 | str "Description" 98 | ] 99 | ] 100 | ] 101 | tbody [] [ 102 | for it in members do 103 | let id = Guid.NewGuid().ToString() 104 | yield tr [] [ 105 | td [] [ 106 | Helpers.createAnchorIcon (it.Details.FormatUsage(40)) 107 | ] 108 | td [ 109 | Class "member-name" 110 | ] [ 111 | code [ 112 | Class "function-or-value" 113 | HTMLAttr.Custom("data-guid", id) 114 | ] [ 115 | str (it.Details.FormatUsage(40)) 116 | ] 117 | ] 118 | td [ 119 | Class "member-name" 120 | ] [ 121 | yield! signature it 122 | ] 123 | 124 | td [ 125 | Class "xmldoc" 126 | ] [ 127 | yield! renderObsoleteMessage it 128 | yield! repoSourceLink it 129 | yield! commentBlock it.Comment 130 | yield! compiledName it 131 | ] 132 | ] 133 | ] 134 | ] 135 | ] 136 | -------------------------------------------------------------------------------- /docsTool/templates/partNested.fs: -------------------------------------------------------------------------------- 1 | module PartNested 2 | 3 | open System 4 | open Fable.React 5 | open Fable.React.Props 6 | open FSharp.MetadataFormat 7 | open Helpers 8 | 9 | let partNested (types : Type array) (modules : Module array) = 10 | [ 11 | if types.Length > 0 then 12 | yield table [ Class "table" ] [ 13 | thead [] [ 14 | tr [] [ 15 | th [Class "fit"] [ 16 | 17 | ] 18 | th [] [ 19 | str "Type" 20 | ] 21 | th [] [ 22 | str "Description" 23 | ] 24 | ] 25 | ] 26 | tbody [] [ 27 | for t in types do 28 | yield tr [] [ 29 | td [] [ 30 | Helpers.createAnchorIcon t.Name 31 | ] 32 | td [Class "type-name"] [ 33 | a [Href (sprintf "%s.html" t.UrlName)] [ 34 | str t.Name 35 | ] 36 | ] 37 | td [Class "xmldoc"] [ 38 | yield! renderObsoleteMessage t 39 | yield RawText t.Comment.Blurb 40 | ] 41 | ] 42 | ] 43 | ] 44 | if modules.Length > 0 then 45 | yield table [ Class "table" ] [ 46 | thead [] [ 47 | tr [] [ 48 | th [Class "fit"] [ 49 | 50 | ] 51 | th [] [ 52 | str "Module" 53 | ] 54 | th [] [ 55 | str "Description" 56 | ] 57 | ] 58 | ] 59 | tbody [] [ 60 | for t in modules do 61 | yield tr [] [ 62 | td [] [ 63 | Helpers.createAnchorIcon t.Name 64 | ] 65 | td [Class "Modules-name"] [ 66 | a [Href (sprintf "%s.html" t.UrlName)] [ 67 | str t.Name 68 | ] 69 | ] 70 | td [Class "xmldoc"] [ 71 | yield! renderObsoleteMessage t 72 | yield RawText t.Comment.Blurb 73 | ] 74 | ] 75 | ] 76 | ] 77 | ] 78 | -------------------------------------------------------------------------------- /docsTool/templates/types.fs: -------------------------------------------------------------------------------- 1 | module Types 2 | 3 | open System 4 | open Fable.React 5 | open Fable.React.Props 6 | open FSharp.MetadataFormat 7 | open PartMembers 8 | open Helpers 9 | 10 | let generateTypeDocs (model : TypeInfo) (props) = 11 | let members = model.Type.AllMembers 12 | let comment = model.Type.Comment 13 | let ``type`` = model.Type 14 | let byCategory = 15 | members 16 | |> List.groupBy (fun m -> m.Category) 17 | |> List.sortBy (fun (k,v) -> if String.IsNullOrEmpty(k) then "ZZZ" else k ) 18 | |> List.mapi (fun i (k,v) -> { 19 | Index = i 20 | GroupKey = k 21 | Members = v |> List.sortBy (fun m -> if m.Kind = MemberKind.StaticParameter then "" else m.Name) 22 | Name = if String.IsNullOrEmpty(k) then "Other type members" else k 23 | }) 24 | [ 25 | yield h1 [] [ 26 | str model.Type.Name 27 | ] 28 | 29 | yield p [] [ 30 | yield! renderObsoleteMessage model.Type 31 | yield! renderNamespace model.Namespace 32 | if model.HasParentModule then 33 | yield br [] 34 | yield span [] [ 35 | str "Parent Module: " 36 | 37 | a [ 38 | Href (sprintf "%s.html" model.ParentModule.Value.UrlName) 39 | ] [ 40 | str model.ParentModule.Value.Name 41 | ] 42 | ] 43 | 44 | 45 | if ``type``.Attributes |> Seq.isEmpty |> not then 46 | yield br [] 47 | yield span [] [ 48 | yield str "Attributes: " 49 | 50 | yield br [] 51 | 52 | for attr in ``type``.Attributes do 53 | yield str (attr.Format()) 54 | yield br [] 55 | ] 56 | ] 57 | 58 | yield div [ 59 | Class "xmldoc" 60 | ] [ 61 | for sec in comment.Sections do 62 | if byCategory |> Seq.exists (fun m -> m.GroupKey = sec.Key) |> not then 63 | if sec.Key <> "" then 64 | yield h2 [] [ 65 | str sec.Key 66 | ] 67 | yield RawText sec.Value 68 | ] 69 | 70 | if byCategory |> Seq.length > 1 then 71 | yield h2 [] [ 72 | str "Table of contents" 73 | ] 74 | 75 | yield ul [] [ 76 | for g in byCategory do 77 | yield li [] [ 78 | a [ 79 | Href (sprintf "#section%d" g.Index) 80 | ] [ 81 | str g.Name 82 | ] 83 | ] 84 | ] 85 | 86 | for g in byCategory do 87 | if byCategory |> Seq.length > 1 then 88 | yield h2 [] [ 89 | str g.Name 90 | 91 | a [ 92 | Name (sprintf "section%d" g.Index) 93 | ] [ 94 | str " " 95 | ] 96 | ] 97 | 98 | match comment.Sections |> Seq.tryFind (fun kvp -> kvp.Key = g.GroupKey) with 99 | | Some info -> 100 | yield div [ 101 | Class "xmldoc" 102 | ] [ 103 | str info.Value 104 | ] 105 | | None -> yield nothing 106 | 107 | yield! partMembers "Union Cases" "Union Case" (g.Members |> Seq.filter(fun m -> m.Kind = MemberKind.UnionCase)) 108 | yield! partMembers "Record Fields" "Record Field" (g.Members |> Seq.filter(fun m -> m.Kind = MemberKind.RecordField)) 109 | yield! partMembers "Static parameters" "Static parameters" (g.Members |> Seq.filter(fun m -> m.Kind = MemberKind.StaticParameter)) 110 | yield! partMembers "Contructors" "Constructor" (g.Members |> Seq.filter(fun m -> m.Kind = MemberKind.Constructor)) 111 | yield! partMembers "Instance members" "Instance member" (g.Members |> Seq.filter(fun m -> m.Kind = MemberKind.InstanceMember)) 112 | yield! partMembers "Static members" "Static member" (g.Members |> Seq.filter(fun m -> m.Kind = MemberKind.StaticMember)) 113 | ] 114 | -------------------------------------------------------------------------------- /paket.dependencies: -------------------------------------------------------------------------------- 1 | source https://www.nuget.org/api/v2 2 | source https://api.nuget.org/v3/index.json 3 | storage: none 4 | clitool dotnet-mono 0.5.2 5 | nuget FSharp.Core 6 | nuget Microsoft.SourceLink.GitHub prerelease copy_local: true 7 | nuget Microsoft.NETFramework.ReferenceAssemblies copy_local: true 8 | nuget Expecto 8.13.1 9 | nuget Fantomas 4.0.0-alpha-006 10 | nuget YoloDev.Expecto.TestSdk 0.8.0 11 | nuget Microsoft.NET.Test.Sdk 15.7.2 12 | nuget altcover ~> 6 13 | nuget Myriad.Core 0.2.8 14 | nuget Myriad.Sdk 0.2.8 15 | nuget Newtonsoft.Json 16 | nuget fsharp.compiler.service 17 | github baronfel/Newtonsoft.Json.FSharp.Idiomatic src/Newtonsoft.Json.FSharp.Idiomatic/Newtonsoft.Json.FSharp.Idiomatic.fs 18 | 19 | 20 | // [ FAKE GROUP ] 21 | group Build 22 | storage: none 23 | source https://www.nuget.org/api/v2 24 | source https://api.nuget.org/v3/index.json 25 | nuget Fake.IO.FileSystem 5.20.0 26 | nuget Fake.Core.Target 5.20.0 27 | nuget Fake.Core.ReleaseNotes 5.20.0 28 | nuget FAKE.Core.Environment 5.20.0 29 | nuget Fake.DotNet.Cli 5.20.0 30 | nuget FAKE.Core.Process 5.20.0 31 | nuget Fake.DotNet.AssemblyInfoFile 5.20.0 32 | nuget Fake.Tools.Git 5.20.0 33 | nuget Fake.DotNet.Paket 5.20.0 34 | nuget Fake.Api.GitHub 5.20.0 35 | nuget Fake.BuildServer.AppVeyor 5.20.0 36 | nuget Fake.BuildServer.Travis 5.20.0 37 | nuget Fantomas 38 | nuget Argu 39 | 40 | group Docs 41 | storage: none 42 | source https://www.nuget.org/api/v2 43 | source https://api.nuget.org/v3/index.json 44 | nuget Argu 45 | nuget FSharp.Core 46 | nuget Fake.IO.FileSystem 47 | nuget FAKE.Core.Environment 48 | nuget Fake.DotNet.Cli 49 | nuget FSharp.Formatting 4.0.0-rc1 50 | nuget FSharp.Literate 4.0.0-rc1 51 | nuget Fable.React 52 | nuget Dotnet.ProjInfo.Workspace.FCS 53 | nuget FSharp.Compiler.Service 34.1.1 54 | 55 | group Analyzers 56 | source https://www.nuget.org/api/v2 57 | source https://api.nuget.org/v3/index.json 58 | nuget BinaryDefense.FSharp.Analyzers.Hashing 0.1.0 59 | -------------------------------------------------------------------------------- /src/BinaryDefense.JsonWrapper.Core/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | // Auto-Generated by FAKE; do not edit 2 | namespace System 3 | open System.Reflection 4 | 5 | [] 6 | [] 7 | [] 8 | [] 9 | [] 10 | [] 11 | [] 12 | [] 13 | do () 14 | 15 | module internal AssemblyVersionInformation = 16 | let [] AssemblyTitle = "BinaryDefense.JsonWrapper.Core" 17 | let [] AssemblyProduct = "BinaryDefense.JsonWrapper" 18 | let [] AssemblyVersion = "0.1.1" 19 | let [] AssemblyMetadata_ReleaseDate = "2020-06-05T00:00:00.0000000-04:00" 20 | let [] AssemblyFileVersion = "0.1.1" 21 | let [] AssemblyInformationalVersion = "0.1.1" 22 | let [] AssemblyMetadata_ReleaseChannel = "release" 23 | let [] AssemblyMetadata_GitHash = "aac89b8ca30516e1131edf059e62fd7835311da4" 24 | -------------------------------------------------------------------------------- /src/BinaryDefense.JsonWrapper.Core/BinaryDefense.JsonWrapper.Core.fs: -------------------------------------------------------------------------------- 1 | namespace BinaryDefense.JsonWrapper.Core 2 | 3 | open Newtonsoft.Json.Linq 4 | open Newtonsoft.Json 5 | open System 6 | 7 | type IHaveJToken = 8 | abstract member InnerData : JToken 9 | 10 | module Attributes = 11 | type MustExist() = 12 | inherit Attribute() 13 | 14 | type MissingJsonFieldException(fieldName : string, ?jtoken : JToken) = 15 | inherit Exception(sprintf "Failed to find json key \"%s\" on JToken" fieldName ) 16 | member __.JsonFieldName = fieldName 17 | member __.JToken = jtoken 18 | 19 | 20 | module Converters = 21 | 22 | type IHaveJTokenConverter() = 23 | inherit JsonConverter() 24 | override __.CanConvert t = 25 | typeof.IsAssignableFrom(t) 26 | 27 | override __.WriteJson(writer, value, serializer) = 28 | match value with 29 | | :? IHaveJToken as it -> serializer.Serialize(writer, it.InnerData) 30 | | _ -> InvalidCastException("Did not implement type IHaveJToken") |> raise 31 | 32 | override __.ReadJson(reader, t, _existingValue, serializer) = 33 | let jtoken = JToken.ReadFrom reader 34 | let ctor = t.GetConstructor([|typeof; typeof|]) 35 | ctor.Invoke([| 36 | jtoken 37 | serializer 38 | |]) 39 | 40 | 41 | let recommendedConverters : JsonConverter list = [ 42 | IHaveJTokenConverter () 43 | Newtonsoft.Json.FSharp.Idiomatic.OptionConverter() 44 | ] 45 | -------------------------------------------------------------------------------- /src/BinaryDefense.JsonWrapper.Core/BinaryDefense.JsonWrapper.Core.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | netstandard2.1 6 | true 7 | true 8 | embedded 9 | preview 10 | 11 | 12 | BinaryDefense.JsonWrapper.Core 13 | Holds the core types associated with BinaryDefense.Myriad.Plugins.JsonWrapper 14 | 15 | 16 | 17 | True 18 | paket-files/Newtonsoft.Json.FSharp.Idiomatic.fs 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/BinaryDefense.JsonWrapper.Core/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | Microsoft.SourceLink.GitHub 3 | Microsoft.NETFramework.ReferenceAssemblies 4 | Newtonsoft.Json 5 | 6 | File: Newtonsoft.Json.FSharp.Idiomatic.fs 7 | -------------------------------------------------------------------------------- /src/BinaryDefense.Myriad.Plugins.JsonWrapper/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | // Auto-Generated by FAKE; do not edit 2 | namespace System 3 | open System.Reflection 4 | 5 | [] 6 | [] 7 | [] 8 | [] 9 | [] 10 | [] 11 | [] 12 | [] 13 | do () 14 | 15 | module internal AssemblyVersionInformation = 16 | let [] AssemblyTitle = "BinaryDefense.Myriad.Plugins.JsonWrapper" 17 | let [] AssemblyProduct = "BinaryDefense.JsonWrapper" 18 | let [] AssemblyVersion = "0.1.1" 19 | let [] AssemblyMetadata_ReleaseDate = "2020-06-05T00:00:00.0000000-04:00" 20 | let [] AssemblyFileVersion = "0.1.1" 21 | let [] AssemblyInformationalVersion = "0.1.1" 22 | let [] AssemblyMetadata_ReleaseChannel = "release" 23 | let [] AssemblyMetadata_GitHash = "aac89b8ca30516e1131edf059e62fd7835311da4" 24 | -------------------------------------------------------------------------------- /src/BinaryDefense.Myriad.Plugins.JsonWrapper/BinaryDefense.Myriad.Plugins.JsonWrapper.fs: -------------------------------------------------------------------------------- 1 | namespace Myriad.Plugins 2 | 3 | open System 4 | open FSharp.Compiler 5 | open FSharp.Compiler.SyntaxTree 6 | open FsAst 7 | open Myriad.Core 8 | open FSharp.Compiler.Range 9 | open FSharp.Compiler.XmlDoc 10 | open BinaryDefense.JsonWrapper.Core 11 | open Newtonsoft.Json 12 | open Newtonsoft.Json.Linq 13 | open System.Collections.Generic 14 | 15 | 16 | module Reflection = 17 | 18 | open Microsoft.FSharp.Quotations.Patterns 19 | /// Gets a statically type property name of an instance 20 | let rec propertyName quotation = 21 | match quotation with 22 | | PropertyGet(_, propertyInfo, _) -> propertyInfo.Name 23 | | Lambda(_, expr) -> propertyName expr 24 | | _ -> "" 25 | 26 | /// Gets a statically typed method name of an instance 27 | let methodName quotation= 28 | match quotation with 29 | | Lambda (_, Call (_, mi, _)) -> mi.Name 30 | | _ -> failwith "%A is not a valid getMethodName expression, expected Lamba(_ Call(_, _, _))" 31 | 32 | 33 | module DSL = 34 | 35 | let hashDirective (parsedHashDirective : ParsedHashDirective) = 36 | SynModuleDecl.HashDirective (parsedHashDirective, range0) 37 | 38 | let noWarn args = 39 | ParsedHashDirective("nowarn", args, range0 ) 40 | |> hashDirective 41 | 42 | /// Creates : open {{namespace}} 43 | let openNamespace (``namespace`` : LongIdentWithDots) = 44 | SynModuleDecl.CreateOpen (``namespace``) 45 | 46 | /// Creates a tuple for method signatures 47 | let createTypleSynType args = 48 | SynType.Tuple(false, args |> List.map(fun s -> false,s), range0) 49 | 50 | /// Creates : if {{ifCheck}} then {{ifbody}} else {{elseBody}} 51 | let createIfThenElse ifCheck ifBody elseBody = 52 | SynExpr.IfThenElse(ifCheck, ifBody, elseBody, DebugPointForBinding.DebugPointAtBinding range0, false, range0, range0) 53 | 54 | /// Creates : let {{leftSide}} = {{rightSide}} 55 | /// 56 | /// A more concrete example: let myVar = "something" 57 | let createLetAssignment leftSide rightSide continuation = 58 | let emptySynValData = SynValData.SynValData(None, SynValInfo.Empty, None) 59 | let headPat = SynPat.Named(SynPat.Wild range0, leftSide, false, None, range0) 60 | let binding = SynBinding.Binding(None, SynBindingKind.NormalBinding, false, false, [], PreXmlDoc.Empty, emptySynValData, headPat, None, rightSide, range0, DebugPointForBinding.DebugPointAtBinding range0 ) 61 | SynExpr.LetOrUse(false, false, [binding], continuation, range0) 62 | 63 | 64 | let private pipeRightIdent = Ident.Create "op_PipeRight" 65 | 66 | // Creates : {{synExpr1}} |> {{synExpr2}} 67 | let pipeRight synExpr2 synExpr1 = 68 | SynExpr.CreateApp(SynExpr.CreateAppInfix(SynExpr.CreateIdent pipeRightIdent, synExpr1), synExpr2) 69 | 70 | 71 | module SystemObject = 72 | let getHashCodeMethod = 73 | Reflection.methodName <@ fun (x : obj) -> x.GetHashCode() @> 74 | 75 | let equalsMethod = 76 | Reflection.methodName <@ fun (x : obj) -> x.Equals(null) @> 77 | 78 | module FSharpCore = 79 | let raiseIdent = Ident.Create (nameof raise) 80 | 81 | let isNullIdent = Ident.Create (nameof isNull) 82 | let isNull ident = 83 | SynExpr.CreateApp(SynExpr.CreateIdent isNullIdent , SynExpr.CreateIdent ident ) 84 | 85 | 86 | module System = 87 | let ``namespace`` = typeof.Namespace 88 | 89 | module JToken = 90 | let ``namespace`` = typeof.Namespace 91 | let name = typeof.Name 92 | let nameLongIdent = LongIdentWithDots.CreateString name 93 | 94 | let private toObjectMethod = 95 | Reflection.methodName <@ fun (x : JToken) -> x.ToObject() @> 96 | 97 | let instanceToObject (instance : Ident) (generic : SynType) (serializer : Ident) = 98 | let instanceAndMethod = LongIdentWithDots.Create [instance.idText; toObjectMethod] 99 | let args = SynExpr.CreateIdent serializer 100 | SynExpr.CreateInstanceMethodCall(instanceAndMethod, [generic], args) 101 | 102 | 103 | let private fromObjectMethod = 104 | nameof JToken.FromObject 105 | 106 | let staticFromObject jtoken serializer = 107 | let instanceAndMethod = LongIdentWithDots.Create [name; fromObjectMethod] 108 | let args = 109 | SynExpr.CreateParenedTuple [ 110 | SynExpr.CreateIdent jtoken 111 | SynExpr.CreateIdent serializer 112 | ] 113 | SynExpr.CreateInstanceMethodCall(instanceAndMethod, args) 114 | 115 | let private getHashCodeMethod = 116 | Reflection.methodName <@ fun (x : JToken) -> x.GetHashCode() @> 117 | 118 | let instanceGetHashCode (instance : Ident) = 119 | let instanceAndMethod = LongIdentWithDots.Create [instance.idText; getHashCodeMethod] 120 | SynExpr.CreateInstanceMethodCall instanceAndMethod 121 | 122 | let private deepEqualsMethod = 123 | nameof JToken.DeepEquals 124 | 125 | let staticDeepEquals arg1 arg2 = 126 | let deepEqualFunc = 127 | SynExpr.CreateLongIdent ( 128 | LongIdentWithDots.Create [ 129 | name 130 | deepEqualsMethod 131 | ] 132 | ) 133 | let deepEqualArgs = SynExpr.CreateParenedTuple [arg1; arg2] 134 | SynExpr.CreateApp(deepEqualFunc, deepEqualArgs) 135 | 136 | module IHaveJToken = 137 | 138 | let name = typeof.Name 139 | let ``namespace`` = typeof.Namespace 140 | 141 | let innerDataProperty = 142 | Reflection.propertyName <@fun (x : IHaveJToken) -> x.InnerData @> 143 | 144 | let instanceInnerData (instance : Ident) = 145 | SynExpr.CreateLongIdent( 146 | LongIdentWithDots.Create [ 147 | instance.idText 148 | innerDataProperty 149 | ] 150 | ) 151 | 152 | module MissingJsonFieldException = 153 | let name = typeof.Name 154 | let nameLongIdent = LongIdentWithDots.CreateString name 155 | 156 | let ctor fieldName jtoken = 157 | let func = SynExpr.CreateLongIdent nameLongIdent 158 | let args = SynExpr.CreateParenedTuple [fieldName; jtoken] 159 | SynExpr.CreateApp(func, args) 160 | 161 | type ModuleTree = 162 | | Module of string * ModuleTree list 163 | | Class of ``namespace``: LongIdent * SynTypeDefn 164 | 165 | module ModuleTree = 166 | 167 | let fromExtractRecords (xs : list>) = 168 | let rec addPath subFilePath parts nodes = 169 | match parts with 170 | | [] -> nodes 171 | | hp :: tp -> 172 | addHeadPath subFilePath hp tp nodes 173 | and addHeadPath subFilePath (part : string) remainingParts (nodes : ModuleTree list)= 174 | match nodes with 175 | | [] -> 176 | let classes = 177 | if remainingParts |> List.isEmpty then 178 | (snd subFilePath) |> List.map(fun c -> Class(fst subFilePath, c)) 179 | else 180 | List.empty 181 | Module(part, addPath subFilePath remainingParts classes ) 182 | |> List.singleton 183 | | Module(title, subnodes) :: nodes when title = part -> Module(title, addPath subFilePath remainingParts subnodes ) :: nodes 184 | | hn :: tn -> hn :: addHeadPath subFilePath part remainingParts tn 185 | 186 | ([], xs) 187 | ||> List.fold(fun state (moduleIdent, synTypeDefns) -> 188 | let pathParts = moduleIdent |> List.map(fun i -> i.idText) 189 | addPath (moduleIdent, synTypeDefns) pathParts state 190 | ) 191 | 192 | 193 | 194 | module internal Create = 195 | 196 | /// Keeps a map of known generated types and their Deconstruct out parameters. Used in nesting Deconstructs. 197 | let knownDeconstructs = new List() 198 | 199 | type GetterAccessor = 200 | /// The backing field can be missing on the JToken, should be used with Nullable or Option types 201 | | CanBeMissing 202 | /// The backing field must exist on the JToken, should be used with Nullable or Option types 203 | | MustExist 204 | 205 | let createWrapperClass (parentNamespace : LongIdent) (parent: LongIdent) (fields: SynField list) = 206 | 207 | let jsonSerializerFullName = typeof.Name 208 | let jsonSerializerFullNameLongIdent = LongIdentWithDots.CreateString jsonSerializerFullName 209 | 210 | let info = SynComponentInfoRcd.Create parent 211 | let jtokenIdenName = "jtoken" 212 | let jtokenIdent = Ident.Create jtokenIdenName 213 | let jsonSerializerName = "serializer" 214 | let jsonSerializerNameIdent = Ident.Create jsonSerializerName 215 | let selfIden = "this" 216 | let unitArg = SynPatRcd.Const { SynPatConstRcd.Const = SynConst.Unit ; Range = range.Zero } 217 | 218 | 219 | let createCtor = 220 | let jtokenArg = SynSimplePat.CreateTyped(jtokenIdent, (SynType.CreateLongIdent JToken.nameLongIdent)) 221 | let serializerArg = SynSimplePat.CreateTyped(jsonSerializerNameIdent, (SynType.CreateLongIdent jsonSerializerFullNameLongIdent)) 222 | SynMemberDefn.CreateImplicitCtor [jtokenArg; serializerArg] 223 | 224 | let createGetterSynValdatea = 225 | let memberFlags : MemberFlags = { 226 | IsInstance = true 227 | IsDispatchSlot = false 228 | IsOverrideOrExplicitImpl = false 229 | IsFinal = false 230 | MemberKind = MemberKind.PropertyGet 231 | } 232 | SynValData.SynValData(Some memberFlags, SynValInfo.Empty, None) 233 | 234 | let createSetterSynValData () = 235 | let memberFlags : MemberFlags = { 236 | IsInstance = true 237 | IsDispatchSlot = false 238 | IsOverrideOrExplicitImpl = false 239 | IsFinal = false 240 | MemberKind = MemberKind.PropertySet 241 | } 242 | SynValData.SynValData(Some memberFlags, SynValInfo.Empty, None) 243 | 244 | 245 | let createGetSetMember (fieldName : Ident) (jsonFieldName : string) (fieldTy : SynType) (getAccessor : GetterAccessor) = 246 | 247 | let getMemberExpr = 248 | 249 | let varName = Ident.Create "selectedToken" 250 | let continuation = 251 | //Generates the function call {varName}.ToObject(serializer) 252 | let toObjectCall = 253 | JToken.instanceToObject varName fieldTy jsonSerializerNameIdent 254 | 255 | match getAccessor with 256 | | MustExist -> 257 | let ifCheck = FSharpCore.isNull varName 258 | let ifBody = 259 | let createException = 260 | let arg1 = SynExpr.CreateConst(SynConst.CreateString(jsonFieldName)) 261 | let arg2 = SynExpr.CreateIdent jtokenIdent 262 | MissingJsonFieldException.ctor arg1 arg2 263 | // Generates exception |> raise 264 | createException |> DSL.pipeRight (SynExpr.CreateIdent FSharpCore.raiseIdent) 265 | // Generates if isNull {varName} then raise exception 266 | let existCheck = DSL.createIfThenElse ifCheck ifBody None 267 | SynExpr.CreateSequential [ 268 | existCheck 269 | toObjectCall 270 | ] 271 | 272 | | CanBeMissing -> 273 | toObjectCall 274 | 275 | //Generates the jtoken.[{jsonFieldName}] 276 | let idx = [SynIndexerArg.One(SynExpr.CreateConstString jsonFieldName, false, range.Zero )] 277 | let jTokenAccessor = SynExpr.DotIndexedGet( SynExpr.Ident jtokenIdent, idx, range.Zero, range.Zero ) 278 | // Generates let v = jtoken.[{jsonFieldName}] 279 | DSL.createLetAssignment varName jTokenAccessor continuation 280 | 281 | let getMember = 282 | { SynBindingRcd.Null with 283 | Kind = SynBindingKind.NormalBinding 284 | Pattern = SynPatRcd.CreateLongIdent(LongIdentWithDots.Create ([selfIden; fieldName.idText]) , [unitArg]) 285 | ValData = createGetterSynValdatea 286 | ReturnInfo = SynBindingReturnInfoRcd.Create fieldTy |> Some 287 | Expr = getMemberExpr 288 | } 289 | 290 | let argVarName = Ident.Create "newValue" 291 | 292 | let setArg = 293 | let arg = 294 | let named = SynPatRcd.CreateNamed(argVarName, SynPatRcd.CreateWild) 295 | SynPatRcd.CreateTyped(named, fieldTy) 296 | |> SynPatRcd.CreateParen 297 | arg 298 | 299 | let setMemberExpr = 300 | //Generates JToken.FromObject(x, serializer) 301 | let fromObjectFuncWithArgs = JToken.staticFromObject argVarName jsonSerializerNameIdent 302 | //Generates the jtoken.["jsonFieldName"] <- Newtonsoft.Json.Linq.JToken.FromObject(x, serializer) 303 | let idx = [SynIndexerArg.One(SynExpr.CreateConstString jsonFieldName, false, range.Zero )] 304 | SynExpr.DotIndexedSet( SynExpr.Ident jtokenIdent, idx, fromObjectFuncWithArgs, range.Zero, range.Zero, range0 ) 305 | 306 | let setMember = 307 | { SynBindingRcd.Null with 308 | Kind = SynBindingKind.NormalBinding 309 | Pattern = SynPatRcd.CreateLongIdent(LongIdentWithDots.Create ([selfIden; fieldName.idText]) , [setArg]) 310 | ValData = createSetterSynValData () 311 | Expr = setMemberExpr 312 | } 313 | 314 | [ getMember; setMember] 315 | |> List.map SynMemberDefn.CreateMember 316 | 317 | let createGetSetMembersFromRecord = 318 | fields 319 | |> List.collect(fun f -> 320 | let frcd = f.ToRcd 321 | let fieldIdent = match frcd.Id with None -> failwith "no field name" | Some f -> f 322 | 323 | let jsonFieldName = 324 | // Find the JsonProperty attribute 325 | let attr = 326 | frcd.Attributes 327 | |> Seq.collect (fun a -> a.Attributes) 328 | |> Seq.tryFind(fun a -> 329 | let attrName = a.TypeName.AsString 330 | attrName.Contains "JsonProperty" 331 | ) 332 | match attr |> Option.map (fun a -> a.ArgExpr) with 333 | | Some (SynExpr.Paren(SynExpr.Const(SynConst.String(suppliedJsonFieldName, _), _), _ , _, _)) -> suppliedJsonFieldName 334 | | _ -> fieldIdent.idText 335 | 336 | 337 | let getAccessorCreation = 338 | // Find the Attribute.MustExist attribute 339 | let attr = 340 | frcd.Attributes 341 | |> Seq.collect (fun a -> a.Attributes) 342 | |> Seq.tryFind(fun a -> 343 | let attrName = a.TypeName.AsString 344 | attrName.Contains (typeof.Name) 345 | ) 346 | match attr with 347 | | Some _ -> GetterAccessor.MustExist 348 | | None -> GetterAccessor.CanBeMissing 349 | createGetSetMember fieldIdent jsonFieldName frcd.Type getAccessorCreation 350 | ) 351 | 352 | let createOverrideGetHashCode = 353 | let body = JToken.instanceGetHashCode jtokenIdent 354 | 355 | let memberFlags : MemberFlags = { 356 | IsInstance = true 357 | IsDispatchSlot = false 358 | IsOverrideOrExplicitImpl = true 359 | IsFinal = false 360 | MemberKind = MemberKind.Member 361 | } 362 | let valData = SynValData.SynValData(Some memberFlags, SynValInfo.Empty, None) 363 | let getHashCodeMember = 364 | { SynBindingRcd.Null with 365 | Kind = SynBindingKind.NormalBinding 366 | Pattern = SynPatRcd.CreateLongIdent(LongIdentWithDots.Create ([selfIden; SystemObject.getHashCodeMethod]) , [unitArg]) 367 | ValData = valData 368 | Expr = body 369 | } 370 | getHashCodeMember 371 | |> SynMemberDefn.CreateMember 372 | 373 | let createOverrideEquals = 374 | let arg1VarName = "objToCompare" 375 | let arg1VarNameIdent = Ident.Create arg1VarName 376 | 377 | let matchStatement = 378 | let clause1 = 379 | let aliasedName = "jTokenToCompare" 380 | let aliasedNameIdent = Ident.Create aliasedName 381 | let leftSide = 382 | let castedToInteface = SynPat.IsInst(SynType.CreateLongIdent(LongIdentWithDots.CreateString IHaveJToken.name), range0) 383 | SynPat.Named (castedToInteface, aliasedNameIdent,false, None, range0) 384 | let rightSide = 385 | let arg1 = IHaveJToken.instanceInnerData aliasedNameIdent 386 | let arg2 = SynExpr.CreateIdent jtokenIdent 387 | JToken.staticDeepEquals arg1 arg2 388 | SynMatchClause.Clause(leftSide, None, rightSide, range0, DebugPointForTarget.Yes) 389 | let clause2 = 390 | SynMatchClause.Clause(SynPat.Wild range0, None, SynExpr.CreateConst (SynConst.Bool(false)), range0, DebugPointForTarget.Yes) 391 | let matchCheck = SynExpr.CreateIdent arg1VarNameIdent 392 | SynExpr.CreateMatch(matchCheck, [clause1; clause2]) 393 | let memberFlags : MemberFlags = { 394 | IsInstance = true 395 | IsDispatchSlot = false 396 | IsOverrideOrExplicitImpl = true 397 | IsFinal = false 398 | MemberKind = MemberKind.Member 399 | } 400 | let valData = SynValData.SynValData(Some memberFlags, SynValInfo.Empty, None) 401 | let equalArg = 402 | let arg = 403 | let named = SynPatRcd.CreateNamed(arg1VarNameIdent, SynPatRcd.CreateWild ) 404 | SynPatRcd.CreateTyped(named, SynType.CreateLongIdent "obj") 405 | |> SynPatRcd.CreateParen 406 | arg 407 | let equalMember = 408 | { SynBindingRcd.Null with 409 | Kind = SynBindingKind.NormalBinding 410 | Pattern = SynPatRcd.CreateLongIdent(LongIdentWithDots.Create ([selfIden; SystemObject.equalsMethod]) , [equalArg]) 411 | ValData = valData 412 | Expr = matchStatement 413 | } 414 | equalMember 415 | |> SynMemberDefn.CreateMember 416 | 417 | let createInterfaceImpl = 418 | let implementedMembers = 419 | let createInnerDataMemberVal = 420 | let memberFlags : MemberFlags = { 421 | IsInstance = true 422 | IsDispatchSlot = false 423 | IsOverrideOrExplicitImpl = true 424 | IsFinal = false 425 | MemberKind = MemberKind.Member 426 | } 427 | SynValData.SynValData(Some memberFlags, SynValInfo.Empty, None) 428 | let bindingRecord = { 429 | SynBindingRcd.Null with 430 | ValData = createInnerDataMemberVal 431 | Pattern = SynPatRcd.CreateLongIdent (LongIdentWithDots.Create [selfIden; IHaveJToken.innerDataProperty], []) 432 | Expr = SynExpr.CreateIdent jtokenIdent 433 | } 434 | 435 | [ SynMemberDefn.CreateMember(bindingRecord)] 436 | SynMemberDefn.Interface(SynType.CreateLongIdent(IHaveJToken.name), Some implementedMembers, range0) 437 | 438 | 439 | let getDeconstructType fieldTy = 440 | match fieldTy with 441 | | SynType.LongIdent ident -> 442 | match knownDeconstructs |> Seq.tryFind(fun (k : string) -> k.EndsWith ident.AsString) with 443 | | Some key -> Some key 444 | | _ -> None 445 | | _ -> None 446 | 447 | /// Allows for pattern matching against properties 448 | let createDeconstruct = 449 | let deconstructMethodName ="Deconstruct" 450 | let outField str = sprintf "out%s" str 451 | let memberArgs = 452 | let arg argName fieldTy = 453 | let named = SynPatRcd.CreateNamed(Ident.Create argName, SynPatRcd.CreateWild ) 454 | let typ = SynType.CreateApp(SynType.CreateLongIdent "outref", [SynType.Anon(range0)], false ) 455 | SynPatRcd.CreateTyped(named, typ) 456 | 457 | 458 | fields 459 | |> Seq.map(fun f -> 460 | let rcd = f.ToRcd 461 | let x = rcd.Id |> Option.get 462 | let argRcd = arg (outField x.idText) rcd.Type 463 | argRcd 464 | ) 465 | |> Seq.toList 466 | |> SynPatRcd.CreateTuple 467 | |> SynPatRcd.CreateParen 468 | |> List.singleton 469 | let createInnerDataMemberVal = 470 | let memberFlags : MemberFlags = { 471 | IsInstance = true 472 | IsDispatchSlot = false 473 | IsOverrideOrExplicitImpl = false 474 | IsFinal = false 475 | MemberKind = MemberKind.Member 476 | } 477 | SynValData.SynValData(Some memberFlags, SynValInfo.Empty, None) 478 | let body = 479 | fields 480 | |> List.map(fun f -> 481 | let rcd = f.ToRcd 482 | let ident = rcd.Id |> Option.get 483 | let fieldTy = rcd.Type 484 | let fieldName = ident.idText 485 | let rightside = 486 | match getDeconstructType fieldTy with 487 | | Some _ -> 488 | LongIdentWithDots.Create [selfIden; fieldName; deconstructMethodName ] 489 | |> SynExpr.CreateInstanceMethodCall 490 | | None -> SynExpr.CreateLongIdent( LongIdentWithDots.Create [selfIden; fieldName ]) 491 | 492 | SynExpr.LongIdentSet (LongIdentWithDots.CreateString (outField fieldName), rightside, range0 ) 493 | ) 494 | |> SynExpr.CreateSequential 495 | 496 | let bindingRecord = { 497 | SynBindingRcd.Null with 498 | ValData = createInnerDataMemberVal 499 | Pattern = SynPatRcd.CreateLongIdent (LongIdentWithDots.Create [selfIden; deconstructMethodName], memberArgs) 500 | Expr = body 501 | XmlDoc = PreXmlDoc.Create ["This allows the class to be pattern matched against"] 502 | } 503 | let parentName = parent.Head.idText 504 | let synExprType = 505 | fields 506 | |> List.map(fun f -> 507 | let rcd = f.ToRcd 508 | rcd.Type 509 | ) 510 | |> DSL.createTypleSynType 511 | 512 | let fullTypeName = sprintf "%s.%s" (String.Join('.', parentNamespace)) parentName 513 | knownDeconstructs.Add(fullTypeName) 514 | 515 | SynMemberDefn.CreateMember(bindingRecord) 516 | 517 | let members = [ 518 | createCtor 519 | yield! createGetSetMembersFromRecord 520 | createOverrideGetHashCode 521 | createOverrideEquals 522 | createDeconstruct 523 | createInterfaceImpl 524 | ] 525 | 526 | SynModuleDecl.CreateType(info, members) 527 | 528 | 529 | let createJsonWrapperClass (namespaceId: LongIdent) (typeDefn: SynTypeDefn) = 530 | let (TypeDefn(synComponentInfo, synTypeDefnRepr, _members, _range)) = typeDefn 531 | let (ComponentInfo(_attributes, _typeParams, _constraints, recordId, _doc, _preferPostfix, _access, _range)) = synComponentInfo 532 | 533 | match synTypeDefnRepr with 534 | | SynTypeDefnRepr.Simple(SynTypeDefnSimpleRepr.Record(_accessibility, recordFields, _recordRange), _range) -> 535 | 536 | let createWrapperClass = createWrapperClass namespaceId recordId recordFields 537 | 538 | let declarations = [ 539 | createWrapperClass 540 | ] 541 | 542 | declarations 543 | | _ -> failwithf "Not a record type" 544 | 545 | 546 | [] 547 | module Generator = 548 | type JsonWrapperAttribute() = 549 | inherit Attribute() 550 | 551 | [] 552 | type JsonWrapperGenerator() = 553 | 554 | interface IMyriadGenerator with 555 | member __.Generate(namespace', ast: ParsedInput) = 556 | let findRecordsByOurMarkerAttribute xs = 557 | xs 558 | |> List.map(fun (ident, rcds) -> 559 | ident, rcds |> List.filter (Ast.hasAttribute) 560 | ) 561 | 562 | let groupRecordsByNamespace xs = 563 | xs 564 | |> List.map((fun (ident : LongIdent, records) -> String.Join('.', ident), records)) 565 | |> List.groupBy(fst) 566 | |> List.map(fun (key, (xs)) -> 567 | Ident.CreateLong(key), xs |> List.collect(snd) 568 | ) 569 | 570 | let filterOutEmptyModules xs = 571 | xs 572 | |> List.choose(fun (ident,records) -> 573 | match records with 574 | | [] -> None 575 | | _ -> Some(ident,records) 576 | ) 577 | 578 | let namespaceAndrecords = 579 | Ast.extractRecords ast 580 | |> findRecordsByOurMarkerAttribute 581 | |> groupRecordsByNamespace 582 | |> filterOutEmptyModules 583 | 584 | let moduleTrie = 585 | namespaceAndrecords 586 | |> ModuleTree.fromExtractRecords 587 | 588 | let rec createModulesAndClasses (moduleTree) = 589 | ([], moduleTree) 590 | ||> List.fold(fun state x -> 591 | state @ 592 | match x with 593 | | Module(name, mods) -> 594 | [ 595 | let moduleId = SynComponentInfoRcd.Create (Ident.CreateLong name) 596 | let decls = createModulesAndClasses mods 597 | SynModuleDecl.CreateNestedModule(moduleId, decls) 598 | ] 599 | | Class (ns, rcd) -> 600 | Create.createJsonWrapperClass ns rcd 601 | ) 602 | 603 | 604 | 605 | let openNamespaces = [ 606 | DSL.noWarn ["0058"] //TODO: https://github.com/BinaryDefense/JsonWrapper/issues/2 607 | DSL.openNamespace (LongIdentWithDots.CreateString (System.``namespace``) ) 608 | DSL.openNamespace (LongIdentWithDots.CreateString (JToken.``namespace``) ) 609 | DSL.openNamespace (LongIdentWithDots.CreateString (typeof.Namespace) ) 610 | DSL.openNamespace (LongIdentWithDots.CreateString (IHaveJToken.``namespace``) ) 611 | ] 612 | 613 | let namespaceOrModule = 614 | {SynModuleOrNamespaceRcd.CreateNamespace(Ident.CreateLong namespace') 615 | with 616 | IsRecursive = true 617 | Declarations = [ 618 | yield! openNamespaces 619 | yield! createModulesAndClasses moduleTrie 620 | ] } 621 | 622 | namespaceOrModule 623 | 624 | -------------------------------------------------------------------------------- /src/BinaryDefense.Myriad.Plugins.JsonWrapper/BinaryDefense.Myriad.Plugins.JsonWrapper.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | true 6 | true 7 | embedded 8 | preview 9 | 10 | 11 | BinaryDefense.Myriad.Plugins.JsonWrapper 12 | A plugin for [Myriad](https://github.com/MoiraeSoftware/myriad) for generating statically typed lossless wrappers around JToken given a schema. 13 | 14 | 15 | true 16 | true 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | true 26 | %(Identity) 27 | true 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/BinaryDefense.Myriad.Plugins.JsonWrapper/build/BinaryDefense.Myriad.Plugins.JsonWrapper.InTest.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/BinaryDefense.Myriad.Plugins.JsonWrapper/build/BinaryDefense.Myriad.Plugins.JsonWrapper.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/BinaryDefense.Myriad.Plugins.JsonWrapper/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | Microsoft.SourceLink.GitHub 3 | Microsoft.NETFramework.ReferenceAssemblies 4 | Myriad.Core 5 | Myriad.Sdk 6 | Newtonsoft.Json 7 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | false 6 | 7 | true 8 | true 9 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | true 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/JsonWrapperPlugin.Tests/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | // Auto-Generated by FAKE; do not edit 2 | namespace System 3 | open System.Reflection 4 | 5 | [] 6 | [] 7 | [] 8 | [] 9 | [] 10 | [] 11 | [] 12 | [] 13 | do () 14 | 15 | module internal AssemblyVersionInformation = 16 | let [] AssemblyTitle = "JsonWrapperPlugin.Tests" 17 | let [] AssemblyProduct = "BinaryDefense.JsonWrapper" 18 | let [] AssemblyVersion = "0.1.1" 19 | let [] AssemblyMetadata_ReleaseDate = "2020-06-05T00:00:00.0000000-04:00" 20 | let [] AssemblyFileVersion = "0.1.1" 21 | let [] AssemblyInformationalVersion = "0.1.1" 22 | let [] AssemblyMetadata_ReleaseChannel = "beta" 23 | let [] AssemblyMetadata_GitHash = "2eb4f5aa9e8883f024409339b3b1a53aaf91562f" 24 | -------------------------------------------------------------------------------- /tests/JsonWrapperPlugin.Tests/Generated.fs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // This code was generated by myriad. 3 | // Changes to this file will be lost when the code is regenerated. 4 | //------------------------------------------------------------------------------ 5 | namespace rec DataSchema 6 | 7 | #nowarn "0058" 8 | open System 9 | open Newtonsoft.Json.Linq 10 | open Newtonsoft.Json 11 | open BinaryDefense.JsonWrapper.Core 12 | 13 | module Example = 14 | type SimpleSchema(jtoken: JToken, serializer: JsonSerializer) = 15 | member this.one 16 | with get () = 17 | let selectedToken = jtoken.["one"] 18 | selectedToken.ToObject serializer 19 | and set (newValue: int) = 20 | jtoken.["one"] <- JToken.FromObject(newValue, serializer) 21 | 22 | member this.two 23 | with get () = 24 | let selectedToken = jtoken.["two"] 25 | selectedToken.ToObject serializer 26 | and set (newValue: string) = 27 | jtoken.["two"] <- JToken.FromObject(newValue, serializer) 28 | 29 | member this.three 30 | with get () = 31 | let selectedToken = jtoken.["three"] 32 | selectedToken.ToObject serializer 33 | and set (newValue: System.Guid) = 34 | jtoken.["three"] <- JToken.FromObject(newValue, serializer) 35 | 36 | override this.GetHashCode () = jtoken.GetHashCode() 37 | 38 | override this.Equals(objToCompare: obj) = 39 | match objToCompare with 40 | | :? IHaveJToken as jTokenToCompare -> JToken.DeepEquals(jTokenToCompare.InnerData, jtoken) 41 | | _ -> false 42 | 43 | ///This allows the class to be pattern matched against 44 | member this.Deconstruct(outone: outref<_>, outtwo: outref<_>, outthree: outref<_>) = 45 | outone <- this.one 46 | outtwo <- this.two 47 | outthree <- this.three 48 | 49 | interface IHaveJToken with 50 | override this.InnerData = jtoken 51 | 52 | type DifferentBackingFieldSchema(jtoken: JToken, serializer: JsonSerializer) = 53 | member this.one 54 | with get () = 55 | let selectedToken = jtoken.["not_one"] 56 | selectedToken.ToObject serializer 57 | and set (newValue: int) = 58 | jtoken.["not_one"] <- JToken.FromObject(newValue, serializer) 59 | 60 | member this.two 61 | with get () = 62 | let selectedToken = jtoken.["two"] 63 | selectedToken.ToObject serializer 64 | and set (newValue: int) = 65 | jtoken.["two"] <- JToken.FromObject(newValue, serializer) 66 | 67 | override this.GetHashCode () = jtoken.GetHashCode() 68 | 69 | override this.Equals(objToCompare: obj) = 70 | match objToCompare with 71 | | :? IHaveJToken as jTokenToCompare -> JToken.DeepEquals(jTokenToCompare.InnerData, jtoken) 72 | | _ -> false 73 | 74 | ///This allows the class to be pattern matched against 75 | member this.Deconstruct(outone: outref<_>, outtwo: outref<_>) = 76 | outone <- this.one 77 | outtwo <- this.two 78 | 79 | interface IHaveJToken with 80 | override this.InnerData = jtoken 81 | 82 | type NullableFieldSchema(jtoken: JToken, serializer: JsonSerializer) = 83 | member this.one 84 | with get () = 85 | let selectedToken = jtoken.["one"] 86 | selectedToken.ToObject> serializer 87 | and set (newValue: System.Nullable) = 88 | jtoken.["one"] <- JToken.FromObject(newValue, serializer) 89 | 90 | member this.two 91 | with get () = 92 | let selectedToken = jtoken.["two"] 93 | selectedToken.ToObject serializer 94 | and set (newValue: string) = 95 | jtoken.["two"] <- JToken.FromObject(newValue, serializer) 96 | 97 | override this.GetHashCode () = jtoken.GetHashCode() 98 | 99 | override this.Equals(objToCompare: obj) = 100 | match objToCompare with 101 | | :? IHaveJToken as jTokenToCompare -> JToken.DeepEquals(jTokenToCompare.InnerData, jtoken) 102 | | _ -> false 103 | 104 | ///This allows the class to be pattern matched against 105 | member this.Deconstruct(outone: outref<_>, outtwo: outref<_>) = 106 | outone <- this.one 107 | outtwo <- this.two 108 | 109 | interface IHaveJToken with 110 | override this.InnerData = jtoken 111 | 112 | type NullableMissingFieldSchema(jtoken: JToken, serializer: JsonSerializer) = 113 | member this.one 114 | with get () = 115 | let selectedToken = jtoken.["one"] 116 | if isNull selectedToken 117 | then MissingJsonFieldException("one", jtoken) |> raise 118 | selectedToken.ToObject> serializer 119 | and set (newValue: System.Nullable) = 120 | jtoken.["one"] <- JToken.FromObject(newValue, serializer) 121 | 122 | member this.two 123 | with get () = 124 | let selectedToken = jtoken.["two"] 125 | selectedToken.ToObject serializer 126 | and set (newValue: int) = 127 | jtoken.["two"] <- JToken.FromObject(newValue, serializer) 128 | 129 | override this.GetHashCode () = jtoken.GetHashCode() 130 | 131 | override this.Equals(objToCompare: obj) = 132 | match objToCompare with 133 | | :? IHaveJToken as jTokenToCompare -> JToken.DeepEquals(jTokenToCompare.InnerData, jtoken) 134 | | _ -> false 135 | 136 | ///This allows the class to be pattern matched against 137 | member this.Deconstruct(outone: outref<_>, outtwo: outref<_>) = 138 | outone <- this.one 139 | outtwo <- this.two 140 | 141 | interface IHaveJToken with 142 | override this.InnerData = jtoken 143 | 144 | type OptionalFieldSchema(jtoken: JToken, serializer: JsonSerializer) = 145 | member this.one 146 | with get () = 147 | let selectedToken = jtoken.["one"] 148 | selectedToken.ToObject serializer 149 | and set (newValue: int option) = 150 | jtoken.["one"] <- JToken.FromObject(newValue, serializer) 151 | 152 | member this.two 153 | with get () = 154 | let selectedToken = jtoken.["two"] 155 | selectedToken.ToObject serializer 156 | and set (newValue: int) = 157 | jtoken.["two"] <- JToken.FromObject(newValue, serializer) 158 | 159 | override this.GetHashCode () = jtoken.GetHashCode() 160 | 161 | override this.Equals(objToCompare: obj) = 162 | match objToCompare with 163 | | :? IHaveJToken as jTokenToCompare -> JToken.DeepEquals(jTokenToCompare.InnerData, jtoken) 164 | | _ -> false 165 | 166 | ///This allows the class to be pattern matched against 167 | member this.Deconstruct(outone: outref<_>, outtwo: outref<_>) = 168 | outone <- this.one 169 | outtwo <- this.two 170 | 171 | interface IHaveJToken with 172 | override this.InnerData = jtoken 173 | 174 | type InnerType(jtoken: JToken, serializer: JsonSerializer) = 175 | member this.one 176 | with get () = 177 | let selectedToken = jtoken.["one"] 178 | selectedToken.ToObject serializer 179 | and set (newValue: int option) = 180 | jtoken.["one"] <- JToken.FromObject(newValue, serializer) 181 | 182 | member this.two 183 | with get () = 184 | let selectedToken = jtoken.["two"] 185 | selectedToken.ToObject serializer 186 | and set (newValue: string) = 187 | jtoken.["two"] <- JToken.FromObject(newValue, serializer) 188 | 189 | override this.GetHashCode () = jtoken.GetHashCode() 190 | 191 | override this.Equals(objToCompare: obj) = 192 | match objToCompare with 193 | | :? IHaveJToken as jTokenToCompare -> JToken.DeepEquals(jTokenToCompare.InnerData, jtoken) 194 | | _ -> false 195 | 196 | ///This allows the class to be pattern matched against 197 | member this.Deconstruct(outone: outref<_>, outtwo: outref<_>) = 198 | outone <- this.one 199 | outtwo <- this.two 200 | 201 | interface IHaveJToken with 202 | override this.InnerData = jtoken 203 | 204 | type OuterType(jtoken: JToken, serializer: JsonSerializer) = 205 | member this.foo 206 | with get () = 207 | let selectedToken = jtoken.["foo"] 208 | selectedToken.ToObject serializer 209 | and set (newValue: InnerType) = 210 | jtoken.["foo"] <- JToken.FromObject(newValue, serializer) 211 | 212 | member this.count 213 | with get () = 214 | let selectedToken = jtoken.["count"] 215 | selectedToken.ToObject serializer 216 | and set (newValue: int) = 217 | jtoken.["count"] <- JToken.FromObject(newValue, serializer) 218 | 219 | override this.GetHashCode () = jtoken.GetHashCode() 220 | 221 | override this.Equals(objToCompare: obj) = 222 | match objToCompare with 223 | | :? IHaveJToken as jTokenToCompare -> JToken.DeepEquals(jTokenToCompare.InnerData, jtoken) 224 | | _ -> false 225 | 226 | ///This allows the class to be pattern matched against 227 | member this.Deconstruct(outfoo: outref<_>, outcount: outref<_>) = 228 | outfoo <- this.foo.Deconstruct() 229 | outcount <- this.count 230 | 231 | interface IHaveJToken with 232 | override this.InnerData = jtoken 233 | 234 | module Nested = 235 | module OneThing = 236 | type Data(jtoken: JToken, serializer: JsonSerializer) = 237 | member this.Foo 238 | with get () = 239 | let selectedToken = jtoken.["Foo"] 240 | selectedToken.ToObject serializer 241 | and set (newValue: string) = 242 | jtoken.["Foo"] <- JToken.FromObject(newValue, serializer) 243 | 244 | override this.GetHashCode () = jtoken.GetHashCode() 245 | 246 | override this.Equals(objToCompare: obj) = 247 | match objToCompare with 248 | | :? IHaveJToken as jTokenToCompare -> JToken.DeepEquals(jTokenToCompare.InnerData, jtoken) 249 | | _ -> false 250 | 251 | ///This allows the class to be pattern matched against 252 | member this.Deconstruct(outFoo: outref<_>) = outFoo <- this.Foo 253 | 254 | interface IHaveJToken with 255 | override this.InnerData = jtoken 256 | 257 | type Data2(jtoken: JToken, serializer: JsonSerializer) = 258 | member this.Foo 259 | with get () = 260 | let selectedToken = jtoken.["Foo"] 261 | selectedToken.ToObject serializer 262 | and set (newValue: string) = 263 | jtoken.["Foo"] <- JToken.FromObject(newValue, serializer) 264 | 265 | member this.Another 266 | with get () = 267 | let selectedToken = jtoken.["Another"] 268 | selectedToken.ToObject serializer 269 | and set (newValue: Data) = 270 | jtoken.["Another"] <- JToken.FromObject(newValue, serializer) 271 | 272 | override this.GetHashCode () = jtoken.GetHashCode() 273 | 274 | override this.Equals(objToCompare: obj) = 275 | match objToCompare with 276 | | :? IHaveJToken as jTokenToCompare -> JToken.DeepEquals(jTokenToCompare.InnerData, jtoken) 277 | | _ -> false 278 | 279 | ///This allows the class to be pattern matched against 280 | member this.Deconstruct(outFoo: outref<_>, outAnother: outref<_>) = 281 | outFoo <- this.Foo 282 | outAnother <- this.Another.Deconstruct() 283 | 284 | interface IHaveJToken with 285 | override this.InnerData = jtoken 286 | 287 | module MORE = 288 | type Data2(jtoken: JToken, serializer: JsonSerializer) = 289 | member this.Foo 290 | with get () = 291 | let selectedToken = jtoken.["Foo"] 292 | selectedToken.ToObject serializer 293 | and set (newValue: string) = 294 | jtoken.["Foo"] <- JToken.FromObject(newValue, serializer) 295 | 296 | member this.Bar 297 | with get () = 298 | let selectedToken = jtoken.["Bar"] 299 | selectedToken.ToObject serializer 300 | and set (newValue: System.DateTimeOffset) = 301 | jtoken.["Bar"] <- JToken.FromObject(newValue, serializer) 302 | 303 | override this.GetHashCode () = jtoken.GetHashCode() 304 | 305 | override this.Equals(objToCompare: obj) = 306 | match objToCompare with 307 | | :? IHaveJToken as jTokenToCompare -> JToken.DeepEquals(jTokenToCompare.InnerData, jtoken) 308 | | _ -> false 309 | 310 | ///This allows the class to be pattern matched against 311 | member this.Deconstruct(outFoo: outref<_>, outBar: outref<_>) = 312 | outFoo <- this.Foo 313 | outBar <- this.Bar 314 | 315 | interface IHaveJToken with 316 | override this.InnerData = jtoken 317 | 318 | module TwoThing = 319 | type Data(jtoken: JToken, serializer: JsonSerializer) = 320 | member this.Fee 321 | with get () = 322 | let selectedToken = jtoken.["Fee"] 323 | selectedToken.ToObject serializer 324 | and set (newValue: int) = 325 | jtoken.["Fee"] <- JToken.FromObject(newValue, serializer) 326 | 327 | override this.GetHashCode () = jtoken.GetHashCode() 328 | 329 | override this.Equals(objToCompare: obj) = 330 | match objToCompare with 331 | | :? IHaveJToken as jTokenToCompare -> JToken.DeepEquals(jTokenToCompare.InnerData, jtoken) 332 | | _ -> false 333 | 334 | ///This allows the class to be pattern matched against 335 | member this.Deconstruct(outFee: outref<_>) = outFee <- this.Fee 336 | 337 | interface IHaveJToken with 338 | override this.InnerData = jtoken 339 | 340 | type Data2(jtoken: JToken, serializer: JsonSerializer) = 341 | member this.Bar 342 | with get () = 343 | let selectedToken = jtoken.["Bar"] 344 | selectedToken.ToObject serializer 345 | and set (newValue: string) = 346 | jtoken.["Bar"] <- JToken.FromObject(newValue, serializer) 347 | 348 | member this.Another 349 | with get () = 350 | let selectedToken = jtoken.["Another"] 351 | selectedToken.ToObject serializer 352 | and set (newValue: OneThing.Data) = 353 | jtoken.["Another"] <- JToken.FromObject(newValue, serializer) 354 | 355 | member this.MORE 356 | with get () = 357 | let selectedToken = jtoken.["MORE"] 358 | selectedToken.ToObject serializer 359 | and set (newValue: OneThing.MORE.Data2) = 360 | jtoken.["MORE"] <- JToken.FromObject(newValue, serializer) 361 | 362 | member this.Another22 363 | with get () = 364 | let selectedToken = jtoken.["Another22"] 365 | selectedToken.ToObject serializer 366 | and set (newValue: Data) = 367 | jtoken.["Another22"] <- JToken.FromObject(newValue, serializer) 368 | 369 | override this.GetHashCode () = jtoken.GetHashCode() 370 | 371 | override this.Equals(objToCompare: obj) = 372 | match objToCompare with 373 | | :? IHaveJToken as jTokenToCompare -> JToken.DeepEquals(jTokenToCompare.InnerData, jtoken) 374 | | _ -> false 375 | 376 | ///This allows the class to be pattern matched against 377 | member this.Deconstruct( 378 | outBar: outref<_>, 379 | outAnother: outref<_>, 380 | outMORE: outref<_>, 381 | outAnother22: outref<_>) 382 | = 383 | outBar <- this.Bar 384 | outAnother <- this.Another.Deconstruct() 385 | outMORE <- this.MORE.Deconstruct() 386 | outAnother22 <- this.Another22.Deconstruct() 387 | 388 | interface IHaveJToken with 389 | override this.InnerData = jtoken 390 | 391 | module Mixed = 392 | module Foo = 393 | type Hello(jtoken: JToken, serializer: JsonSerializer) = 394 | member this.Foo 395 | with get () = 396 | let selectedToken = jtoken.["Foo"] 397 | selectedToken.ToObject serializer 398 | and set (newValue: string) = 399 | jtoken.["Foo"] <- JToken.FromObject(newValue, serializer) 400 | 401 | override this.GetHashCode () = jtoken.GetHashCode() 402 | 403 | override this.Equals(objToCompare: obj) = 404 | match objToCompare with 405 | | :? IHaveJToken as jTokenToCompare -> JToken.DeepEquals(jTokenToCompare.InnerData, jtoken) 406 | | _ -> false 407 | 408 | ///This allows the class to be pattern matched against 409 | member this.Deconstruct(outFoo: outref<_>) = outFoo <- this.Foo 410 | 411 | interface IHaveJToken with 412 | override this.InnerData = jtoken 413 | -------------------------------------------------------------------------------- /tests/JsonWrapperPlugin.Tests/JsonWrapperPlugin.Tests.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | preview 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Types.fs 15 | DataSchema 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/JsonWrapperPlugin.Tests/Main.fs: -------------------------------------------------------------------------------- 1 | module ExpectoTemplate 2 | 3 | open Expecto 4 | 5 | [] 6 | let main argv = Tests.runTestsInAssembly defaultConfig argv 7 | -------------------------------------------------------------------------------- /tests/JsonWrapperPlugin.Tests/Tests.fs: -------------------------------------------------------------------------------- 1 | module Tests 2 | 3 | open System 4 | open Expecto 5 | open DataSchema 6 | open Newtonsoft.Json 7 | open Newtonsoft.Json.Linq 8 | open BinaryDefense.JsonWrapper.Core 9 | open Myriad.Plugins 10 | open DataSchema.Example 11 | 12 | let scrubDefaultDUConverter (s: System.Collections.Generic.IList) = 13 | s 14 | |> Seq.tryFind (fun c -> c.GetType () = typeof) 15 | |> Option.iter (s.Remove >> ignore) 16 | 17 | // Serializer Settings 18 | let converters = Converters.recommendedConverters 19 | 20 | let serializationSettings = 21 | let s = JsonSerializerSettings() 22 | scrubDefaultDUConverter s.Converters 23 | for c in converters do s.Converters.Add c 24 | s 25 | 26 | let jsonSettings = serializationSettings 27 | let jsonSerializer =JsonSerializer.CreateDefault jsonSettings 28 | 29 | 30 | [] 31 | let simpleTests = 32 | testList "simple schema" [ 33 | let jsonStr = 34 | """{ 35 | "one": 42, 36 | "two": "Hitchhikers Guide", 37 | "three": "f971a6c0-ed00-46e5-b657-3fea2e368ba9" 38 | } 39 | """ 40 | testCase "Gets keys it knows about" <| fun _ -> 41 | let jtoken = JToken.Parse jsonStr 42 | 43 | let test1 = SimpleSchema(jtoken, jsonSerializer) 44 | 45 | Expect.equal test1.one 42 "" 46 | Expect.equal test1.two "Hitchhikers Guide" "" 47 | Expect.equal test1.three (Guid.Parse "f971a6c0-ed00-46e5-b657-3fea2e368ba9") "" 48 | testCase "Set keys works" <| fun _ -> 49 | let jtoken = JToken.Parse jsonStr 50 | 51 | let test1 = SimpleSchema(jtoken, jsonSerializer) 52 | 53 | Expect.equal test1.one 42 "" 54 | test1.one <- 100 55 | Expect.equal test1.one 100 "" 56 | Expect.equal (jtoken.Value("one")) 100 "" 57 | 58 | testCase "Deconstruct" <| fun _ -> 59 | let jtoken = JToken.Parse jsonStr 60 | 61 | let test1 = SimpleSchema(jtoken, jsonSerializer) 62 | let foo = System.Guid.Parse("f971a6c0-ed00-46e5-b657-3fea2e368ba9") 63 | match test1.Deconstruct() with 64 | | (42, "Hitchhikers Guide", foo) -> () 65 | | fallthru -> failwithf "Couldn't match %A" fallthru 66 | ] 67 | 68 | [] 69 | let jsonPropertyTests = 70 | testList "JsonProperty tests" [ 71 | let jsonStr = 72 | """{ 73 | "one": 42, 74 | "not_one": 9001, 75 | "two" : -1000 76 | } 77 | """ 78 | testCase "Gets keys supplied by user" <| fun _ -> 79 | let jtoken = JToken.Parse jsonStr 80 | 81 | let test1 = DifferentBackingFieldSchema(jtoken, jsonSerializer) 82 | 83 | Expect.equal test1.one 9001 "" 84 | Expect.equal test1.two -1000 "" 85 | 86 | testCase "Set keys works" <| fun _ -> 87 | let jtoken = JToken.Parse jsonStr 88 | 89 | let test1 = DifferentBackingFieldSchema(jtoken, jsonSerializer) 90 | 91 | Expect.equal test1.one 9001 "" 92 | test1.one <- 100 93 | Expect.equal test1.one 100 "" 94 | Expect.equal (jtoken.Value("one")) 42 "" 95 | Expect.equal (jtoken.Value("not_one")) 100 "" 96 | ] 97 | 98 | 99 | 100 | [] 101 | let nullablePropertyFieldExistTests = 102 | testList "Nullable Field Exists Property tests" [ 103 | let jsonStr = 104 | """{ 105 | "one": null, 106 | "two" : null 107 | } 108 | """ 109 | let nullInt = (Nullable()) 110 | let inline nullable x = Nullable<_> x 111 | testCase "Gets keys supplied by user" <| fun _ -> 112 | let jtoken = JToken.Parse jsonStr 113 | 114 | let test1 = NullableFieldSchema(jtoken, jsonSerializer) 115 | 116 | Expect.equal test1.one nullInt "" 117 | Expect.equal test1.two null "" 118 | 119 | testCase "Set keys works" <| fun _ -> 120 | let jtoken = JToken.Parse jsonStr 121 | 122 | let test1 = NullableFieldSchema(jtoken, jsonSerializer) 123 | 124 | Expect.equal test1.one nullInt "" 125 | test1.one <- nullable 100 126 | Expect.equal test1.one (nullable 100) "" 127 | Expect.equal (jtoken.Value>("one")) (nullable 100) "" 128 | ] 129 | 130 | 131 | 132 | [] 133 | let nullablePropertyFieldDoesNotExistTests = 134 | testList "Nullable Field Does Not Exists Property tests" [ 135 | let jsonStr = 136 | """{ 137 | "two" : -1000 138 | } 139 | """ 140 | let nullInt = (Nullable()) 141 | let inline nullable x = Nullable<_> x 142 | testCase "Gets keys supplied by user" <| fun _ -> 143 | let jtoken = JToken.Parse jsonStr 144 | 145 | let action () = 146 | let test1 = NullableMissingFieldSchema(jtoken, jsonSerializer) 147 | 148 | Expect.equal test1.one nullInt "" 149 | Expect.equal test1.two -1000 "" 150 | 151 | Expect.throwsT action "" 152 | 153 | testCase "Set keys works" <| fun _ -> 154 | let jtoken = JToken.Parse jsonStr 155 | 156 | let test1 = NullableMissingFieldSchema(jtoken, jsonSerializer) 157 | 158 | test1.one <- nullable 100 159 | Expect.equal test1.one (nullable 100) "" 160 | Expect.equal (jtoken.Value>("one")) (nullable 100) "" 161 | ] 162 | 163 | 164 | 165 | [] 166 | let optionPropertyTests = 167 | testList "Option Field Exists Property tests" [ 168 | let jsonStr = 169 | """{ 170 | "one" : null, 171 | "two" : -1000 172 | } 173 | """ 174 | let nullInt = (Nullable()) 175 | let inline nullable x = Nullable<_> x 176 | testCase "Gets keys supplied by user" <| fun _ -> 177 | let jtoken = JToken.Parse jsonStr 178 | 179 | let test1 = OptionalFieldSchema(jtoken, jsonSerializer) 180 | 181 | Expect.equal test1.one None "" 182 | Expect.equal test1.two -1000 "" 183 | 184 | testCase "Set keys works" <| fun _ -> 185 | let jtoken = JToken.Parse jsonStr 186 | 187 | let test1 = OptionalFieldSchema(jtoken, jsonSerializer) 188 | 189 | test1.one <- Some 100 190 | Expect.equal test1.one (Some 100) "" 191 | Expect.equal (jtoken.["one"].ToObject<_>(jsonSerializer)) (Some 100) "" 192 | ] 193 | 194 | 195 | 196 | [] 197 | let traversalTests = 198 | testList "traversal tests" [ 199 | let innerJsonStr = 200 | """ { 201 | "one" : null, 202 | "two" : "-1000" 203 | } 204 | """ 205 | let jsonStr = 206 | sprintf """{ 207 | "foo" : %s, 208 | "count" : 10 209 | } 210 | """ innerJsonStr 211 | testCase "Gets keys supplied by user" <| fun _ -> 212 | let outerJToken = JToken.Parse jsonStr 213 | 214 | let outer = OuterType(outerJToken, jsonSerializer) 215 | 216 | let innerJToken = JToken.Parse innerJsonStr 217 | let innerExpected = InnerType(innerJToken, jsonSerializer) 218 | let innerActual = outer.foo 219 | 220 | Expect.equal innerActual.one innerExpected.one "" 221 | Expect.equal innerActual.two innerExpected.two "" 222 | 223 | Expect.equal innerActual innerExpected "" 224 | 225 | testCase "Deconstruct" <| fun _ -> 226 | let outerJToken = JToken.Parse jsonStr 227 | 228 | let outer = OuterType(outerJToken, jsonSerializer) 229 | match outer.Deconstruct() with 230 | | ((None,"-1000"), 10) -> () 231 | | fallthru -> failwithf "Couldn't match %A" fallthru 232 | ] 233 | 234 | 235 | // search tags: debug, debugger, attach 236 | module Debugging = 237 | let waitForDebuggerAttached (programName) = 238 | #if DEBUG 239 | if not(System.Diagnostics.Debugger.IsAttached) then 240 | printfn "Please attach a debugger for %s, PID: %d" programName (System.Diagnostics.Process.GetCurrentProcess().Id) 241 | while not(System.Diagnostics.Debugger.IsAttached) do 242 | System.Threading.Thread.Sleep(100) 243 | System.Diagnostics.Debugger.Break() 244 | #else 245 | () 246 | #endif 247 | 248 | 249 | // let createSynTypeDefn () = 250 | // SynComponentInfo.ComponentInfo() 251 | // SynTypeDefn.TypeDefn 252 | 253 | // [] 254 | // let moduleTreeTests = 255 | // ftestList "ModuleTree tests" [ 256 | // testCase "test 1" <| fun () -> 257 | // let input : list>= [ 258 | // (Ident.CreateLong "Nested.OneThing"), 259 | // [ 260 | // "LOL" 261 | // ] 262 | // ] 263 | // let expected = [ 264 | // ModuleTree.Module("Nested",[ 265 | // ModuleTree.Module("OneThing", [ 266 | // ModuleTree.Class "" 267 | // ]) 268 | // ]) 269 | // ] 270 | 271 | // let actual = ModuleTree.fromExtractRecords input 272 | // Expect.equal actual expected "" 273 | 274 | // testCase "test 2" <| fun () -> 275 | // let input : list>= [ 276 | // (Ident.CreateLong "Nested.OneThing"), ["lol"] 277 | // (Ident.CreateLong "Nested.TwoThing"), ["nope"] 278 | // ] 279 | // let expected = [ 280 | // ModuleTree.Module("Nested",[ 281 | // ModuleTree.Module("OneThing", [ 282 | // ModuleTree.Class "" 283 | // ]) 284 | // ModuleTree.Module("TwoThing", [ 285 | // ModuleTree.Class "" 286 | // ]) 287 | // ]) 288 | // ] 289 | // // Debugging.waitForDebuggerAttached "test2" 290 | // let actual = ModuleTree.fromExtractRecords input 291 | // Expect.equal actual expected "" 292 | // ] 293 | -------------------------------------------------------------------------------- /tests/JsonWrapperPlugin.Tests/Types.fs: -------------------------------------------------------------------------------- 1 | namespace Example 2 | open Myriad.Plugins 3 | open Newtonsoft.Json 4 | open BinaryDefense.JsonWrapper.Core 5 | 6 | module consts = 7 | let [] not_one = "not_one" 8 | 9 | [] 10 | type SimpleSchema = { 11 | one: int 12 | two: string 13 | three : System.Guid 14 | } 15 | 16 | [] 17 | type DifferentBackingFieldSchema = { 18 | //TODO: Support consts: https://github.com/MoiraeSoftware/myriad/issues/44 19 | // [] 20 | [] 21 | one: int 22 | two: int 23 | } 24 | 25 | [] 26 | type NullableFieldSchema = { 27 | one: System.Nullable 28 | two: string 29 | } 30 | 31 | 32 | [] 33 | type NullableMissingFieldSchema = { 34 | [] 35 | one: System.Nullable 36 | two: int 37 | } 38 | 39 | [] 40 | type OptionalFieldSchema = { 41 | one: int option 42 | two: int 43 | } 44 | 45 | 46 | [] 47 | type InnerType = { 48 | one: int option 49 | two: string 50 | } 51 | 52 | [] 53 | type OuterType = { 54 | foo: InnerType 55 | count : int 56 | } 57 | 58 | module Nested = 59 | module OneThing = 60 | [] 61 | type Data = { 62 | Foo : string 63 | } 64 | [] 65 | type Data2 = { 66 | Foo : string 67 | Another : Data 68 | } 69 | module MORE = 70 | 71 | [] 72 | type Data2 = { 73 | Foo : string 74 | Bar : System.DateTimeOffset 75 | } 76 | 77 | module TwoThing = 78 | [] 79 | type Data = { 80 | Fee : int 81 | } 82 | [] 83 | type Data2 = { 84 | Bar : string 85 | Another : OneThing.Data 86 | MORE : OneThing.MORE.Data2 87 | Another22 : Data 88 | } 89 | 90 | 91 | // Modules without the attribute should not be in our end result 92 | module Mixed = 93 | 94 | module Foo = 95 | [] 96 | type Hello = { 97 | Foo : string 98 | } 99 | module Bar = 100 | type Bye = { 101 | Bar : string 102 | } 103 | -------------------------------------------------------------------------------- /tests/JsonWrapperPlugin.Tests/coverage.netcoreapp3.1.xml.acv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryDefense/JsonWrapper/0101ca26c8fdca5cf6f492b0f4119af9fd4d5c32/tests/JsonWrapperPlugin.Tests/coverage.netcoreapp3.1.xml.acv -------------------------------------------------------------------------------- /tests/JsonWrapperPlugin.Tests/paket.references: -------------------------------------------------------------------------------- 1 | Expecto 2 | FSharp.Core 3 | dotnet-mono 4 | Microsoft.NET.Test.Sdk 5 | YoloDev.Expecto.TestSdk 6 | altcover 7 | Microsoft.NETFramework.ReferenceAssemblies 8 | Myriad.Core 9 | Myriad.Sdk 10 | Newtonsoft.Json 11 | --------------------------------------------------------------------------------