├── .gitattributes ├── .gitignore ├── .travis.yml ├── .vscode └── tasks.json ├── Build.ps1 ├── Install.ps1 ├── LICENSE ├── README.md ├── appveyor.yml ├── build.cmd ├── build.sh ├── global.json ├── messagetemplates-fsharp.sln ├── src ├── FsMessageTemplates │ ├── FsMessageTemplates.fsproj │ ├── MessageTemplates.fs │ └── MessageTemplates.fsi └── FsMtParser │ ├── FsMtParser.fs │ ├── FsMtParser.fsproj │ ├── FsMtParserFull.fs │ ├── FsMtParserFullTest.fsx │ └── FsMtParserTest.fsx ├── test └── FsMessageTemplates.Tests │ ├── CsToFs.fs │ ├── FSharpTypesDestructuringPolicy.fs │ ├── FsMessageTemplates.Tests.fsproj │ ├── FsTests.Capture.fs │ ├── FsTests.Format.fs │ ├── FsTests.Parser.fs │ ├── MtAssert.fs │ ├── Tk.fs │ └── XunitSupport.fs └── tooling ├── configure-dotnet-cli-osx.sh ├── dotnet-cli-preview2 ├── dotnet-cli-preview2.sln ├── global.json ├── src │ ├── FsMessageTemplates │ │ ├── FsMessageTemplates.xproj │ │ └── project.json │ └── FsMtParser │ │ ├── FsMtParser.xproj │ │ └── project.json └── test │ └── FsMessageTemplates.Tests │ ├── FsMessageTemplates.Tests.xproj │ └── project.json └── net45 ├── FsMessageTemplates.net45.sln ├── FsMessageTemplates └── FsMessageTemplates.fsproj ├── FsMtParser └── FsMtParser.fsproj └── Tests ├── Tests.fsproj └── packages.config /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | [Dd]ebug/ 11 | [Dd]ebugPublic/ 12 | [Rr]elease/ 13 | x64/ 14 | build/ 15 | bld/ 16 | [Bb]in/ 17 | [Oo]bj/ 18 | 19 | # Roslyn cache directories 20 | *.ide/ 21 | # Visual Studio 2015 cache/options directory 22 | .vs/ 23 | 24 | # DNX 25 | project.lock.json 26 | artifacts/ 27 | 28 | # MSTest test Results 29 | [Tt]est[Rr]esult*/ 30 | [Bb]uild[Ll]og.* 31 | 32 | #NUNIT 33 | *.VisualState.xml 34 | TestResult.xml 35 | 36 | # Build Results of an ATL Project 37 | [Dd]ebugPS/ 38 | [Rr]eleasePS/ 39 | dlldata.c 40 | 41 | *_i.c 42 | *_p.c 43 | *_i.h 44 | *.ilk 45 | *.meta 46 | *.obj 47 | *.pch 48 | *.pdb 49 | *.pgc 50 | *.pgd 51 | *.rsp 52 | *.sbr 53 | *.tlb 54 | *.tli 55 | *.tlh 56 | *.tmp 57 | *.tmp_proj 58 | *.log 59 | *.vspscc 60 | *.vssscc 61 | .builds 62 | *.pidb 63 | *.svclog 64 | *.scc 65 | 66 | # Chutzpah Test files 67 | _Chutzpah* 68 | 69 | # Visual C++ cache files 70 | ipch/ 71 | *.aps 72 | *.ncb 73 | *.opensdf 74 | *.sdf 75 | *.cachefile 76 | 77 | # Visual Studio profiler 78 | *.psess 79 | *.vsp 80 | *.vspx 81 | 82 | # TFS 2012 Local Workspace 83 | $tf/ 84 | 85 | # Guidance Automation Toolkit 86 | *.gpState 87 | 88 | # ReSharper is a .NET coding add-in 89 | _ReSharper*/ 90 | *.[Rr]e[Ss]harper 91 | *.DotSettings.user 92 | 93 | # JustCode is a .NET coding addin-in 94 | .JustCode 95 | 96 | # TeamCity is a build add-in 97 | _TeamCity* 98 | 99 | # DotCover is a Code Coverage Tool 100 | *.dotCover 101 | 102 | # NCrunch 103 | _NCrunch_* 104 | .*crunch*.local.xml 105 | 106 | # MightyMoose 107 | *.mm.* 108 | AutoTest.Net/ 109 | 110 | # Web workbench (sass) 111 | .sass-cache/ 112 | 113 | # Installshield output folder 114 | [Ee]xpress/ 115 | 116 | # DocProject is a documentation generator add-in 117 | DocProject/buildhelp/ 118 | DocProject/Help/*.HxT 119 | DocProject/Help/*.HxC 120 | DocProject/Help/*.hhc 121 | DocProject/Help/*.hhk 122 | DocProject/Help/*.hhp 123 | DocProject/Help/Html2 124 | DocProject/Help/html 125 | 126 | # Click-Once directory 127 | publish/ 128 | 129 | # Publish Web Output 130 | *.[Pp]ublish.xml 131 | *.azurePubxml 132 | ## TODO: Comment the next line if you want to checkin your 133 | ## web deploy settings but do note that will include unencrypted 134 | ## passwords 135 | #*.pubxml 136 | 137 | # NuGet Packages Directory 138 | packages/* 139 | ## TODO: If the tool you use requires repositories.config 140 | ## uncomment the next line 141 | #!packages/repositories.config 142 | 143 | # Enable "build/" folder in the NuGet Packages folder since 144 | # NuGet packages use it for MSBuild targets. 145 | # This line needs to be after the ignore of the build folder 146 | # (and the packages folder if the line above has been uncommented) 147 | !packages/build/ 148 | 149 | # Windows Azure Build Output 150 | csx/ 151 | *.build.csdef 152 | 153 | # Windows Store app package directory 154 | AppPackages/ 155 | 156 | # Others 157 | sql/ 158 | *.Cache 159 | ClientBin/ 160 | [Ss]tyle[Cc]op.* 161 | ~$* 162 | *~ 163 | *.dbmdl 164 | *.dbproj.schemaview 165 | *.pfx 166 | *.publishsettings 167 | node_modules/ 168 | bower_components/ 169 | 170 | # RIA/Silverlight projects 171 | Generated_Code/ 172 | 173 | # Backup & report files from converting an old project file 174 | # to a newer Visual Studio version. Backup files are not needed, 175 | # because we have git ;-) 176 | _UpgradeReport_Files/ 177 | Backup*/ 178 | UpgradeLog*.XML 179 | UpgradeLog*.htm 180 | 181 | # SQL Server files 182 | *.mdf 183 | *.ldf 184 | 185 | # Business Intelligence projects 186 | *.rdl.data 187 | *.bim.layout 188 | *.bim_*.settings 189 | 190 | # Microsoft Fakes 191 | FakesAssemblies/ 192 | 193 | # LightSwitch generated files 194 | GeneratedArtifacts/ 195 | _Pvt_Extensions/ 196 | ModelManifest.xml 197 | 198 | /.dotnetcli 199 | /tooling/net45/packages 200 | *.userprefs 201 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | mono: none 3 | # we install dotnet SDK manually 4 | 5 | env: 6 | matrix: 7 | - CLI_VERSION=2.2.300 8 | 9 | #Ubuntu 14.04 10 | sudo: required 11 | dist: trusty 12 | 13 | #OS X 10.12 14 | osx_image: xcode9 15 | 16 | addons: 17 | apt: 18 | packages: 19 | - gettext 20 | - libcurl4-openssl-dev 21 | - libicu-dev 22 | - libssl-dev 23 | - libunwind8 24 | - zlib1g 25 | 26 | os: 27 | - osx 28 | - linux 29 | 30 | 31 | before_install: 32 | - if test "$TRAVIS_OS_NAME" == "osx"; then ./tooling/configure-dotnet-cli-osx.sh; fi 33 | # Download script to install dotnet cli 34 | - curl -L --create-dirs https://dot.net/v1/dotnet-install.sh -o ./scripts/obtain/install.sh 35 | - find ./scripts -name "*.sh" -exec chmod +x {} \; 36 | - export DOTNET_INSTALL_DIR="$PWD/.dotnetcli" 37 | - ./scripts/obtain/install.sh --version "$CLI_VERSION" --install-dir "$DOTNET_INSTALL_DIR" --no-path 38 | # add dotnet to PATH 39 | - export PATH="$DOTNET_INSTALL_DIR:$PATH" 40 | 41 | script: 42 | - ./build.sh 43 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "0.1.0", 5 | "osx": { 6 | "command": "./build.sh", 7 | "showOutput": "always", 8 | "isShellCommand": true, 9 | "tasks": [ 10 | { 11 | "taskName": "build", 12 | "problemMatcher": "$msCompile", 13 | "isBuildCommand": true 14 | } 15 | ] 16 | }, 17 | "windows": { 18 | "command": "powershell", 19 | "args": [ 20 | "-ExecutionPolicy", 21 | "Unrestricted", 22 | ".\\Build.ps1" 23 | ], 24 | "taskSelector": "/t:", 25 | "showOutput": "always", 26 | "tasks": [ 27 | { 28 | "taskName": "build", 29 | // Show the output window only if unrecognized errors occur. 30 | "showOutput": "silent", 31 | // Use the standard MS compiler pattern to detect errors, warnings and infos 32 | "problemMatcher": "$msCompile" 33 | } 34 | ] 35 | } 36 | } -------------------------------------------------------------------------------- /Build.ps1: -------------------------------------------------------------------------------- 1 | echo "build: Build started" 2 | 3 | Push-Location $PSScriptRoot 4 | 5 | if(Test-Path .\artifacts) { 6 | echo "build: Cleaning .\artifacts" 7 | Remove-Item .\artifacts -Force -Recurse 8 | } 9 | 10 | & dotnet --version 11 | & dotnet restore --no-cache 12 | 13 | $branch = @{ $true = $env:APPVEYOR_REPO_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$env:APPVEYOR_REPO_BRANCH -ne $NULL]; 14 | $revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "local" }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL]; 15 | $suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and $revision -ne "local"] 16 | $commitHash = $(git rev-parse --short HEAD) 17 | $buildSuffix = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[$suffix -ne ""] 18 | 19 | echo "build: Package version suffix is $suffix" 20 | echo "build: Build version suffix is $buildSuffix" 21 | 22 | foreach ($src in ls src/*) { 23 | Push-Location $src 24 | 25 | echo "build: Packaging project in $src" 26 | 27 | & dotnet build -c Release --version-suffix=$buildSuffix 28 | 29 | if($suffix) { 30 | & dotnet pack -c Release --include-source --no-build -o ..\..\artifacts --version-suffix=$suffix 31 | } else { 32 | & dotnet pack -c Release --include-source --no-build -o ..\..\artifacts 33 | } 34 | if($LASTEXITCODE -ne 0) { exit 1 } 35 | 36 | Pop-Location 37 | } 38 | 39 | foreach ($test in ls test/*.Tests) { 40 | Push-Location $test 41 | 42 | echo "build: Testing project in $test" 43 | 44 | & dotnet test -c Release 45 | if($LASTEXITCODE -ne 0) { exit 3 } 46 | 47 | Pop-Location 48 | } 49 | 50 | Pop-Location 51 | -------------------------------------------------------------------------------- /Install.ps1: -------------------------------------------------------------------------------- 1 | mkdir -Force ".\build\" | Out-Null 2 | Invoke-WebRequest "https://dot.net/v1/dotnet-install.ps1" -OutFile ".\build\installcli.ps1" 3 | $env:DOTNET_INSTALL_DIR = "$pwd\.dotnetcli" 4 | & .\build\installcli.ps1 -InstallDir "$env:DOTNET_INSTALL_DIR" -NoPath -Version 2.2.300 5 | $env:Path = "$env:DOTNET_INSTALL_DIR;$env:Path" 6 | 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # FsMessageTemplates 3 | 4 | An implementation of {named} string replacements, which allows formatting, parsing, and capturing properties. 5 | 6 | `FsMessageTemplates` is compatible with the [Message Templates Standard](http://messagetemplates.org/). 7 | 8 | The F# implementation was designed to be paket-github referenced, or installed as a nuget package. 9 | 10 | [![Build status](https://ci.appveyor.com/api/projects/status/e2y2xpegw081p0tl?svg=true)](https://ci.appveyor.com/project/adamchester/messagetemplates-fsharp) 11 | [![NuGet](https://img.shields.io/nuget/v/FsMessageTemplates.svg?maxAge=2592000)](https://www.nuget.org/packages/FsMessageTemplates) 12 | 13 | ### Samples 14 | 15 | #### Format an F# Record 16 | ```fsharp 17 | type User = { Id:int; Name:string } 18 | format (parse "Hello, {@user}") [| {Id=1; Name="Adam"} |] 19 | // > val it : string = "Hello, User { Id: 1, Name: "Adam" }" 20 | ``` 21 | 22 | ### Message Template Syntax 23 | 24 | [Message Templates](http://messagetemplates.org/) are a superset of standard .NET format strings, so any format string acceptable to `string.Format()` will also be correctly processed by `FsMessageTemplates`. 25 | 26 | * Property names are written between `{` and `}` brackets 27 | * Brackets can be escaped by doubling them, e.g. `{{` will be rendered as `{` 28 | * Formats that use numeric property names, like `{0}` and `{1}` exclusively, will be matched with the `Format` method's parameters by treating the property names as indexes; this is identical to `string.Format()`'s behaviour 29 | * If any of the property names are non-numeric, then all property names will be matched from left-to-right with the `Format` method's parameters 30 | * Property names may be prefixed with an optional operator, `@` or `$`, to control how the property is serialised 31 | * Property names may be suffixed with an optional format, e.g. `:000`, to control how the property is rendered; these format strings behave exactly as their counterparts within the `string.Format()` syntax 32 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}' 2 | image: Visual Studio 2017 3 | configuration: Release 4 | install: 5 | - ps: ./Install.ps1 6 | build_script: 7 | - ps: ./Build.ps1 8 | test: off 9 | artifacts: 10 | - path: artifacts/FsMessageTemplates.*.nupkg 11 | - path: artifacts/FsMtParser.*.nupkg 12 | deploy: 13 | - provider: NuGet 14 | api_key: 15 | secure: ZdlULX6DI2b/fKecXcFFuKZmMP+q2lz+us4pAeGqFl7mK0Be2/hG7LDSfZgIgAKI 16 | skip_symbols: true 17 | on: 18 | branch: /^(dev|master)$/ 19 | 20 | -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | @powershell .\Build.ps1 %* -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | for path in src/*/*.fsproj; do 4 | dotnet build -c Release ${path} 5 | done 6 | 7 | for path in test/*/*.fsproj; do 8 | dotnet test -c Release ${path} 9 | done 10 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "2.2.300" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /messagetemplates-fsharp.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio 15 3 | VisualStudioVersion = 15.0.27004.2008 4 | MinimumVisualStudioVersion = 15.0.26124.0 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A0C55358-DBDB-4212-B2D8-EC0ED08E2789}" 6 | EndProject 7 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FsMessageTemplates", "src\FsMessageTemplates\FsMessageTemplates.fsproj", "{73711F66-D1E5-4CB4-9922-84799CDC23EA}" 8 | EndProject 9 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FsMtParser", "src\FsMtParser\FsMtParser.fsproj", "{83781E5F-A52A-4E75-919F-C769525E54F4}" 10 | EndProject 11 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{32327697-0077-4B51-A6FD-C29D58A90387}" 12 | EndProject 13 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FsMessageTemplates.Tests", "test\FsMessageTemplates.Tests\FsMessageTemplates.Tests.fsproj", "{8806A9E9-5675-496D-896A-01DC9782A179}" 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Debug|x64 = Debug|x64 19 | Debug|x86 = Debug|x86 20 | Release|Any CPU = Release|Any CPU 21 | Release|x64 = Release|x64 22 | Release|x86 = Release|x86 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {73711F66-D1E5-4CB4-9922-84799CDC23EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {73711F66-D1E5-4CB4-9922-84799CDC23EA}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {73711F66-D1E5-4CB4-9922-84799CDC23EA}.Debug|x64.ActiveCfg = Debug|Any CPU 28 | {73711F66-D1E5-4CB4-9922-84799CDC23EA}.Debug|x64.Build.0 = Debug|Any CPU 29 | {73711F66-D1E5-4CB4-9922-84799CDC23EA}.Debug|x86.ActiveCfg = Debug|Any CPU 30 | {73711F66-D1E5-4CB4-9922-84799CDC23EA}.Debug|x86.Build.0 = Debug|Any CPU 31 | {73711F66-D1E5-4CB4-9922-84799CDC23EA}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {73711F66-D1E5-4CB4-9922-84799CDC23EA}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {73711F66-D1E5-4CB4-9922-84799CDC23EA}.Release|x64.ActiveCfg = Release|Any CPU 34 | {73711F66-D1E5-4CB4-9922-84799CDC23EA}.Release|x64.Build.0 = Release|Any CPU 35 | {73711F66-D1E5-4CB4-9922-84799CDC23EA}.Release|x86.ActiveCfg = Release|Any CPU 36 | {73711F66-D1E5-4CB4-9922-84799CDC23EA}.Release|x86.Build.0 = Release|Any CPU 37 | {83781E5F-A52A-4E75-919F-C769525E54F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {83781E5F-A52A-4E75-919F-C769525E54F4}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {83781E5F-A52A-4E75-919F-C769525E54F4}.Debug|x64.ActiveCfg = Debug|Any CPU 40 | {83781E5F-A52A-4E75-919F-C769525E54F4}.Debug|x64.Build.0 = Debug|Any CPU 41 | {83781E5F-A52A-4E75-919F-C769525E54F4}.Debug|x86.ActiveCfg = Debug|Any CPU 42 | {83781E5F-A52A-4E75-919F-C769525E54F4}.Debug|x86.Build.0 = Debug|Any CPU 43 | {83781E5F-A52A-4E75-919F-C769525E54F4}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {83781E5F-A52A-4E75-919F-C769525E54F4}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {83781E5F-A52A-4E75-919F-C769525E54F4}.Release|x64.ActiveCfg = Release|Any CPU 46 | {83781E5F-A52A-4E75-919F-C769525E54F4}.Release|x64.Build.0 = Release|Any CPU 47 | {83781E5F-A52A-4E75-919F-C769525E54F4}.Release|x86.ActiveCfg = Release|Any CPU 48 | {83781E5F-A52A-4E75-919F-C769525E54F4}.Release|x86.Build.0 = Release|Any CPU 49 | {8806A9E9-5675-496D-896A-01DC9782A179}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {8806A9E9-5675-496D-896A-01DC9782A179}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {8806A9E9-5675-496D-896A-01DC9782A179}.Debug|x64.ActiveCfg = Debug|Any CPU 52 | {8806A9E9-5675-496D-896A-01DC9782A179}.Debug|x64.Build.0 = Debug|Any CPU 53 | {8806A9E9-5675-496D-896A-01DC9782A179}.Debug|x86.ActiveCfg = Debug|Any CPU 54 | {8806A9E9-5675-496D-896A-01DC9782A179}.Debug|x86.Build.0 = Debug|Any CPU 55 | {8806A9E9-5675-496D-896A-01DC9782A179}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {8806A9E9-5675-496D-896A-01DC9782A179}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {8806A9E9-5675-496D-896A-01DC9782A179}.Release|x64.ActiveCfg = Release|Any CPU 58 | {8806A9E9-5675-496D-896A-01DC9782A179}.Release|x64.Build.0 = Release|Any CPU 59 | {8806A9E9-5675-496D-896A-01DC9782A179}.Release|x86.ActiveCfg = Release|Any CPU 60 | {8806A9E9-5675-496D-896A-01DC9782A179}.Release|x86.Build.0 = Release|Any CPU 61 | EndGlobalSection 62 | GlobalSection(SolutionProperties) = preSolution 63 | HideSolutionNode = FALSE 64 | EndGlobalSection 65 | GlobalSection(NestedProjects) = preSolution 66 | {73711F66-D1E5-4CB4-9922-84799CDC23EA} = {A0C55358-DBDB-4212-B2D8-EC0ED08E2789} 67 | {83781E5F-A52A-4E75-919F-C769525E54F4} = {A0C55358-DBDB-4212-B2D8-EC0ED08E2789} 68 | {8806A9E9-5675-496D-896A-01DC9782A179} = {32327697-0077-4B51-A6FD-C29D58A90387} 69 | EndGlobalSection 70 | GlobalSection(ExtensibilityGlobals) = postSolution 71 | SolutionGuid = {850A98AE-E5C4-40C2-A116-81B46A496BEA} 72 | EndGlobalSection 73 | EndGlobal 74 | -------------------------------------------------------------------------------- /src/FsMessageTemplates/FsMessageTemplates.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 1.0.0 6 | alpha 7 | 8 | 9 | 10 | Adam Chester 11 | en-US 12 | FsMessageTemplates 13 | F# Message Templates - the ability to format {named} string values, and capture the properties 14 | 15 | 16 | 17 | F# Message Templates 18 | message;template;serilog;F#;fsharp;logging;semantic;structured 19 | RC release 20 | http://messagetemplates.org/images/messagetemplates-nuget.png 21 | https://github.com/messagetemplates/messagetemplates-fsharp 22 | https://www.apache.org/licenses/LICENSE-2.0 23 | false 24 | git 25 | https://github.com/messagetemplates/messagetemplates-fsharp 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/FsMessageTemplates/MessageTemplates.fs: -------------------------------------------------------------------------------- 1 | namespace FsMessageTemplates 2 | 3 | open System.Text 4 | open System.IO 5 | 6 | type DestrHint = Default = 0 | Stringify = 1 | Destructure = 2 7 | type Direction = Left = 0 | Right = 1 8 | 9 | [] 10 | module Extensions = 11 | 12 | type DestrHint with 13 | member inline this.ToDestrChar () = if this = DestrHint.Default then "" 14 | elif this = DestrHint.Destructure then "@" 15 | else "$" 16 | static member inline FromChar c = if c = '@' then DestrHint.Destructure 17 | elif c = '$' then DestrHint.Stringify 18 | else DestrHint.Default 19 | 20 | type System.Text.StringBuilder with 21 | /// Appends the text if the condition is true, returning the string builer for chaining. 22 | member inline this.AppendIf (cond:bool, s:string) = if cond then this.Append(s) else this 23 | /// Assists with reused string builders 24 | member inline this.ToStringAndClear () = let s = this.ToString() in this.Clear() |> ignore; s 25 | 26 | [] 27 | type AlignInfo = 28 | new (direction:Direction, width:int) = { _direction=direction; _width=width; } 29 | new (isValid:bool) = { _direction=Direction.Left; _width=(if isValid then -1 else -2) } 30 | val private _direction:Direction 31 | val private _width:int 32 | member this.Direction with get() = this._direction 33 | member this.Width with get() = this._width 34 | member this.IsEmpty = this.Width = -1 35 | member internal this.IsValid = this.Width <> -2 36 | static member Empty = AlignInfo(isValid=true) 37 | static member Invalid = AlignInfo(isValid=false) 38 | 39 | [] 40 | type Property(name:string, pos:int, destr:DestrHint, align: AlignInfo, format: string) = 41 | static member Empty = Property("", -1, DestrHint.Default, AlignInfo.Empty, null) 42 | member __.Name = name 43 | member __.Pos = pos 44 | member __.Destr = destr 45 | member __.Align = align 46 | member __.Format = format 47 | member x.IsPositional with get() = x.Pos >= 0 48 | member internal x.AppendPropertyString (sb:StringBuilder, includeDestr:bool, name:string) = 49 | sb .Append("{") 50 | .AppendIf(includeDestr && x.Destr <> DestrHint.Default, x.Destr.ToDestrChar()) 51 | .Append(name) 52 | .AppendIf(not x.Align.IsEmpty, "," + ((if x.Align.Direction = Direction.Left then "-" else "") + string x.Align.Width)) 53 | .Append(match x.Format with null -> "" | _ -> ":" + x.Format) 54 | .Append("}") 55 | override x.ToString() = x.AppendPropertyString(StringBuilder(), true, name).ToString() 56 | 57 | type Token = 58 | | TextToken of startIndex: int * text: string 59 | | PropToken of startIndex: int * prop: Property 60 | override x.ToString() = match x with | TextToken (_, s) -> s | PropToken (_, pd) -> (string pd) 61 | 62 | type Template(formatString:string, tokens: Token[], isNamed:bool, properties:Property[]) = 63 | let named = if isNamed then properties else Unchecked.defaultof 64 | let positional = if isNamed then Unchecked.defaultof else properties 65 | member this.Tokens = tokens :> Token seq 66 | member this.FormatString = formatString 67 | member this.Properties = properties :> Property seq 68 | member internal this.Named = named 69 | member internal this.Positionals = positional 70 | member internal this.HasAnyProperties = properties.Length > 0 71 | override this.ToString() = sprintf "T (tokens = %A)" tokens 72 | 73 | type ScalarKeyValuePair = TemplatePropertyValue * TemplatePropertyValue 74 | and PropertyNameAndValue = { Name:string; Value:TemplatePropertyValue } 75 | and TemplatePropertyValue = 76 | | ScalarValue of obj 77 | | SequenceValue of TemplatePropertyValue list 78 | | StructureValue of typeTag:string * values:PropertyNameAndValue list 79 | | DictionaryValue of data: ScalarKeyValuePair list 80 | static member Empty = Unchecked.defaultof 81 | 82 | [] 83 | module Log = 84 | /// Describes a function for logging warning messages. 85 | type SelfLogger = (string * obj[]) -> unit 86 | 87 | /// A logger which ignores any warning messages. 88 | let inline nullLogger (_: string, _: obj[]) = () 89 | 90 | module Defaults = 91 | let maxDepth = 10 92 | let scalarNull = ScalarValue null 93 | 94 | type Destructurer = DestructureRequest -> TemplatePropertyValue 95 | and 96 | [] 97 | DestructureRequest(destructurer:Destructurer, value:obj, maxDepth:int, currentDepth:int, hint:DestrHint) = 98 | member x.Hint = hint 99 | member x.Value = value 100 | member x.Destructurer = destructurer 101 | member x.MaxDepth = maxDepth 102 | member x.CurrentDepth = currentDepth 103 | member internal x.Log = nullLogger 104 | /// During destructuring, this is called to 'recursively' destructure child properties 105 | /// or sequence elements. 106 | member x.TryAgainWithValue (newValue:obj) = 107 | let nextDepth = x.CurrentDepth + 1 108 | if nextDepth > x.MaxDepth then Defaults.scalarNull 109 | else 110 | let childRequest = DestructureRequest(destructurer, newValue, maxDepth, nextDepth, hint) 111 | // now invoke the full destructurer again on the new value 112 | x.Destructurer childRequest 113 | member inline internal x.TryAgainWithValueAndDestr (newValue:obj, destructurer:Destructurer) = 114 | let nextDepth = x.CurrentDepth + 1 115 | if nextDepth > x.MaxDepth then Defaults.scalarNull 116 | else 117 | let childRequest = DestructureRequest(destructurer, newValue, maxDepth, nextDepth, hint) 118 | // now invoke the full destructurer again on the new value 119 | x.Destructurer childRequest 120 | 121 | [] 122 | module Empty = 123 | let textToken = TextToken(0, "") 124 | let propToken = PropToken(0, Property.Empty) 125 | let textTokenArray = [| textToken |] 126 | let scalarNull = Defaults.scalarNull 127 | 128 | module Parser = 129 | 130 | [] 131 | type Range(startIndex:int, endIndex:int) = 132 | member this.Start = startIndex 133 | member this.End = endIndex 134 | member this.Length = (endIndex - startIndex) + 1 135 | member this.GetSubString (s:string) = s.Substring(startIndex, this.Length) 136 | member this.IncreaseBy startNum endNum = Range(startIndex+startNum, endIndex+endNum) 137 | member this.Right (startFromIndex:int) = 138 | if startFromIndex < startIndex then invalidArg "startFromIndex" "startFromIndex must be >= Start" 139 | Range(startIndex, this.End) 140 | override __.ToString() = (string startIndex) + ", " + (string endIndex) 141 | member this.IsEmpty = startIndex = -1 && endIndex = -1 142 | static member Empty = Range(-1, -1) 143 | 144 | let inline findNextNonPropText (startAt : int) (template : string) (foundText : string->unit) : int = 145 | // Finds the next text token (starting from the 'startAt' index) and returns the next character 146 | // index within the template string. If the end of the template string is reached, or the start 147 | // of a property token is found (i.e. a single { character), then the 'consumed' text is passed 148 | // to the 'foundText' method, and index of the next character is returned. 149 | let mutable escapedBuilder = Unchecked.defaultof // don't create one until it's needed 150 | let inline append (ch : char) = if not (isNull escapedBuilder) then escapedBuilder.Append(ch) |> ignore 151 | let inline createStringBuilderAndPopulate i = 152 | if isNull escapedBuilder then 153 | escapedBuilder <- StringBuilder() // found escaped open-brace, take the slow path 154 | for chIndex = startAt to i-1 do append template.[chIndex] // append all existing chars 155 | let rec go i = 156 | if i >= template.Length then 157 | template.Length // bail out at the end of the string 158 | else 159 | let ch = template.[i] 160 | match ch with 161 | | '{' -> 162 | if (i+1) < template.Length && template.[i+1] = '{' then 163 | createStringBuilderAndPopulate i 164 | append ch; go (i+2) 165 | else i // found an open brace (potentially a property), so bail out 166 | | '}' when (i+1) < template.Length && template.[i+1] = '}' -> 167 | createStringBuilderAndPopulate i 168 | append ch; go (i+2) 169 | | _ -> 170 | append ch; go (i+1) 171 | 172 | let nextIndex = go startAt 173 | if (nextIndex > startAt) then // if we 'consumed' any characters, signal that we 'foundText' 174 | if isNull escapedBuilder then 175 | foundText (Range(startAt, nextIndex - 1).GetSubString(template)) 176 | else 177 | foundText (escapedBuilder.ToString()) 178 | nextIndex 179 | 180 | /// Parses a text token inside the template string, starting at a specified character number within the 181 | /// template string, and returning the 'next' character index + the parsed text token. 182 | /// The text token is 'finished' when an open brace is encountered (or the end of the template string is 183 | /// reached, whichever comes first). 184 | let inline parseTextToken (startAt:int) (template:string) : int*Token = 185 | let mutable textFound : string = "" 186 | let nextIndex = findNextNonPropText startAt template (fun t -> textFound <- t) 187 | if nextIndex <> startAt then nextIndex, Token.TextToken(startAt, textFound) 188 | else startAt, Empty.textToken 189 | 190 | let inline isValidInPropName c = c = '_' || System.Char.IsLetterOrDigit c 191 | let inline isValidInDestrHint c = c = '@' || c = '$' 192 | let inline isValidInAlignment c = c = '-' || System.Char.IsDigit c 193 | let inline isValidInFormat c = c <> '}' && (c = ' ' || System.Char.IsLetterOrDigit c || System.Char.IsPunctuation c) 194 | let inline isValidCharInPropTag c = c = ':' || isValidInPropName c || isValidInFormat c || isValidInDestrHint c 195 | let inline tryGetFirstChar predicate (s:string) first = 196 | let len = s.Length 197 | let rec go i = 198 | if i >= len then -1 199 | else if not (predicate s.[i]) then go (i+1) else i 200 | go first 201 | 202 | let inline tryGetFirstCharRng predicate (s:string) (rng:Range) = 203 | let rec go i = 204 | if i > rng.End then -1 205 | else if not (predicate s.[i]) then go (i+1) else i 206 | go rng.Start 207 | 208 | let inline hasAnyInvalidRng isValid (s:string) (rng:Range) = 209 | match tryGetFirstChar (not< false | i -> i <= rng.End 211 | 212 | let inline hasAnyInvalid isValid (s:string) = 213 | hasAnyInvalidRng isValid s (Range(0, s.Length - 1)) 214 | 215 | /// just like System.String.IndexOf(char) but within a string range 216 | let inline rngIndexOf (s:string) (rng:Range) (c:char) = tryGetFirstCharRng ((=) c) s rng 217 | 218 | /// Attemps to parse an integer from the range within a string. Returns Int32.MinValue if the 219 | /// string does not contain an integer. The '-' is allowed only as the first character. 220 | let inline tryParseIntFromRng (invalidValue:int) (s:string) (rng:Range) = 221 | if rng.Length = 1 && '0' <= s.[0] && s.[0] <= '9' then 222 | int (s.[rng.Start]) - 48 223 | else 224 | let indexOfLastCharPlus1 = rng.End+1 225 | let rec inside isNeg numSoFar i = 226 | if i = indexOfLastCharPlus1 then 227 | if isNeg then -numSoFar else numSoFar 228 | else 229 | let c = s.[i] 230 | if c = '-' then 231 | if i = rng.Start then inside true (numSoFar) (i+1) 232 | else invalidValue // no '-' character allowed other than first char 233 | elif '0' <= c && c <= '9' then 234 | inside isNeg (10*numSoFar + int c - 48) (i+1) 235 | else invalidValue 236 | 237 | inside false 0 rng.Start 238 | 239 | /// Attemps to parse an integer from the string. Returns -1 if the string does 240 | /// not contain an integer. 241 | let inline parseIntOrNegative1 s = 242 | if System.String.IsNullOrEmpty(s) then -1 243 | else tryParseIntFromRng -1 s (Range(0, s.Length-1)) 244 | 245 | let inline tryParseAlignInfoRng (s:string) (rng:Range) : AlignInfo = 246 | match s, rng with 247 | | s, rng when (rng.Start > rng.End) || (hasAnyInvalidRng isValidInAlignment s rng) -> 248 | AlignInfo(isValid=false) 249 | | s, rng -> 250 | let invalidAlignWidth = System.Int32.MinValue 251 | let width = 252 | match tryParseIntFromRng invalidAlignWidth s rng with 253 | | System.Int32.MinValue -> 0 // not a valid align number (e.g. dash in wrong spot) 254 | | n -> n 255 | if width = 0 then AlignInfo(isValid=false) 256 | else 257 | let isNegativeAlignWidth = width < 0 258 | let direction = if isNegativeAlignWidth then Direction.Left else Direction.Right 259 | AlignInfo(direction, abs(width)) 260 | 261 | let inline tryGetPropInSubString (t:string) (within : Range) : Token = 262 | /// Given a template such has "Hello, {@name,-10:abc}!" and a *within* range 263 | /// of Start=8, End=19 (i.e. the full 'insides' of the property tag between { and }, 264 | /// this returns the Start/End pairs of the Name, Align, and Format components. If 265 | /// anything 'invalid' is found then the first Range is a value of None. 266 | let nameRange, alignRange, formatRange = 267 | match (rngIndexOf t within ','), (rngIndexOf t within ':') with 268 | | -1, -1 -> within, Range.Empty, Range.Empty // neither align nor format 269 | | -1, fmtIdx -> Range(within.Start, fmtIdx-1), Range.Empty, Range(fmtIdx+1, within.End) // has format part, but does not have align part 270 | | alIdx, -1 -> Range(within.Start, alIdx-1), Range(alIdx+1, within.End), Range.Empty // has align part, but does not have format part 271 | | alIdx, fmtIdx when alIdx < fmtIdx && alIdx <> (fmtIdx-1) -> // has both parts in correct order 272 | let align = Range(alIdx+1, fmtIdx-1) 273 | let fmt = Range(fmtIdx+1, within.End) 274 | Range(within.Start, alIdx-1), align, fmt 275 | | alIdx, fmtIdx when alIdx > fmtIdx -> 276 | Range(within.Start, fmtIdx-1), Range.Empty, Range(fmtIdx+1, within.End) // has format part, no align (but one or more commas *inside* the format string) 277 | | _, _ -> Range.Empty, Range.Empty, Range.Empty // hammer time; you can't split this 278 | 279 | if nameRange.IsEmpty then Empty.propToken 280 | else 281 | let destr = DestrHint.FromChar t.[nameRange.Start] 282 | let propertyName = match destr with 283 | | DestrHint.Default -> nameRange.GetSubString t 284 | | _ -> Range(nameRange.Start+1, nameRange.End).GetSubString t 285 | if propertyName = "" || (hasAnyInvalid isValidInPropName propertyName) then Empty.propToken 286 | elif (not formatRange.IsEmpty) && (hasAnyInvalidRng isValidInFormat t formatRange) then Empty.propToken 287 | else 288 | if alignRange.IsEmpty then 289 | let format = if formatRange.IsEmpty then null else formatRange.GetSubString t 290 | let pt = Property(propertyName, parseIntOrNegative1 propertyName, destr, AlignInfo.Empty, format) 291 | PropToken(within.Start - 1, pt) 292 | else 293 | let ai = tryParseAlignInfoRng t alignRange 294 | if not ai.IsValid then Empty.propToken 295 | else 296 | let format = if formatRange.IsEmpty then null else formatRange.GetSubString t 297 | let pt = Property(propertyName, parseIntOrNegative1 propertyName, destr, ai, format) 298 | PropToken(within.Start - 1, pt) 299 | 300 | /// Parses the property token in the template string from the start character index, and 301 | /// calls the 'foundProp' method. If the property is malformed, the 'appendTextChar' method 302 | /// is called instead for each character consumed. Finally, the next character index is returned. 303 | let findPropOrAppendText (start:int) (template:string) (foundText: string -> unit) 304 | (foundProp: Property -> unit) : int = 305 | // skip over characters after the open-brace, until we reach a character that 306 | // is *NOT* a valid part of the property tag. this will be the close brace character 307 | // if the template is actually a well-formed property tag. 308 | let nextInvalidCharIndex = 309 | match tryGetFirstChar (not << isValidCharInPropTag) template (start+1) with 310 | | -1 -> template.Length | idx -> idx 311 | 312 | // if we stopped at the end of the string or the last char wasn't a close brace 313 | // then we treat all the characters we found as a text token, and finish. 314 | if nextInvalidCharIndex = template.Length || template.[nextInvalidCharIndex] <> '}' then 315 | foundText (Range(start, nextInvalidCharIndex - 1).GetSubString(template)) 316 | nextInvalidCharIndex 317 | else 318 | // skip over the trailing "}" close prop tag 319 | let nextIndex = nextInvalidCharIndex + 1 320 | let propInsidesRng = Range(start + 1, nextIndex - 2) 321 | match tryGetPropInSubString template propInsidesRng with 322 | | PropToken (_,pt) as p when not (obj.ReferenceEquals(p, Empty.propToken)) -> foundProp pt 323 | | _ -> foundText (Range(start, nextIndex - 1).GetSubString(template)) 324 | 325 | nextIndex 326 | 327 | let parsePropertyToken (start: int) (mt: string) (rz: ResizeArray) : int = 328 | let mutable wasProperty = false 329 | let mutable text = "" 330 | let foundProp (p:Property) = rz.Add (PropToken (start, p)); wasProperty <- true 331 | let foundText t = text <- t 332 | let nextIndex = findPropOrAppendText start mt foundText foundProp 333 | if not wasProperty then 334 | rz.Add (TextToken (start, text)) 335 | nextIndex 336 | 337 | let parseTokens (mt:string) = 338 | let tlen = mt.Length 339 | if tlen = 0 then Empty.textTokenArray 340 | else 341 | let sb = StringBuilder() 342 | let rz = ResizeArray() 343 | let rec go start = 344 | if start >= tlen then rz.ToArray() 345 | else match parseTextToken start mt with 346 | | next, tok when next <> start -> 347 | rz.Add tok; go next 348 | | _, _ -> 349 | // no text token parsed, try a property. note that this will 350 | // append a text token if the property turns out to be malformed. 351 | go (parsePropertyToken start mt rz) 352 | go 0 353 | 354 | let parseParts (s:string) (foundText: string->unit) (foundProp: Property->unit) = 355 | let tlen = s.Length 356 | let rec go start = 357 | if start >= tlen then () 358 | else match findNextNonPropText start s foundText with 359 | | next when next <> start -> go next 360 | | _ -> go (findPropOrAppendText start s foundText foundProp) 361 | go 0 362 | 363 | let parse (s:string) = 364 | let tokens = s |> parseTokens 365 | let mutable allPos, anyPos = true, false 366 | let rzProps = ResizeArray() 367 | for i = 0 to (tokens.Length-1) do 368 | let t = tokens.[i] 369 | match t with 370 | | PropToken (_, pt) -> 371 | rzProps.Add pt 372 | if pt.IsPositional then anyPos <- true else allPos <- false 373 | | _ -> () 374 | let properties = rzProps.ToArray() 375 | Template(s, tokens, not allPos, properties) 376 | 377 | module Destructure = 378 | open System 379 | 380 | // perf 381 | let inline isEmptyKeepTrying (tpv:TemplatePropertyValue) = Object.ReferenceEquals(tpv, null) 382 | let inline tryCastAs<'T> (o:obj) = match o with | :? 'T as res -> res | _ -> Unchecked.defaultof<'T> 383 | 384 | let scalarTypes = 385 | [ typeof; typeof; typeof; typeof 386 | typeof; typeof; typeof; typeof 387 | typeof; typeof; typeof; typeof 388 | typeof; typeof; typeof; typeof 389 | typeof; typeof; ] 390 | 391 | let scalarTypeHash = System.Collections.Generic.HashSet(scalarTypes) 392 | 393 | let inline tryBuiltInTypesOrNull (r:DestructureRequest) = 394 | if r.Value = null then ScalarValue null 395 | elif scalarTypeHash.Contains(r.Value.GetType()) then (ScalarValue r.Value) 396 | else TemplatePropertyValue.Empty 397 | 398 | let inline tryNullable (r:DestructureRequest) = 399 | let t = r.Value.GetType() 400 | let isNullable = t.IsConstructedGenericType && t.GetGenericTypeDefinition() = typedefof> 401 | if isNullable then 402 | match tryCastAs>(r.Value) with 403 | | n when (Object.ReferenceEquals(null, n)) -> (ScalarValue null) 404 | | n when (not n.HasValue) -> (ScalarValue null) 405 | | n when n.HasValue -> r.TryAgainWithValue(box (n.GetValueOrDefault())) 406 | | _ -> TemplatePropertyValue.Empty 407 | else TemplatePropertyValue.Empty 408 | 409 | let inline tryEnum (r:DestructureRequest) = 410 | match tryCastAs(r.Value) with 411 | | e when (Object.ReferenceEquals(null, e)) -> TemplatePropertyValue.Empty 412 | | e -> (ScalarValue (e)) 413 | 414 | let inline tryByteArrayMaxBytes (maxBytes:int) (r:DestructureRequest) = 415 | match tryCastAs(r.Value) with 416 | | bytes when (Object.ReferenceEquals(null, bytes)) -> TemplatePropertyValue.Empty 417 | | bytes when bytes.Length <= maxBytes -> ScalarValue bytes 418 | | bytes -> 419 | let inline toHexString (b:byte) = b.ToString("X2") 420 | let start = bytes |> Seq.take maxBytes |> Seq.map toHexString |> String.Concat 421 | let description = start + "... (" + string bytes.Length + " bytes)" 422 | ScalarValue (description) 423 | 424 | let inline tryByteArray r = tryByteArrayMaxBytes 1024 r 425 | 426 | let inline tryReflectionTypes (r:DestructureRequest) = 427 | match r.Value with 428 | | :? Type as t -> ScalarValue t 429 | | :? System.Reflection.MemberInfo as m -> ScalarValue m 430 | | _ -> TemplatePropertyValue.Empty 431 | 432 | let inline tryScalarDestructure (r:DestructureRequest) = 433 | match tryBuiltInTypesOrNull r with 434 | | ekt1 when isEmptyKeepTrying ekt1 -> 435 | match tryNullable r with 436 | | ekt2 when isEmptyKeepTrying ekt2 -> 437 | match tryEnum r with 438 | | ekt3 when isEmptyKeepTrying ekt3 -> 439 | match tryByteArray r with 440 | | ekt4 when isEmptyKeepTrying ekt4 -> 441 | match tryReflectionTypes r with 442 | | ekt5 when isEmptyKeepTrying ekt5 -> TemplatePropertyValue.Empty 443 | | tpv -> tpv 444 | | tpv -> tpv 445 | | tpv -> tpv 446 | | tpv -> tpv 447 | | tpv -> tpv 448 | 449 | 450 | let inline tryNull (r:DestructureRequest) = 451 | match r.Value with | null -> Empty.scalarNull | _ -> TemplatePropertyValue.Empty 452 | let inline tryStringifyDestructurer (r:DestructureRequest) = 453 | match r.Hint with | DestrHint.Stringify -> ScalarValue (r.Value.ToString()) | _ -> TemplatePropertyValue.Empty 454 | 455 | let inline tryDelegateString (r:DestructureRequest) = 456 | if r.Hint <> DestrHint.Destructure then TemplatePropertyValue.Empty 457 | else 458 | match tryCastAs(r.Value) with 459 | | e when (Object.ReferenceEquals(null, e)) -> TemplatePropertyValue.Empty 460 | | e -> (ScalarValue (string e)) 461 | 462 | open System.Reflection 463 | let inline getTypeInfo (t:Type) = 464 | System.Reflection.IntrospectionExtensions.GetTypeInfo t 465 | let inline isValidScalarDictionaryKeyType (t:Type) = 466 | scalarTypeHash.Contains(t) || (getTypeInfo t).IsEnum 467 | let inline isScalarDict (t: Type) = 468 | t.IsConstructedGenericType 469 | && t.GetGenericTypeDefinition() = typedefof> 470 | && isValidScalarDictionaryKeyType(t.GenericTypeArguments.[0]) 471 | 472 | let inline tryEnumerableDestr (r:DestructureRequest) = 473 | let valueType = r.Value.GetType() 474 | match tryCastAs(r.Value) with 475 | | e when Object.ReferenceEquals(null, e) -> TemplatePropertyValue.Empty 476 | | e when isScalarDict valueType -> 477 | let keyProp, valueProp = ref Unchecked.defaultof, ref Unchecked.defaultof 478 | let getKey o = if !keyProp = null then keyProp := o.GetType().GetRuntimeProperty("Key") 479 | (!keyProp).GetValue(o) 480 | let getValue o = if !valueProp = null then valueProp := o.GetType().GetRuntimeProperty("Value") 481 | (!valueProp).GetValue(o) 482 | let skvps = e |> Seq.cast 483 | |> Seq.map (fun o -> getKey o, getValue o) 484 | |> Seq.map (fun (key, value) -> 485 | // only attempt the built-in scalars for the keyValue, because only 486 | // scalar values are supported as dictionary keys. However, do full 487 | // destructuring for the dictionary entry values. 488 | r.TryAgainWithValueAndDestr (key, tryScalarDestructure), r.TryAgainWithValue (value)) 489 | |> Seq.toList 490 | DictionaryValue skvps 491 | | e -> SequenceValue(e |> Seq.cast |> Seq.map r.TryAgainWithValue |> Seq.toList) 492 | 493 | let inline scalarStringCatchAllDestr (r:DestructureRequest) = ScalarValue (r.Value.ToString()) 494 | 495 | let inline isPublicInstanceReadProp (p:PropertyInfo) = 496 | p.CanRead && p.GetMethod.IsPublic && not (p.GetMethod.IsStatic) && 497 | (p.Name <> "Item" || p.GetIndexParameters().Length = 0) 498 | 499 | let inline tryObjectStructureDestructuring (r:DestructureRequest) = 500 | if r.Hint <> DestrHint.Destructure then TemplatePropertyValue.Empty 501 | else 502 | let ty = r.Value.GetType() 503 | let typeTag = match ty.Name with 504 | | s when s.Length = 0 || not (Char.IsLetter s.[0]) -> null 505 | | s -> s 506 | 507 | let rzPubProps = ResizeArray() 508 | for rtp in ty.GetRuntimeProperties() do 509 | if isPublicInstanceReadProp rtp then rzPubProps.Add rtp 510 | 511 | // Recursively destructure the child properties 512 | let rec loopDestrChildren i acc = 513 | if i < 0 then acc 514 | else 515 | let pi = rzPubProps.[i] 516 | try 517 | let propValue = pi.GetValue(r.Value) 518 | let propTpv = { Name=pi.Name; Value=r.TryAgainWithValue propValue } 519 | loopDestrChildren (i-1) (propTpv :: acc) 520 | with 521 | | :? TargetParameterCountException as ex -> 522 | r.Log("The property accessor {0} is a non-default indexer", [|pi|]) 523 | loopDestrChildren (i-1) (acc) 524 | | :? TargetInvocationException as ex -> 525 | r.Log("The property accessor {0} threw exception {1}", [| pi; ex; |]) 526 | let propValue = "The property accessor threw an exception:" + ex.InnerException.GetType().Name 527 | let propTpv = { Name=pi.Name; Value=r.TryAgainWithValue propValue } 528 | loopDestrChildren (i-1) (propTpv :: acc) 529 | 530 | let childStructureValues = loopDestrChildren (rzPubProps.Count-1) [] 531 | StructureValue(typeTag, childStructureValues) 532 | 533 | /// A destructurer that does nothing by returning TemplatePropertyValue.Empty 534 | let inline alwaysKeepTrying (_:DestructureRequest) = TemplatePropertyValue.Empty 535 | 536 | /// Attempts all built-in destructurers in the correct order, falling 537 | /// back to 'scalarStringCatchAllDestr' (stringify) if no better option 538 | /// is found first. Also supports custom scalars and custom object 539 | /// destructuring at the appropriate points in the pipline. Note this is 540 | /// called recursively in a tight loop during the process of capturing 541 | /// template property values, which means it needs to be fairly fast. 542 | let inline tryAllWithCustom (tryCustomScalarTypes: Destructurer) 543 | (tryCustomDestructure: Destructurer) 544 | (request: DestructureRequest) = 545 | // Performance :( 546 | match tryNull request with 547 | | tpv when isEmptyKeepTrying tpv -> 548 | match tryStringifyDestructurer request with 549 | | tpv when isEmptyKeepTrying tpv -> 550 | match tryScalarDestructure request with 551 | | tpv when isEmptyKeepTrying tpv -> 552 | match tryCustomScalarTypes request with 553 | | tpv when isEmptyKeepTrying tpv -> 554 | match tryDelegateString request with 555 | | tpv when isEmptyKeepTrying tpv -> 556 | match tryCustomDestructure request with 557 | | tpv when isEmptyKeepTrying tpv -> 558 | match tryEnumerableDestr request with 559 | | tpv when isEmptyKeepTrying tpv -> 560 | match tryObjectStructureDestructuring request with 561 | | tpv when isEmptyKeepTrying tpv -> 562 | match scalarStringCatchAllDestr request with 563 | | tpv when isEmptyKeepTrying tpv -> TemplatePropertyValue.Empty 564 | | tpv -> tpv 565 | | tpv -> tpv 566 | | tpv -> tpv 567 | | tpv -> tpv 568 | | tpv -> tpv 569 | | tpv -> tpv 570 | | tpv -> tpv 571 | | tpv -> tpv 572 | | tpv -> tpv 573 | 574 | module Capturing = 575 | /// Determines if a TemplatePropertyValue is considered 'empty' (i.e. null) during the 576 | /// destructuring process. This is to avoid lots of Option<'T> allocations. 577 | let inline isEmptyKeepTrying (tpv) = Destructure.isEmptyKeepTrying (tpv) 578 | 579 | let createCustomDestructurer (tryScalars:Destructurer option) (tryCustomObjects: Destructurer option) : Destructurer = 580 | let tryScalars = if tryScalars.IsSome then tryScalars.Value else Destructure.alwaysKeepTrying 581 | let tryCustomObjects = if tryCustomObjects.IsSome then tryCustomObjects.Value else Destructure.alwaysKeepTrying 582 | Destructure.tryAllWithCustom tryScalars tryCustomObjects 583 | 584 | let defaultDestructureNoCustoms : Destructurer = Destructure.tryAllWithCustom Destructure.alwaysKeepTrying Destructure.alwaysKeepTrying 585 | 586 | open Microsoft.FSharp.Reflection 587 | 588 | let destructureFSharpTypes (req: DestructureRequest) : TemplatePropertyValue = 589 | let value = req.Value 590 | match req.Value.GetType() with 591 | | t when FSharpType.IsTuple t -> 592 | let tupleValues = 593 | value 594 | |> FSharpValue.GetTupleFields 595 | |> Seq.map req.TryAgainWithValue 596 | |> Seq.toList 597 | SequenceValue tupleValues 598 | | t when t.IsConstructedGenericType && t.GetGenericTypeDefinition() = typedefof> -> 599 | let objEnumerable = value :?> System.Collections.IEnumerable |> Seq.cast 600 | SequenceValue(objEnumerable |> Seq.map req.TryAgainWithValue |> Seq.toList) 601 | | t when FSharpType.IsUnion t -> 602 | let case, fields = FSharpValue.GetUnionFields(value, t) 603 | let properties = 604 | (case.GetFields(), fields) 605 | ||> Seq.map2 (fun propInfo value -> 606 | { Name = propInfo.Name 607 | Value = req.TryAgainWithValue value }) 608 | |> Seq.toList 609 | StructureValue(case.Name, properties) 610 | | _ -> TemplatePropertyValue.Empty 611 | 612 | let builtInFSharpTypesDestructurer : Destructurer = destructureFSharpTypes 613 | 614 | let capturePositionals (log:SelfLogger) (destr:Destructurer) (maxDepth:int) 615 | (template:string) (props:Property[]) (args:obj[]) = 616 | let result = ref (Array.zeroCreate(args.Length)) 617 | for p in props do 618 | if p.Pos < 0 || p.Pos >= args.Length then 619 | log("Unassigned positional value {0} in: {1}", [|p.Pos; template|]) 620 | else 621 | // only destructure it once 622 | if obj.ReferenceEquals(null, Array.get !result p.Pos) then 623 | let req = DestructureRequest(destr, args.[p.Pos], maxDepth, 1, hint=p.Destr) 624 | Array.set !result p.Pos { Name=p.Name; Value=destr req } 625 | 626 | let mutable next = 0 627 | for i = 0 to (!result).Length - 1 do 628 | let resultElem = Array.get !result i 629 | if not (obj.ReferenceEquals(null, resultElem)) then 630 | Array.set !result next (Array.get !result i) 631 | next <- next + 1 632 | 633 | if next <> (!result).Length then 634 | System.Array.Resize(result, next) 635 | 636 | !result 637 | 638 | let captureNamed (log:SelfLogger) (destr:Destructurer) (maxDepth:int) 639 | (template:string) (props:Property[]) (args:obj[]) = 640 | let mutable matchedRun = props.Length 641 | if props.Length <> args.Length then 642 | matchedRun <- min props.Length args.Length 643 | log("property count does not match parameter count: {0}", [|template|]) 644 | let result = Array.zeroCreate(matchedRun) 645 | for i = 0 to matchedRun-1 do 646 | let p = props.[i] 647 | let req = DestructureRequest(destr, args.[i], maxDepth, 1, hint=p.Destr) 648 | Array.set result i { Name=p.Name; Value=destr req } 649 | result 650 | 651 | /// Captures properties, matching the obj arguments to properties either positionally 652 | /// (if all properties are positional) or from left-to-right if one or more are non-positional. The 653 | /// Destructurer is used to convert the obj arguments into TemplatePropertyValue objects. 654 | let capturePropertiesWith (log:SelfLogger) (destr:Destructurer) (maxDepth:int) (t:Template) (args: obj[]) = 655 | if (args = null || args.Length = 0) && t.HasAnyProperties then Array.empty 656 | elif not t.HasAnyProperties then Array.empty 657 | else 658 | let props = if t.Positionals <> null then t.Positionals else t.Named 659 | let capturer = if t.Positionals <> null then capturePositionals else captureNamed 660 | capturer log destr maxDepth t.FormatString props args 661 | 662 | let capturePropertiesCustom (d: Destructurer) (maxDepth: int) (t: Template) (args: obj[]) = 663 | capturePropertiesWith nullLogger d maxDepth t args 664 | :> PropertyNameAndValue seq 665 | 666 | let captureProperties (t:Template) (args:obj[]) = 667 | capturePropertiesWith nullLogger defaultDestructureNoCustoms Defaults.maxDepth t args 668 | :> PropertyNameAndValue seq 669 | 670 | /// The same as 'captureProperties' except the message template is first 671 | /// parsed, then the properties are captured. 672 | let captureMessageProperties (s:string) (args:obj[]) = 673 | captureProperties (Parser.parse s) args 674 | 675 | module Formatting = 676 | /// Recursively writes the string representation of the template property 677 | /// value to the provided TextWriter. The provided format string is only 678 | /// used for a ScalarValue v when v implements System.IFormattable or the 679 | /// ScalarValue v when the TextWriter.FormatProvider has a format of type 680 | /// ICustomFormatter. 681 | let rec writePropValue (w: TextWriter) (tpv: TemplatePropertyValue) (format: string) = 682 | match tpv with 683 | | ScalarValue sv -> 684 | match sv with 685 | | null -> w.Write "null" 686 | | :? string as s -> 687 | if format = "l" then w.Write s 688 | else 689 | w.Write "\"" 690 | w.Write (s.Replace("\"", "\\\"")) 691 | w.Write "\"" 692 | | _ -> 693 | let customFormatter = w.FormatProvider.GetFormat(typeof) :?> System.ICustomFormatter 694 | match customFormatter with 695 | | cf when not (isNull cf) -> 696 | w.Write (cf.Format(format, sv, w.FormatProvider)) 697 | | _ -> 698 | match sv with 699 | | :? System.IFormattable as f -> w.Write (f.ToString(format, w.FormatProvider)) 700 | | _ -> w.Write(sv.ToString()) 701 | 702 | | SequenceValue svs -> 703 | w.Write '[' 704 | let lastIndex = svs.Length - 1 705 | svs |> List.iteri (fun i sv -> 706 | writePropValue w sv null 707 | if i <> lastIndex then w.Write ", " 708 | ) 709 | w.Write ']' 710 | | StructureValue(typeTag, values) -> 711 | if typeTag <> null then w.Write typeTag; w.Write ' ' 712 | w.Write "{ " 713 | let lastIndex = values.Length - 1 714 | for i = 0 to lastIndex do 715 | let tp = values.[i] 716 | w.Write tp.Name; w.Write ": " 717 | writePropValue w tp.Value null 718 | w.Write (if i = lastIndex then " " else ", ") 719 | w.Write "}" 720 | | DictionaryValue(data) -> 721 | w.Write '[' 722 | data |> List.iter (fun (entryKey, entryValue) -> 723 | w.Write '(' 724 | writePropValue w entryKey null 725 | w.Write ": " 726 | writePropValue w entryValue null 727 | w.Write ")" 728 | ) 729 | w.Write ']' 730 | 731 | /// Converts a 'matched' token and template property value into a rendered string. 732 | /// For properties, the System.String.Format rules are applied, including alignment 733 | /// and System.IFormattable rules, along with the additional MessageTemplates rules 734 | /// for named properties and destructure-formatting. 735 | let inline writeToken (buffer: StringBuilder) (w: TextWriter) (token:Token) (value:TemplatePropertyValue) = 736 | match token, value with 737 | | Token.TextToken (_, raw), _ -> w.Write raw 738 | | Token.PropToken (_, pt), pv -> 739 | if Destructure.isEmptyKeepTrying pv then 740 | let propertyTokenAsString = pt.AppendPropertyString(buffer, true, pt.Name).ToStringAndClear() 741 | w.Write propertyTokenAsString 742 | else 743 | if pt.Align.IsEmpty then 744 | writePropValue w pv pt.Format 745 | else 746 | let alignWriter = new StringWriter(w.FormatProvider) 747 | writePropValue alignWriter pv pt.Format 748 | let valueAsString = alignWriter.ToString() 749 | if valueAsString.Length >= pt.Align.Width then 750 | w.Write valueAsString 751 | else 752 | let pad = pt.Align.Width - valueAsString.Length 753 | if pt.Align.Direction = Direction.Right then w.Write (System.String(' ', pad)) 754 | w.Write valueAsString 755 | if pt.Align.Direction = Direction.Left then w.Write (System.String(' ', pad)) 756 | 757 | let inline createValuesByPropNameDictionary (values:PropertyNameAndValue seq) = 758 | System.Linq.Enumerable.ToDictionary(source=values, 759 | keySelector=(fun tp -> tp.Name), 760 | elementSelector=(fun tp -> tp.Value)) 761 | 762 | let inline getByName1to5 (values: PropertyNameAndValue[]) (nme: string) = 763 | let valueCount = values.Length 764 | if values.[0].Name = nme then values.[0].Value 765 | elif valueCount > 1 && values.[1].Name = nme then values.[1].Value 766 | elif valueCount > 2 && values.[2].Name = nme then values.[2].Value 767 | elif valueCount > 3 && values.[3].Name = nme then values.[3].Value 768 | elif valueCount > 4 && values.[4].Name = nme then values.[4].Value 769 | else TemplatePropertyValue.Empty 770 | 771 | let inline getByNameDict (valuesDict: System.Collections.Generic.IDictionary) nme = 772 | let tpv = ref TemplatePropertyValue.Empty 773 | valuesDict.TryGetValue (nme, tpv) |> ignore 774 | !tpv 775 | 776 | let captureThenFormat (w: TextWriter) (template:Template) (values:obj[]) = 777 | let values = Capturing.capturePropertiesWith 778 | nullLogger Capturing.defaultDestructureNoCustoms Defaults.maxDepth 779 | template values 780 | let valueCount = values.Length 781 | let getValueForPropName = 782 | match valueCount with 783 | | 0 -> fun _ -> TemplatePropertyValue.Empty 784 | | 1 | 2 | 3 | 4 | 5 -> getByName1to5 values 785 | | _ -> getByNameDict (createValuesByPropNameDictionary values) 786 | let buffer = StringBuilder() 787 | for t in template.Tokens do 788 | match t with 789 | | Token.TextToken _ as tt -> writeToken buffer w tt TemplatePropertyValue.Empty 790 | | Token.PropToken (_, pd) as tp -> 791 | let value = getValueForPropName pd.Name 792 | writeToken buffer w tp value 793 | 794 | let parseCaptureFormat (tw: TextWriter) (template: string) (args: obj[]) = 795 | captureThenFormat tw (Parser.parse template) args 796 | 797 | let format template values = 798 | use tw = new StringWriter() 799 | captureThenFormat tw template values 800 | tw.ToString() 801 | 802 | let formatCustom (t:Template) w getValueByName = 803 | let buffer = StringBuilder() 804 | for tok in t.Tokens do 805 | match tok with 806 | | Token.TextToken _ as tt -> writeToken buffer w tt TemplatePropertyValue.Empty 807 | | Token.PropToken (_, pd) as tp -> 808 | let value = getValueByName pd.Name 809 | writeToken buffer w tp value 810 | 811 | let bprintsm (sb:StringBuilder) template args = 812 | use tw = new StringWriter(sb) 813 | parseCaptureFormat tw template args 814 | 815 | let sprintsm (p:System.IFormatProvider) template args = 816 | use tw = new StringWriter(p) 817 | parseCaptureFormat tw template args 818 | tw.ToString() 819 | 820 | let fprintsm (tw:TextWriter) template args = 821 | parseCaptureFormat tw template args 822 | 823 | let bprintm template (sb:StringBuilder) args = 824 | use tw = new StringWriter(sb) 825 | captureThenFormat tw template args 826 | 827 | let sprintm template (provider:System.IFormatProvider) args = 828 | use tw = new StringWriter(provider) 829 | captureThenFormat tw template args 830 | tw.ToString() 831 | 832 | let fprintm template tw args = 833 | captureThenFormat tw template args 834 | 835 | -------------------------------------------------------------------------------- /src/FsMessageTemplates/MessageTemplates.fsi: -------------------------------------------------------------------------------- 1 | namespace FsMessageTemplates 2 | 3 | /// A hint at how a property should be destructured. The '@' character means 4 | /// 'Destructure' whereas the '$' means stringify. 5 | type DestrHint = Default = 0 | Stringify = 1 | Destructure = 2 6 | 7 | /// The alignment direction. 8 | type Direction = Left = 0 | Right = 1 9 | 10 | /// Represents the aligment information within a message template. 11 | [] 12 | type AlignInfo = 13 | new: Direction:Direction * Width:int -> AlignInfo 14 | member Direction : Direction 15 | member Width : int 16 | member IsEmpty: bool 17 | member internal IsValid: bool 18 | val private _direction:Direction 19 | val private _width:int 20 | static member Empty: AlignInfo 21 | static member internal Invalid: AlignInfo 22 | 23 | /// Represents the details about property parsed from within a message template. 24 | [] 25 | type Property = 26 | /// Constructs a new instance of a template property. 27 | new: name:string 28 | * pos:int 29 | * destr:DestrHint 30 | * align: AlignInfo 31 | * format: string 32 | -> Property 33 | /// The name of the property. 34 | member Name:string 35 | /// If the property was positional (i.e. {0} or {1}, instead of {name}), this 36 | /// is the position number. 37 | member Pos:int 38 | /// The destructuring hint (i.e. if {@name} was used then Destructure, if {$name} 39 | /// was used, then Stringify). 40 | member Destr:DestrHint 41 | /// The alignment information (i.e. if {@name,-10} was parsed from the template, this 42 | /// would be AlignInfo(Direction.Right, 10)). 43 | member Align:AlignInfo 44 | /// The format information (i.e. if {@name:0,000} was parsed from the template, this 45 | /// would be the string "0,000"). 46 | member Format:string 47 | /// When the property is positional (i.e. if {0}, {1}, etc was used instead of a 48 | /// property name, this returns true. Get the postion number from the Pos field. 49 | member IsPositional : bool 50 | 51 | /// A token parsed from a message template. 52 | type Token = 53 | /// A piece of text within a message template. 54 | | TextToken of startIndex: int * text: string 55 | /// A property within a message template. 56 | | PropToken of startIndex: int * prop: Property 57 | 58 | /// A template, including the message and parsed properties. 59 | [] 60 | type Template = 61 | new: formatString:string * tokens: Token[] * isNamed:bool * properties:Property[] -> Template 62 | member Tokens : Token seq 63 | member FormatString : string 64 | member Properties : Property seq 65 | member internal Named : Property [] 66 | member internal Positionals : Property [] 67 | 68 | /// A key and value pair, used as part of . 69 | type ScalarKeyValuePair = TemplatePropertyValue * TemplatePropertyValue 70 | /// Describes the kinds of destructured property values that can be 71 | /// captured from a message template. 72 | and TemplatePropertyValue = 73 | | ScalarValue of obj 74 | | SequenceValue of TemplatePropertyValue list 75 | | StructureValue of typeTag:string * values:PropertyNameAndValue list 76 | | DictionaryValue of data: ScalarKeyValuePair list 77 | static member Empty : TemplatePropertyValue 78 | /// A property and it's associated destructured value. 79 | and PropertyNameAndValue = { Name:string; Value:TemplatePropertyValue } 80 | 81 | /// A function that attempts to destructure a property and value object into a 82 | /// more friendly (and immutable) . This returns 83 | /// Unchecked.defaultOf TemplatePropertyValue (i.e. null; aka TemplatePropertyValue.Empty) 84 | /// if the destructuring was not possible. 85 | type Destructurer = DestructureRequest -> TemplatePropertyValue 86 | and 87 | /// Describes a request for an object to be destructured into a 88 | /// TemplatePropertyValue. 89 | [] 90 | DestructureRequest = 91 | new: destructurer:Destructurer * value:obj * maxDepth:int * currentDepth:int * hint:DestrHint -> DestructureRequest 92 | member Hint: DestrHint 93 | member Value: obj 94 | member Destructurer: Destructurer 95 | member TryAgainWithValue: newValue:obj -> TemplatePropertyValue 96 | 97 | module Parser = 98 | /// Parses a message template string. 99 | val parse: template:string -> Template 100 | 101 | module Capturing = 102 | /// Creates a customised default destructurer which optionally 103 | /// specifies different scalar and object destructurers. If the 104 | /// caller gave None for both, the destructurer returned would be 105 | /// the same as the default built-in one. A scalar destructurer 106 | /// must return a TemplatePropertyValue.ScalarValue (or Empty), 107 | /// whereas the object destructurer is free to return any kind 108 | /// (or Empty). 109 | val createCustomDestructurer : tryScalars: Destructurer option 110 | -> tryObjects: Destructurer option 111 | -> Destructurer 112 | 113 | /// Provides better support for destructuring F# types like 114 | /// Discriminated Unions, Tuples, and Lists. 115 | val builtInFSharpTypesDestructurer : Destructurer 116 | 117 | /// Extracts the properties for a template from the array of objects. 118 | val captureProperties: template:Template -> args:obj[] -> PropertyNameAndValue seq 119 | 120 | /// Extracts the properties from a message template and the array of objects. 121 | val captureMessageProperties: template:string -> args:obj[] -> PropertyNameAndValue seq 122 | 123 | /// "Captures" properties for the template using the provided Destructurer, 124 | /// Template, and args. If the template has *all* positional properties, the 125 | /// positional indexes are used to match the args to the template properties. 126 | /// Otherwise, arguments are matched left-to-right with the properties, and 127 | /// any extra (unmatched) properties are ignored. 128 | val capturePropertiesCustom: 129 | destr:Destructurer -> maxDepth:int -> template:Template -> args:obj[] -> PropertyNameAndValue seq 130 | 131 | module Formatting = 132 | /// Formats and appends the template message to a TextWriter, using the provided 133 | /// function to look up each TemplatePropertyValue for each property name. 134 | val formatCustom: template:Template 135 | -> tw:System.IO.TextWriter 136 | -> getValueByName: (string -> TemplatePropertyValue) 137 | -> unit 138 | 139 | /// Formats a message template as a string, replacing the properties 140 | /// with the provided values. 141 | val format: template:Template -> values:obj[] -> string 142 | 143 | /// Prints the message template to a string builder. 144 | val bprintsm: sb:System.Text.StringBuilder -> template:string -> args:obj[] -> unit 145 | 146 | /// Prints the message template to a string. 147 | val sprintsm: provider:System.IFormatProvider -> template:string -> args:obj[] -> string 148 | 149 | /// Prints the message template a text writer. 150 | val fprintsm: tw:System.IO.TextWriter -> template:string -> args:obj[] -> unit 151 | 152 | /// Prints the message template to a string builder. 153 | val bprintm: template:Template -> sb:System.Text.StringBuilder -> args:obj[] -> unit 154 | 155 | /// Prints the message template to a string. 156 | val sprintm: template:Template -> provider:System.IFormatProvider -> args:obj[] -> string 157 | 158 | /// Prints the message template a text writer. 159 | val fprintm: template:Template -> tw:System.IO.TextWriter -> args:obj[] -> unit 160 | -------------------------------------------------------------------------------- /src/FsMtParser/FsMtParser.fs: -------------------------------------------------------------------------------- 1 | module FsMtParser 2 | 3 | open System.Text 4 | 5 | type Property(name : string, format : string) = 6 | static let emptyInstance = Property("", null) 7 | static member empty = emptyInstance 8 | member x.name = name 9 | member x.format = format 10 | member internal x.AppendPropertyString(sb : StringBuilder, ?replacementName) = 11 | sb.Append("{") 12 | .Append(defaultArg replacementName name) 13 | .Append(match x.format with null | "" -> "" | _ -> ":" + x.format) 14 | .Append("}") 15 | override x.ToString() = x.AppendPropertyString(StringBuilder()).ToString() 16 | 17 | module internal ParserBits = 18 | 19 | let inline isLetterOrDigit c = System.Char.IsLetterOrDigit c 20 | let inline isValidInPropName c = c = '_' || System.Char.IsLetterOrDigit c 21 | let inline isValidInFormat c = c <> '}' && (c = ' ' || isLetterOrDigit c || System.Char.IsPunctuation c) 22 | let inline isValidCharInPropTag c = c = ':' || isValidInPropName c || isValidInFormat c 23 | 24 | [] 25 | type Range(startIndex : int, endIndex : int) = 26 | member inline x.start = startIndex 27 | member inline x.``end`` = endIndex 28 | member inline x.length = (endIndex - startIndex) + 1 29 | member inline x.getSubstring (s : string) = s.Substring(startIndex, x.length) 30 | member inline x.isEmpty = startIndex = -1 && endIndex = -1 31 | static member inline substring (s : string, startIndex, endIndex) = s.Substring(startIndex, (endIndex - startIndex) + 1) 32 | static member inline empty = Range(-1, -1) 33 | 34 | let inline tryGetFirstCharInRange predicate (s : string) (range : Range) = 35 | let rec go i = 36 | if i > range.``end`` then -1 37 | else if not (predicate s.[i]) then go (i+1) else i 38 | go range.start 39 | 40 | let inline tryGetFirstChar predicate (s : string) first = 41 | tryGetFirstCharInRange predicate s (Range(first, s.Length - 1)) 42 | 43 | let inline hasAnyInRange predicate (s : string) (range : Range) = 44 | match tryGetFirstChar (predicate) s range.start with 45 | | -1 -> 46 | false 47 | | i -> 48 | i <= range.``end`` 49 | 50 | let inline hasAny predicate (s : string) = hasAnyInRange predicate s (Range(0, s.Length - 1)) 51 | let inline indexOfInRange s range c = tryGetFirstCharInRange ((=) c) s range 52 | 53 | let inline tryGetPropInRange (template : string) (within : Range) : Property = 54 | // Attempts to validate and parse a property token within the specified range inside 55 | // the template string. If the property insides contains any invalid characters, 56 | // then the `Property.Empty' instance is returned (hence the name 'try') 57 | let nameRange, formatRange = 58 | match indexOfInRange template within ':' with 59 | | -1 -> 60 | within, Range.empty // no format 61 | | formatIndex -> 62 | Range(within.start, formatIndex-1), Range(formatIndex+1, within.``end``) // has format part 63 | let propertyName = nameRange.getSubstring template 64 | if propertyName = "" || (hasAny (not<unit) : int = 74 | // Finds the next text token (starting from the 'startAt' index) and returns the next character 75 | // index within the template string. If the end of the template string is reached, or the start 76 | // of a property token is found (i.e. a single { character), then the 'consumed' text is passed 77 | // to the 'foundText' method, and index of the next character is returned. 78 | let mutable escapedBuilder = Unchecked.defaultof // don't create one until it's needed 79 | let inline append (ch : char) = if not (isNull escapedBuilder) then escapedBuilder.Append(ch) |> ignore 80 | let inline createStringBuilderAndPopulate i = 81 | if isNull escapedBuilder then 82 | escapedBuilder <- StringBuilder() // found escaped open-brace, take the slow path 83 | for chIndex = startAt to i-1 do append template.[chIndex] // append all existing chars 84 | let rec go i = 85 | if i >= template.Length then 86 | template.Length // bail out at the end of the string 87 | else 88 | let ch = template.[i] 89 | match ch with 90 | | '{' -> 91 | if (i+1) < template.Length && template.[i+1] = '{' then 92 | createStringBuilderAndPopulate i 93 | append ch; go (i+2) 94 | else i // found an open brace (potentially a property), so bail out 95 | | '}' when (i+1) < template.Length && template.[i+1] = '}' -> 96 | createStringBuilderAndPopulate i 97 | append ch; go (i+2) 98 | | _ -> 99 | append ch; go (i+1) 100 | 101 | let nextIndex = go startAt 102 | if (nextIndex > startAt) then // if we 'consumed' any characters, signal that we 'foundText' 103 | if isNull escapedBuilder then 104 | foundText (Range.substring(template, startAt, nextIndex - 1)) 105 | else 106 | foundText (escapedBuilder.ToString()) 107 | nextIndex 108 | 109 | let findPropOrText (start : int) (template : string) 110 | (foundText : string -> unit) 111 | (foundProp : Property -> unit) : int = 112 | // Attempts to find the indices of the next property in the template 113 | // string (starting from the 'start' index). Once the start and end of 114 | // the property token is known, it will be further validated (by the 115 | // tryGetPropInRange method). If the range turns out to be invalid, it's 116 | // not a property token, and we return it as text instead. We also need 117 | // to handle some special case here: if the end of the string is reached, 118 | // without finding the close brace (we just signal 'foundText' in that case). 119 | let nextInvalidCharIndex = 120 | match tryGetFirstChar (not << isValidCharInPropTag) template (start+1) with 121 | | -1 -> 122 | template.Length 123 | | idx -> 124 | idx 125 | 126 | if nextInvalidCharIndex = template.Length || template.[nextInvalidCharIndex] <> '}' then 127 | foundText (Range.substring(template, start, (nextInvalidCharIndex - 1))) 128 | nextInvalidCharIndex 129 | else 130 | let nextIndex = nextInvalidCharIndex + 1 131 | let propInsidesRng = Range(start + 1, nextIndex - 2) 132 | match tryGetPropInRange template propInsidesRng with 133 | | prop when not (obj.ReferenceEquals(prop, Property.empty)) -> 134 | foundProp prop 135 | | _ -> 136 | foundText (Range.substring(template, start, (nextIndex - 1))) 137 | nextIndex 138 | 139 | /// Parses template strings such as "Hello, {PropertyWithFormat:##.##}" 140 | /// and calls the 'foundTextF' or 'foundPropF' functions as the text or 141 | /// property tokens are encountered. 142 | let parseParts (template : string) foundTextF foundPropF = 143 | let tlen = template.Length 144 | let rec go start = 145 | if start >= tlen then () 146 | else match ParserBits.findNextNonPropText start template foundTextF with 147 | | next when next <> start -> 148 | go next 149 | | _ -> 150 | go (ParserBits.findPropOrText start template foundTextF foundPropF) 151 | go 0 152 | -------------------------------------------------------------------------------- /src/FsMtParser/FsMtParser.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 1.0.0 6 | alpha 7 | 8 | 9 | 10 | Adam Chester 11 | en-US 12 | FsMtParser 13 | F# Message Templates parser - the ability to parse {named} string values 14 | 15 | 16 | 17 | F# Message Templates parser 18 | message;template;serilog;F#;fsharp;logging;semantic;structured 19 | Alpha release 20 | http://messagetemplates.org/images/messagetemplates-nuget.png 21 | https://github.com/messagetemplates/messagetemplates-fsharp 22 | https://www.apache.org/licenses/LICENSE-2.0 23 | false 24 | git 25 | https://github.com/messagetemplates/messagetemplates-fsharp 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/FsMtParser/FsMtParserFull.fs: -------------------------------------------------------------------------------- 1 | module FsMtParserFull 2 | 3 | open System.Text 4 | 5 | type CaptureHint = Default = 0 | Structure = 1 | Stringify = 2 6 | type AlignDirection = Right = 0 | Left = 1 7 | 8 | [] 9 | type AlignInfo = 10 | new (direction : AlignDirection, width : int) = { _direction=direction; _width=width; } 11 | new (isValid : bool) = { _direction = AlignDirection.Right; _width = (if isValid then -1 else -2) } 12 | val private _direction : AlignDirection 13 | val private _width : int 14 | member this.direction with get() = this._direction 15 | member this.width with get() = this._width 16 | member this.isEmpty = this.width = -1 17 | member internal this.isValid = this.width <> -2 18 | static member empty = AlignInfo(isValid=true) 19 | static member invalid = AlignInfo(isValid=false) 20 | 21 | type Property(name : string, format : string, captureHint : CaptureHint, align : AlignInfo) = 22 | static let emptyInstance = Property("", null, CaptureHint.Default, AlignInfo.empty) 23 | static member empty = emptyInstance 24 | member x.name = name 25 | member x.format = format 26 | member x.captureHint = captureHint 27 | member x.align = align 28 | member internal x.AppendPropertyString(sb : StringBuilder, ?replacementName) = 29 | sb.Append("{") 30 | .Append(match x.captureHint with CaptureHint.Structure -> "@" | CaptureHint.Stringify -> "$" | _ -> "") 31 | .Append(defaultArg replacementName name) 32 | .Append(match x.format with null | "" -> "" | _ -> ":" + x.format) |> ignore 33 | 34 | if not (x.align.isEmpty) then 35 | sb.Append(if x.align.direction = AlignDirection.Right then ",-" else ",") 36 | .Append(string x.align.width) |> ignore 37 | 38 | sb.Append("}") 39 | override x.ToString() = x.AppendPropertyString(StringBuilder()).ToString() 40 | 41 | module internal ParserBits = 42 | 43 | let inline isLetterOrDigit c = System.Char.IsLetterOrDigit c 44 | let inline isValidInPropName c = c = '_' || System.Char.IsLetterOrDigit c 45 | let inline isValidInAlignment c = c = '-' || System.Char.IsDigit c 46 | let inline isValidInCaptureHint c = c = '@' || c = '$' 47 | let inline isValidInFormat c = c <> '}' && (c = ' ' || isLetterOrDigit c || System.Char.IsPunctuation c) 48 | let inline isValidCharInPropTag c = c = ':' || isValidInPropName c || isValidInFormat c || isValidInCaptureHint c 49 | 50 | [] 51 | type Range(startIndex : int, endIndex : int) = 52 | member inline x.start = startIndex 53 | member inline x.``end`` = endIndex 54 | member inline x.length = (endIndex - startIndex) + 1 55 | member inline x.getSubstring (s : string) = s.Substring(startIndex, x.length) 56 | member inline x.isEmpty = startIndex = -1 && endIndex = -1 57 | override x.ToString() = sprintf "(start=%i, end=%i)" x.start x.``end`` 58 | static member inline substring (s : string, startIndex, endIndex) = s.Substring(startIndex, (endIndex - startIndex) + 1) 59 | static member inline empty = Range(-1, -1) 60 | 61 | let inline tryGetFirstCharInRange predicate (s : string) (range : Range) = 62 | let rec go i = 63 | if i > range.``end`` then -1 64 | else if not (predicate s.[i]) then go (i+1) else i 65 | go range.start 66 | 67 | let inline tryGetFirstChar predicate (s : string) first = 68 | tryGetFirstCharInRange predicate s (Range(first, s.Length - 1)) 69 | 70 | let inline hasAnyInRange predicate (s : string) (range : Range) = 71 | match tryGetFirstChar (predicate) s range.start with 72 | | -1 -> 73 | false 74 | | i -> 75 | i <= range.``end`` 76 | 77 | let inline hasAny predicate (s : string) = hasAnyInRange predicate s (Range(0, s.Length - 1)) 78 | let inline indexOfInRange s range c = tryGetFirstCharInRange ((=) c) s range 79 | 80 | /// Attemps to parse an integer from the range within a string. Returns invalidValue if the 81 | /// string does not contain an integer. The '-' is allowed only as the first character. 82 | let inline tryParseIntFromRng (invalidValue : int) (s : string) (range : Range) = 83 | if range.length = 1 && '0' <= s.[0] && s.[0] <= '9' then 84 | int (s.[range.start]) - 48 85 | else 86 | let indexOfLastCharPlus1 = range.``end``+1 87 | let rec inside isNeg numSoFar i = 88 | if i = indexOfLastCharPlus1 then 89 | if isNeg then -numSoFar else numSoFar 90 | else 91 | let c = s.[i] 92 | if c = '-' then 93 | if i = range.start then inside true (numSoFar) (i+1) 94 | else invalidValue // no '-' character allowed other than first char 95 | elif '0' <= c && c <= '9' then 96 | inside isNeg (10*numSoFar + int c - 48) (i+1) 97 | else invalidValue 98 | 99 | inside false 0 range.start 100 | 101 | /// Will parse ",-10" as AlignInfo(direction=Left, width=10). 102 | /// Will parse ",5" as AlignInfo(direction=Right, width=5). 103 | /// Will parse ",0" as AlignInfo(isValid=false) because 0 is not a valid alignment. 104 | /// Will parse ",-0" as AlignInfo(isValid=false) because 0 is not a valid alignment. 105 | /// Will parse ",,5" as AlignInfo(isValid=false) because ',5' is not an int. 106 | /// Will parse ",5-" as AlignInfo(isValid=false) because '-' is in the wrong place. 107 | /// Will parse ",asdf" as AlignInfo(isValid=false) because 'asdf' is not an int. 108 | let inline tryParseAlignInfoRng (s:string) (rng:Range) = 109 | match s, rng with 110 | | s, rng when (rng.start > rng.``end``) || (hasAnyInRange (not << isValidInAlignment) s rng) -> 111 | AlignInfo.invalid 112 | 113 | | s, rng -> 114 | let width = 115 | match tryParseIntFromRng (System.Int32.MinValue) s rng with 116 | | System.Int32.MinValue -> 0 // not a valid align number (e.g. dash in wrong spot) 117 | | n -> n 118 | 119 | if width = 0 then AlignInfo.invalid 120 | else 121 | let isNegativeAlignWidth = width < 0 122 | let direction = if isNegativeAlignWidth then AlignDirection.Left else AlignDirection.Right 123 | AlignInfo(direction, abs(width)) 124 | 125 | /// Attempts to validate and parse a property token within the specified range. If the property 126 | /// insides contains any invalid characters, then the `Property.empty' instance is returned. 127 | let inline tryGetPropInRange (template : string) (within : Range) : Property = 128 | let nameRange, alignRange, formatRange = 129 | match indexOfInRange template within ',', indexOfInRange template within ':' with 130 | | -1, -1 -> 131 | // neither align nor format 132 | within, Range.empty, Range.empty 133 | 134 | | -1, fmtIdx -> 135 | // has format part, but does not have align part 136 | Range(within.start, fmtIdx-1), Range.empty, Range(fmtIdx+1, within.``end``) 137 | 138 | | alIdx, -1 -> 139 | // has align part, but does not have format part 140 | Range(within.start, alIdx-1), Range(alIdx+1, within.``end``), Range.empty 141 | 142 | | alIdx, fmtIdx when alIdx < fmtIdx && alIdx <> (fmtIdx-1) -> 143 | // has both align and format parts, in the correct order 144 | let align = Range(alIdx+1, fmtIdx-1) 145 | let fmt = Range(fmtIdx+1, within.``end``) 146 | Range(within.start, alIdx-1), align, fmt 147 | 148 | | alIdx, fmtIdx when alIdx > fmtIdx -> 149 | // has format part, no align (but one or more commas *inside* the format string) 150 | Range(within.start, fmtIdx-1), Range.empty, Range(fmtIdx+1, within.``end``) 151 | 152 | | _, _ -> 153 | Range.empty, Range.empty, Range.empty 154 | 155 | if nameRange.isEmpty then 156 | Property.empty // property name is empty 157 | else 158 | let maybeCaptureHintChar = template.[nameRange.start] 159 | let propertyNameStartIndex, captureHint = 160 | match maybeCaptureHintChar with 161 | | '@' -> nameRange.start+1, CaptureHint.Structure 162 | | '$' -> nameRange.start+1, CaptureHint.Stringify 163 | | _ -> nameRange.start, CaptureHint.Default 164 | 165 | let propertyName = Range.substring (template, propertyNameStartIndex, nameRange.``end``) 166 | if propertyName = "" || (hasAny (not< Property(propertyName, null, captureHint, AlignInfo.empty) 175 | | true, false -> Property(propertyName, formatRange.getSubstring template, captureHint, AlignInfo.empty) 176 | | false, _ -> 177 | let formatString = if formatRange.isEmpty then null else formatRange.getSubstring template 178 | match tryParseAlignInfoRng template alignRange with 179 | | ai when ai.isValid -> Property(propertyName, formatString, captureHint, ai) 180 | | _ -> Property.empty // align has invalid characters 181 | 182 | let inline findNextNonPropText (startAt : int) (template : string) (foundText : string->unit) : int = 183 | // Finds the next text token (starting from the 'startAt' index) and returns the next character 184 | // index within the template string. If the end of the template string is reached, or the start 185 | // of a property token is found (i.e. a single { character), then the 'consumed' text is passed 186 | // to the 'foundText' method, and index of the next character is returned. 187 | let mutable escapedBuilder = Unchecked.defaultof // don't create one until it's needed 188 | let inline append (ch : char) = if not (isNull escapedBuilder) then escapedBuilder.Append(ch) |> ignore 189 | let inline createStringBuilderAndPopulate i = 190 | if isNull escapedBuilder then 191 | escapedBuilder <- StringBuilder() // found escaped open-brace, take the slow path 192 | for chIndex = startAt to i-1 do append template.[chIndex] // append all existing chars 193 | let rec go i = 194 | if i >= template.Length then 195 | template.Length // bail out at the end of the string 196 | else 197 | let ch = template.[i] 198 | match ch with 199 | | '{' -> 200 | if (i+1) < template.Length && template.[i+1] = '{' then 201 | createStringBuilderAndPopulate i 202 | append ch; go (i+2) 203 | else i // found an open brace (potentially a property), so bail out 204 | | '}' when (i+1) < template.Length && template.[i+1] = '}' -> 205 | createStringBuilderAndPopulate i 206 | append ch; go (i+2) 207 | | _ -> 208 | append ch; go (i+1) 209 | 210 | let nextIndex = go startAt 211 | if (nextIndex > startAt) then // if we 'consumed' any characters, signal that we 'foundText' 212 | if isNull escapedBuilder then 213 | foundText (Range.substring(template, startAt, nextIndex - 1)) 214 | else 215 | foundText (escapedBuilder.ToString()) 216 | nextIndex 217 | 218 | let findPropOrText (start : int) (template : string) 219 | (foundText : string -> unit) 220 | (foundProp : Property -> unit) : int = 221 | // Attempts to find the indices of the next property in the template 222 | // string (starting from the 'start' index). Once the start and end of 223 | // the property token is known, it will be further validated (by the 224 | // tryGetPropInRange method). If the range turns out to be invalid, it's 225 | // not a property token, and we return it as text instead. We also need 226 | // to handle some special case here: if the end of the string is reached, 227 | // without finding the close brace (we just signal 'foundText' in that case). 228 | let nextInvalidCharIndex = 229 | match tryGetFirstChar (not << isValidCharInPropTag) template (start+1) with 230 | | -1 -> 231 | template.Length 232 | | idx -> 233 | idx 234 | 235 | if nextInvalidCharIndex = template.Length || template.[nextInvalidCharIndex] <> '}' then 236 | foundText (Range.substring(template, start, (nextInvalidCharIndex - 1))) 237 | nextInvalidCharIndex 238 | else 239 | let nextIndex = nextInvalidCharIndex + 1 240 | let propInsidesRng = Range(start + 1, nextIndex - 2) 241 | match tryGetPropInRange template propInsidesRng with 242 | | prop when not (obj.ReferenceEquals(prop, Property.empty)) -> 243 | foundProp prop 244 | | _ -> 245 | foundText (Range.substring(template, start, (nextIndex - 1))) 246 | nextIndex 247 | 248 | /// Parses template strings such as "Hello, {PropertyWithFormat:##.##}" 249 | /// and calls the 'foundTextF' or 'foundPropF' functions as the text or 250 | /// property tokens are encountered. 251 | let parseParts (template : string) foundTextF foundPropF = 252 | let tlen = template.Length 253 | let rec go start = 254 | if start >= tlen then () 255 | else match ParserBits.findNextNonPropText start template foundTextF with 256 | | next when next <> start -> 257 | go next 258 | | _ -> 259 | go (ParserBits.findPropOrText start template foundTextF foundPropF) 260 | go 0 -------------------------------------------------------------------------------- /src/FsMtParser/FsMtParserFullTest.fsx: -------------------------------------------------------------------------------- 1 | #load "FsMtParserFull.fs" 2 | 3 | type Token = TextToken of string | PropToken of FsMtParserFull.Property 4 | let tokens = ResizeArray() 5 | 6 | let foundText text = 7 | tokens.Add(TextToken(text)) 8 | () // printfn "TEXT: %s" 9 | let foundProp prop = 10 | tokens.Add(PropToken(prop)) 11 | () //printfn "PROP: %A" prop 12 | 13 | #time "on" 14 | ;; 15 | 16 | tokens.Clear() 17 | FsMtParserFull.parseParts "Hello {{@adam:blah}}, how are {{you}}?" foundText foundProp 18 | tokens 19 | 20 | for i = 0 to 1000000 do 21 | // tokens.Clear() 22 | FsMtParserFull.parseParts "Hello {adam:#0.000}, how are {you? you crazy invalid prop}" foundText foundProp 23 | // printfn "%A" (tokens.ToArray()) 24 | -------------------------------------------------------------------------------- /src/FsMtParser/FsMtParserTest.fsx: -------------------------------------------------------------------------------- 1 | #load "FsMtParser.fs" 2 | 3 | type Token = TextToken of string | PropToken of FsMtParser.Property 4 | let tokens = ResizeArray() 5 | 6 | let foundText text = 7 | tokens.Add(TextToken(text)) 8 | () // printfn "TEXT: %s" 9 | let foundProp prop = 10 | tokens.Add(PropToken(prop)) 11 | () //printfn "PROP: %A" prop 12 | 13 | #time "on" 14 | ;; 15 | 16 | tokens.Clear() 17 | FsMtParser.parseParts "This {{adam:blah}}, is all text {{you}}?" foundText foundProp 18 | FsMtParser.parseParts "This {{@adam:blah}}, is all text {{you}}?" foundText foundProp 19 | FsMtParser.parseParts "This {adam:blah}, has actual valid properties?" foundText foundProp 20 | tokens 21 | 22 | for i = 0 to 1000000 do 23 | // tokens.Clear() 24 | FsMtParser.parseParts "Hello {adam:#0.000}, how are {you? you crazy invalid prop}" foundText foundProp 25 | // printfn "%A" (tokens.ToArray()) 26 | -------------------------------------------------------------------------------- /test/FsMessageTemplates.Tests/CsToFs.fs: -------------------------------------------------------------------------------- 1 | module FsTests.CsToFs 2 | 3 | open FsMessageTemplates 4 | open Tk 5 | 6 | type Kvp<'a,'b> = System.Collections.Generic.KeyValuePair<'a,'b> 7 | 8 | type FsMt = FsMessageTemplates.Template 9 | type CsMt = MessageTemplates.MessageTemplate 10 | 11 | type CsTextToken = MessageTemplates.Parsing.TextToken 12 | type CsPropertyToken = MessageTemplates.Parsing.PropertyToken 13 | type CsDestructuring = MessageTemplates.Parsing.Destructuring 14 | type CsAlignmentDirection = MessageTemplates.Parsing.AlignmentDirection 15 | type CsMessageTemplateToken = MessageTemplates.Parsing.MessageTemplateToken 16 | type CsTemplateProperty = MessageTemplates.Structure.TemplateProperty 17 | type CsTemplatePropertyValue = MessageTemplates.Structure.TemplatePropertyValue 18 | type CsScalarValue = MessageTemplates.Structure.ScalarValue 19 | type CsSequenceValue = MessageTemplates.Structure.SequenceValue 20 | type CsStructureValue = MessageTemplates.Structure.StructureValue 21 | type CsDictionaryValue = MessageTemplates.Structure.DictionaryValue 22 | 23 | let (|Null|Value|) (x: _ System.Nullable) = if x.HasValue then Value x.Value else Null 24 | 25 | /// Converts a C# TextToken to an F# Token.Text 26 | let textToToken (tt: CsTextToken) = TextToken(tt.StartIndex, tt.Text) 27 | 28 | /// Converts a C# PropertyToken to an F# Token.Prop 29 | let propToToken (pr: CsPropertyToken) = 30 | let pos = match pr.TryGetPositionalValue() with | true, i -> i | false, _ -> -1 31 | let destr = match pr.Destructuring with 32 | | CsDestructuring.Default -> DestrHint.Default 33 | | CsDestructuring.Destructure -> DestrHint.Destructure 34 | | CsDestructuring.Stringify -> DestrHint.Stringify 35 | | d -> failwithf "unknown destructure %A" d 36 | let getDirection d = match d with 37 | | CsAlignmentDirection.Left -> Direction.Left 38 | | CsAlignmentDirection.Right -> Direction.Right 39 | | _ -> failwithf "unknown direction %A" d 40 | let align = match pr.Alignment with 41 | | Value v -> AlignInfo(getDirection v.Direction, v.Width) 42 | | Null _ -> AlignInfo.Empty 43 | PropToken(pr.StartIndex, Property(pr.PropertyName, pos, destr, align, pr.Format)) 44 | 45 | /// Converts a C# MessageTemplateToken to an F# Token 46 | let mttToToken (mtt: CsMessageTemplateToken) : Token = 47 | match mtt with 48 | | :? CsPropertyToken as pt -> propToToken pt 49 | | :? CsTextToken as tt -> textToToken tt 50 | | _ -> failwithf "unknown token %A" mtt 51 | 52 | /// Converts a C# TemplatePropertyValue to an F# TemplatePropertyValue 53 | let rec templatePropertyValue (tpv: CsTemplatePropertyValue) : TemplatePropertyValue = 54 | match tpv with 55 | | :? MessageTemplates.Structure.ScalarValue as sv -> ScalarValue sv.Value 56 | | :? MessageTemplates.Structure.SequenceValue as sev -> SequenceValue (sev.Elements |> Seq.map templatePropertyValue |> Seq.toList) 57 | | :? MessageTemplates.Structure.DictionaryValue as dv -> 58 | let keyMap (kvp:Kvp) = templatePropertyValue kvp.Key 59 | let valueMap (kvp:Kvp) = templatePropertyValue kvp.Value 60 | DictionaryValue (dv.Elements |> Seq.map (fun kvp -> keyMap kvp, valueMap kvp) |> Seq.toList) 61 | | :? MessageTemplates.Structure.StructureValue as strv -> 62 | let structureValues = strv.Properties |> Seq.map (fun p -> PropertyNameAndValue(p.Name, templatePropertyValue p.Value)) |> Seq.toList 63 | StructureValue (strv.TypeTag, structureValues) 64 | | _ -> failwithf "unknown template property value type %A" tpv 65 | 66 | let templateProperty (tp: CsTemplateProperty) = PropertyNameAndValue(tp.Name, (templatePropertyValue tp.Value)) 67 | 68 | 69 | /// Creates a C# TemplatePropertyValue from an F# TemplatePropertyValue 70 | let rec createCsTpvFromFsTpv (fsTpv: TemplatePropertyValue) : CsTemplatePropertyValue = 71 | match fsTpv with 72 | | ScalarValue v -> CsScalarValue v :> CsTemplatePropertyValue 73 | | SequenceValue values -> CsSequenceValue(values |> List.map createCsTpvFromFsTpv) :> CsTemplatePropertyValue 74 | | StructureValue(typeTag, values) -> 75 | let csTemplateProperties = 76 | values |> List.map (fun fsPnV -> CsTemplateProperty(fsPnV.Name, createCsTpvFromFsTpv fsPnV.Value)) 77 | CsStructureValue(csTemplateProperties, typeTag) :> CsTemplatePropertyValue 78 | | DictionaryValue(data) -> 79 | let fsTpvToCsScalar = function ScalarValue sv -> CsScalarValue(sv) | _ -> failwithf "cannot convert tpv to Scalar %A" fsTpv 80 | let csValues = data |> Seq.map (fun (k, v) -> Kvp(fsTpvToCsScalar k, createCsTpvFromFsTpv v)) 81 | CsDictionaryValue csValues :> CsTemplatePropertyValue 82 | 83 | /// Creates a C# ITemplatePropertyValueFactor from an F# Destructurer 84 | let createTpvFactory (destr: Destructurer) : MessageTemplates.Core.IMessageTemplatePropertyValueFactory = 85 | { new MessageTemplates.Core.IMessageTemplatePropertyValueFactory with 86 | member __.CreatePropertyValue(value: obj, destructureObjects: bool) : MessageTemplates.Structure.TemplatePropertyValue = 87 | let hint = if destructureObjects then DestrHint.Destructure else DestrHint.Default 88 | let req = DestructureRequest (destr, value, 10, 1, hint=hint) 89 | destr req |> createCsTpvFromFsTpv 90 | } 91 | 92 | let fsDestrToCsDestrPolicy (destr: Destructurer) = 93 | { new MessageTemplates.Core.IDestructuringPolicy with 94 | member x.TryDestructure(value: obj, pvf: MessageTemplates.Core.IMessageTemplatePropertyValueFactory, result: byref): bool = 95 | let req = DestructureRequest(destr, value, 10, 1, hint=DestrHint.Destructure) 96 | let tpv = destr req |> createCsTpvFromFsTpv 97 | result <- tpv 98 | true 99 | } 100 | 101 | /// Converts a C# IDestructuringPolicy to an F# Destructurer. 102 | let toFsDestructurer (dp: MessageTemplates.Core.IDestructuringPolicy) : Destructurer = 103 | fun (req: DestructureRequest) -> 104 | let factory = createTpvFactory req.Destructurer 105 | let success, value = dp.TryDestructure (req.Value, factory) 106 | if success then templatePropertyValue value 107 | else Unchecked.defaultof 108 | -------------------------------------------------------------------------------- /test/FsMessageTemplates.Tests/FSharpTypesDestructuringPolicy.fs: -------------------------------------------------------------------------------- 1 | module Destructurama 2 | 3 | open System.Reflection 4 | open FSharp.Reflection 5 | 6 | // From Destructurama.FSharp 7 | type public FSharpTypesDestructuringPolicy() = 8 | interface MessageTemplates.Core.IDestructuringPolicy with 9 | member __.TryDestructure(value, 10 | propertyValueFactory : MessageTemplates.Core.IMessageTemplatePropertyValueFactory, 11 | result: byref) = 12 | let cpv obj = propertyValueFactory.CreatePropertyValue(obj, true) 13 | let lep (n:System.Reflection.PropertyInfo) (v:obj) = 14 | MessageTemplates.Structure.TemplateProperty(n.Name, cpv v) 15 | match value.GetType() with 16 | | t when FSharp.Reflection.FSharpType.IsTuple t -> 17 | let tupleValues = value |> FSharp.Reflection.FSharpValue.GetTupleFields 18 | |> Seq.map cpv 19 | result <- MessageTemplates.Structure.SequenceValue(tupleValues) 20 | true 21 | | t when t.IsConstructedGenericType && t.GetGenericTypeDefinition() = typedefof> -> 22 | let objEnumerable = value :?> System.Collections.IEnumerable |> Seq.cast 23 | result <- MessageTemplates.Structure.SequenceValue(objEnumerable |> Seq.map cpv) 24 | true 25 | | t when FSharp.Reflection.FSharpType.IsUnion t -> 26 | let case, fields = FSharp.Reflection.FSharpValue.GetUnionFields(value, t) 27 | let properties = (case.GetFields(), fields) ||> Seq.map2 lep 28 | result <- MessageTemplates.Structure.StructureValue(properties, case.Name) 29 | true 30 | | _ -> false 31 | 32 | 33 | -------------------------------------------------------------------------------- /test/FsMessageTemplates.Tests/FsMessageTemplates.Tests.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /test/FsMessageTemplates.Tests/FsTests.Capture.fs: -------------------------------------------------------------------------------- 1 | module FsTests.Capture 2 | 3 | open System 4 | open Tk 5 | open FsMessageTemplates 6 | open Asserts 7 | 8 | [] 9 | let ``no values provided yields no properties`` (lang) = 10 | MtAssert.DestructuredAs(lang, "this {will} {capture} {nothing}, I hope", [||], []) 11 | 12 | [] 13 | let ``one named property and one value yields the correct named property`` (lang) = 14 | MtAssert.DestructuredAs(lang, "this {will} work, I hope", [|"might"|], 15 | expected = [ PropertyNameAndValue("will", ScalarValue "might") ]) 16 | 17 | [] 18 | let ``one enumerable property yeilds a sequence value`` (lang) = 19 | MtAssert.DestructuredAs(lang, "this {will} work, I hope", [|[|1..3|]|], 20 | expected = [ PropertyNameAndValue("will", SequenceValue 21 | [ ScalarValue 1; ScalarValue 2; ScalarValue 3; ])]) 22 | 23 | type Chair() = 24 | member __.Back with get() = "straight" 25 | member __.Legs with get() = [|1;2;3;4|] 26 | override __.ToString() = "a chair" 27 | 28 | let chairStructureValue = 29 | let propNamesAndValues = [ PropertyNameAndValue("Back", ScalarValue "straight") 30 | PropertyNameAndValue("Legs", SequenceValue ([ ScalarValue 1; ScalarValue 2; ScalarValue 3; ScalarValue 4 ])) 31 | ] 32 | StructureValue ("Chair", propNamesAndValues) 33 | 34 | [] 35 | let ``a destructured dictionary yeilds dictionary values`` (lang) = 36 | let inputDictionary = System.Collections.Generic.Dictionary(dict [| "key", Chair(); |]) 37 | MtAssert.DestructuredAs(lang, "this {@will} work, I hope", [| inputDictionary |], 38 | expected = [ PropertyNameAndValue("will", DictionaryValue [ ScalarValue "key", chairStructureValue ]) ]) 39 | 40 | [] 41 | let ``an F# 'dict' (which is not Dictionary<_,_>) yeilds a sequence->structure value`` (lang) = 42 | let inputDictionary = dict [| "firstDictEntryKey", Chair(); |] 43 | MtAssert.DestructuredAs(lang, "this {@will} work, I hope", [| inputDictionary |], 44 | expected = [ 45 | PropertyNameAndValue("will", 46 | SequenceValue [ 47 | StructureValue("KeyValuePair`2", 48 | [ PropertyNameAndValue("Key", ScalarValue "firstDictEntryKey") 49 | PropertyNameAndValue("Value", chairStructureValue) ]) 50 | ]) 51 | ]) 52 | 53 | [] 54 | let ``a class instance is captured as a structure value`` (lang) = 55 | MtAssert.DestructuredAs(lang, "I sat at {@Chair}", [|Chair()|], 56 | [ PropertyNameAndValue("Chair", chairStructureValue) ]) 57 | 58 | [] 59 | let ``one positional property and one value yields the correct positional property`` (lang) = 60 | MtAssert.DestructuredAs(lang, "this {0} work, I hope", [|"will"|], [ PropertyNameAndValue("0", ScalarValue "will") ]) 61 | 62 | [] 63 | let ``multiple positional property and the same number of values yields the correct positional properties`` (lang) = 64 | MtAssert.DestructuredAs(lang, "{0} {1} {2} {3} {4} {5}, I hope", [|"this"; 10; true;"this"; 10; true|], 65 | [ PropertyNameAndValue("0", ScalarValue ("this")) 66 | PropertyNameAndValue("1", ScalarValue (10)) 67 | PropertyNameAndValue("2", ScalarValue (true)) 68 | PropertyNameAndValue("3", ScalarValue ("this")) 69 | PropertyNameAndValue("4", ScalarValue (10)) 70 | PropertyNameAndValue("5", ScalarValue (true)) ]) 71 | 72 | type MyScalarEnum = Zero=0 | One=1 | Two=2 73 | 74 | type ExpectedScalarResult = Different of obj | Same 75 | with override x.ToString() = sprintf "%A" x 76 | 77 | let scalars : (obj * ExpectedScalarResult) list = [ 78 | box (1s), Same 79 | box (2us), Same 80 | box (3), Same 81 | box (4u), Same 82 | box (5L), Same 83 | box (6UL), Same 84 | box ("7"), Same 85 | box (true), Same 86 | box (8uy), Same 87 | box (char 9), Same 88 | box (DateTime(2010, 10, 10)), Same 89 | box (DateTimeOffset(2000, 11, 11, 11, 11, 11, 11, offset=TimeSpan.FromHours 11.0)), Same 90 | box (12.12M), Same 91 | box (13.13), Same 92 | box (Guid.NewGuid()), Same 93 | box (null), Same 94 | box (14.14f), Same 95 | box (TimeSpan(15, 15, 15, 15)), Same 96 | box (Uri("http://localhost:1616/16")), Same 97 | box (box MyScalarEnum.Two), Same 98 | box (box MyScalarEnum.One), Same 99 | box (Nullable(15)), Different (box 15) 100 | box (Nullable()), Different (box null) 101 | ] 102 | 103 | let scalarInputAsObj (v:obj, e:ExpectedScalarResult) = 104 | match e with Same -> v | Different de -> de 105 | let getScalarExpected (v:obj, e:ExpectedScalarResult) = 106 | match e with Same -> v | Different de -> de 107 | 108 | [] 109 | let ``scalar types are captured correctly when positional`` (lang) = 110 | let positionalFmtString = String.Join(" ", (scalars |> Seq.mapi (fun i _ -> "{" + string i + "}"))) 111 | let valuesArray = scalars |> Seq.map (scalarInputAsObj) |> Seq.toArray 112 | let expected = scalars |> List.mapi (fun i s -> PropertyNameAndValue(string i, ScalarValue (getScalarExpected s))) 113 | MtAssert.DestructuredAs(lang, positionalFmtString, valuesArray, expected) 114 | 115 | [] 116 | let ``scalar types are captured correctly when positionally out of order`` (lang) = 117 | let numberedOutOfOrder = scalars |> List.mapi (fun i items -> i, items) |> List.sortBy (fun x -> x.GetHashCode()) 118 | let posTokensStringsOutOfOrder = numberedOutOfOrder |> Seq.map (fun (i, _) -> "{" + string i + "}") 119 | let outOfOrder = numberedOutOfOrder |> List.map (fun (i, items) -> items) 120 | let positionalFmtString = String.Join(" ", posTokensStringsOutOfOrder) 121 | let values = outOfOrder |> Seq.map (scalarInputAsObj) |> Seq.toArray 122 | let expected = outOfOrder |> List.mapi (fun i s -> PropertyNameAndValue(string i, ScalarValue (getScalarExpected s))) 123 | MtAssert.DestructuredAs(lang, positionalFmtString, values, expected) 124 | 125 | [] 126 | let ``scalar types are captured correctly when named`` (lang) = 127 | let namedFmtString = String.Join(" ", (scalars |> Seq.mapi (fun i v -> "{named" + (string i) + "}"))) 128 | let values = scalars |> Seq.map (fun s -> (scalarInputAsObj s)) |> Seq.toArray 129 | let expected = scalars |> List.mapi (fun i s -> PropertyNameAndValue("named" + string i, ScalarValue (getScalarExpected s))) 130 | MtAssert.DestructuredAs(lang, namedFmtString, values, expected) 131 | 132 | [] 133 | let ``multiple positional property capture the correct integral scalar types`` (lang) = 134 | let values : obj[] = [| 1s; 2us; 3; 4u; 5L; 6UL |] 135 | MtAssert.DestructuredAs(lang, "{0} {1} {2}, I hope, {3} {4} {5}", values, 136 | [ PropertyNameAndValue("0", ScalarValue (box 1s)) 137 | PropertyNameAndValue("1", ScalarValue (box 2us)) 138 | PropertyNameAndValue("2", ScalarValue (box 3)) 139 | PropertyNameAndValue("3", ScalarValue (box 4u)) 140 | PropertyNameAndValue("4", ScalarValue (box 5L)) 141 | PropertyNameAndValue("5", ScalarValue (box 6UL)) ]) 142 | 143 | [] 144 | let ``multiple positional nullable properties capture the correct integral scalar types`` (lang) = 145 | let values : obj[] = [| 146 | (Nullable 1s) 147 | (Nullable 2us) 148 | (Nullable 3) 149 | (Nullable 4u) 150 | (Nullable 5L) 151 | (Nullable 6UL)|] 152 | MtAssert.DestructuredAs(lang, "{0} {1} {2}, I hope, {3} {4} {5}", values, 153 | [ PropertyNameAndValue("0", ScalarValue (box 1s)) 154 | PropertyNameAndValue("1", ScalarValue (box 2us)) 155 | PropertyNameAndValue("2", ScalarValue (box 3)) 156 | PropertyNameAndValue("3", ScalarValue (box 4u)) 157 | PropertyNameAndValue("4", ScalarValue (box 5L)) 158 | PropertyNameAndValue("5", ScalarValue (box 6UL)) ]) 159 | 160 | type MyDu = Case1 | Case2 161 | type MyDuWithTuple = 162 | | Case1AB of a:int * b:string 163 | | Case2AB 164 | 165 | [] 166 | module St = 167 | /// named scalar value 168 | let scal (name, value) = PropertyNameAndValue(name, ScalarValue value) 169 | /// named structure value 170 | let strv (name, values) = StructureValue(name, values) 171 | /// property name and value 172 | let pnv (name, value) = PropertyNameAndValue(name, value) 173 | 174 | [] 175 | let ``Default destructuring an F# union works`` (lang) = 176 | // Both cases have no properties/attributes 177 | MtAssert.DestructuredAs(lang, "I like {@first} and {@second}", [| Case1; Case2 |], 178 | expected=[ 179 | pnv("first", strv ("MyDu", [ scal("Tag", 0); scal("IsCase1", true); scal("IsCase2", false) ])) 180 | pnv("second", strv ("MyDu", [ scal("Tag", 1); scal("IsCase1", false); scal("IsCase2", true) ])) 181 | ]) 182 | 183 | // One DU case (Case1AB: a:int * b:string) has properties/attributes 184 | // TOD: fix this. The difference between C# and F# is confusing. The 185 | // 'Name' of second (Case2AB) is `_Case2AB` for F# and null for C#... 186 | let expected : PropertyNameAndValue list = 187 | match lang with 188 | | "C#" -> 189 | [ pnv ("first", strv ("Case1AB", [ scal("a", 1) 190 | scal("b", "2") 191 | scal("Tag", 0) 192 | scal("IsCase1AB", true) 193 | scal("IsCase2AB", false) ])) 194 | pnv ("second", strv ("_Case2AB", [ scal("Tag", 1) 195 | scal("IsCase1AB", false) 196 | scal("IsCase2AB", true) ])) ] 197 | | "F#" -> 198 | [ pnv ("first", strv ("Case1AB", [ scal("a", 1) 199 | scal("b", "2") 200 | scal("Tag", 0) 201 | scal("IsCase1AB", true) 202 | scal("IsCase2AB", false) ])) 203 | pnv ("second", strv (null, [ scal("Tag", 1) 204 | scal("IsCase1AB", false) 205 | scal("IsCase2AB", true) ])) ] 206 | | _ -> failwithf "unknown language '%s''" lang 207 | 208 | MtAssert.DestructuredAs(lang, "I like {@first} and {@second}", 209 | [| Case1AB(1,"2"); Case2AB |], expected) 210 | 211 | [] 212 | let ``Default destructuring an F# tuple works`` (lang) = 213 | 214 | let values : obj[] = [| 123,"abc"; 456, "def" |] 215 | let tyTag = values.[0].GetType().Name 216 | MtAssert.DestructuredAs(lang, "I like {@first} and {@second}", values, 217 | expected=[ 218 | pnv("first", strv (tyTag, [ scal("Item1", 123); scal("Item2", "abc"); ])) 219 | pnv("second", strv (tyTag, [ scal("Item1", 456); scal("Item2", "def"); ])) 220 | ]) 221 | 222 | let values : obj[] = [| 1,2,3; 4,5,6 |] 223 | let tyTag = values.[0].GetType().Name 224 | MtAssert.DestructuredAs(lang, "I like {@first} and {@second}", values, 225 | expected=[ 226 | pnv("first", strv (tyTag, [ scal("Item1", 1); scal("Item2", 2); scal("Item3", 3); ])) 227 | pnv("second", strv (tyTag, [ scal("Item1", 4); scal("Item2", 5); scal("Item3", 6); ])) 228 | ]) 229 | -------------------------------------------------------------------------------- /test/FsMessageTemplates.Tests/FsTests.Format.fs: -------------------------------------------------------------------------------- 1 | module FsTests.Format 2 | 3 | open System 4 | open System.Globalization 5 | open Xunit 6 | open Asserts 7 | 8 | type Chair() = 9 | member __.Back with get() = "straight" 10 | member __.Legs with get() = [|1;2;3;4|] 11 | override __.ToString() = "a chair" 12 | 13 | type Receipt() = 14 | member __.Sum with get() = 12.345m 15 | member __.When with get() = System.DateTime(2013, 5, 20, 16, 39, 0) 16 | override __.ToString() = "a receipt" 17 | 18 | // Delegate1 works with tuple arguments. 19 | type MyDelegate = delegate of (int * int) -> int 20 | 21 | [] 22 | let ``a delegate is rendered as a string`` (lang) = 23 | let myDel = MyDelegate(fun (i1,i2) -> i1+i2) 24 | MtAssert.RenderedAs(lang, "What even would a {del} print?", [| myDel |], 25 | "What even would a \"FsTests.Format+MyDelegate\" print?") 26 | 27 | [] 28 | let ``a class instance is rendered in simple notation`` (lang) = 29 | MtAssert.RenderedAs(lang, "I sat at {@Chair}", [|Chair()|], 30 | "I sat at Chair { Back: \"straight\", Legs: [1, 2, 3, 4] }") 31 | 32 | [] 33 | let ``a class instance is rendered in simple notation using format provider`` (lang) = 34 | MtAssert.RenderedAs(lang, "I received {@Receipt}", [|Receipt()|], 35 | "I received Receipt { Sum: 12,345, When: 20/05/2013 16:39:00 }", 36 | provider=CultureInfo("fr-FR")) 37 | 38 | type ChairRecord = { Back:string; Legs: int array } 39 | 40 | [] 41 | let ``an F# record object is rendered in simple notation with type`` (lang) = 42 | MtAssert.RenderedAs(lang, "I sat at {@Chair}", [|{ Back="straight"; Legs=[|1;2;3;4|] }|], 43 | "I sat at ChairRecord { Back: \"straight\", Legs: [1, 2, 3, 4] }") 44 | 45 | type ReceiptRecord = { Sum: double; When: System.DateTime } 46 | 47 | [] 48 | let ``an F# record object is rendered in simple notation with type using format provider`` (lang) = 49 | MtAssert.RenderedAs(lang, "I received {@Receipt}", 50 | [| { Sum=12.345; When=DateTime(2013, 5, 20, 16, 39, 0) } |], 51 | "I received ReceiptRecord { Sum: 12,345, When: 20/05/2013 16:39:00 }", 52 | provider=(CultureInfo("fr-FR"))) 53 | 54 | [] 55 | let ``an object with default destructuring is rendered as a string literal`` (lang) = 56 | MtAssert.RenderedAs(lang, "I sat at {Chair}", [|Chair()|], "I sat at \"a chair\"") 57 | 58 | [] 59 | let ``an object with stringify destructuring is rendered as a string`` (lang) = 60 | MtAssert.RenderedAs(lang, "I sat at {$Chair}", [|Chair()|], "I sat at \"a chair\"") 61 | 62 | [] 63 | let ``multiple properties are rendered in order`` (lang) = 64 | MtAssert.RenderedAs(lang, "Just biting {Fruit} number {Count}", [| "Apple"; 12 |], 65 | "Just biting \"Apple\" number 12") 66 | 67 | [] 68 | let ``a template with only positional properties is analyzed and rendered positionally`` (lang) = 69 | MtAssert.RenderedAs(lang, "{1}, {0}", [|"world"; "Hello"|], "\"Hello\", \"world\"") 70 | 71 | [] 72 | let ``a template with only positional properties uses format provider`` (lang) = 73 | MtAssert.RenderedAs(lang, "{1}, {0}", [| 12.345; "Hello" |], "\"Hello\", 12,345", 74 | provider=(CultureInfo("fr-FR"))) 75 | 76 | // Debatable what the behavior should be, here. 77 | [] 78 | let ``a template with names and positionals uses names for all values`` (lang) = 79 | MtAssert.RenderedAs(lang, "{1}, {Place}, {5}" , [|"world"; "Hello"; "yikes"|], 80 | "\"world\", \"Hello\", \"yikes\"") 81 | 82 | [] 83 | let ``missing positional parameters render as text like standard formats`` (lang) = 84 | MtAssert.RenderedAs(lang, "{1}, {0}", [|"world"|], "{1}, \"world\"") 85 | 86 | [] 87 | let ``extra positional parameters are ignored`` (lang) = 88 | MtAssert.RenderedAs(lang, "{1}, {0}", [|"world"; "world"; "world"|], "\"world\", \"world\"") 89 | 90 | [] 91 | let ``the same positional parameter repeated many times with literal format is reused`` (lang) = 92 | MtAssert.RenderedAs(lang, "{1:l}{1:l}{1:l}{0}{1:l}{1:l}{1:l}", [|"a";"b"|], "bbb\"a\"bbb") 93 | 94 | [] 95 | let ``the same missing positional parameters render literally`` (lang) = 96 | MtAssert.RenderedAs(lang, "{1}{2}{3}{4}{5}{6}{7}{8}{9}", [|"a"|], "{1}{2}{3}{4}{5}{6}{7}{8}{9}") 97 | MtAssert.RenderedAs(lang, "{1}{2}{3}{4}{5}{0}{6}{7}{8}{9}", [|"a"|], "{1}{2}{3}{4}{5}\"a\"{6}{7}{8}{9}") 98 | 99 | [] 100 | let ``multiple properties use format provider`` (lang) = 101 | MtAssert.RenderedAs(lang, "Income was {Income} at {Date:d}", [| 1234.567; DateTime(2013, 5, 20) |], 102 | "Income was 1234,567 at 20/05/2013", provider=(CultureInfo("fr-FR"))) 103 | 104 | [] 105 | let ``format strings are propagated`` (lang) = 106 | MtAssert.RenderedAs(lang, "Welcome, customer {CustomerId:0000}", [|12|], 107 | "Welcome, customer 0012") 108 | 109 | type Cust() = 110 | let c = Chair() 111 | member __.Seat = c 112 | member __.Number = 1234 113 | override __.ToString() = "1234" 114 | 115 | let ``get alignment structure values`` () : obj[] seq = seq { 116 | let values : obj[] = [| 1234 |] 117 | yield [| "C#"; values; "cus #{CustomerId,-10}, pleasure to see you"; "cus #1234 , pleasure to see you" |] 118 | yield [| "C#"; values; "cus #{CustomerId,-10}, pleasure to see you"; "cus #1234 , pleasure to see you" |] 119 | yield [| "C#"; values; "cus #{CustomerId,-10:000000}, pleasure to see you"; "cus #001234 , pleasure to see you" |] 120 | yield [| "C#"; values; "cus #{CustomerId,10}, pleasure to see you"; "cus # 1234, pleasure to see you" |] 121 | yield [| "C#"; values; "cus #{CustomerId,10:000000}, pleasure to see you"; "cus # 001234, pleasure to see you" |] 122 | yield [| "C#"; values; "cus #{CustomerId,10:0,0}, pleasure to see you"; "cus # 1,234, pleasure to see you" |] 123 | yield [| "C#"; values; "cus #{CustomerId:0,0}, pleasure to see you"; "cus #1,234, pleasure to see you" |] 124 | yield [| "F#"; values; "cus #{CustomerId,-10}, pleasure to see you"; "cus #1234 , pleasure to see you" |] 125 | yield [| "F#"; values; "cus #{CustomerId,-10:000000}, pleasure to see you"; "cus #001234 , pleasure to see you" |] 126 | yield [| "F#"; values; "cus #{CustomerId,10}, pleasure to see you"; "cus # 1234, pleasure to see you" |] 127 | yield [| "F#"; values; "cus #{CustomerId,10:000000}, pleasure to see you"; "cus # 001234, pleasure to see you" |] 128 | yield [| "F#"; values; "cus #{CustomerId,10:0,0}, pleasure to see you"; "cus # 1,234, pleasure to see you" |] 129 | yield [| "F#"; values; "cus #{CustomerId:0,0}, pleasure to see you"; "cus #1,234, pleasure to see you" |] 130 | 131 | let values : obj[] = [| Cust() |] 132 | yield [| "C#"; values; "cus #{$cust:0,0}, pleasure to see you"; "cus #\"1234\", pleasure to see you" |] 133 | yield [| "F#"; values; "cus #{$cust:0,0}, pleasure to see you"; "cus #\"1234\", pleasure to see you" |] 134 | // formats/alignments don't propagate through to the 'destructured' inside values 135 | // They only apply to the outer (fully rendered) property text 136 | yield [| "C#"; values; "cus #{@cust,80:0,0}, pleasure to see you"; "cus # Cust { Seat: Chair { Back: \"straight\", Legs: [1, 2, 3, 4] }, Number: 1234 }, pleasure to see you" |] 137 | yield [| "F#"; values; "cus #{@cust,80:0,0}, pleasure to see you"; "cus # Cust { Seat: Chair { Back: \"straight\", Legs: [1, 2, 3, 4] }, Number: 1234 }, pleasure to see you" |] 138 | } 139 | 140 | [] 141 | let ``alignment strings are propagated`` (lang:string) (values:obj[]) (template:string) (expected:string) = 142 | MtAssert.RenderedAs(lang, template, values, expected) 143 | 144 | [] 145 | let ``format provider is used`` (lang) = 146 | MtAssert.RenderedAs(lang, 147 | "Please pay {Sum}", [|12.345|], "Please pay 12,345", 148 | provider=(CultureInfo("fr-FR"))) 149 | 150 | type Tree = Seq of nums: double list | Leaf of double | Trunk of double * DateTimeOffset * (Tree list) 151 | type ItemsUnion = ChairItem of c:ChairRecord | ReceiptItem of r:ReceiptRecord 152 | 153 | [] 154 | let ``an F# discriminated union object is formatted with provider correctly`` (lang) = 155 | let provider = CultureInfo("fr-FR") 156 | let template = "I like {@item1} and {@item2}" 157 | let values : obj[] = [| ChairItem({ Back="straight"; Legs=[|1;2;3;4|] }) 158 | ReceiptItem({ Sum=12.345; When=DateTime(2013, 5, 20, 16, 39, 0) }) |] 159 | let expected = "I like " 160 | + "ChairItem { c: ChairRecord { Back: \"straight\", Legs: [1, 2, 3, 4] }, Tag: 0, IsChairItem: True, IsReceiptItem: False }" 161 | + " and " 162 | + "ReceiptItem { r: ReceiptRecord { Sum: 12,345, When: 20/05/2013 16:39:00 }, Tag: 1, IsChairItem: False, IsReceiptItem: True }" 163 | MtAssert.RenderedAs(lang, template, values, expected, provider) 164 | 165 | [] 166 | let ``Rendered F# DU or Tuple fields are 'null' when depth is 1`` (lang) = 167 | let provider = (CultureInfo("fr-FR")) 168 | let template = "I like {@item1} and {@item2} and {@item3}" 169 | let values : obj[] = [| Leaf 12.345 170 | Leaf 12.345 171 | Trunk (12.345, DateTimeOffset(2013, 5, 20, 16, 39, 00, TimeSpan.FromHours 9.5), [Leaf 12.345; Leaf 12.345]) |] 172 | // all fields should be rendered 173 | let expected = "I like Leaf { Item: null, Tag: null, IsSeq: null, IsLeaf: null, IsTrunk: null } and " 174 | + "Leaf { Item: null, Tag: null, IsSeq: null, IsLeaf: null, IsTrunk: null } and " 175 | + "Trunk { Item1: null, Item2: null, Item3: null, Tag: null, IsSeq: null, IsLeaf: null, IsTrunk: null }" 176 | 177 | MtAssert.RenderedAs(lang, template, values, expected, provider, maxDepth=1) 178 | 179 | [] 180 | let ``Rendered F# DU or Tuple fields on level3 are 'null' when depth is 2`` (lang) = 181 | let provider = (CultureInfo("fr-FR")) 182 | let template = "I like {@item1} and {@item2} and {@item3} and {@item4}" 183 | let values : obj[] = [| Leaf 12.345 184 | Leaf 12.345 185 | Trunk (12.345, DateTimeOffset(2013, 5, 20, 16, 39, 00, TimeSpan.FromHours 9.5), [Leaf 12.345; Leaf 12.345]) 186 | ChairItem { Back="slanted"; Legs=[|1;2;3;4;5|] } |] 187 | 188 | // Render fields deeper than level 2 with 'null' values 189 | // In this case, only The Trunk.Item3 (Tree list) is after level 2 190 | let expected = "I like Leaf { Item: 12,345, Tag: 1, IsSeq: False, IsLeaf: True, IsTrunk: False } and " 191 | + "Leaf { Item: 12,345, Tag: 1, IsSeq: False, IsLeaf: True, IsTrunk: False } and " 192 | + "Trunk { Item1: 12,345, Item2: 20/05/2013 16:39:00 +09:30, Item3: [null, null], Tag: 2, IsSeq: False, IsLeaf: False, IsTrunk: True } and " 193 | + "ChairItem { c: ChairRecord { Back: null, Legs: null }, Tag: 0, IsChairItem: True, IsReceiptItem: False }" 194 | 195 | MtAssert.RenderedAs(lang, template, values, expected, provider, maxDepth=2) 196 | 197 | [] 198 | let ``Destructred F# objects captured with a custom destructurer render with format provider`` (lang) = 199 | let provider = CultureInfo("fr-FR") 200 | let template = "I like {@item1} 201 | and {@item2} 202 | and {@item3} 203 | and {@item4}" 204 | let values : obj[] = [| Leaf 12.345 205 | Leaf 12.345 206 | Trunk (12.345, DateTimeOffset(2013, 5, 20, 16, 39, 00, TimeSpan.FromHours 9.5), [Leaf 12.345; Leaf 12.345]) 207 | Trunk (1.1, DateTimeOffset(2013, 5, 20, 16, 39, 00, TimeSpan.FromHours 9.5), [Seq [1.1;2.2;3.3]; Seq [4.4]]) 208 | |] 209 | let expected = "I like Leaf { Item: 12,345 } 210 | and Leaf { Item: 12,345 } 211 | and Trunk { Item1: 12,345, Item2: 20/05/2013 16:39:00 +09:30, Item3: [Leaf { Item: 12,345 }, Leaf { Item: 12,345 }] } 212 | and Trunk { Item1: 1,1, Item2: 20/05/2013 16:39:00 +09:30, Item3: [Seq { nums: [1,1, 2,2, 3,3] }, Seq { nums: [4,4] }] }" 213 | let customFsharpDestr = CsToFs.toFsDestructurer (Destructurama.FSharpTypesDestructuringPolicy()) 214 | let destr = FsMessageTemplates.Capturing.createCustomDestructurer None (Some customFsharpDestr) 215 | MtAssert.RenderedAs(lang, template, values, expected, provider, additionalDestrs=[destr]) 216 | 217 | open FsMessageTemplates.Capturing 218 | 219 | [] 220 | let ``Destructred F# objects via the built-in F# types destructurer render with format provider`` (lang) = 221 | let provider = CultureInfo("fr-FR") 222 | let template = "I like {@item1} 223 | and {@item2} 224 | and {@item3} 225 | and {@item4}" 226 | let values : obj[] = [| Leaf 12.345 227 | Leaf 12.345 228 | Trunk (12.345, DateTimeOffset(2013, 5, 20, 16, 39, 00, TimeSpan.FromHours 9.5), [Leaf 12.345; Leaf 12.345]) 229 | Trunk (1.1, DateTimeOffset(2013, 5, 20, 16, 39, 00, TimeSpan.FromHours 9.5), [Seq [1.1;2.2;3.3]; Seq [4.4]]) 230 | |] 231 | let expected = "I like Leaf { Item: 12,345 } 232 | and Leaf { Item: 12,345 } 233 | and Trunk { Item1: 12,345, Item2: 20/05/2013 16:39:00 +09:30, Item3: [Leaf { Item: 12,345 }, Leaf { Item: 12,345 }] } 234 | and Trunk { Item1: 1,1, Item2: 20/05/2013 16:39:00 +09:30, Item3: [Seq { nums: [1,1, 2,2, 3,3] }, Seq { nums: [4,4] }] }" 235 | 236 | let destr = createCustomDestructurer None (Some builtInFSharpTypesDestructurer) 237 | MtAssert.RenderedAs(lang, template, values, expected, provider, additionalDestrs=[destr]) 238 | 239 | type Size = Regular = 1 | Large = 2 240 | 241 | type BernieSandersSizeFormatter (innerFormatProvider : IFormatProvider) = 242 | interface IFormatProvider with 243 | member this.GetFormat ty = 244 | match ty with 245 | | t when t = typeof -> this :> obj 246 | | _ -> innerFormatProvider.GetFormat ty 247 | interface ICustomFormatter with 248 | member this.Format (format : string, arg : obj, provider : IFormatProvider) = 249 | match arg with 250 | | :? Size as s -> 251 | match s with Size.Large -> "YUUUGE" | _ -> sprintf "%A" s 252 | | :? IFormattable as f -> 253 | f.ToString(format, innerFormatProvider) 254 | | _ -> 255 | arg.ToString() 256 | 257 | [] 258 | let ``Applies custom formatter to enums`` (lang) = 259 | let provider = (BernieSandersSizeFormatter CultureInfo.InvariantCulture) :> IFormatProvider 260 | let template = "Size {size}" 261 | let values : obj[] = [| Size.Large |] 262 | let expected = "Size YUUUGE" 263 | MtAssert.RenderedAs(lang, template, values, expected, provider) -------------------------------------------------------------------------------- /test/FsMessageTemplates.Tests/FsTests.Parser.fs: -------------------------------------------------------------------------------- 1 | module FsTests.Parser 2 | 3 | open Xunit 4 | open System.Globalization 5 | open System 6 | open FsMessageTemplates 7 | 8 | (* 9 | These tests run against both the C# and F# version. This helps to maintain the same 10 | behaviour in both implementations. 11 | *) 12 | let assertParsedAs lang (message: string) expectedTokens = 13 | FsTests.Asserts.MtAssert.ParsedAs(lang, message, expectedTokens) 14 | 15 | [] 16 | let ``FsMtParser does something without crashing`` () = 17 | let foundText t = () 18 | let foundProp p = () 19 | FsMtParser.parseParts "Hello {adam:#0.000}, how are {you? you crazy invalid prop}" foundText foundProp 20 | 21 | [] 22 | let ``align info defaults are correct`` () = 23 | Assert.Equal (FsMessageTemplates.Direction.Left, AlignInfo().Direction) 24 | Assert.Equal (0, AlignInfo().Width) 25 | Assert.Equal (false, AlignInfo().IsEmpty) 26 | 27 | [] 28 | let ``an empty message is a single text token`` (lang) = 29 | assertParsedAs lang "" [Tk.text 0 ""] 30 | 31 | [] 32 | let ``a message without properties is a single text token`` (lang) = 33 | assertParsedAs lang "Hello, world!" 34 | [Tk.text 0 "Hello, world!"] 35 | 36 | [] 37 | let ``a message with property only is a single property token`` (lang) = 38 | let template = "{Hello}" 39 | assertParsedAs lang template 40 | [Tk.prop 0 template "Hello"] 41 | 42 | [] 43 | let ``doubled left brackets are parsed as a single bracket`` (lang) = 44 | let template = "{{ Hi! }" 45 | assertParsedAs lang template 46 | [Tk.text 0 "{ Hi! }"] 47 | 48 | [] 49 | let ``doubled left brackets are parsed as a single bracket inside text`` (lang) = 50 | let template = "Well, {{ Hi!" 51 | assertParsedAs lang template 52 | [Tk.text 0 "Well, { Hi!"] 53 | 54 | [] 55 | let ``doubled left and right brackets inside a property are parsed as text`` (lang) = 56 | let template = "Hello, {nam{{adam}}}, how's it going?" 57 | assertParsedAs lang template 58 | // This is what I think *should* be returned: [Tk.text 0 "Hello, {nam{adam}}, how's it going?"] 59 | // This is what is *actually* returned: 60 | [Tk.text 0 "Hello, " // <- Arguably, there should be a single text token for the whole input 61 | Tk.text 7 "{nam{{adam}" // <- the double-braces are "wrong", does anyone care? 62 | Tk.text 18 "}, how's it going?"] 63 | 64 | [] 65 | let ``doubled right brackets are parsed as a single bracket`` (lang) = 66 | let template = "Nice }}-: mo" 67 | assertParsedAs lang template 68 | [Tk.text 0 "Nice }-: mo"] 69 | 70 | [] 71 | let ``a malformed property tag is parsed as text`` (lang) = 72 | let template = "{0 space}" 73 | assertParsedAs lang template 74 | [Tk.text 0 template] 75 | 76 | [] 77 | let ``an integer property name is parsed as positional property`` (lang) = 78 | let template = "{0} text {1} other text {2}" 79 | assertParsedAs lang template 80 | [Tk.propp 0 0 81 | Tk.text 3 " text " 82 | Tk.propp 9 1 83 | Tk.text 12 " other text " 84 | Tk.propp 24 2] 85 | 86 | [] 87 | let ``formats can contain colons`` (lang) = 88 | let template = "{Time:hh:mm}" 89 | assertParsedAs lang template 90 | [Tk.propf 0 template "Time" "hh:mm" ] 91 | 92 | [] 93 | let ``formats can contain commas`` (lang) = 94 | let template = ",{CustomerId:0,0}," 95 | assertParsedAs lang template 96 | [Tk.text 0 "," 97 | Tk.propf 1 "{CustomerId:0,0}" "CustomerId" "0,0" 98 | Tk.text 17 ","] 99 | 100 | [] 101 | let ``formats with align right can contain commas`` (lang) = 102 | let template = "big long x{0,5:0,00}x{1,5:0,00}x{2,5:0,00}x" 103 | assertParsedAs lang template 104 | [ Tk.text 0 "big long x" 105 | Tk.propparf 10 0 5 "0,00" 106 | Tk.text 20 "x" 107 | Tk.propparf 21 1 5 "0,00" 108 | Tk.text 31 "x" 109 | Tk.propparf 32 2 5 "0,00" 110 | Tk.text 42 "x"] 111 | 112 | [] 113 | let ``formats with align left can contain commas`` (lang) = 114 | let template = "big long x{0,-5:0,00}x{1,-5:0,00}x{2,-5:0,00}x" 115 | assertParsedAs lang template 116 | [ Tk.text 0 "big long x" 117 | Tk.proppalf 10 0 5 "0,00" 118 | Tk.text 21 "x" 119 | Tk.proppalf 22 1 5 "0,00" 120 | Tk.text 33 "x" 121 | Tk.proppalf 34 2 5 "0,00" 122 | Tk.text 45 "x" ] 123 | 124 | 125 | [] 126 | let ``zero values alignment is parsed as text`` (lang) = 127 | let template1 = "{Hello,-0}" 128 | assertParsedAs lang template1 129 | [Tk.text 0 template1] 130 | 131 | let template2 = "{Hello,0}" 132 | assertParsedAs lang template2 133 | [Tk.text 0 template2] 134 | 135 | 136 | [] 137 | let ``non-number alignment is parsed as text`` (lang) = 138 | let t1 = "{Hello,-aa}" 139 | assertParsedAs lang t1 [Tk.text 0 t1] 140 | 141 | let t2 = "{Hello,aa}" 142 | assertParsedAs lang t2 [Tk.text 0 t2] 143 | 144 | let t3 = "{Hello,-10-1}" 145 | assertParsedAs lang t3 [Tk.text 0 t3] 146 | 147 | let t4 = "{Hello,10-1}" 148 | assertParsedAs lang t4 [Tk.text 0 t4] 149 | 150 | [] 151 | let ``empty alignment is parsed as text`` (lang) = 152 | let t1 = "{Hello,}" 153 | assertParsedAs lang t1 [Tk.text 0 t1] 154 | 155 | let t2 = "{Hello,:format}" 156 | assertParsedAs lang t2 [Tk.text 0 t2] 157 | 158 | [] 159 | let ``multiple tokens have the correct indexes`` (lang) = 160 | let template = "{Greeting}, {Name}!" 161 | assertParsedAs lang template 162 | [Tk.prop 0 "{Greeting}" "Greeting" 163 | Tk.text 10 ", " 164 | Tk.prop 12 "{Name}" "Name" 165 | Tk.text 18 "!" ] 166 | 167 | [] 168 | let ``missing right bracket is parsed as text`` (lang) = 169 | let template = "{Hello" 170 | assertParsedAs lang template [Tk.text 0 template] 171 | 172 | [] 173 | let ``destructure hint is parsed correctly`` (lang) = 174 | let template = "{@Hello}" 175 | assertParsedAs lang template [Tk.propd 0 template "Hello"] 176 | 177 | [] 178 | let ``stringify hint is parsed correctly`` (lang) = 179 | let template = "{$Hello}" 180 | assertParsedAs lang template [Tk.propds 0 template "Hello"] 181 | 182 | [] 183 | let ``destructuring with empty property name is parsed as text`` (lang) = 184 | let template = "{@}" 185 | assertParsedAs lang template [Tk.text 0 template] 186 | 187 | [] 188 | let ``underscores are valid in property names`` (lang) = 189 | assertParsedAs lang "{_123_Hello}" [Tk.prop 0 "{_123_Hello}" "_123_Hello"] 190 | 191 | //[] 192 | //let ``property names starting with zero are positional`` (lang) = 193 | // assertParsedAs lang "{0001}" [Tk.propp 0 1] 194 | 195 | -------------------------------------------------------------------------------- /test/FsMessageTemplates.Tests/MtAssert.fs: -------------------------------------------------------------------------------- 1 | module FsTests.Asserts 2 | 3 | open System 4 | open FsMessageTemplates 5 | open System.Collections.Generic 6 | 7 | let invariantProvider = (System.Globalization.CultureInfo.InvariantCulture :> System.IFormatProvider) 8 | 9 | type FsToken = FsMessageTemplates.Token 10 | type FsDestr = FsMessageTemplates.DestrHint 11 | type FsAlign = FsMessageTemplates.AlignInfo 12 | type FsProp = FsMessageTemplates.Property 13 | type FsMtParserToken = TextToken of string | PropToken of FsMtParser.Property 14 | type FsMtParserFullToken = TextToken of string | PropToken of FsMtParserFull.Property 15 | 16 | let captureHintToDestrHit = function 17 | | FsMtParserFull.CaptureHint.Default -> DestrHint.Default 18 | | FsMtParserFull.CaptureHint.Stringify -> DestrHint.Stringify 19 | | FsMtParserFull.CaptureHint.Structure -> DestrHint.Destructure 20 | | ch -> failwithf "unexpected CatureHint %A" ch 21 | 22 | let toAlignInfo (ai : FsMtParserFull.AlignInfo) = 23 | if ai.isEmpty then AlignInfo.Empty 24 | else 25 | let direction = match ai.direction with FsMtParserFull.AlignDirection.Left -> Direction.Left | _ -> Direction.Right 26 | AlignInfo(direction, ai.width) 27 | 28 | /// Parses message templates from the different implementations in semi-compatible ways by 29 | /// using FsMessageTemplates.Token as the target type. 30 | let parsedAs lang message (expectedTokens: FsToken seq) = 31 | let mutable ignoreStartIndex = false 32 | let parsed = 33 | match lang with 34 | | "C#" -> MessageTemplates.MessageTemplate.Parse(message).Tokens |> Seq.map CsToFs.mttToToken |> List.ofSeq 35 | | "F#" -> (FsMessageTemplates.Parser.parse message).Tokens |> List.ofSeq 36 | | "F#MtParser" -> 37 | ignoreStartIndex <- true // FsMtParser doesn't support index 38 | let tokens = ResizeArray() 39 | let foundText s = tokens.Add(FsMtParserToken.TextToken(s)) 40 | let foundProp p = tokens.Add(FsMtParserToken.PropToken(p)) 41 | FsMtParser.parseParts message foundText foundProp 42 | tokens 43 | |> Seq.map (function 44 | | FsMtParserToken.TextToken s -> FsToken.TextToken(0, s) 45 | | FsMtParserToken.PropToken p -> FsToken.PropToken(0, FsProp(p.name, -1, FsDestr.Default, FsAlign.Empty, p.format)) ) 46 | |> List.ofSeq 47 | | "F#MtParserFull" -> 48 | ignoreStartIndex <- true // FsMtParser doesn't support index 49 | let tokens = ResizeArray() 50 | let foundText s = tokens.Add(FsMtParserFullToken.TextToken(s)) 51 | let foundProp p = tokens.Add(FsMtParserFullToken.PropToken(p)) 52 | FsMtParserFull.parseParts message foundText foundProp 53 | tokens 54 | |> Seq.map (function 55 | | FsMtParserFullToken.TextToken s -> FsToken.TextToken(0, s) 56 | | FsMtParserFullToken.PropToken p -> FsToken.PropToken(0, FsProp(p.name, -1, captureHintToDestrHit p.captureHint, toAlignInfo p.align, p.format)) ) 57 | |> List.ofSeq 58 | | other -> failwithf "unexpected lang '%s'" other 59 | 60 | let setStartIndexZeroIfIgnored tokens = 61 | match ignoreStartIndex with 62 | | false -> tokens 63 | | true -> tokens |> Seq.map (function 64 | | FsToken.TextToken (i, t) -> FsToken.TextToken(0, t) 65 | | FsToken.PropToken (i, p) -> FsToken.PropToken(0, p)) 66 | 67 | let expected = expectedTokens |> Seq.cast |> setStartIndexZeroIfIgnored |> Seq.toList 68 | Xunit.Assert.Equal (expected, parsed) 69 | 70 | let capture lang (messageTemplate:string) (args: obj list) = 71 | let argsArray = (args |> Seq.cast |> Seq.toArray) // force 'args' to be IEnumerable 72 | match lang with 73 | | "F#" -> FsMessageTemplates.Capturing.captureMessageProperties messageTemplate argsArray |> List.ofSeq 74 | | "C#" -> MessageTemplates.MessageTemplate.Capture(messageTemplate, argsArray) |> Seq.map CsToFs.templateProperty |> List.ofSeq 75 | | other -> failwithf "unexpected lang '%s'" other 76 | 77 | let renderp lang (provider:IFormatProvider) messageTemplate args = 78 | let argsArray = (args |> Seq.cast |> Seq.toArray) // force 'args' to be IEnumerable 79 | match lang with 80 | | "C#" -> MessageTemplates.MessageTemplate.Format(provider, messageTemplate, argsArray) 81 | | "F#" -> FsMessageTemplates.Formatting.sprintsm provider messageTemplate argsArray 82 | | other -> failwithf "unexpected lang '%s'" other 83 | 84 | let render lang template args = 85 | renderp lang System.Globalization.CultureInfo.InvariantCulture template args 86 | 87 | type MtAssert() = 88 | 89 | /// Asserts the 90 | static member ParsedAs (lang, message, expectedTokens: FsToken seq) = 91 | parsedAs lang message expectedTokens 92 | 93 | /// Captures properties from the C# or F# version in compatible ways. 94 | static member internal Capture(lang, template, values, ?maxDepth, ?additionalScalars, ?additionalDestrs) = 95 | let maxDepth = defaultArg maxDepth 10 96 | let additionalScalars = defaultArg additionalScalars Seq.empty 97 | let additionalDestrs = defaultArg additionalDestrs Seq.empty 98 | match lang with 99 | | "C#" -> 100 | let mt = MessageTemplates.Parsing.MessageTemplateParser().Parse template 101 | let csDestrs = additionalDestrs |> Seq.map CsToFs.fsDestrToCsDestrPolicy 102 | let captured = CsToFs.CsMt.CaptureWith(maxDepth, additionalScalars, csDestrs, mt, values) 103 | captured |> Seq.map CsToFs.templateProperty |> Seq.toList 104 | | "F#" -> 105 | let mt = FsMessageTemplates.Parser.parse template 106 | let emptyKeepTrying = Unchecked.defaultof 107 | let tryScalars : Destructurer = fun r -> 108 | let exists = additionalScalars |> Seq.exists ((=) (r.Value.GetType())) 109 | if exists then ScalarValue(r.Value) else emptyKeepTrying 110 | let destrNoneForNull (r: DestructureRequest) (d:Destructurer) = 111 | match d (DestructureRequest (r.Destructurer, r.Value, 10, 1, hint=r.Hint)) with 112 | | tpv when tpv = TemplatePropertyValue.Empty -> None 113 | | tpv -> Some tpv 114 | let tryDestrs : Destructurer = fun r -> 115 | match additionalDestrs |> Seq.tryPick (destrNoneForNull r) with 116 | | None -> emptyKeepTrying 117 | | Some tpv -> tpv 118 | let destr = Capturing.createCustomDestructurer (Some tryScalars) (Some tryDestrs) 119 | FsMessageTemplates.Capturing.capturePropertiesCustom destr maxDepth mt values 120 | |> Seq.toList 121 | | other -> failwithf "unexpected lang %s" other 122 | 123 | /// Formats either the C# or F# version in compatible ways. 124 | static member internal Format(lang, template, values, ?provider, ?maxDepth, ?additionalScalars, ?additionalDestrs) = 125 | let provider = defaultArg provider invariantProvider 126 | let maxDepth = defaultArg maxDepth 10 127 | let additionalScalars = defaultArg additionalScalars Seq.empty 128 | let additionalDestrs = defaultArg additionalDestrs Seq.empty 129 | match lang with 130 | | "C#" -> 131 | let mt = MessageTemplates.Parsing.MessageTemplateParser().Parse template 132 | let csDestrs = additionalDestrs |> Seq.map CsToFs.fsDestrToCsDestrPolicy 133 | let captured = CsToFs.CsMt.CaptureWith(maxDepth, additionalScalars, csDestrs, mt, values) 134 | let propsByName = captured 135 | |> fun tpvl -> MessageTemplates.Core.TemplatePropertyValueDictionary(tpvl) 136 | 137 | mt.Render(properties=propsByName, formatProvider=provider) 138 | | "F#" -> 139 | let mt = FsMessageTemplates.Parser.parse template 140 | let emptyKeepTrying = Unchecked.defaultof 141 | let tryScalars : Destructurer = fun r -> 142 | let exists = additionalScalars |> Seq.exists ((=) (r.Value.GetType())) 143 | if exists then ScalarValue(r.Value) else emptyKeepTrying 144 | let destrNoneForNull (r: DestructureRequest) (d:Destructurer) = 145 | match d (DestructureRequest (r.Destructurer, r.Value, 10, 1, hint=r.Hint)) with 146 | | tpv when tpv = TemplatePropertyValue.Empty -> None 147 | | tpv -> Some tpv 148 | let tryDestrs : Destructurer = fun r -> 149 | match additionalDestrs |> Seq.tryPick (destrNoneForNull r) with 150 | | None -> emptyKeepTrying 151 | | Some tpv -> tpv 152 | let destr = Capturing.createCustomDestructurer (Some tryScalars) (Some tryDestrs) 153 | let propsByName = FsMessageTemplates.Capturing.capturePropertiesCustom destr maxDepth mt values 154 | |> Seq.map (fun tpv -> tpv.Name, tpv.Value) 155 | |> dict 156 | let getValueByName name = 157 | let exists, value = propsByName.TryGetValue(name) 158 | if exists then value else TemplatePropertyValue.Empty 159 | use tw = new System.IO.StringWriter(formatProvider=provider) 160 | FsMessageTemplates.Formatting.formatCustom mt tw getValueByName 161 | let formatCustomOutput = tw.ToString() 162 | 163 | // if invariant and no custom scalars and types provided, verify 'format' gives the same result 164 | // if maxDepth is different, we also can't assert the other overloads produce the same output, as 165 | // only formatCustom allows this to change. 166 | if maxDepth=10 && additionalScalars = Seq.empty && additionalDestrs = Seq.empty then 167 | if provider = invariantProvider then 168 | // format should be the same 169 | let formatOutput = FsMessageTemplates.Formatting.format mt values 170 | Xunit.Assert.Equal (formatCustomOutput, formatOutput) 171 | 172 | // bprintm should be the same 173 | let sb = System.Text.StringBuilder() 174 | FsMessageTemplates.Formatting.bprintm mt sb values 175 | Xunit.Assert.Equal (formatCustomOutput, sb.ToString()) 176 | 177 | // bprintsm should be the same 178 | sb.Clear() |> ignore 179 | FsMessageTemplates.Formatting.bprintsm sb template values 180 | Xunit.Assert.Equal (formatCustomOutput, sb.ToString()) 181 | 182 | // fprintm should be the same 183 | use fprintTw = new System.IO.StringWriter(formatProvider=provider) 184 | FsMessageTemplates.Formatting.fprintm mt fprintTw values 185 | Xunit.Assert.Equal (formatCustomOutput, fprintTw.ToString()) 186 | 187 | // fprintsm should be the same 188 | use fprintsTw = new System.IO.StringWriter(formatProvider=provider) 189 | FsMessageTemplates.Formatting.fprintsm fprintsTw template values 190 | Xunit.Assert.Equal (formatCustomOutput, fprintsTw.ToString()) 191 | 192 | // sprint*m should be the same 193 | Xunit.Assert.Equal (formatCustomOutput, FsMessageTemplates.Formatting.sprintm mt provider values) 194 | Xunit.Assert.Equal (formatCustomOutput, FsMessageTemplates.Formatting.sprintsm provider template values) 195 | 196 | formatCustomOutput 197 | 198 | | other -> failwithf "unexpected lang %s" other 199 | 200 | static member DestructuredAs(lang, template, values, expected, ?maxDepth, ?additionalScalars, ?additionalDestrs) = 201 | let maxDepth = defaultArg maxDepth 10 202 | let additionalScalars = defaultArg additionalScalars Seq.empty 203 | let additionalDestrs = defaultArg additionalDestrs Seq.empty 204 | let actual = MtAssert.Capture(lang, template, values, maxDepth, additionalScalars, additionalDestrs) 205 | 206 | Xunit.Assert.StrictEqual (expected, actual) 207 | 208 | static member RenderedAs(lang, template, values, expected, ?provider, ?maxDepth, ?additionalScalars, ?additionalDestrs) = 209 | let provider = defaultArg provider invariantProvider 210 | let maxDepth = defaultArg maxDepth 10 211 | let additionalScalars = defaultArg additionalScalars Seq.empty 212 | let additionalDestrs = defaultArg additionalDestrs Seq.empty 213 | let actual = MtAssert.Format(lang, template, values, provider, maxDepth, additionalScalars, additionalDestrs) 214 | 215 | // Using XUnit.Assert because it has a better message on failure for string compares 216 | Xunit.Assert.Equal (expected, actual) 217 | -------------------------------------------------------------------------------- /test/FsMessageTemplates.Tests/Tk.fs: -------------------------------------------------------------------------------- 1 | module Tk 2 | 3 | open FsMessageTemplates 4 | 5 | let PropertyNameAndValue (name:string, value:TemplatePropertyValue) = { Name=name; Value=value } 6 | let text tindex raw = Token.TextToken(tindex, raw) 7 | let desDef = DestrHint.Default 8 | let emptyAlign = AlignInfo.Empty 9 | let emptyPos = -1 10 | let emptyFormat = null 11 | let prop tindex (_:string) name = PropToken(tindex, Property(name, emptyPos, desDef, emptyAlign, emptyFormat)) 12 | let propf tindex (_:string) name format = PropToken(tindex, Property(name, emptyPos, desDef, emptyAlign, format)) 13 | let propd tindex (_:string) name = PropToken(tindex, Property(name, emptyPos, DestrHint.Destructure, emptyAlign, emptyFormat)) 14 | let propds tindex (_:string) name = PropToken(tindex, Property(name, emptyPos, DestrHint.Stringify, emptyAlign, emptyFormat)) 15 | let propar tindex (_:string) name rightWidth = PropToken(tindex, Property(name, emptyPos, desDef, AlignInfo(Direction.Right, rightWidth), emptyFormat)) 16 | let proparf tindex (_:string) name rightWidth format = PropToken(tindex, Property(name, emptyPos, desDef, AlignInfo(Direction.Right, rightWidth), format)) 17 | let propal tindex (_:string) name leftWidth = PropToken(tindex, Property(name, emptyPos, desDef, AlignInfo(Direction.Left, leftWidth), emptyFormat)) 18 | let propalf tindex (_:string) name leftWidth format = PropToken(tindex, Property(name, emptyPos, desDef, AlignInfo(Direction.Left, leftWidth), format)) 19 | let propp tindex num = PropToken(tindex, Property(string num, num, desDef, emptyAlign, emptyFormat)) 20 | let proppalf tindex num leftWidth format = PropToken(tindex, Property(string num, num, desDef, AlignInfo(Direction.Left, leftWidth), format)) 21 | let propparf tindex num rightWidth format = PropToken(tindex, Property(string num, num, desDef, AlignInfo(Direction.Right, rightWidth), format)) 22 | -------------------------------------------------------------------------------- /test/FsMessageTemplates.Tests/XunitSupport.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module FsTests.XunitSupport 3 | 4 | open Xunit 5 | open System 6 | open System.Globalization 7 | 8 | type LangTheoryAttribute() = 9 | inherit Xunit.TheoryAttribute() 10 | 11 | type CSharpAndFSharpAttribute() = 12 | inherit Xunit.Sdk.DataAttribute() 13 | override __.GetData _ = [[|box "C#"|]; [|box "F#"|]] |> Seq.ofList 14 | 15 | type FullParserImplementationsAttribute() = 16 | inherit Xunit.Sdk.DataAttribute() 17 | override __.GetData _ = [[|box "C#"|]; [|box "F#"|]; [|box "F#MtParserFull"|]] |> Seq.ofList 18 | 19 | /// Indicates that all implementations must pass the test. 20 | type AllImplementationsAttribute() = 21 | inherit Xunit.Sdk.DataAttribute() 22 | override __.GetData _ = [[|box "C#"|]; [|box "F#"|]; [|box "F#MtParser"|]; [|box "F#MtParserFull"|]] |> Seq.ofList 23 | -------------------------------------------------------------------------------- /tooling/configure-dotnet-cli-osx.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | brew config 4 | 5 | # instructions taken from https://www.microsoft.com/net/core#macos 6 | brew update 7 | brew install openssl 8 | mkdir -p /usr/local/lib 9 | ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/ 10 | ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/ 11 | -------------------------------------------------------------------------------- /tooling/dotnet-cli-preview2/dotnet-cli-preview2.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3558CBB1-CECB-4A63-ACB1-E18B1E75F64C}" 7 | ProjectSection(SolutionItems) = preProject 8 | global.json = global.json 9 | EndProjectSection 10 | EndProject 11 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "FsMtParser", "src\FsMtParser\FsMtParser.xproj", "{D9B11BCE-E604-45D1-A57E-C4B80087547F}" 12 | EndProject 13 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "FsMessageTemplates", "src\FsMessageTemplates\FsMessageTemplates.xproj", "{A46B8984-F9C0-4922-917D-ED36A653CDFB}" 14 | EndProject 15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7A44889B-71A9-4B42-9B4C-83F205943021}" 16 | EndProject 17 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{0C9018F7-882F-48E3-A68D-BDFFC4CBD3BF}" 18 | EndProject 19 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Tets", "test\FsMessageTemplates.Tests\FsMessageTemplates.Tests.xproj", "{1D200B0C-69F8-4547-9283-71290C05BB4E}" 20 | EndProject 21 | Global 22 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 23 | Debug|Any CPU = Debug|Any CPU 24 | Release|Any CPU = Release|Any CPU 25 | EndGlobalSection 26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 27 | {D9B11BCE-E604-45D1-A57E-C4B80087547F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {D9B11BCE-E604-45D1-A57E-C4B80087547F}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {D9B11BCE-E604-45D1-A57E-C4B80087547F}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {D9B11BCE-E604-45D1-A57E-C4B80087547F}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {A46B8984-F9C0-4922-917D-ED36A653CDFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {A46B8984-F9C0-4922-917D-ED36A653CDFB}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {A46B8984-F9C0-4922-917D-ED36A653CDFB}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {A46B8984-F9C0-4922-917D-ED36A653CDFB}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {1D200B0C-69F8-4547-9283-71290C05BB4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {1D200B0C-69F8-4547-9283-71290C05BB4E}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {1D200B0C-69F8-4547-9283-71290C05BB4E}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {1D200B0C-69F8-4547-9283-71290C05BB4E}.Release|Any CPU.Build.0 = Release|Any CPU 39 | EndGlobalSection 40 | GlobalSection(SolutionProperties) = preSolution 41 | HideSolutionNode = FALSE 42 | EndGlobalSection 43 | GlobalSection(NestedProjects) = preSolution 44 | {D9B11BCE-E604-45D1-A57E-C4B80087547F} = {7A44889B-71A9-4B42-9B4C-83F205943021} 45 | {A46B8984-F9C0-4922-917D-ED36A653CDFB} = {7A44889B-71A9-4B42-9B4C-83F205943021} 46 | {1D200B0C-69F8-4547-9283-71290C05BB4E} = {0C9018F7-882F-48E3-A68D-BDFFC4CBD3BF} 47 | EndGlobalSection 48 | EndGlobal 49 | -------------------------------------------------------------------------------- /tooling/dotnet-cli-preview2/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "projects": [ "src", "test" ], 3 | "sdk": { 4 | "version": "1.0.0-preview2-003121" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tooling/dotnet-cli-preview2/src/FsMessageTemplates/FsMessageTemplates.xproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | a46b8984-f9c0-4922-917d-ed36a653cdfb 10 | FsMessageTemplates 11 | .\obj 12 | .\bin\ 13 | 14 | 15 | 16 | 2.0 17 | 18 | 19 | -------------------------------------------------------------------------------- /tooling/dotnet-cli-preview2/src/FsMessageTemplates/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0-rc-*", 3 | "description": "F# Message Templates - the ability to format {named} string values, and capture the properties", 4 | "authors": [ "Adam Chester" ], 5 | "packOptions": { 6 | "tags": [ "message", "template", "serilog", "F#", "fsharp", "logging", "semantic", "structured" ], 7 | "projectUrl": "https://github.com/messagetemplates/messagetemplates-fsharp", 8 | "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0", 9 | "iconUrl": "http://messagetemplates.org/images/messagetemplates-nuget.png" 10 | }, 11 | "buildOptions": { 12 | "xmlDoc": true, 13 | "compilerName": "fsc", 14 | "compile": { 15 | "includeFiles": [ 16 | "../../../../src/FsMessageTemplates/MessageTemplates.fsi", 17 | "../../../../src/FsMessageTemplates/MessageTemplates.fs" 18 | ] 19 | } 20 | }, 21 | "tools": { 22 | "dotnet-compile-fsc": { 23 | "version": "1.0.0-preview2-*", 24 | "imports": [ 25 | "dnxcore50", 26 | "portable-net45+win81", 27 | "netstandard1.3" 28 | ] 29 | } 30 | }, 31 | "frameworks": { 32 | "net45": { 33 | "buildOptions": { 34 | "define": [ "REFLECTION_API_EVOLVED" ] 35 | }, 36 | "dependencies": { 37 | "FSharp.Core": "4.0.0.1" 38 | } 39 | }, 40 | "netstandard1.6": { 41 | "buildOptions": { 42 | "define": [ "REFLECTION_API_EVOLVED" ] 43 | }, 44 | "dependencies": { 45 | "NETStandard.Library": "1.6.0", 46 | "Microsoft.FSharp.Core.netcore": "1.0.0-alpha-*" 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /tooling/dotnet-cli-preview2/src/FsMtParser/FsMtParser.xproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | d9b11bce-e604-45d1-a57e-c4b80087547f 10 | FsMtParser 11 | .\obj 12 | .\bin\ 13 | 14 | 15 | 16 | 2.0 17 | 18 | 19 | -------------------------------------------------------------------------------- /tooling/dotnet-cli-preview2/src/FsMtParser/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0-rc-*", 3 | "description": "F# Message Templates parser - the ability to parse {named} string values", 4 | "authors": [ "Adam Chester" ], 5 | "packOptions": { 6 | "tags": [ "message", "template", "serilog", "F#", "fsharp", "logging", "semantic", "structured" ], 7 | "projectUrl": "https://github.com/messagetemplates/messagetemplates-fsharp", 8 | "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0", 9 | "iconUrl": "http://messagetemplates.org/images/messagetemplates-nuget.png" 10 | }, 11 | "buildOptions": { 12 | "xmlDoc": true, 13 | "compilerName": "fsc", 14 | "compile": { 15 | "includeFiles": [ 16 | "../../../../src/FsMtParser/FsMtParser.fs" 17 | ] 18 | } 19 | }, 20 | "tools": { 21 | "dotnet-compile-fsc": { 22 | "version": "1.0.0-preview2-*", 23 | "imports": [ 24 | "dnxcore50", 25 | "portable-net45+win81", 26 | "netstandard1.3" 27 | ] 28 | } 29 | }, 30 | "frameworks": { 31 | "net45": { 32 | "dependencies": { 33 | "FSharp.Core": "4.0.0.1" 34 | } 35 | }, 36 | "netstandard1.6": { 37 | "dependencies": { 38 | "NETStandard.Library": "1.6.0", 39 | "Microsoft.FSharp.Core.netcore": "1.0.0-alpha-*" 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /tooling/dotnet-cli-preview2/test/FsMessageTemplates.Tests/FsMessageTemplates.Tests.xproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14.0.25420 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | 1d200b0c-69f8-4547-9283-71290c05bb4e 10 | FsMessageTemplates.Tests 11 | .\obj 12 | .\bin\ 13 | 14 | 15 | 2.0 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /tooling/dotnet-cli-preview2/test/FsMessageTemplates.Tests/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "testRunner": "xunit", 3 | "dependencies": { 4 | "FsMessageTemplates": { "target": "project" }, 5 | "FsMtParser": { "target": "project" }, 6 | "MessageTemplates": "1.0.0-*", 7 | "xunit": "2.2.0-*", 8 | "dotnet-test-xunit": "2.2.0-preview2-*" 9 | }, 10 | "buildOptions": { 11 | "xmlDoc": true, 12 | "compilerName": "fsc", 13 | "compile": { 14 | "includeFiles": [ 15 | "../../../../test/FsMessageTemplates.Tests/FSharpTypesDestructuringPolicy.fs", 16 | "../../../../test/FsMessageTemplates.Tests/Tk.fs", 17 | "../../../../test/FsMessageTemplates.Tests/CsToFs.fs", 18 | "../../../../test/FsMessageTemplates.Tests/XunitSupport.fs", 19 | "../../../../test/FsMessageTemplates.Tests/MtAssert.fs", 20 | "../../../../test/FsMessageTemplates.Tests/FsTests.Parser.fs", 21 | "../../../../test/FsMessageTemplates.Tests/FsTests.Capture.fs", 22 | "../../../../test/FsMessageTemplates.Tests/FsTests.Format.fs" 23 | ] 24 | } 25 | }, 26 | "tools": { 27 | "dotnet-compile-fsc": { 28 | "version": "1.0.0-preview2-*" 29 | } 30 | }, 31 | "frameworks": { 32 | "net452": { 33 | "assemblies": { 34 | "FSharp.Core": "4.0.0.1" 35 | } 36 | } 37 | //, 38 | //"netcoreapp1.0": { 39 | // "define": [ ], 40 | // "dependencies": { 41 | // "Microsoft.NETCore.App": { "type": "platform", "version": "1.0.0" }, 42 | // "System.Diagnostics.StackTrace": "4.0.2", 43 | // "System.Reflection.TypeExtensions": "4.1.0", 44 | // "System.Globalization": "4.0.11", 45 | // "Microsoft.FSharp.Core.netcore": "1.0.0-alpha-*" 46 | // } 47 | //} 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tooling/net45/FsMessageTemplates.net45.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FsMtParser", "FsMtParser\FsMtParser.fsproj", "{6B24ACE8-3A8E-43AE-8CFF-772B0164B245}" 7 | EndProject 8 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Tests", "Tests\Tests.fsproj", "{FCEAAD62-96CB-48A7-90AB-EE0ADDCC59DD}" 9 | EndProject 10 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FsMessageTemplates", "FsMessageTemplates\FsMessageTemplates.fsproj", "{236CC78B-681C-488D-B76D-2EB15DCE5687}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {6B24ACE8-3A8E-43AE-8CFF-772B0164B245}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {6B24ACE8-3A8E-43AE-8CFF-772B0164B245}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {6B24ACE8-3A8E-43AE-8CFF-772B0164B245}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {6B24ACE8-3A8E-43AE-8CFF-772B0164B245}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {FCEAAD62-96CB-48A7-90AB-EE0ADDCC59DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {FCEAAD62-96CB-48A7-90AB-EE0ADDCC59DD}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {FCEAAD62-96CB-48A7-90AB-EE0ADDCC59DD}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {FCEAAD62-96CB-48A7-90AB-EE0ADDCC59DD}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {236CC78B-681C-488D-B76D-2EB15DCE5687}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {236CC78B-681C-488D-B76D-2EB15DCE5687}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {236CC78B-681C-488D-B76D-2EB15DCE5687}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {236CC78B-681C-488D-B76D-2EB15DCE5687}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | EndGlobal 35 | -------------------------------------------------------------------------------- /tooling/net45/FsMessageTemplates/FsMessageTemplates.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | 2.0 8 | 236cc78b-681c-488d-b76d-2eb15dce5687 9 | Library 10 | FsMessageTemplates 11 | FsMessageTemplates 12 | v4.5 13 | 4.4.0.0 14 | true 15 | FsMessageTemplates 16 | 17 | 18 | true 19 | full 20 | false 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | 3 25 | bin\Debug\FsMessageTemplates.XML 26 | 27 | 28 | pdbonly 29 | true 30 | true 31 | bin\Release\ 32 | TRACE 33 | 3 34 | bin\Release\FsMessageTemplates.XML 35 | 36 | 37 | 38 | 39 | True 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | MessageTemplates.fsi 48 | 49 | 50 | MessageTemplates.fs 51 | 52 | 53 | 54 | 11 55 | 56 | 57 | 58 | 59 | $(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\3.0\Framework\v4.0\Microsoft.FSharp.Targets 60 | 61 | 62 | 63 | 64 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets 65 | 66 | 67 | 68 | 69 | 76 | -------------------------------------------------------------------------------- /tooling/net45/FsMtParser/FsMtParser.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | 2.0 8 | 6b24ace8-3a8e-43ae-8cff-772b0164b245 9 | Library 10 | FsMtParser 11 | FsMtParser 12 | v4.5 13 | 4.4.0.0 14 | true 15 | FsMtParser 16 | 17 | 18 | true 19 | full 20 | false 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | 3 25 | bin\Debug\FsMtParser.XML 26 | 27 | 28 | pdbonly 29 | true 30 | true 31 | bin\Release\ 32 | TRACE 33 | 3 34 | bin\Release\FsMtParser.XML 35 | 36 | 37 | 11 38 | 39 | 40 | 41 | 42 | $(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\3.0\Framework\v4.0\Microsoft.FSharp.Targets 43 | 44 | 45 | 46 | 47 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets 48 | 49 | 50 | 51 | 52 | 53 | 54 | FsMtParserTest.fsx 55 | 56 | 57 | FsMtParser.fs 58 | 59 | 60 | FsMtParserFullTest.fsx 61 | 62 | 63 | FsMtParserFull.fs 64 | 65 | 66 | 67 | 68 | 69 | True 70 | 71 | 72 | 73 | 74 | 75 | 82 | -------------------------------------------------------------------------------- /tooling/net45/Tests/Tests.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | Debug 7 | AnyCPU 8 | 2.0 9 | fceaad62-96cb-48a7-90ab-ee0addcc59dd 10 | Library 11 | Tests 12 | Tests 13 | v4.6.1 14 | 4.4.0.0 15 | true 16 | Tests 17 | 18 | 19 | 20 | 21 | 22 | true 23 | full 24 | false 25 | false 26 | bin\Debug\ 27 | DEBUG;TRACE 28 | 3 29 | bin\Debug\Tests.XML 30 | 31 | 32 | pdbonly 33 | true 34 | true 35 | bin\Release\ 36 | TRACE 37 | 3 38 | bin\Release\Tests.XML 39 | 40 | 41 | 11 42 | 43 | 44 | 45 | 46 | $(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\3.0\Framework\v4.0\Microsoft.FSharp.Targets 47 | 48 | 49 | 50 | 51 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets 52 | 53 | 54 | 55 | 56 | 57 | 58 | FSharpTypesDestructuringPolicy.fs 59 | 60 | 61 | Tk.fs 62 | 63 | 64 | CsToFs.fs 65 | 66 | 67 | MtAssert.fs 68 | 69 | 70 | XunitSupport.fs 71 | 72 | 73 | FsTests.Parser.fs 74 | 75 | 76 | FsTests.Capture.fs 77 | 78 | 79 | FsTests.Format.fs 80 | 81 | 82 | 83 | 84 | 85 | ..\packages\MessageTemplates.1.0.0-rc-00260\lib\net452\MessageTemplates.dll 86 | True 87 | 88 | 89 | 90 | True 91 | 92 | 93 | 94 | 95 | 96 | FsMessageTemplates 97 | {236cc78b-681c-488d-b76d-2eb15dce5687} 98 | True 99 | 100 | 101 | FsMtParser 102 | {6b24ace8-3a8e-43ae-8cff-772b0164b245} 103 | True 104 | 105 | 106 | ..\packages\xunit.abstractions.2.0.1\lib\net35\xunit.abstractions.dll 107 | True 108 | 109 | 110 | ..\packages\xunit.assert.2.2.0-beta5-build3474\lib\netstandard1.0\xunit.assert.dll 111 | True 112 | 113 | 114 | ..\packages\xunit.extensibility.core.2.2.0-beta5-build3474\lib\net45\xunit.core.dll 115 | True 116 | 117 | 118 | ..\packages\xunit.extensibility.execution.2.2.0-beta5-build3474\lib\net45\xunit.execution.desktop.dll 119 | True 120 | 121 | 122 | 123 | 124 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 125 | 126 | 127 | 128 | 135 | -------------------------------------------------------------------------------- /tooling/net45/Tests/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | --------------------------------------------------------------------------------