├── .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 | [](https://ci.appveyor.com/project/adamchester/messagetemplates-fsharp)
11 | [](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 |
--------------------------------------------------------------------------------