├── .github └── workflows │ ├── codeql-analysis.yml │ ├── go.yml │ └── release.yml ├── .gitignore ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── docs ├── README.md ├── about.md ├── examples.md ├── img │ ├── bafi.svg │ ├── bafiIcon.svg │ ├── calc.jpg │ ├── favicon.ico │ └── scheme.svg └── js │ ├── bafi.wasm │ └── wasm_exec.js ├── filesTest.yaml ├── functions.go ├── functions_test.go ├── go.mod ├── go.sum ├── lua ├── functions.lua └── json.lua ├── main.go ├── main_test.go ├── mkdocs.yml ├── template.tmpl ├── testdata.xml └── workspace.code-workspace /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '44 2 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Set up Go 16 | uses: actions/setup-go@v2 17 | with: 18 | go-version: 1.21 19 | 20 | - name: Build 21 | run: go build -v ./... 22 | - name: Test 23 | run: go test -v -cover ./... 24 | goreportcard: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: refresh goreportcard 28 | uses: creekorful/goreportcard-action@v1.0 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: releaseBuild 2 | 3 | on: release 4 | 5 | jobs: 6 | update-mkdocs: 7 | name: Deploy mkdocs documentation 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | with: 12 | fetch-depth: 0 13 | - uses: actions/setup-python@v2 14 | with: 15 | python-version: 3.x 16 | - run: pip install mkdocs 17 | - run: mkdocs gh-deploy --force 18 | release-windows-amd64: 19 | name: release windows/amd64 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: checkout 23 | uses: actions/checkout@v2 24 | - name: compile and release 25 | uses: mmalcek/go-release.action@v1.0.6 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | PROJECT_NAME: bafi 29 | CGO_ENABLED: 0 30 | GOARCH: amd64 31 | GOOS: windows 32 | CMD_PATH: -buildvcs=false 33 | EXTRA_FILES: "docs/README.md docs/examples.md docs/about.md LICENSE testdata.xml template.tmpl lua/json.lua lua/functions.lua" 34 | release-linux-amd64: 35 | name: release linux/amd64 36 | runs-on: ubuntu-latest 37 | steps: 38 | - name: checkout 39 | uses: actions/checkout@v2 40 | - name: compile and release 41 | uses: mmalcek/go-release.action@v1.0.6 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | PROJECT_NAME: bafi 45 | CGO_ENABLED: 0 46 | GOARCH: amd64 47 | GOOS: linux 48 | CMD_PATH: -buildvcs=false 49 | EXTRA_FILES: "docs/README.md docs/examples.md docs/about.md LICENSE testdata.xml template.tmpl lua/json.lua lua/functions.lua" 50 | release-linux-386: 51 | name: release linux/386 52 | runs-on: ubuntu-latest 53 | steps: 54 | - name: checkout 55 | uses: actions/checkout@v2 56 | - name: compile and release 57 | uses: mmalcek/go-release.action@v1.0.6 58 | env: 59 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | PROJECT_NAME: bafi 61 | CGO_ENABLED: 0 62 | GOARCH: "386" 63 | GOOS: linux 64 | CMD_PATH: -buildvcs=false 65 | EXTRA_FILES: "docs/README.md docs/examples.md docs/about.md LICENSE testdata.xml template.tmpl lua/json.lua lua/functions.lua" 66 | release-darwin-amd64: 67 | name: release darwin/amd64 68 | runs-on: ubuntu-latest 69 | steps: 70 | - name: checkout 71 | uses: actions/checkout@v2 72 | - name: compile and release 73 | uses: mmalcek/go-release.action@v1.0.6 74 | env: 75 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 76 | PROJECT_NAME: bafi 77 | CGO_ENABLED: 0 78 | GOARCH: amd64 79 | GOOS: darwin 80 | CMD_PATH: -buildvcs=false 81 | EXTRA_FILES: "docs/README.md docs/examples.md docs/about.md LICENSE testdata.xml template.tmpl lua/json.lua lua/functions.lua" 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | bafi 3 | site 4 | *.exe 5 | *.exe~ 6 | *.csv 7 | output.* 8 | temp.txt 9 | cover.out 10 | oryxBuildBinary 11 | 12 | .DS_Store 13 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "golang.go", 4 | "casualjim.gotemplate" 5 | ] 6 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 mmalcek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go](https://github.com/mmalcek/bafi/actions/workflows/go.yml/badge.svg)](https://github.com/mmalcek/bafi/actions/workflows/go.yml) 2 | [![CodeQL](https://github.com/mmalcek/bafi/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/mmalcek/bafi/actions/workflows/codeql-analysis.yml) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/mmalcek/bafi)](https://goreportcard.com/report/github.com/mmalcek/bafi) 4 | [![License](https://img.shields.io/github/license/mmalcek/bafi)](https://github.com/mmalcek/bafi/blob/main/LICENSE) 5 | [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go#text-processing) 6 | [![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/mmalcek/bafi?label=latest%20release)](https://github.com/mmalcek/bafi/releases/latest) 7 | 8 | # Universal JSON, BSON, YAML, CSV, XML, mt940 translator to ANY format using templates 9 | 10 | 11 | 12 | ## Key features 13 | 14 | - Various input formats **(json, bson, yaml, csv, xml, mt940)** 15 | - Flexible output formatting using text templates 16 | - Support for [Lua](https://www.lua.org/pil/contents.html) custom functions which allows very flexible data manipulation 17 | - stdin/stdout support which allows get data from source -> translate -> delivery to destination. This allows easily translate data between different web services like **REST to SOAP, SOAP to REST, REST to CSV, ...** 18 | - Merge multiple input files in various formats into single output file formated using template 19 | - Support chatGPT queries to analyze or format data (experimental) 20 | 21 | ## Documentation [https://mmalcek.github.io/bafi/](https://mmalcek.github.io/bafi/) 22 | 23 | ## Releases (Windows, MAC, Linux) [https://github.com/mmalcek/bafi/releases](https://github.com/mmalcek/bafi/releases) 24 | 25 | usage: 26 | 27 | ``` 28 | bafi.exe -i testdata.xml -t template.tmpl -o output.txt 29 | ``` 30 | 31 | or 32 | 33 | ``` 34 | curl.exe -s https://api.predic8.de/shop/customers/ | bafi.exe -f json -t "?{{toXML .}}" 35 | ``` 36 | 37 | or 38 | 39 | ``` 40 | curl -s https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml | ./bafi -f xml -gk myChatGPTToken -gq "What's the current CZK rate?" 41 | ``` 42 | 43 | More examples and description in [documentation](https://mmalcek.github.io/bafi/) 44 | 45 | **If you like this app you can buy me a coffe ;)** 46 | 47 | 48 | Buy Me a Coffee at ko-fi.com 49 | 50 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # BaFi 2 | 3 | **Universal JSON, BSON, YAML, CSV, XML, mt940 translator to ANY format using templates** 4 | 5 | **Github repository** 6 | 7 | - [https://github.com/mmalcek/bafi](https://github.com/mmalcek/bafi) 8 | 9 | **Releases (Windows, MAC, Linux)** 10 | 11 | - [https://github.com/mmalcek/bafi/releases](https://github.com/mmalcek/bafi/releases) 12 | 13 | ## Key features 14 | 15 | - Various input formats **(json, bson, yaml, csv, xml, mt940)** 16 | - Flexible output formatting using text templates 17 | - Output can be anything: HTML page, SQL Query, Shell script, CSV file, ... 18 | - Support for [Lua](https://www.lua.org/pil/contents.html) custom functions which allows very flexible data manipulation 19 | - stdin/stdout support which allows get data from source -> translate -> delivery to destination. This allows easily translate data between different web services like **REST to SOAP, SOAP to REST, REST to CSV, ...** 20 | - Merge multiple input files in various formats into single output file formated using template 21 | - Support chatGPT queries to analyze or format data (experimental) 22 | 23 | 24 | 25 | [![Go](https://github.com/mmalcek/bafi/actions/workflows/go.yml/badge.svg)](https://github.com/mmalcek/bafi/actions/workflows/go.yml) 26 | [![CodeQL](https://github.com/mmalcek/bafi/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/mmalcek/bafi/actions/workflows/codeql-analysis.yml) 27 | [![Go Report Card](https://goreportcard.com/badge/github.com/mmalcek/bafi)](https://goreportcard.com/report/github.com/mmalcek/bafi) 28 | [![License](https://img.shields.io/github/license/mmalcek/bafi)](https://github.com/mmalcek/bafi/blob/main/LICENSE) 29 | [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go#text-processing) 30 | [![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/mmalcek/bafi?label=latest%20release)](https://github.com/mmalcek/bafi/releases/latest) 31 | 32 | 33 | 34 | **If you like this app you can buy me a coffe ;)** 35 | 36 | 37 | Buy Me a Coffee at ko-fi.com 38 | 39 | 40 | ## How does it work? 41 | 42 | Application automaticaly parse input data into object which can be simply accessed in tamplate using dot notation where first dot represent root of object **{{ . }}**. 43 | 44 | For example JSON document **myUser.json** 45 | 46 | ```json 47 | { 48 | "user": { 49 | "name": "John Doe", 50 | "age": 25, 51 | "address": { 52 | "street": "Main Street", 53 | "city": "New York", 54 | "state": "NY" 55 | }, 56 | "favourite_colors": ["red", "green", "blue"] 57 | } 58 | } 59 | ``` 60 | 61 | - Get user name: 62 | 63 | ```sh 64 | bafi.exe -i myUser.json -t '?{{.user.name}}' 65 | ``` 66 | 67 | - Use function to change all letters to uppercase: 68 | 69 | ```sh 70 | bafi.exe -i myUser.json -t '?{{upper .user.name}}' 71 | ``` 72 | 73 | - Use IF statement to compare user age to 20: 74 | 75 | ```sh 76 | bafi.exe -i myUser.json -t '?User is {{if gt (toInt .user.age) 20}}old{{else}}young{{end}}.' 77 | ``` 78 | 79 | - List favourite colors: 80 | 81 | ```sh 82 | bafi.exe -i myUser.json -t '?{{range .user.favourite_colors}}{{.}},{{end}}' 83 | ``` 84 | 85 | - Format data using template file **myTemplate.tmpl** and save output to **myUser.txt**: 86 | 87 | ```sh 88 | bafi.exe -i myUser.json -t myTemplate.tmpl -o myUser.txt 89 | ``` 90 | 91 | ``` 92 | {{- /* Content of myTemplate.tmpl file */ -}} 93 | User: {{.user.name}} 94 | Age: {{.user.age}} 95 | Address: {{.user.address.street}}, {{.user.address.city}} - {{.user.address.state}} 96 | {{- /* Create list of colors and remove comma at the end */ -}} 97 | {{- $colors := ""}}{{range .user.favourite_colors}}{{$colors = print $colors . ", "}}{{end}} 98 | {{- $colors = print (trimSuffix $colors ", " )}} 99 | Favourite colors: {{$colors}} 100 | ``` 101 | 102 | note: in Powershell you must use .\\bafi.exe e.g. 103 | 104 | ```powershell 105 | .\bafi.exe -i input.csv -t "?{{toXML .}}" 106 | curl.exe -s someurl.com/api/xxx | .\bafi.exe -f json -t "?{{toXML .}}" 107 | ``` 108 | 109 | More examples [here](examples/#template) 110 | 111 | 112 | 125 | 126 | ## Online demo (WASM) 127 | 128 | **Just try it here :)** 129 | 143 | 144 | 153 |
154 | 161 | 162 | 163 |

164 | 
165 | ## Command line arguments
166 | 
167 | - **-i input.xml** Input file name.
168 |   - If not defined app tries read stdin
169 |   - If prefixed with "?" (**-i ?files.yaml**) app will expect yaml file with multiple files description. See [example](examples/#multiple-input-files)
170 | - **-o output.txt** Output file name.
171 |   - If not defined result is send to stdout
172 | - **-t template.tmpl** Template file. Alternatively you can use _inline_ template
173 |   - inline template must start with **?** e.g. -t **"?{{.someValue}}"**
174 | - **-f json** Input format.
175 |   - Supported formats: **json, bson, yaml, csv, xml, mt940**
176 |   - If not defined (for file input) app tries detect input format automatically by file extension
177 | - **-d ','** Data delimiter
178 |   - format CSV:
179 |     - Can be defined as string e.g. -d ',' or as [hex](https://www.asciitable.com/asciifull.gif) value prefixed by **0x** e.g. 'TAB' can be defined as -f 0x09. Default delimiter is comma (**,**)
180 |   - format mt940:
181 |     - For Multiple messages in one file (e.g. Multicash). Can be defined as string e.g. -d "-\}\r\n" or "\r\n$" . If delimiter is set BaFi will return array of mt940 messages
182 | - **-v** Show current verion
183 | - **-h** list available command line arguments
184 | - **-gk myChatGPTToken** - ChatGPT token
185 | - **-gq "What's the current CZK rate?"** - ChatGPT query
186 | - **-gm gpt35** - ChatGPT model. Currently supportsed options "gpt35"(default), "gpt4", "gpt4o", "gpt4o-mini"
187 | 
188 | ```sh
189 | bafi.exe -i testdata.xml -t template.tmpl -o output.txt
190 | ```
191 | 
192 | More examples [here](examples/#command-line)
193 | 
194 | ## Templates
195 | 
196 | Bafi uses [text/template](https://pkg.go.dev/text/template). Here is a quick summary how to use. Examples are based on _testdata.xml_ included in project
197 | 
198 | note: in **vscode** you can use [gotemplate-syntax](https://marketplace.visualstudio.com/items?itemName=casualjim.gotemplate) for syntax highlighting
199 | 
200 | ### Comments
201 | 
202 | ```
203 | {{/* a comment */}}
204 | {{- /* a comment with white space trimmed from preceding and following text */ -}}
205 | ```
206 | 
207 | ### Trim new line
208 | 
209 | New line before or after text can be trimmed by adding dash
210 | 
211 | ```
212 | {{- .TOP_LEVEL}}, {{.TOP_LEVEL -}}
213 | ```
214 | 
215 | ### Accessing data
216 | 
217 | Data are accessible by _pipline_ which is represented by dot
218 | 
219 | - Simplest template
220 | 
221 | ```
222 | {{.}}
223 | ```
224 | 
225 | - Get data form inner node
226 | 
227 | ```
228 | {{.TOP_LEVEL}}
229 | ```
230 | 
231 | - Get data from XML tag. XML tags are autoprefixed by dash and accessible as index
232 | 
233 | ```
234 | {{index .TOP_LEVEL "-description"}}
235 | ```
236 | 
237 | - Convert TOP_LEVEL node to JSON
238 | 
239 | ```
240 | {{toJSON .TOP_LEVEL}}
241 | ```
242 | 
243 | ### Variables
244 | 
245 | You can store selected data to [template variable](https://pkg.go.dev/text/template#hdr-Variables)
246 | 
247 | ```
248 | {{$myVar := .TOP_LEVEL}}
249 | ```
250 | 
251 | ### Actions
252 | 
253 | Template allows to use [actions](https://pkg.go.dev/text/template#hdr-Actions), for example
254 | 
255 | Iterate over lines
256 | 
257 | ```
258 | {{range .TOP_LEVEL.DATA_LINE}}{{.val1}}{{end}}
259 | ```
260 | 
261 | If statement
262 | 
263 | ```
264 | {{if gt (int $val1) (int $val2)}}Value1{{else}}Value2{{end}} is greater
265 | ```
266 | 
267 | ### Functions
268 | 
269 | In go templates all operations are done by functions where function name is followed by operands
270 | 
271 | For example:
272 | 
273 | count val1+val2
274 | 
275 | ```
276 | {{add $val1 $val2}}
277 | ```
278 | 
279 | count (val1+val2)/val3
280 | 
281 | ```
282 | {{div (add $val1 $val2) $val3}}
283 | ```
284 | 
285 | This is called [Polish notation](https://en.wikipedia.org/wiki/Polish_notation) or "Prefix notation" also used in another languages like [Lisp]()
286 | 
287 | The key benefit of using this notation is that order of operations is clear. For example **6/2\*(1+2)** - even diferent calculators may have different opinion on order of operations in this case. With Polish notation order of operations is strictly defined (from inside to outside) **div 6 (mul 2 (add 1 2))** . This brings benefits with increasing number of operations especially in templates where math and non-math operations can be mixed together.
288 | 
289 |  
290 | 
291 | For example we have json array of items numbered from **0**
292 | 
293 | ```json
294 | { "items": ["item-0", "item-1", "item-2", "item-3"] }
295 | ```
296 | 
297 | We need change items numbering to start with **1**. To achieve this we have to do series of operations: 1. trim prefix "item-" -> 2. convert to int -> 3. add 1 -> 4. convert to string -> 5. append "item-" for all items in range. This can be done in one line
298 | 
299 | ```
300 | {{ range .items }}{{ print "item-" (toString (add1 (toInt (trimPrefix . "item-")))) }} {{ end }}
301 | ```
302 | 
303 | or alternatively (slightly shorter) print formatted string - examples [here](https://zetcode.com/golang/string-format/), documentation [here](https://golang.org/pkg/fmt/)
304 | 
305 | ```
306 | {{ range .items }}{{ printf "item-%d " (add1 (toInt (trimPrefix . "item-"))) }}{{ end }}
307 | ```
308 | 
309 | but BaFi also tries automaticaly cast variables so the shortest option is
310 | 
311 | ```
312 | {{range .items}}{{print "item-" (add1 (trimPrefix . "item-"))}} {{end}}
313 | ```
314 | 
315 | Expected result: **item-1 item-2 item-3 item-4**
316 | 
317 | There are 3 categories of functions
318 | 
319 | #### Native functions
320 | 
321 | text/template integrates [native functions](https://pkg.go.dev/text/template#hdr-Functions) to work with data
322 | 
323 | #### Additional functions
324 | 
325 | Asside of integated functions bafi contains additional common functions
326 | 
327 | ##### Math functions
328 | 
329 | - **add** - {{add .Value1 .Value2}}
330 | - **add1** - {{add1 .Value1}} = Value1+1
331 | - **sub** - substract
332 | - **div** - divide
333 | - **mod** - modulo
334 | - **mul** - multiply
335 | - **randInt** - return random integer {{randInt .Min .Max}}
336 | - **add1f** - "...f" functions parse float but provide **decimal** operations using [shopspring decimal](https://github.com/shopspring/decimal)
337 | - **addf**
338 | - **subf**
339 | - **divf**
340 | - **mulf**
341 | - **round** - {{round .Value1 2}} - will round to 2 decimals
342 | - **max** - {{round .Value1 .Value2 .Value3 ...}} get Max value from range
343 | - **min** - get Min value from range
344 | - **maxf**
345 | - **minf**
346 | 
347 | ##### Date functions
348 | 
349 | - **dateFormat** - {{dateFormat .Value "oldFormat" "newFormat"}} - [GO time format](https://programming.guide/go/format-parse-string-time-date-example.html)
350 |   - {{dateFormat "2021-08-26T22:14:00" "2006-01-02T15:04:05" "02.01.2006-15:04"}}
351 | - **dateFormatTZ** - {{dateFormatTZ .Value "oldFormat" "newFormat" "timeZone"}}
352 |   - This fuction is similar to dateFormat but applies timezone offset - [Timezones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
353 |   - {{dateFormatTZ "2021-08-26T03:35:00.000+04:00" "2006-01-02T15:04:05.000-07:00" "02.01.2006-15:04" "Europe/Prague"}}
354 | - **dateToInt** - {{dateToInt .Value "dateFormat"}} - convert date to integer (unixtime, int64), usefull for comparing dates
355 | - **intToDate** - {{intToDate .Value "dateFormat"}} - convert integer (unixtime, int64) to date, usefull for comparing dates
356 | - **now** - {{now "02.01.2006"}} - GO format date (see notes below)
357 | 
358 | ##### String functions
359 | 
360 | - **addSubstring** - {{addSubstring $myString, "XX", $position}} add substring to $position in string (if $position is 1,2,3 = Adding from right, if -1,-2,-3 = Adding from left)
361 | - **atoi** - {{atoi "042"}} - string to int. Result will be 42. atoi must be used especially for convert strings with leading zeroes
362 | - **b64enc** - encode to base64
363 | - **b64dec** - decode from base64
364 | - **b32enc** - oncode to base32
365 | - **b32dec** - decode from base32
366 | - **contains** - check if string contains substring e.g. {{contains "aaxbb" "xb"}}
367 | - **indexOf** - {{indexOf "aaxbb" "xb"}} - returns indexOf first char of substring in string
368 | - **isArray** - {{isArray .Value1}} - check if value is array
369 | - **isBool** - {{isBool .Value1}} - check if value is bool
370 | - **isInt** - {{isInt .Value1}} - check if value is int
371 | - **isFloat64** - {{isFloat64 .Value1}} - check if value is float64
372 | - **isString** - {{isString .Value1}} - check if value is string
373 | - **isMap** - {{isMap .Value1}} - check if value is map
374 | - **regexMatch** - {{regexMatch pattern .Value1}} more about go [regex](https://gobyexample.com/regular-expressions)
375 | - **replaceAll** - {{replaceAll "oldValue" "newValue" .Value}} - replace all occurences of "oldValue" with "newValue" e.g. {{replaceAll "x" "Z" "aaxbb"}} -> "aaZbb"
376 | - **replaceAllRegex** - {{replaceAllRegex "regex" "newValue" .Value}} - replace all occurences of "regex" with "newValue" e.g. {{replaceAllRegex "[a-d]", "Z" "aaxbb"}} -> "ZZxZZ"
377 | - **lower** - to lowercase
378 | - **trim** - remove leading and trailing whitespace
379 | - **trimPrefix** - {{trimPrefix "!Hello World!" "!"}} - returns "Hello World!"
380 | - **trimSuffix** - {{trimSuffix "!Hello World!" "!"}} - returns "!HelloWorld"
381 | - **mapJSON** - convert stringified JSON to map so it can be used as object or translated to other formats (e.g. "toXML"). Check template.tmpl for example
382 | - **mustArray** - {{mustArray .Value1}} - convert to array. Useful with XML where single node is not treated as array
383 | - **toBool** - {{toBool "true"}} - string to bool
384 | - **toDecimal** - {{toDecimal "3.14159"}} - cast to decimal (if error return 0)
385 | - **toDecimalString** - {{toDecimalString "3.14159"}} - cast to decimal string (if error return "error message")
386 | - **toFloat64** - {{float64 "3.14159"}} - cast to float64
387 | - **toInt** - {{int true}} - cast to int. Result will be 1. If you need convert string with leading zeroes use "atoi"
388 | - **toInt64** - {{int64 "42"}} - cast to int64. Result will be 42. If you need convert string with leading zeroes use "atoi"
389 | - **toString** - {{toString 42}} - int to string
390 | - **toJSON** - convert input object to JSON
391 | - **toBSON** - convert input object to BSON
392 | - **toYAML** - convert input object to YAML
393 | - **toXML** - convert input object to XML
394 | - **trimAll** - {{trimAll "!Hello World!" "!"}} - returns "Hello World"
395 | - **upper** - to uppercase
396 | - **uuid** - generate UUID
397 | 
398 | #### Lua custom functions
399 | 
400 | You can write your own custom lua functions defined in ./lua/functions.lua file
401 | 
402 | Call Lua function in template ("sum" - Lua function name)
403 | 
404 | ```
405 | {{lua "sum" .val1 .val2}}
406 | ```
407 | 
408 | - Input is always passed as stringified JSON and should be decoded (json.decode(incomingData))
409 | - Output must be passed as string
410 | - lua table array starts with 1
411 | - Lua [documentation](http://www.lua.org/manual/5.1/)
412 | 
413 | Minimal functions.lua example
414 | 
415 | ```lua
416 | json = require './lua/json'
417 | 
418 | function sum(incomingData)
419 |     dataTable = json.decode(incomingData)
420 |     return tostring(tonumber(dataTable[1]) + tonumber(dataTable[2]))
421 | end
422 | ```
423 | 
424 | Check [examples](examples/) and **template.tmpl** and **testdata.xml** for advanced examples
425 | 


--------------------------------------------------------------------------------
/docs/about.md:
--------------------------------------------------------------------------------
 1 | # About BaFi
 2 | 
 3 | ## What BaFi stands for
 4 | BaFi is an acronym for [**Babel Fish**](https://hitchhikers.fandom.com/wiki/Babel_Fish) from [The Hitchhiker's Guide to the Galaxy](https://en.wikipedia.org/wiki/The_Hitchhiker's_Guide_to_the_Galaxy)
 5 | 
 8 | 
 9 | ## Motivation
10 | This tool has been created for [systems@work](https://systemsatwork.com) internal purposes. 
11 | It is and always will be Open-source/[MIT license](https://github.com/mmalcek/bafi/blob/main/LICENSE).
12 | 
13 | ## Questions
14 | If you have any questions, please feel free to open discussion [here](https://github.com/mmalcek/bafi/discussions) or report an issue [here](https://github.com/mmalcek/bafi/issues).
15 | 


--------------------------------------------------------------------------------
/docs/examples.md:
--------------------------------------------------------------------------------
  1 | # Examples
  2 | 
  3 | ## Command line
  4 | 
  5 | note: in Powershell you must use .\\bafi.exe e.g.
  6 | 
  7 | ```powershell
  8 | .\bafi.exe -i input.csv -t "?{{toXML .}}"
  9 | curl.exe -s someurl.com/api/xxx | .\bafi.exe -f json -t "?{{toXML .}}"
 10 | ```
 11 | 
 12 | ### Basic
 13 | 
 14 | Get data from _testdata.xml_ -> process using _template.tmpl_ -> save output as _output.txt_
 15 | 
 16 | ```sh
 17 | bafi.exe -i testdata.xml -t template.tmpl -o output.txt
 18 | ```
 19 | 
 20 | ### Inline template
 21 | 
 22 | Get data from _testdata.xml_ -> process using inline template -> save output as _output.json_
 23 | 
 24 | ```sh
 25 | bafi.exe -i testdata.xml -o output.json -t "?{{toJSON .}}"
 26 | ```
 27 | 
 28 | note: BaFi inline template must start with **?** e.g. **"?{{toJSON .}}"**
 29 | [How to format inline templates](https://pkg.go.dev/text/template#hdr-Examples)
 30 | 
 31 | ### Stdin/REST
 32 | 
 33 | Get data from REST api -> convert to XML -> output to Stdout.
 34 | 
 35 | ```sh
 36 | curl.exe -s https://api.predic8.de/shop/customers/ | bafi.exe -f json -t "?{{toXML .}}"
 37 | ```
 38 | 
 39 | More info about curl [here](https://curl.se/) but you can of course use any tool with stdout
 40 | 
 41 | ### Append output file
 42 | 
 43 | Redirect stdout to file and append ( > = replace, >> = apppend )
 44 | 
 45 | ```sh
 46 | bafi.exe -i testdata.xml -t template.tmpl >> output.txt
 47 | ```
 48 | 
 49 | ## Template
 50 | 
 51 | Examples are based on testdata.tmpl included in project
 52 | 
 53 | ### XML to CSV
 54 | 
 55 | - command
 56 | 
 57 | ```sh
 58 | bafi.exe -i testdata.xml -t myTemplate.tmpl -o output.csv
 59 | ```
 60 | 
 61 | - myTemplate.tmpl
 62 | 
 63 | ```
 64 | Employee,Date,val1,val2,val3,SUM,LuaMultiply,linkedText
 65 | {{- range .TOP_LEVEL.DATA_LINE}}
 66 | {{index .Employee "-ID"}},
 67 | {{- dateFormat .Trans_Date "2006-01-02" "02.01.2006"}},
 68 | {{- .val1}},{{.val2}},{{.val3}},
 69 | {{- add .val1 .val2}},
 70 | {{- lua "mul" .val1 .val2}},"{{index .Linked_Text "-VALUE"}}"
 71 | {{- end}}
 72 | ```
 73 | 
 74 | ### JSON to CSV
 75 | 
 76 | - command
 77 | 
 78 | ```sh
 79 | curl.exe -s https://api.predic8.de/shop/customers/ | bafi.exe -f json -t myTemplate.tmpl -o output.html
 80 | ```
 81 | 
 82 | - myTemplate.tmpl
 83 | 
 84 | ```
 85 | name,surname
 86 | {{- range .customers}}
 87 | "{{.firstname}}","{{.lastname}}"
 88 | {{- end}}
 89 | ```
 90 | 
 91 | ### JSON to HTML
 92 | 
 93 | - command
 94 | 
 95 | ```sh
 96 | curl.exe -s https://api.predic8.de/shop/customers/ | bafi.exe -f json -t myTemplate.tmpl -o output.html
 97 | ```
 98 | 
 99 | - myTemplate.tmpl
100 | 
101 | ```
102 | 
103 |     
104 |         
105 |             
106 |             {{- range .customers}}
107 |             
108 |             {{- end }}
109 |         
NameSurname
{{.firstname}}{{.lastname}}
110 | 111 | 112 | 113 | 116 | ``` 117 | 118 | ### JSON to custom XML 119 | 120 | - command 121 | 122 | ```sh 123 | curl.exe -s https://api.predic8.de/shop/customers/ | bafi.exe -f json -t myTemplate.tmpl -o output.xml 124 | ``` 125 | 126 | - myTemplate.tmpl 127 | 128 | ``` 129 | 130 | 131 | {{- range .customers}} 132 | 133 | {{.firstname}} 134 | {{.lastname}} 135 | 136 | {{- end }} 137 | 138 | ``` 139 | 140 | ### XML to custom JSON 141 | 142 | - command 143 | 144 | ```sh 145 | bafi.exe -i testdata.xml -t myTemplate.tmpl -o output.json 146 | ``` 147 | 148 | - myTemplate.tmpl 149 | 150 | ``` 151 | {{- $new := "{\"employees\": [" }} 152 | {{- range .TOP_LEVEL.DATA_LINE}} 153 | {{- $new = print $new "{\"employeeID\":\"" (index .Employee "-ID") "\", \"val1\":" .val1 "}," }} 154 | {{- end}} 155 | {{- /* Trim trailing comma, alternatively you can remove last char by "(slice $new 0 (sub (len $new) 1))" */}} 156 | {{- $new = print (trimSuffix $new "," ) "]}"}} 157 | {{ $new}} 158 | ``` 159 | 160 | JSON in $new variable can be mapped to struct and autoformatted to other formats like: 161 | 162 | - Transform $new to YAML 163 | 164 | ``` 165 | {{toYAML (mapJSON $new) -}} 166 | ``` 167 | 168 | - Transform $new to XML 169 | 170 | ``` 171 | {{toXML (mapJSON $new) -}} 172 | ``` 173 | 174 | ### CSV to text 175 | 176 | - command 177 | 178 | ```sh 179 | bafi.exe -i users.csv -t myTemplate.tmpl -o output.txt 180 | ``` 181 | 182 | users.csv 183 | 184 | ``` 185 | name,surname 186 | John,"Jack Doe" 187 | ``` 188 | 189 | - myTemplate.tmpl 190 | 191 | ``` 192 | Users: 193 | {{- range .}} 194 | Name: {{.name}}, Surname: {{.surname}} 195 | {{- end}} 196 | ``` 197 | 198 | note: CSV file must be **[RFC4180](https://datatracker.ietf.org/doc/html/rfc4180)** compliant, file must have header line and separator must be **comma ( , )**. Or you can use command line argument -d ( e.g. **-d ';'** or **-d 0x09** ) to define separator(delimiter). 199 | 200 | ### mt940 to CSV 201 | 202 | - mt940 returns simple struct (Header,Fields,[]Transactions) of strings and additional parsing needs to be done in template. This allows full flexibility on data processing 203 | - Identifiers are prefixed by **"F\_"** (e.g. **:20:** = **.Fields.F_20**) 204 | - if parameter -d (delimiter e.g. -d "-\}\r\n" or "\r\n$") is defined for files with multiple messages (e.g. - Multicash), app returns array of mt940 messages. 205 | - Note: This is actually good place to use integrated [LUA interpreter](/bafi/#lua-custom-functions) where you can create your own set of custom functions to parse data and easily reuse them in templates. 206 | 207 | - command 208 | 209 | ```sh 210 | bafi.exe -i message.sta -t myTemplate.tmpl -o output.csv 211 | ``` 212 | 213 | - myTemplate.tmpl 214 | 215 | ``` 216 | Reference, balance, VS 217 | {{- $F20 := .Fields.F_20 }}{{ $F60F := .Fields.F_60F }} 218 | {{range .Transactions }} 219 | {{- $vsS := add (indexOf .F_86 "?21") 3 }} {{- $vsE := add $vsS 17 -}} 220 | {{- $F20}}, {{$F60F}}, {{slice .F_86 $vsS $vsE}} 221 | {{ end }} 222 | ``` 223 | 224 | ### Any SQL to XML 225 | 226 | Bafi can be used in combination with very interesting tool **USQL** [https://github.com/xo/usql](https://github.com/xo/usql). USQL allows query almost any SQL like database (MSSQL,MySQL,postgres, ...) and get result in various formats. In this example we use -J for JSON. Output can be further processed by BaFi and templates 227 | 228 | ```sh 229 | usql.exe mssql://user:password@server/instance/database -c "SELECT * FROM USERS" -J -q | bafi.exe -f json -t "?{{toXML .}}" 230 | ``` 231 | 232 | ### MongoDump to CSV 233 | 234 | - command 235 | 236 | ```sh 237 | bafi.exe -i users.bson -t myTemplate.tmpl -o output.html 238 | ``` 239 | 240 | - myTemplate.tmpl 241 | 242 | ``` 243 | name,surname 244 | {{- range .}} 245 | "{{.firstname}}","{{.lastname}}" 246 | {{- end}} 247 | ``` 248 | 249 | ### Dashes in key names 250 | 251 | If key name contains dashes ( - ) bafi will fail with error "bad character U+002D '-'" for example: 252 | 253 | ``` 254 | {{.my-key.subkey}} 255 | ``` 256 | 257 | This is known limitation of go templates which can be solved by workaround 258 | 259 | ``` 260 | {{index . "my-key" "subkey"}} 261 | ``` 262 | 263 | ### Input autoformat to XXX 264 | 265 | Input data can be easily fomated to oher formats by functions **toXML,toJSON,toBSON,toYAML**. In this case its not necesarry add template file because it's as easy as 266 | 267 | ```sh 268 | curl.exe -s https://api.predic8.de/shop/customers/ | bafi.exe -f json -t "?{{toXML .}}" -o output.xml 269 | curl.exe -s https://api.predic8.de/shop/customers/ | bafi.exe -f json -t "?{{toJSON .}}" -o output.json 270 | curl.exe -s https://api.predic8.de/shop/customers/ | bafi.exe -f json -t "?{{toBSON .}}" -o output.bson 271 | curl.exe -s https://api.predic8.de/shop/customers/ | bafi.exe -f json -t "?{{toYAML .}}" -o output.yml 272 | ``` 273 | 274 | ### ChatGPT query 275 | 276 | ```sh 277 | curl -s https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml | ./bafi -f xml -gk myChatGPTToken -gq "What's the current CZK rate?" 278 | curl -s https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml | ./bafi -f xml -gk myChatGPTToken -gq "format rates to html" -gm gpt4 279 | curl -s "https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&hourly=temperature_2m&past_days=7&forecast_days=0" | ./bafi -f json -gk "myChatGPTToken" -gm gpt4o-mini -gq "Create forecast for next 2 days based on provided data" 280 | ./bafi -i invoice.json -gk myChatGPTToken -gq "create XML UBL format invoice" -o invoice.xml 281 | 282 | ``` 283 | 284 | ### Multiple input files 285 | 286 | Bafi can read multiple input files and merge them into one output file. This will require aditional file with files description. 287 | Description file must be in YAML format as described below and prefixed by question mark **"?"** for examle **bafi.exe -i ?files.yaml** 288 | 289 | Example: 290 | 291 | - batch file which gets the data from multiple sources **myFiles.bat** 292 | 293 | ```sh 294 | curl -s https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml > ecbRates.xml 295 | curl -s https://goweather.herokuapp.com/weather/prague > pragueWeather.json 296 | ``` 297 | 298 | - Files description **myFiles.yaml** 299 | 300 | ```yaml 301 | - file: ./ecbRates.xml # file path 302 | format: xml # File format 303 | label: RATES # Label which will be used in the template {{ .RATES }} 304 | - file: ./pragueWeather.json 305 | format: json 306 | label: WEATHER 307 | ``` 308 | 309 | - Template file **myTemplate.tmpl** which will generate simple HTML page with data 310 | 311 | ```html 312 | 313 | 314 |

Weather in Prague

315 |

Temperatre: {{.WEATHER.temperature}}

316 |

Wind: {{.WEATHER.wind}}

317 |

ECB Exchange rates from: {{dateFormat (index .RATES.Envelope.Cube.Cube "-time") "2006-01-02" "02.01.2006" }}

318 | 319 | 320 | {{- range .RATES.Envelope.Cube.Cube.Cube }} 321 | 322 | {{- end}} 323 |
currencyrate
{{index . "-currency" }}{{index . "-rate" }}
324 | 325 | 326 | 327 | 330 | ``` 331 | 332 | - Finally run bafi 333 | 334 | ```sh 335 | bafi.exe -t myTemplate.tmpl -i ?myFiles.yaml -o output.html 336 | ``` 337 | -------------------------------------------------------------------------------- /docs/img/bafi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 42 | 46 | 51 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /docs/img/bafiIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 37 | 39 | 43 | 51 | 55 | 60 | 68 | 69 | 70 | 74 | 75 | -------------------------------------------------------------------------------- /docs/img/calc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmalcek/bafi/0aa8972647b5af9854f923097f8575f541a760f2/docs/img/calc.jpg -------------------------------------------------------------------------------- /docs/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmalcek/bafi/0aa8972647b5af9854f923097f8575f541a760f2/docs/img/favicon.ico -------------------------------------------------------------------------------- /docs/img/scheme.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 38 | 40 | 44 | 48 | 53 | 60 | Input 75 | 86 | Output 101 | Template 116 | Lua 131 | 138 | 145 | 152 | 159 | 163 | 168 | 172 | 177 | 181 | 186 | 190 | 195 | BaFi 206 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /docs/js/bafi.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmalcek/bafi/0aa8972647b5af9854f923097f8575f541a760f2/docs/js/bafi.wasm -------------------------------------------------------------------------------- /docs/js/wasm_exec.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | "use strict"; 6 | 7 | (() => { 8 | const enosys = () => { 9 | const err = new Error("not implemented"); 10 | err.code = "ENOSYS"; 11 | return err; 12 | }; 13 | 14 | if (!globalThis.fs) { 15 | let outputBuf = ""; 16 | globalThis.fs = { 17 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused 18 | writeSync(fd, buf) { 19 | outputBuf += decoder.decode(buf); 20 | const nl = outputBuf.lastIndexOf("\n"); 21 | if (nl != -1) { 22 | console.log(outputBuf.substring(0, nl)); 23 | outputBuf = outputBuf.substring(nl + 1); 24 | } 25 | return buf.length; 26 | }, 27 | write(fd, buf, offset, length, position, callback) { 28 | if (offset !== 0 || length !== buf.length || position !== null) { 29 | callback(enosys()); 30 | return; 31 | } 32 | const n = this.writeSync(fd, buf); 33 | callback(null, n); 34 | }, 35 | chmod(path, mode, callback) { callback(enosys()); }, 36 | chown(path, uid, gid, callback) { callback(enosys()); }, 37 | close(fd, callback) { callback(enosys()); }, 38 | fchmod(fd, mode, callback) { callback(enosys()); }, 39 | fchown(fd, uid, gid, callback) { callback(enosys()); }, 40 | fstat(fd, callback) { callback(enosys()); }, 41 | fsync(fd, callback) { callback(null); }, 42 | ftruncate(fd, length, callback) { callback(enosys()); }, 43 | lchown(path, uid, gid, callback) { callback(enosys()); }, 44 | link(path, link, callback) { callback(enosys()); }, 45 | lstat(path, callback) { callback(enosys()); }, 46 | mkdir(path, perm, callback) { callback(enosys()); }, 47 | open(path, flags, mode, callback) { callback(enosys()); }, 48 | read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, 49 | readdir(path, callback) { callback(enosys()); }, 50 | readlink(path, callback) { callback(enosys()); }, 51 | rename(from, to, callback) { callback(enosys()); }, 52 | rmdir(path, callback) { callback(enosys()); }, 53 | stat(path, callback) { callback(enosys()); }, 54 | symlink(path, link, callback) { callback(enosys()); }, 55 | truncate(path, length, callback) { callback(enosys()); }, 56 | unlink(path, callback) { callback(enosys()); }, 57 | utimes(path, atime, mtime, callback) { callback(enosys()); }, 58 | }; 59 | } 60 | 61 | if (!globalThis.process) { 62 | globalThis.process = { 63 | getuid() { return -1; }, 64 | getgid() { return -1; }, 65 | geteuid() { return -1; }, 66 | getegid() { return -1; }, 67 | getgroups() { throw enosys(); }, 68 | pid: -1, 69 | ppid: -1, 70 | umask() { throw enosys(); }, 71 | cwd() { throw enosys(); }, 72 | chdir() { throw enosys(); }, 73 | } 74 | } 75 | 76 | if (!globalThis.crypto) { 77 | throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)"); 78 | } 79 | 80 | if (!globalThis.performance) { 81 | throw new Error("globalThis.performance is not available, polyfill required (performance.now only)"); 82 | } 83 | 84 | if (!globalThis.TextEncoder) { 85 | throw new Error("globalThis.TextEncoder is not available, polyfill required"); 86 | } 87 | 88 | if (!globalThis.TextDecoder) { 89 | throw new Error("globalThis.TextDecoder is not available, polyfill required"); 90 | } 91 | 92 | const encoder = new TextEncoder("utf-8"); 93 | const decoder = new TextDecoder("utf-8"); 94 | 95 | globalThis.Go = class { 96 | constructor() { 97 | this.argv = ["js"]; 98 | this.env = {}; 99 | this.exit = (code) => { 100 | if (code !== 0) { 101 | console.warn("exit code:", code); 102 | } 103 | }; 104 | this._exitPromise = new Promise((resolve) => { 105 | this._resolveExitPromise = resolve; 106 | }); 107 | this._pendingEvent = null; 108 | this._scheduledTimeouts = new Map(); 109 | this._nextCallbackTimeoutID = 1; 110 | 111 | const setInt64 = (addr, v) => { 112 | this.mem.setUint32(addr + 0, v, true); 113 | this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); 114 | } 115 | 116 | const setInt32 = (addr, v) => { 117 | this.mem.setUint32(addr + 0, v, true); 118 | } 119 | 120 | const getInt64 = (addr) => { 121 | const low = this.mem.getUint32(addr + 0, true); 122 | const high = this.mem.getInt32(addr + 4, true); 123 | return low + high * 4294967296; 124 | } 125 | 126 | const loadValue = (addr) => { 127 | const f = this.mem.getFloat64(addr, true); 128 | if (f === 0) { 129 | return undefined; 130 | } 131 | if (!isNaN(f)) { 132 | return f; 133 | } 134 | 135 | const id = this.mem.getUint32(addr, true); 136 | return this._values[id]; 137 | } 138 | 139 | const storeValue = (addr, v) => { 140 | const nanHead = 0x7FF80000; 141 | 142 | if (typeof v === "number" && v !== 0) { 143 | if (isNaN(v)) { 144 | this.mem.setUint32(addr + 4, nanHead, true); 145 | this.mem.setUint32(addr, 0, true); 146 | return; 147 | } 148 | this.mem.setFloat64(addr, v, true); 149 | return; 150 | } 151 | 152 | if (v === undefined) { 153 | this.mem.setFloat64(addr, 0, true); 154 | return; 155 | } 156 | 157 | let id = this._ids.get(v); 158 | if (id === undefined) { 159 | id = this._idPool.pop(); 160 | if (id === undefined) { 161 | id = this._values.length; 162 | } 163 | this._values[id] = v; 164 | this._goRefCounts[id] = 0; 165 | this._ids.set(v, id); 166 | } 167 | this._goRefCounts[id]++; 168 | let typeFlag = 0; 169 | switch (typeof v) { 170 | case "object": 171 | if (v !== null) { 172 | typeFlag = 1; 173 | } 174 | break; 175 | case "string": 176 | typeFlag = 2; 177 | break; 178 | case "symbol": 179 | typeFlag = 3; 180 | break; 181 | case "function": 182 | typeFlag = 4; 183 | break; 184 | } 185 | this.mem.setUint32(addr + 4, nanHead | typeFlag, true); 186 | this.mem.setUint32(addr, id, true); 187 | } 188 | 189 | const loadSlice = (addr) => { 190 | const array = getInt64(addr + 0); 191 | const len = getInt64(addr + 8); 192 | return new Uint8Array(this._inst.exports.mem.buffer, array, len); 193 | } 194 | 195 | const loadSliceOfValues = (addr) => { 196 | const array = getInt64(addr + 0); 197 | const len = getInt64(addr + 8); 198 | const a = new Array(len); 199 | for (let i = 0; i < len; i++) { 200 | a[i] = loadValue(array + i * 8); 201 | } 202 | return a; 203 | } 204 | 205 | const loadString = (addr) => { 206 | const saddr = getInt64(addr + 0); 207 | const len = getInt64(addr + 8); 208 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); 209 | } 210 | 211 | const timeOrigin = Date.now() - performance.now(); 212 | this.importObject = { 213 | _gotest: { 214 | add: (a, b) => a + b, 215 | }, 216 | gojs: { 217 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) 218 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported 219 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). 220 | // This changes the SP, thus we have to update the SP used by the imported function. 221 | 222 | // func wasmExit(code int32) 223 | "runtime.wasmExit": (sp) => { 224 | sp >>>= 0; 225 | const code = this.mem.getInt32(sp + 8, true); 226 | this.exited = true; 227 | delete this._inst; 228 | delete this._values; 229 | delete this._goRefCounts; 230 | delete this._ids; 231 | delete this._idPool; 232 | this.exit(code); 233 | }, 234 | 235 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) 236 | "runtime.wasmWrite": (sp) => { 237 | sp >>>= 0; 238 | const fd = getInt64(sp + 8); 239 | const p = getInt64(sp + 16); 240 | const n = this.mem.getInt32(sp + 24, true); 241 | fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); 242 | }, 243 | 244 | // func resetMemoryDataView() 245 | "runtime.resetMemoryDataView": (sp) => { 246 | sp >>>= 0; 247 | this.mem = new DataView(this._inst.exports.mem.buffer); 248 | }, 249 | 250 | // func nanotime1() int64 251 | "runtime.nanotime1": (sp) => { 252 | sp >>>= 0; 253 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); 254 | }, 255 | 256 | // func walltime() (sec int64, nsec int32) 257 | "runtime.walltime": (sp) => { 258 | sp >>>= 0; 259 | const msec = (new Date).getTime(); 260 | setInt64(sp + 8, msec / 1000); 261 | this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); 262 | }, 263 | 264 | // func scheduleTimeoutEvent(delay int64) int32 265 | "runtime.scheduleTimeoutEvent": (sp) => { 266 | sp >>>= 0; 267 | const id = this._nextCallbackTimeoutID; 268 | this._nextCallbackTimeoutID++; 269 | this._scheduledTimeouts.set(id, setTimeout( 270 | () => { 271 | this._resume(); 272 | while (this._scheduledTimeouts.has(id)) { 273 | // for some reason Go failed to register the timeout event, log and try again 274 | // (temporary workaround for https://github.com/golang/go/issues/28975) 275 | console.warn("scheduleTimeoutEvent: missed timeout event"); 276 | this._resume(); 277 | } 278 | }, 279 | getInt64(sp + 8), 280 | )); 281 | this.mem.setInt32(sp + 16, id, true); 282 | }, 283 | 284 | // func clearTimeoutEvent(id int32) 285 | "runtime.clearTimeoutEvent": (sp) => { 286 | sp >>>= 0; 287 | const id = this.mem.getInt32(sp + 8, true); 288 | clearTimeout(this._scheduledTimeouts.get(id)); 289 | this._scheduledTimeouts.delete(id); 290 | }, 291 | 292 | // func getRandomData(r []byte) 293 | "runtime.getRandomData": (sp) => { 294 | sp >>>= 0; 295 | crypto.getRandomValues(loadSlice(sp + 8)); 296 | }, 297 | 298 | // func finalizeRef(v ref) 299 | "syscall/js.finalizeRef": (sp) => { 300 | sp >>>= 0; 301 | const id = this.mem.getUint32(sp + 8, true); 302 | this._goRefCounts[id]--; 303 | if (this._goRefCounts[id] === 0) { 304 | const v = this._values[id]; 305 | this._values[id] = null; 306 | this._ids.delete(v); 307 | this._idPool.push(id); 308 | } 309 | }, 310 | 311 | // func stringVal(value string) ref 312 | "syscall/js.stringVal": (sp) => { 313 | sp >>>= 0; 314 | storeValue(sp + 24, loadString(sp + 8)); 315 | }, 316 | 317 | // func valueGet(v ref, p string) ref 318 | "syscall/js.valueGet": (sp) => { 319 | sp >>>= 0; 320 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); 321 | sp = this._inst.exports.getsp() >>> 0; // see comment above 322 | storeValue(sp + 32, result); 323 | }, 324 | 325 | // func valueSet(v ref, p string, x ref) 326 | "syscall/js.valueSet": (sp) => { 327 | sp >>>= 0; 328 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); 329 | }, 330 | 331 | // func valueDelete(v ref, p string) 332 | "syscall/js.valueDelete": (sp) => { 333 | sp >>>= 0; 334 | Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); 335 | }, 336 | 337 | // func valueIndex(v ref, i int) ref 338 | "syscall/js.valueIndex": (sp) => { 339 | sp >>>= 0; 340 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); 341 | }, 342 | 343 | // valueSetIndex(v ref, i int, x ref) 344 | "syscall/js.valueSetIndex": (sp) => { 345 | sp >>>= 0; 346 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); 347 | }, 348 | 349 | // func valueCall(v ref, m string, args []ref) (ref, bool) 350 | "syscall/js.valueCall": (sp) => { 351 | sp >>>= 0; 352 | try { 353 | const v = loadValue(sp + 8); 354 | const m = Reflect.get(v, loadString(sp + 16)); 355 | const args = loadSliceOfValues(sp + 32); 356 | const result = Reflect.apply(m, v, args); 357 | sp = this._inst.exports.getsp() >>> 0; // see comment above 358 | storeValue(sp + 56, result); 359 | this.mem.setUint8(sp + 64, 1); 360 | } catch (err) { 361 | sp = this._inst.exports.getsp() >>> 0; // see comment above 362 | storeValue(sp + 56, err); 363 | this.mem.setUint8(sp + 64, 0); 364 | } 365 | }, 366 | 367 | // func valueInvoke(v ref, args []ref) (ref, bool) 368 | "syscall/js.valueInvoke": (sp) => { 369 | sp >>>= 0; 370 | try { 371 | const v = loadValue(sp + 8); 372 | const args = loadSliceOfValues(sp + 16); 373 | const result = Reflect.apply(v, undefined, args); 374 | sp = this._inst.exports.getsp() >>> 0; // see comment above 375 | storeValue(sp + 40, result); 376 | this.mem.setUint8(sp + 48, 1); 377 | } catch (err) { 378 | sp = this._inst.exports.getsp() >>> 0; // see comment above 379 | storeValue(sp + 40, err); 380 | this.mem.setUint8(sp + 48, 0); 381 | } 382 | }, 383 | 384 | // func valueNew(v ref, args []ref) (ref, bool) 385 | "syscall/js.valueNew": (sp) => { 386 | sp >>>= 0; 387 | try { 388 | const v = loadValue(sp + 8); 389 | const args = loadSliceOfValues(sp + 16); 390 | const result = Reflect.construct(v, args); 391 | sp = this._inst.exports.getsp() >>> 0; // see comment above 392 | storeValue(sp + 40, result); 393 | this.mem.setUint8(sp + 48, 1); 394 | } catch (err) { 395 | sp = this._inst.exports.getsp() >>> 0; // see comment above 396 | storeValue(sp + 40, err); 397 | this.mem.setUint8(sp + 48, 0); 398 | } 399 | }, 400 | 401 | // func valueLength(v ref) int 402 | "syscall/js.valueLength": (sp) => { 403 | sp >>>= 0; 404 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); 405 | }, 406 | 407 | // valuePrepareString(v ref) (ref, int) 408 | "syscall/js.valuePrepareString": (sp) => { 409 | sp >>>= 0; 410 | const str = encoder.encode(String(loadValue(sp + 8))); 411 | storeValue(sp + 16, str); 412 | setInt64(sp + 24, str.length); 413 | }, 414 | 415 | // valueLoadString(v ref, b []byte) 416 | "syscall/js.valueLoadString": (sp) => { 417 | sp >>>= 0; 418 | const str = loadValue(sp + 8); 419 | loadSlice(sp + 16).set(str); 420 | }, 421 | 422 | // func valueInstanceOf(v ref, t ref) bool 423 | "syscall/js.valueInstanceOf": (sp) => { 424 | sp >>>= 0; 425 | this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0); 426 | }, 427 | 428 | // func copyBytesToGo(dst []byte, src ref) (int, bool) 429 | "syscall/js.copyBytesToGo": (sp) => { 430 | sp >>>= 0; 431 | const dst = loadSlice(sp + 8); 432 | const src = loadValue(sp + 32); 433 | if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { 434 | this.mem.setUint8(sp + 48, 0); 435 | return; 436 | } 437 | const toCopy = src.subarray(0, dst.length); 438 | dst.set(toCopy); 439 | setInt64(sp + 40, toCopy.length); 440 | this.mem.setUint8(sp + 48, 1); 441 | }, 442 | 443 | // func copyBytesToJS(dst ref, src []byte) (int, bool) 444 | "syscall/js.copyBytesToJS": (sp) => { 445 | sp >>>= 0; 446 | const dst = loadValue(sp + 8); 447 | const src = loadSlice(sp + 16); 448 | if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { 449 | this.mem.setUint8(sp + 48, 0); 450 | return; 451 | } 452 | const toCopy = src.subarray(0, dst.length); 453 | dst.set(toCopy); 454 | setInt64(sp + 40, toCopy.length); 455 | this.mem.setUint8(sp + 48, 1); 456 | }, 457 | 458 | "debug": (value) => { 459 | console.log(value); 460 | }, 461 | } 462 | }; 463 | } 464 | 465 | async run(instance) { 466 | if (!(instance instanceof WebAssembly.Instance)) { 467 | throw new Error("Go.run: WebAssembly.Instance expected"); 468 | } 469 | this._inst = instance; 470 | this.mem = new DataView(this._inst.exports.mem.buffer); 471 | this._values = [ // JS values that Go currently has references to, indexed by reference id 472 | NaN, 473 | 0, 474 | null, 475 | true, 476 | false, 477 | globalThis, 478 | this, 479 | ]; 480 | this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id 481 | this._ids = new Map([ // mapping from JS values to reference ids 482 | [0, 1], 483 | [null, 2], 484 | [true, 3], 485 | [false, 4], 486 | [globalThis, 5], 487 | [this, 6], 488 | ]); 489 | this._idPool = []; // unused ids that have been garbage collected 490 | this.exited = false; // whether the Go program has exited 491 | 492 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. 493 | let offset = 4096; 494 | 495 | const strPtr = (str) => { 496 | const ptr = offset; 497 | const bytes = encoder.encode(str + "\0"); 498 | new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); 499 | offset += bytes.length; 500 | if (offset % 8 !== 0) { 501 | offset += 8 - (offset % 8); 502 | } 503 | return ptr; 504 | }; 505 | 506 | const argc = this.argv.length; 507 | 508 | const argvPtrs = []; 509 | this.argv.forEach((arg) => { 510 | argvPtrs.push(strPtr(arg)); 511 | }); 512 | argvPtrs.push(0); 513 | 514 | const keys = Object.keys(this.env).sort(); 515 | keys.forEach((key) => { 516 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); 517 | }); 518 | argvPtrs.push(0); 519 | 520 | const argv = offset; 521 | argvPtrs.forEach((ptr) => { 522 | this.mem.setUint32(offset, ptr, true); 523 | this.mem.setUint32(offset + 4, 0, true); 524 | offset += 8; 525 | }); 526 | 527 | // The linker guarantees global data starts from at least wasmMinDataAddr. 528 | // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. 529 | const wasmMinDataAddr = 4096 + 8192; 530 | if (offset >= wasmMinDataAddr) { 531 | throw new Error("total length of command line and environment variables exceeds limit"); 532 | } 533 | 534 | this._inst.exports.run(argc, argv); 535 | if (this.exited) { 536 | this._resolveExitPromise(); 537 | } 538 | await this._exitPromise; 539 | } 540 | 541 | _resume() { 542 | if (this.exited) { 543 | throw new Error("Go program has already exited"); 544 | } 545 | this._inst.exports.resume(); 546 | if (this.exited) { 547 | this._resolveExitPromise(); 548 | } 549 | } 550 | 551 | _makeFuncWrapper(id) { 552 | const go = this; 553 | return function () { 554 | const event = { id: id, this: this, args: arguments }; 555 | go._pendingEvent = event; 556 | go._resume(); 557 | return event.result; 558 | }; 559 | } 560 | } 561 | })(); 562 | -------------------------------------------------------------------------------- /filesTest.yaml: -------------------------------------------------------------------------------- 1 | # Example array of files: 2 | - file: ./testdata.xml # file path 3 | format: xml # File format 4 | label: filesTest # Label which will be used in the template {{ .filesTest }} 5 | -------------------------------------------------------------------------------- /functions.go: -------------------------------------------------------------------------------- 1 | // Inspired by https://github.com/Masterminds/sprig 2 | package main 3 | 4 | import ( 5 | "encoding/base32" 6 | "encoding/base64" 7 | "encoding/json" 8 | "fmt" 9 | "math" 10 | "math/rand" 11 | "reflect" 12 | "regexp" 13 | "strconv" 14 | "strings" 15 | "text/template" 16 | "time" 17 | 18 | "github.com/clbanning/mxj/v2" 19 | "github.com/google/uuid" 20 | "github.com/shopspring/decimal" 21 | "github.com/spf13/cast" 22 | lua "github.com/yuin/gopher-lua" 23 | "go.mongodb.org/mongo-driver/bson" 24 | "gopkg.in/yaml.v3" 25 | ) 26 | 27 | // templateFunctions extends template functions 28 | func templateFunctions() template.FuncMap { 29 | return template.FuncMap{ 30 | "add": add, 31 | "add1": add1, 32 | "sub": sub, 33 | "div": div, 34 | "mod": mod, 35 | "mul": mul, 36 | "addf": addf, 37 | "add1f": add1f, 38 | "subf": subf, 39 | "divf": divf, 40 | "mulf": mulf, 41 | "randInt": randInt, 42 | "round": round, 43 | "max": max, 44 | "min": min, 45 | "maxf": maxf, 46 | "minf": minf, 47 | "dateFormat": dateFormat, 48 | "dateFormatTZ": dateFormatTZ, 49 | "dateToInt": dateToInt, 50 | "intToDate": intToDate, 51 | "now": now, 52 | "b64enc": base64encode, 53 | "b64dec": base64decode, 54 | "b32enc": base32encode, 55 | "b32dec": base32decode, 56 | "uuid": newUUID, 57 | "replaceAll": replaceAll, 58 | "replaceAllRegex": replaceAllRegex, 59 | "regexMatch": regexMatch, 60 | "contains": contains, 61 | "upper": upper, 62 | "lower": lower, 63 | "addSubstring": addSubstring, 64 | "trim": trim, 65 | "trimAll": trimAll, 66 | "trimSuffix": trimSuffix, 67 | "indexOf": indexOf, 68 | "trimPrefix": trimPrefix, 69 | "atoi": atoi, 70 | "toBool": toBool, 71 | "toString": toString, 72 | "toInt": toInt, 73 | "toInt64": toInt64, 74 | "toFloat64": toFloat64, 75 | "toDecimal": toDecimal, 76 | "toDecimalString": toDecimalString, 77 | "toJSON": toJSON, 78 | "toBSON": toBSON, 79 | "toYAML": toYAML, 80 | "toXML": toXML, 81 | "isBool": isBool, 82 | "isInt": isInt, 83 | "isFloat64": isFloat64, 84 | "isString": isString, 85 | "isMap": isMap, 86 | "isArray": isArray, 87 | "mustArray": mustArray, 88 | "mapJSON": mapJSON, 89 | "lua": luaF, 90 | } 91 | } 92 | 93 | // add count 94 | func add(a ...interface{}) (r int64) { 95 | for i := range a { 96 | r += toInt64(a[i]) 97 | } 98 | return 99 | } 100 | 101 | // add1 input+1 102 | func add1(a interface{}) int64 { return toInt64(a) + 1 } 103 | 104 | // sub substitute 105 | func sub(a, b interface{}) int64 { return toInt64(a) - toInt64(b) } 106 | 107 | // div divide 108 | func div(a, b interface{}) int64 { return toInt64(a) / toInt64(b) } 109 | 110 | // mod modulo 111 | func mod(a, b interface{}) int64 { return toInt64(a) % toInt64(b) } 112 | 113 | // mul multiply 114 | func mul(a ...interface{}) (r int64) { 115 | r = 1 116 | for i := range a { 117 | r = r * toInt64(a[i]) 118 | } 119 | return r 120 | } 121 | 122 | // addf count float 123 | func addf(i ...interface{}) float64 { 124 | a := interface{}(float64(0)) 125 | return execDecimalOp(a, i, func(d1, d2 decimal.Decimal) decimal.Decimal { return d1.Add(d2) }) 126 | } 127 | 128 | // add1f inputFloat+1 129 | func add1f(i interface{}) float64 { 130 | return execDecimalOp(i, []interface{}{1}, func(d1, d2 decimal.Decimal) decimal.Decimal { return d1.Add(d2) }) 131 | } 132 | 133 | // subf substitute float 134 | func subf(a interface{}, v ...interface{}) float64 { 135 | return execDecimalOp(a, v, func(d1, d2 decimal.Decimal) decimal.Decimal { return d1.Sub(d2) }) 136 | } 137 | 138 | // divide float 139 | func divf(a interface{}, v ...interface{}) float64 { 140 | return execDecimalOp(a, v, func(d1, d2 decimal.Decimal) decimal.Decimal { return d1.Div(d2) }) 141 | } 142 | 143 | // mulf multiply float 144 | func mulf(a interface{}, v ...interface{}) float64 { 145 | return execDecimalOp(a, v, func(d1, d2 decimal.Decimal) decimal.Decimal { return d1.Mul(d2) }) 146 | } 147 | 148 | // randInt returns random integer in defined range {{randInt min max}} e.g. {{randInt 1 10}} 149 | func randInt(min, max int) int { return rand.Intn(max-min) + min } 150 | 151 | // round float {{round .val 2}} -> 2 decimals or {{round .val 1 0.4}} 0.4 round point 152 | func round(a interface{}, p int, rOpt ...float64) float64 { 153 | roundOn := .5 154 | if len(rOpt) > 0 { 155 | roundOn = rOpt[0] 156 | } 157 | val := toFloat64(a) 158 | places := toFloat64(p) 159 | 160 | var round float64 161 | pow := math.Pow(10, places) 162 | digit := pow * val 163 | _, div := math.Modf(digit) 164 | if div >= roundOn { 165 | round = math.Ceil(digit) 166 | } else { 167 | round = math.Floor(digit) 168 | } 169 | return round / pow 170 | } 171 | 172 | // max return highest from numbers {{max .v1 .v2 .v3}} 173 | func max(a interface{}, numbers ...interface{}) int64 { 174 | aa := toInt64(a) 175 | for i := range numbers { 176 | bb := toInt64(numbers[i]) 177 | if bb > aa { 178 | aa = bb 179 | } 180 | } 181 | return aa 182 | } 183 | 184 | // min return lowest from numbers {{min .v1 .v2 .v3}} 185 | func min(a interface{}, numbers ...interface{}) int64 { 186 | aa := toInt64(a) 187 | for i := range numbers { 188 | bb := toInt64(numbers[i]) 189 | if bb < aa { 190 | aa = bb 191 | } 192 | } 193 | return aa 194 | } 195 | 196 | // maxf return highest from float numbers {{maxf .v1 .v2 .v3}} 197 | func maxf(a interface{}, numbers ...interface{}) float64 { 198 | aa := toFloat64(a) 199 | for i := range numbers { 200 | bb := toFloat64(numbers[i]) 201 | aa = math.Max(aa, bb) 202 | } 203 | return aa 204 | } 205 | 206 | // minf return lowest from float numbers {{minf .v1 .v2 .v3}} 207 | func minf(a interface{}, numbers ...interface{}) float64 { 208 | aa := toFloat64(a) 209 | for i := range numbers { 210 | bb := toFloat64(numbers[i]) 211 | aa = math.Min(aa, bb) 212 | } 213 | return aa 214 | } 215 | 216 | // dateFormat convert date format {{dateFormat "string", "inputPattern", "outputPattern"}} e.g. {{dateFormat "15.03.2021", "02.01.2006", "01022006"}} 217 | func dateFormat(date string, inputFormat string, outputFormat string) string { 218 | timeParsed, err := time.Parse(inputFormat, date) 219 | if err != nil { 220 | return date 221 | } 222 | return timeParsed.Format(outputFormat) 223 | } 224 | 225 | func dateFormatTZ(date string, inputFormat string, outputFormat string, timeZone string) string { 226 | location, err := time.LoadLocation(timeZone) 227 | if err != nil { 228 | return "err: unknownTimeZone" 229 | } 230 | timeParsed, err := time.Parse(inputFormat, date) 231 | if err != nil { 232 | return "err: wrongFormatDefinition" 233 | } 234 | timeParsed = timeParsed.In(location) 235 | return timeParsed.Format(outputFormat) 236 | } 237 | 238 | // convert date to unix timestamp 239 | func dateToInt(date string, inputFormat string) int64 { 240 | timeParsed, err := time.Parse(inputFormat, date) 241 | if err != nil { 242 | return 0 243 | } 244 | return timeParsed.Unix() 245 | } 246 | 247 | // convert unix timestamp to date 248 | func intToDate(unixTime interface{}, outputFormat string) string { 249 | return time.Unix(toInt64(unixTime), 0).Format(outputFormat) 250 | } 251 | 252 | // now return current date/time in specified format 253 | func now(format string) string { 254 | return time.Now().Format(format) 255 | } 256 | 257 | // base64encode encode to base64 258 | func base64encode(v string) string { 259 | return base64.StdEncoding.EncodeToString([]byte(v)) 260 | } 261 | 262 | // base64decode decode from base64 263 | func base64decode(v string) string { 264 | data, err := base64.StdEncoding.DecodeString(v) 265 | if err != nil { 266 | return err.Error() 267 | } 268 | return string(data) 269 | } 270 | 271 | // base32encode encode to base32 272 | func base32encode(v string) string { 273 | return base32.StdEncoding.EncodeToString([]byte(v)) 274 | } 275 | 276 | // base32decode decode from base32 277 | func base32decode(v string) string { 278 | data, err := base32.StdEncoding.DecodeString(v) 279 | if err != nil { 280 | return err.Error() 281 | } 282 | return string(data) 283 | } 284 | 285 | // newUUID returns UUID 286 | func newUUID() string { return uuid.New().String() } 287 | 288 | func replaceAll(old, new, src string) string { 289 | return strings.Replace(src, old, new, -1) 290 | } 291 | 292 | func replaceAllRegex(regex, new, src string) string { 293 | r := regexp.MustCompile(regex) 294 | return r.ReplaceAllString(src, new) 295 | } 296 | 297 | // regexMatch check regex e.g. {{regexMatch "a.b", "aaxbb"}} 298 | func regexMatch(regex string, s string) bool { 299 | match, _ := regexp.MatchString(regex, s) 300 | return match 301 | } 302 | 303 | // contains check if string contains substring e.g. {{contains "aaxbb" "xb"}} 304 | func contains(str string, substr string) bool { 305 | return strings.Contains(str, substr) 306 | } 307 | 308 | // upper string to uppercase 309 | func upper(s string) string { 310 | return strings.ToUpper(s) 311 | } 312 | 313 | // lower string to lowercase 314 | func lower(s string) string { 315 | return strings.ToLower(s) 316 | } 317 | 318 | // addSubstring add substring to string {{addSubstring "abcd", "efg", 2}} -> "abefgcd" 319 | func addSubstring(s string, ss string, pos interface{}) string { 320 | if toInt(pos) >= len(s) || -toInt(pos) >= len(s) { 321 | return "err:substringOutOfRange" 322 | } 323 | switch x := toInt(pos); { 324 | case x == 0: 325 | return s 326 | case x > 0: 327 | return fmt.Sprintf("%s%s%s", s[:len(s)-x], ss, s[len(s)-x:]) 328 | case x < 0: 329 | return fmt.Sprintf("%s%s%s", s[:-x], ss, s[-x:]) 330 | default: 331 | return "inputError" 332 | } 333 | } 334 | 335 | // trim remove leading and trailing white space 336 | func trim(s string) string { 337 | return strings.TrimSpace(s) 338 | } 339 | 340 | // trimAll remove leading and trailing whitespace 341 | func trimAll(a, b string) string { return strings.Trim(a, b) } 342 | 343 | // {{trimPrefix "!Hello World!" "!"}} - returns "Hello World!" 344 | func trimPrefix(a, b string) string { return strings.TrimPrefix(a, b) } 345 | 346 | // trimSuffix - {{trimSuffix "!Hello World!" "!"}} - returns "!HelloWorld" 347 | func trimSuffix(a, b string) string { return strings.TrimSuffix(a, b) } 348 | 349 | // indexOf {{indexOf "abcd", "bc"}} -> 1 350 | // TODO: add to tests and documentation 351 | func indexOf(a, b string) int { 352 | return strings.Index(a, b) 353 | } 354 | 355 | // atoi {{atoi "42"}} - string to int 356 | func atoi(a string) int { i, _ := strconv.Atoi(a); return i } 357 | 358 | func toBool(v interface{}) bool { 359 | return cast.ToBool(v) 360 | } 361 | 362 | // toInt convert to int 363 | func toInt(v interface{}) int { 364 | return cast.ToInt(v) 365 | } 366 | 367 | func toString(v interface{}) string { 368 | return cast.ToString(v) 369 | } 370 | 371 | // toInt64 converts integer types to 64-bit integers 372 | func toInt64(v interface{}) int64 { 373 | return cast.ToInt64(v) 374 | } 375 | 376 | // toFloat64 converts 64-bit floats 377 | func toFloat64(v interface{}) float64 { 378 | return cast.ToFloat64(v) 379 | } 380 | 381 | // toDecimal input to decimal 382 | func toDecimal(i interface{}) decimal.Decimal { 383 | value, err := convertDecimal(i) 384 | if err != nil { 385 | return decimal.Zero 386 | } 387 | return value 388 | } 389 | 390 | // toDecimalString input to decimal string 391 | func toDecimalString(i interface{}) string { 392 | value, err := convertDecimal(i) 393 | if err != nil { 394 | return fmt.Sprintf("err: %s", err.Error()) 395 | } 396 | return value.String() 397 | } 398 | 399 | // toJSON convert to JSON 400 | func toJSON(data interface{}) string { 401 | out, err := json.MarshalIndent(data, "", " ") 402 | if err != nil { 403 | return fmt.Sprintf("err: %s", err.Error()) 404 | } 405 | return string(out) 406 | } 407 | 408 | // toBSON convert to BSON 409 | func toBSON(data interface{}) string { 410 | out, err := bson.Marshal(data) 411 | if err != nil { 412 | return fmt.Sprintf("err: %s", err.Error()) 413 | } 414 | return string(out) 415 | } 416 | 417 | // toYAML convert to YAML 418 | func toYAML(data interface{}) string { 419 | out, err := yaml.Marshal(data) 420 | if err != nil { 421 | return fmt.Sprintf("err: %s", err.Error()) 422 | } 423 | return string(out) 424 | } 425 | 426 | // toXML convert to XML 427 | func toXML(data interface{}) string { 428 | var err error 429 | out := []byte("\r\n") 430 | if reflect.TypeOf(data).Kind() == reflect.Slice { 431 | if reflect.TypeOf(data).Kind() == reflect.Slice { 432 | for i := 0; i < reflect.ValueOf(data).Len(); i++ { 433 | x, err := mxj.AnyXmlIndent(data.([]map[string]interface{})[i], "", " ", "record") 434 | if err != nil { 435 | return fmt.Sprintf("err: %s", err.Error()) 436 | } 437 | out = append(out, string(x)+"\r\n"...) 438 | } 439 | out = append(out, string("\r\n")...) 440 | out, err = mxj.BeautifyXml(out, "", " ") 441 | if err != nil { 442 | return fmt.Sprintf("err: %s", err.Error()) 443 | } 444 | return string(out) 445 | } 446 | 447 | } 448 | out, err = mxj.AnyXmlIndent(data, "", " ", "doc") 449 | if err != nil { 450 | return fmt.Sprintf("err: %s", err.Error()) 451 | } 452 | return string(out) 453 | } 454 | 455 | // isBool check if value is bool 456 | func isBool(i interface{}) bool { 457 | _, ok := i.(bool) 458 | return ok 459 | } 460 | 461 | // isInt check if value is int 462 | func isInt(i interface{}) bool { 463 | _, ok := i.(int) 464 | return ok 465 | } 466 | 467 | // isFloat64 check if value is float64 468 | func isFloat64(i interface{}) bool { 469 | _, ok := i.(float64) 470 | return ok 471 | } 472 | 473 | // isString check if value is string 474 | func isString(v interface{}) bool { 475 | _, ok := v.(string) 476 | return ok 477 | } 478 | 479 | // isMap check if value is map 480 | func isMap(v interface{}) bool { 481 | _, ok := v.(map[string]interface{}) 482 | return ok 483 | } 484 | 485 | // isArray check if value is array 486 | func isArray(v interface{}) bool { 487 | _, ok := v.([]interface{}) 488 | return ok 489 | } 490 | 491 | // mustArray - convert to array. Useful with XML where single record is not treated as array 492 | func mustArray(v interface{}) []interface{} { // convert to []interface{} 493 | if v == nil { 494 | return nil 495 | } 496 | if a, ok := v.([]interface{}); ok { 497 | return a 498 | } 499 | return []interface{}{v} 500 | } 501 | 502 | // mapJSON string JSON to map[string]interface{} so it can be used in pipline -> template 503 | func mapJSON(input string) map[string]interface{} { 504 | var mapData map[string]interface{} 505 | if err := json.Unmarshal([]byte(input), &mapData); err != nil { 506 | testData := make(map[string]interface{}) 507 | testData["error"] = err.Error() 508 | return testData 509 | } 510 | return mapData 511 | } 512 | 513 | // luaF Call LUA function {{lua "functionName" input1 input2 input3 ...} 514 | // 1. Functions must be placed in ./lua/functions, 2. Inputs are passed as stringified json 3. Output of lua function must be string 515 | func luaF(i ...interface{}) string { 516 | if luaData == nil { 517 | return "error: ./lua/functions.lua file missing)" 518 | } 519 | strData, err := json.Marshal(i[1:]) 520 | if err != nil { 521 | return fmt.Sprintf("luaInputError: %s\r\n", err.Error()) 522 | } 523 | if err := luaData.CallByParam( 524 | lua.P{Fn: luaData.GetGlobal(i[0].(string)), NRet: 1, Protect: true}, lua.LString(string(strData))); err != nil { 525 | return fmt.Sprintf("luaError: %s\r\n", err.Error()) 526 | } 527 | if str, ok := luaData.Get(-1).(lua.LString); ok { 528 | luaData.Pop(1) 529 | return str.String() 530 | } 531 | return "luaError: getResult" 532 | } 533 | 534 | // execDecimalOp convert float to decimal 535 | func execDecimalOp(a interface{}, b []interface{}, f func(d1, d2 decimal.Decimal) decimal.Decimal) float64 { 536 | prt := decimal.NewFromFloat(toFloat64(a)) 537 | for _, x := range b { 538 | dx := decimal.NewFromFloat(toFloat64(x)) 539 | prt = f(prt, dx) 540 | } 541 | rslt, _ := prt.Float64() 542 | return rslt 543 | } 544 | 545 | // convertDecimal converts a number to a decimal.Decimal 546 | func convertDecimal(i interface{}) (decimal.Decimal, error) { 547 | switch v := i.(type) { 548 | case decimal.Decimal: 549 | return v, nil 550 | case float64: 551 | return decimal.NewFromFloat(v), nil 552 | case int: 553 | return decimal.NewFromFloat(float64(v)), nil 554 | case int64: 555 | return decimal.NewFromFloat(float64(v)), nil 556 | case string: 557 | value, err := decimal.NewFromString(v) 558 | if err != nil { 559 | return decimal.Zero, err 560 | } 561 | return value, nil 562 | default: 563 | return decimal.Zero, fmt.Errorf("unsupported type: %T", i) 564 | } 565 | } 566 | -------------------------------------------------------------------------------- /functions_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | "testing" 9 | "text/template" 10 | "time" 11 | 12 | "github.com/shopspring/decimal" 13 | ) 14 | 15 | func runt(tpl, expect string) error { 16 | return runtv(tpl, expect, map[string]string{}) 17 | } 18 | 19 | func runtv(tpl, expect string, vars interface{}) error { 20 | t := template.Must(template.New("test").Funcs(templateFunctions()).Parse(tpl)) 21 | var b bytes.Buffer 22 | err := t.Execute(&b, vars) 23 | if err != nil { 24 | return err 25 | } 26 | if expect != b.String() { 27 | return fmt.Errorf("Expected '%s', got '%s'", expect, b.String()) 28 | } 29 | return nil 30 | } 31 | 32 | func TestTemplateFunctions(t *testing.T) { 33 | if err := runt(`{{ add "1" 2 }}`, "3"); err != nil { 34 | t.Errorf("templateError: %v", err.Error()) 35 | } 36 | } 37 | 38 | func TestAdd(t *testing.T) { 39 | result := add("6", 4) 40 | if result != 10 { 41 | t.Errorf("result: %v", result) 42 | } 43 | } 44 | func TestAdd1(t *testing.T) { 45 | result := add1("6") 46 | if result != 7 { 47 | t.Errorf("result: %v", result) 48 | } 49 | } 50 | 51 | func TestSub(t *testing.T) { 52 | result := sub("6", 2) 53 | if result != 4 { 54 | t.Errorf("result: %v", result) 55 | } 56 | } 57 | 58 | func TestDiv(t *testing.T) { 59 | result := div("6", 2) 60 | if result != 3 { 61 | t.Errorf("result: %v", result) 62 | } 63 | } 64 | 65 | func TestMod(t *testing.T) { 66 | result := mod("6", 5) 67 | if result != 1 { 68 | t.Errorf("result: %v", result) 69 | } 70 | } 71 | func TestMul(t *testing.T) { 72 | result := mul("6", 4) 73 | if result != 24 { 74 | t.Errorf("result: %v", result) 75 | } 76 | } 77 | 78 | func TestAddf(t *testing.T) { 79 | result := addf("6.14", 4.12) 80 | if result != 10.26 { 81 | t.Errorf("result: %v", result) 82 | } 83 | } 84 | 85 | func TestAdd1f(t *testing.T) { 86 | result := add1f("6.14") 87 | if result != 7.14 { 88 | t.Errorf("result: %v", result) 89 | } 90 | } 91 | 92 | func TestSubf(t *testing.T) { 93 | result := subf("6.12487", 2.347511) 94 | if result != 3.777359 { 95 | t.Errorf("result: %v", result) 96 | } 97 | } 98 | 99 | func TestDivf(t *testing.T) { 100 | result := divf("6.12487", 2.347511) 101 | if result != 2.6090910756115733 { 102 | t.Errorf("result: %v", result) 103 | } 104 | } 105 | 106 | func TestMulf(t *testing.T) { 107 | result := mulf("6.12487", 2.347511) 108 | if result != 14.37819969857 { 109 | t.Errorf("result: %v", result) 110 | } 111 | } 112 | 113 | func TestRandInt(t *testing.T) { 114 | result := randInt(50, 55) 115 | if result < 50 || result > 55 { 116 | t.Errorf("result: %v", result) 117 | } 118 | } 119 | 120 | func TestRound(t *testing.T) { 121 | if round("6.32487", 2) != 6.32 { 122 | t.Errorf("result: %v", round("6.32487", 2)) 123 | } 124 | if round("6.35", 1, 0.6) != 6.3 { 125 | t.Errorf("result: %v", round("6.35", 1, 0.6)) 126 | } 127 | if round("6.35", 1, 0.4) != 6.4 { 128 | t.Errorf("result: %v", round("6.35", 1, 0.4)) 129 | } 130 | } 131 | 132 | func TestMax(t *testing.T) { 133 | if max("6", 4, "12", "5") != 12 { 134 | t.Errorf("result: %v", max("6", 4, "12", "5")) 135 | } 136 | } 137 | 138 | func TestMin(t *testing.T) { 139 | if min("6", 4, "12", "5") != 4 { 140 | t.Errorf("result: %v", min("6", 4, "12", "5")) 141 | } 142 | } 143 | 144 | func TestMaxf(t *testing.T) { 145 | if maxf("6.32", 4.15, "12.3128", "5") != 12.3128 { 146 | t.Errorf("result: %v", maxf("6.32", 4.15, "12.3128", "5")) 147 | } 148 | } 149 | 150 | func TestMinf(t *testing.T) { 151 | if minf("6.32", 4.15, "12.3128", "5") != 4.15 { 152 | t.Errorf("result: %v", minf("6.32", 4.15, "12.3128", "5")) 153 | } 154 | } 155 | 156 | func TestDateFormat(t *testing.T) { 157 | if dateFormat("15.03.2021", "02.01.2006", "01022006") != "03152021" { 158 | t.Errorf("result: %s", dateFormat("15.03.2021", "02.01.2006", "01022006")) 159 | } 160 | if dateFormat("Hello", "World", "01022006") != "Hello" { 161 | t.Errorf("result: %s", dateFormat("Hello", "World", "01022006")) 162 | } 163 | } 164 | 165 | func TestDateFormatTZ(t *testing.T) { 166 | if dateFormatTZ("2021-08-26T03:35:00.000+04:00", "2006-01-02T15:04:05.000-07:00", "15:04", "Europe/Prague") != "01:35" { 167 | t.Errorf("result: %s", dateFormatTZ("2021-08-26T03:35:00.000+04:00", "2006-01-02T15:04:05.000-07:00", "15:04", "Europe/Prague")) 168 | } 169 | if dateFormatTZ("Hello", "World", "01022006", "42") != "err: unknownTimeZone" { 170 | t.Errorf("result: %s", dateFormatTZ("Hello", "World", "01022006", "42")) 171 | } 172 | if dateFormatTZ("Hello", "World", "01022006", "Europe/Prague") != "err: wrongFormatDefinition" { 173 | t.Errorf("result: %s", dateFormatTZ("Hello", "World", "01022006", "Europe/Prague")) 174 | } 175 | } 176 | 177 | func TestDateToInt(t *testing.T) { 178 | if dateToInt("15.03.2021", "02.01.2006") != 1615766400 { 179 | t.Errorf("result: %v", dateToInt("15.03.2021", "02.01.2006")) 180 | } 181 | } 182 | 183 | func TestIntToDate(t *testing.T) { 184 | if intToDate(1615766400, "02.01.2006") != "15.03.2021" { 185 | t.Errorf("result: %v", intToDate(1615766400, "02.01.2006")) 186 | } 187 | } 188 | 189 | func TestNow(t *testing.T) { 190 | if now("2006-01-02 15:04") != time.Now().Format("2006-01-02 15:04") { 191 | t.Errorf("result: %s", now("2006-01-02 15:04")) 192 | } 193 | } 194 | 195 | func TestBase64encode(t *testing.T) { 196 | if base64encode("Hello World!") != "SGVsbG8gV29ybGQh" { 197 | t.Errorf("result: %v", base64encode("Hello World!")) 198 | } 199 | } 200 | 201 | func TestBase64decode(t *testing.T) { 202 | if base64decode("SGVsbG8gV29ybGQh") != "Hello World!" { 203 | t.Errorf("result: %v", base64decode("SGVsbG8gV29ybGQh")) 204 | } 205 | if base64decode("Hello") != "illegal base64 data at input byte 4" { 206 | t.Errorf("result: %v", base64decode("Hello")) 207 | } 208 | } 209 | 210 | func TestBase32encode(t *testing.T) { 211 | if base32encode("Hello World!") != "JBSWY3DPEBLW64TMMQQQ====" { 212 | t.Errorf("result: %v", base32encode("Hello World!")) 213 | } 214 | } 215 | 216 | func TestBase32decode(t *testing.T) { 217 | if base32decode("JBSWY3DPEBLW64TMMQQQ====") != "Hello World!" { 218 | t.Errorf("result: %v", base32decode("JBSWY3DPEBLW64TMMQQQ====")) 219 | } 220 | if base32decode("Hello") != "illegal base32 data at input byte 1" { 221 | t.Errorf("result: %v", base32decode("Hello")) 222 | } 223 | } 224 | 225 | func TestRegexMatch(t *testing.T) { 226 | if !regexMatch("a.b", "aaxbb") { 227 | t.Errorf("result: %v", regexMatch(`^a.b$`, "aaxbb")) 228 | } 229 | } 230 | 231 | func TestContains(t *testing.T) { 232 | if contains("aaxbb", "a.b") { 233 | t.Errorf("result: %v", contains("aaxbb", "a.b")) 234 | } 235 | if !contains("aaxbb", "ax") { 236 | t.Errorf("result: %v", contains("aaxbb", "a.b")) 237 | } 238 | } 239 | 240 | func TestReplaceAll(t *testing.T) { 241 | if replaceAll("x", "Z", "aaxbb") != "aaZbb" { 242 | t.Errorf("result: %v", replaceAll("aaxbb", "x", "Z")) 243 | } 244 | } 245 | 246 | func TestReplaceAllRegex(t *testing.T) { 247 | if replaceAllRegex("[a-d]", "Z", "aaxbb") != "ZZxZZ" { 248 | t.Errorf("result: %v", replaceAllRegex("aaxbb", `[a-d]`, "Z")) 249 | } 250 | } 251 | 252 | func TestUUID(t *testing.T) { 253 | testUUID := newUUID() 254 | r := regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$") 255 | if !r.MatchString(testUUID) { 256 | t.Errorf("result: %v", testUUID) 257 | } 258 | } 259 | 260 | func TestUpper(t *testing.T) { 261 | if upper("Hello") != "HELLO" { 262 | t.Errorf("result: %v", upper("Hello")) 263 | } 264 | } 265 | 266 | func TestLower(t *testing.T) { 267 | if lower("World") != "world" { 268 | t.Errorf("result: %v", lower("World")) 269 | } 270 | } 271 | 272 | func TestAddSubstring(t *testing.T) { 273 | if addSubstring("Hello!!!", " World", "3") != "Hello World!!!" { 274 | t.Errorf("result: %v", addSubstring("Hello!!!", " World", "3")) 275 | } 276 | if addSubstring("Hello!!!", " World", "-5") != "Hello World!!!" { 277 | t.Errorf("result: %v", addSubstring("Hello!!!", " World", "-5")) 278 | } 279 | if addSubstring("Hello!!!", " World", "0") != "Hello!!!" { 280 | t.Errorf("result: %v", addSubstring("Hello!!!", " World", "0")) 281 | } 282 | if addSubstring("Hello!!!", " World", "15") != "err:substringOutOfRange" { 283 | t.Errorf("result: %v", addSubstring("Hello!!!", " World", "15")) 284 | } 285 | } 286 | 287 | func TestTrim(t *testing.T) { 288 | result := trim("\r\nHello World\r\n") 289 | if result != "Hello World" { 290 | t.Errorf("result: %v", result) 291 | } 292 | } 293 | 294 | func TestTrimAll(t *testing.T) { 295 | result := trimAll("!Hello World!", "!") 296 | if result != "Hello World" { 297 | t.Errorf("result: %v", result) 298 | } 299 | } 300 | 301 | func TestPrefix(t *testing.T) { 302 | result := trimPrefix("!Hello World!", "!") 303 | if result != "Hello World!" { 304 | t.Errorf("result: %v", result) 305 | } 306 | } 307 | 308 | func TestSuffix(t *testing.T) { 309 | result := trimSuffix("!Hello World!", "!") 310 | if result != "!Hello World" { 311 | t.Errorf("result: %v", result) 312 | } 313 | } 314 | 315 | func TestAtoi(t *testing.T) { 316 | result := atoi("42") 317 | if result != 42 { 318 | t.Errorf("result: %v", result) 319 | } 320 | } 321 | 322 | func TestToBool(t *testing.T) { 323 | result := toBool("true") 324 | if !result { 325 | t.Errorf("result: %v", result) 326 | } 327 | result = toBool("false") 328 | if result { 329 | t.Errorf("result: %v", result) 330 | } 331 | } 332 | 333 | func TestToString(t *testing.T) { 334 | result := toString(42) 335 | if result != "42" { 336 | t.Errorf("result: %v", result) 337 | } 338 | } 339 | 340 | func TestToInt(t *testing.T) { 341 | if toInt("42") != 42 { 342 | t.Errorf("result: %d", toInt("42")) 343 | } 344 | } 345 | 346 | func TestToInt64(t *testing.T) { 347 | if toInt64("42") != 42 { 348 | t.Errorf("result: %d", toInt64("42")) 349 | } 350 | } 351 | 352 | func TestToFloat64(t *testing.T) { 353 | if toFloat64("1234567.151234") != 1234567.151234 { 354 | t.Errorf("result: %v", toFloat64("1234567.151234")) 355 | } 356 | } 357 | 358 | func TestToDecimal(t *testing.T) { 359 | x, _ := decimal.NewFromString("1234567.151234") 360 | if !x.Equal(toDecimal("1234567.151234")) { 361 | t.Errorf("result: %v", toDecimal("1234567.151234")) 362 | } 363 | x, _ = decimal.NewFromString("0") 364 | if !x.Equal(toDecimal("1234567.151234a")) { 365 | t.Errorf("result: %v", toDecimal("1234567.151234a")) 366 | } 367 | } 368 | 369 | func TestToDecimalString(t *testing.T) { 370 | if toDecimalString("1234567.151234a") != "err: can't convert 1234567.151234a to decimal" { 371 | t.Errorf("result: %v", toDecimalString("1234567.151234a")) 372 | } 373 | if toDecimalString("1234567.151234") != "1234567.151234" { 374 | t.Errorf("result: %v", toDecimalString("1234567.151234")) 375 | } 376 | } 377 | 378 | func TestLuaF(t *testing.T) { 379 | if luaF("sum", "5", "5") != "10" { 380 | t.Errorf("result: %s", luaF("sum", "5", "5")) 381 | } 382 | if !strings.Contains(luaF("Unknown", "5", "5"), `attempt to call a non-function object`) { 383 | t.Errorf("result: %s", luaF("Unknown", "5", "5")) 384 | } 385 | testData := make(map[string]interface{}) 386 | testData["Hello"] = make(chan int) 387 | if !strings.Contains(luaF("sum", testData), "luaInputError: json: unsupported type: chan int") { 388 | t.Errorf("result: %v", luaF("sum", testData)) 389 | } 390 | } 391 | 392 | func TestToJSON(t *testing.T) { 393 | testData := make(map[string]interface{}) 394 | testData["Hello"] = "World" 395 | result := toJSON(testData) 396 | if !strings.Contains(result, `"Hello": "World"`) { 397 | t.Errorf("result: %v", result) 398 | } 399 | testData["Hello"] = make(chan int) 400 | result = toJSON(testData) 401 | if result != "err: json: unsupported type: chan int" { 402 | t.Errorf("result: %v", result) 403 | } 404 | } 405 | 406 | func TestToBSON(t *testing.T) { 407 | testData := make(map[string]interface{}) 408 | testData["h"] = "w" 409 | result := toBSON(testData) 410 | if result != string([]byte{14, 0, 0, 0, 2, 104, 0, 2, 0, 0, 0, 119, 0, 0}) { 411 | t.Errorf("result: %v", []byte(result)) 412 | } 413 | testData["Hello"] = make(chan int) 414 | result = toBSON(testData) 415 | if result != "err: no encoder found for chan int" { 416 | t.Errorf("result: %v", result) 417 | } 418 | } 419 | 420 | func TestToYAML(t *testing.T) { 421 | testData := make(map[string]interface{}) 422 | testData["Hello"] = "World" 423 | result := toYAML(testData) 424 | if result != `Hello: World 425 | ` { 426 | t.Errorf("result: %v", result) 427 | } 428 | } 429 | 430 | func TestToXML(t *testing.T) { 431 | testData := make(map[string]interface{}) 432 | testData["Hello"] = "World" 433 | result := toXML(testData) 434 | if !strings.Contains(result, "World") { 435 | t.Errorf("result: %v", result) 436 | } 437 | testData2 := make([]map[string]interface{}, 1) 438 | testData2[0] = make(map[string]interface{}) 439 | testData2[0]["Hello"] = "World" 440 | result = toXML(testData2) 441 | if !strings.Contains(result, "World") { 442 | t.Errorf("result: %v", result) 443 | } 444 | 445 | } 446 | 447 | func TestIsBool(t *testing.T) { 448 | if !isBool(true) { 449 | t.Errorf("result: %v", true) 450 | } 451 | if isBool("false") { 452 | t.Errorf("result: %v", false) 453 | } 454 | } 455 | 456 | func TestIsInt(t *testing.T) { 457 | if !isInt(42) { 458 | t.Errorf("result: %v", true) 459 | } 460 | if isInt("42") { 461 | t.Errorf("result: %v", false) 462 | } 463 | } 464 | 465 | func TestIsFloat64(t *testing.T) { 466 | if !isFloat64(42.0) { 467 | t.Errorf("result: %v", true) 468 | } 469 | if isFloat64("42.0") { 470 | t.Errorf("result: %v", false) 471 | } 472 | } 473 | 474 | func TestIsString(t *testing.T) { 475 | if !isString("Hello") { 476 | t.Errorf("result: %v", true) 477 | } 478 | if isString(42) { 479 | t.Errorf("result: %v", false) 480 | } 481 | } 482 | 483 | func TestIsMap(t *testing.T) { 484 | if !isMap(map[string]interface{}{"Hello": "World"}) { 485 | t.Errorf("result: %v", true) 486 | } 487 | if isMap(42) { 488 | t.Errorf("result: %v", false) 489 | } 490 | } 491 | 492 | func TestIsArray(t *testing.T) { 493 | if !isArray([]interface{}{"Hello", "World"}) { 494 | t.Errorf("result: %v", true) 495 | } 496 | if isArray(42) { 497 | t.Errorf("result: %v", false) 498 | } 499 | } 500 | 501 | func TestMustArray(t *testing.T) { 502 | if !isArray(mustArray(nil)) { 503 | t.Errorf("result: %v", true) 504 | } 505 | if !isArray(mustArray([]interface{}{"Hello", "World"})) { 506 | t.Errorf("result: %v", true) 507 | } 508 | if !isArray(mustArray("Hello")) { 509 | t.Errorf("result: %v", false) 510 | } 511 | } 512 | 513 | func TestMapJSON(t *testing.T) { 514 | testData := "{\"Hello\":\"World\"}" 515 | result := mapJSON(testData) 516 | if result["Hello"] != "World" { 517 | t.Errorf("result: %v", result["Hello"]) 518 | } 519 | testData = "{\"Hello\" World\"}" 520 | result = mapJSON(testData) 521 | if !strings.Contains(result["error"].(string), `invalid character 'W'`) { 522 | t.Errorf("result: %v", result) 523 | } 524 | } 525 | 526 | func TestExecDecimalOp(t *testing.T) { 527 | testMulf := func(a interface{}, v ...interface{}) float64 { 528 | return execDecimalOp(a, v, func(d1, d2 decimal.Decimal) decimal.Decimal { return d1.Mul(d2) }) 529 | } 530 | if testMulf(6.2154, "4.35") != 27.03699 { 531 | t.Errorf("result: %v", testMulf(6.2154, "4.35")) 532 | } 533 | testDivf := func(a interface{}, v ...interface{}) float64 { 534 | return execDecimalOp(a, v, func(d1, d2 decimal.Decimal) decimal.Decimal { return d1.Div(d2) }) 535 | } 536 | if testDivf(6.2154, "4.35") != 1.4288275862068966 { 537 | t.Errorf("result: %v", testDivf(6.2154, "4.35")) 538 | } 539 | } 540 | 541 | func TestConvertDecimal(t *testing.T) { 542 | result, err := convertDecimal(decimal.RequireFromString("457842.123845")) 543 | if err != nil { 544 | t.Errorf("result: %v", err) 545 | } 546 | if result.String() != "457842.123845" { 547 | t.Errorf("result: %v", result) 548 | } 549 | _, err = convertDecimal("457842.123845a") 550 | if err.Error() != "can't convert 457842.123845a to decimal" { 551 | t.Errorf("result: %v", err) 552 | } 553 | result, err = convertDecimal("457842.123845") 554 | if err != nil { 555 | t.Errorf("result: %v", err) 556 | } 557 | if result.String() != "457842.123845" { 558 | t.Errorf("result: %v", result) 559 | } 560 | result, err = convertDecimal(42) 561 | if err != nil { 562 | t.Errorf("result: %v", err) 563 | } 564 | if result.String() != "42" { 565 | t.Errorf("result: %v", result) 566 | } 567 | result, err = convertDecimal(457842.123845) 568 | if err != nil { 569 | t.Errorf("result: %v", err) 570 | } 571 | if result.String() != "457842.123845" { 572 | t.Errorf("result: %v", result) 573 | } 574 | 575 | } 576 | 577 | // go test -coverprofile cover.out 578 | // go tool cover -html='cover.out' 579 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mmalcek/bafi 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/clbanning/mxj/v2 v2.7.0 7 | github.com/google/uuid v1.6.0 8 | github.com/mmalcek/mt940 v0.1.1 9 | github.com/sashabaranov/go-openai v1.38.0 10 | github.com/shopspring/decimal v1.4.0 11 | github.com/spf13/cast v1.7.1 12 | github.com/yuin/gopher-lua v1.1.1 13 | go.mongodb.org/mongo-driver v1.17.3 14 | gopkg.in/yaml.v3 v3.0.1 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= 2 | github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 6 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 7 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 8 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 9 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 10 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 11 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 12 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 13 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 14 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 15 | github.com/mmalcek/mt940 v0.1.1 h1:w0LYJk4nQWnMeTtuLL1dY2oNMdtHTnWNVY+y7k0KMQU= 16 | github.com/mmalcek/mt940 v0.1.1/go.mod h1:IzQU3xpykKw6QEHn0i75Xxds7eapEEmwYn5L4B28ZZ8= 17 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 18 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 19 | github.com/sashabaranov/go-openai v1.20.2 h1:nilzF2EKzaHyK4Rk2Dbu/aJEZbtIvskDIXvfS4yx+6M= 20 | github.com/sashabaranov/go-openai v1.20.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= 21 | github.com/sashabaranov/go-openai v1.38.0 h1:hNN5uolKwdbpiqOn7l+Z2alch/0n0rSFyg4n+GZxR5k= 22 | github.com/sashabaranov/go-openai v1.38.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= 23 | github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= 24 | github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 25 | github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= 26 | github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 27 | github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= 28 | github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 29 | github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= 30 | github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 31 | github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= 32 | github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= 33 | go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= 34 | go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= 35 | go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ= 36 | go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= 37 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 38 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 39 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 40 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 41 | -------------------------------------------------------------------------------- /lua/functions.lua: -------------------------------------------------------------------------------- 1 | json = require './lua/json' 2 | 3 | function sum(incomingData) 4 | dataTable = json.decode(incomingData) 5 | return tostring(tonumber(dataTable[1]) + tonumber(dataTable[2])) 6 | end 7 | 8 | function mul(incomingData) 9 | dataTable = json.decode(incomingData) 10 | return tostring(tonumber(dataTable[1]) * tonumber(dataTable[2])) 11 | end -------------------------------------------------------------------------------- /lua/json.lua: -------------------------------------------------------------------------------- 1 | -- (https://github.com/rxi/json.lua) 2 | -- 3 | -- json.lua 4 | -- 5 | -- Copyright (c) 2020 rxi 6 | -- 7 | -- Permission is hereby granted, free of charge, to any person obtaining a copy of 8 | -- this software and associated documentation files (the "Software"), to deal in 9 | -- the Software without restriction, including without limitation the rights to 10 | -- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 11 | -- of the Software, and to permit persons to whom the Software is furnished to do 12 | -- so, subject to the following conditions: 13 | -- 14 | -- The above copyright notice and this permission notice shall be included in all 15 | -- copies or substantial portions of the Software. 16 | -- 17 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | -- SOFTWARE. 24 | -- 25 | 26 | local json = { _version = "0.1.2" } 27 | 28 | ------------------------------------------------------------------------------- 29 | -- Encode 30 | ------------------------------------------------------------------------------- 31 | 32 | local encode 33 | 34 | local escape_char_map = { 35 | [ "\\" ] = "\\", 36 | [ "\"" ] = "\"", 37 | [ "\b" ] = "b", 38 | [ "\f" ] = "f", 39 | [ "\n" ] = "n", 40 | [ "\r" ] = "r", 41 | [ "\t" ] = "t", 42 | } 43 | 44 | local escape_char_map_inv = { [ "/" ] = "/" } 45 | for k, v in pairs(escape_char_map) do 46 | escape_char_map_inv[v] = k 47 | end 48 | 49 | 50 | local function escape_char(c) 51 | return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) 52 | end 53 | 54 | 55 | local function encode_nil(val) 56 | return "null" 57 | end 58 | 59 | 60 | local function encode_table(val, stack) 61 | local res = {} 62 | stack = stack or {} 63 | 64 | -- Circular reference? 65 | if stack[val] then error("circular reference") end 66 | 67 | stack[val] = true 68 | 69 | if rawget(val, 1) ~= nil or next(val) == nil then 70 | -- Treat as array -- check keys are valid and it is not sparse 71 | local n = 0 72 | for k in pairs(val) do 73 | if type(k) ~= "number" then 74 | error("invalid table: mixed or invalid key types") 75 | end 76 | n = n + 1 77 | end 78 | if n ~= #val then 79 | error("invalid table: sparse array") 80 | end 81 | -- Encode 82 | for i, v in ipairs(val) do 83 | table.insert(res, encode(v, stack)) 84 | end 85 | stack[val] = nil 86 | return "[" .. table.concat(res, ",") .. "]" 87 | 88 | else 89 | -- Treat as an object 90 | for k, v in pairs(val) do 91 | if type(k) ~= "string" then 92 | error("invalid table: mixed or invalid key types") 93 | end 94 | table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) 95 | end 96 | stack[val] = nil 97 | return "{" .. table.concat(res, ",") .. "}" 98 | end 99 | end 100 | 101 | 102 | local function encode_string(val) 103 | return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' 104 | end 105 | 106 | 107 | local function encode_number(val) 108 | -- Check for NaN, -inf and inf 109 | if val ~= val or val <= -math.huge or val >= math.huge then 110 | error("unexpected number value '" .. tostring(val) .. "'") 111 | end 112 | return string.format("%.14g", val) 113 | end 114 | 115 | 116 | local type_func_map = { 117 | [ "nil" ] = encode_nil, 118 | [ "table" ] = encode_table, 119 | [ "string" ] = encode_string, 120 | [ "number" ] = encode_number, 121 | [ "boolean" ] = tostring, 122 | } 123 | 124 | 125 | encode = function(val, stack) 126 | local t = type(val) 127 | local f = type_func_map[t] 128 | if f then 129 | return f(val, stack) 130 | end 131 | error("unexpected type '" .. t .. "'") 132 | end 133 | 134 | 135 | function json.encode(val) 136 | return ( encode(val) ) 137 | end 138 | 139 | 140 | ------------------------------------------------------------------------------- 141 | -- Decode 142 | ------------------------------------------------------------------------------- 143 | 144 | local parse 145 | 146 | local function create_set(...) 147 | local res = {} 148 | for i = 1, select("#", ...) do 149 | res[ select(i, ...) ] = true 150 | end 151 | return res 152 | end 153 | 154 | local space_chars = create_set(" ", "\t", "\r", "\n") 155 | local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") 156 | local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") 157 | local literals = create_set("true", "false", "null") 158 | 159 | local literal_map = { 160 | [ "true" ] = true, 161 | [ "false" ] = false, 162 | [ "null" ] = nil, 163 | } 164 | 165 | 166 | local function next_char(str, idx, set, negate) 167 | for i = idx, #str do 168 | if set[str:sub(i, i)] ~= negate then 169 | return i 170 | end 171 | end 172 | return #str + 1 173 | end 174 | 175 | 176 | local function decode_error(str, idx, msg) 177 | local line_count = 1 178 | local col_count = 1 179 | for i = 1, idx - 1 do 180 | col_count = col_count + 1 181 | if str:sub(i, i) == "\n" then 182 | line_count = line_count + 1 183 | col_count = 1 184 | end 185 | end 186 | error( string.format("%s at line %d col %d", msg, line_count, col_count) ) 187 | end 188 | 189 | 190 | local function codepoint_to_utf8(n) 191 | -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa 192 | local f = math.floor 193 | if n <= 0x7f then 194 | return string.char(n) 195 | elseif n <= 0x7ff then 196 | return string.char(f(n / 64) + 192, n % 64 + 128) 197 | elseif n <= 0xffff then 198 | return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) 199 | elseif n <= 0x10ffff then 200 | return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, 201 | f(n % 4096 / 64) + 128, n % 64 + 128) 202 | end 203 | error( string.format("invalid unicode codepoint '%x'", n) ) 204 | end 205 | 206 | 207 | local function parse_unicode_escape(s) 208 | local n1 = tonumber( s:sub(1, 4), 16 ) 209 | local n2 = tonumber( s:sub(7, 10), 16 ) 210 | -- Surrogate pair? 211 | if n2 then 212 | return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) 213 | else 214 | return codepoint_to_utf8(n1) 215 | end 216 | end 217 | 218 | 219 | local function parse_string(str, i) 220 | local res = "" 221 | local j = i + 1 222 | local k = j 223 | 224 | while j <= #str do 225 | local x = str:byte(j) 226 | 227 | if x < 32 then 228 | decode_error(str, j, "control character in string") 229 | 230 | elseif x == 92 then -- `\`: Escape 231 | res = res .. str:sub(k, j - 1) 232 | j = j + 1 233 | local c = str:sub(j, j) 234 | if c == "u" then 235 | local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) 236 | or str:match("^%x%x%x%x", j + 1) 237 | or decode_error(str, j - 1, "invalid unicode escape in string") 238 | res = res .. parse_unicode_escape(hex) 239 | j = j + #hex 240 | else 241 | if not escape_chars[c] then 242 | decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") 243 | end 244 | res = res .. escape_char_map_inv[c] 245 | end 246 | k = j + 1 247 | 248 | elseif x == 34 then -- `"`: End of string 249 | res = res .. str:sub(k, j - 1) 250 | return res, j + 1 251 | end 252 | 253 | j = j + 1 254 | end 255 | 256 | decode_error(str, i, "expected closing quote for string") 257 | end 258 | 259 | 260 | local function parse_number(str, i) 261 | local x = next_char(str, i, delim_chars) 262 | local s = str:sub(i, x - 1) 263 | local n = tonumber(s) 264 | if not n then 265 | decode_error(str, i, "invalid number '" .. s .. "'") 266 | end 267 | return n, x 268 | end 269 | 270 | 271 | local function parse_literal(str, i) 272 | local x = next_char(str, i, delim_chars) 273 | local word = str:sub(i, x - 1) 274 | if not literals[word] then 275 | decode_error(str, i, "invalid literal '" .. word .. "'") 276 | end 277 | return literal_map[word], x 278 | end 279 | 280 | 281 | local function parse_array(str, i) 282 | local res = {} 283 | local n = 1 284 | i = i + 1 285 | while 1 do 286 | local x 287 | i = next_char(str, i, space_chars, true) 288 | -- Empty / end of array? 289 | if str:sub(i, i) == "]" then 290 | i = i + 1 291 | break 292 | end 293 | -- Read token 294 | x, i = parse(str, i) 295 | res[n] = x 296 | n = n + 1 297 | -- Next token 298 | i = next_char(str, i, space_chars, true) 299 | local chr = str:sub(i, i) 300 | i = i + 1 301 | if chr == "]" then break end 302 | if chr ~= "," then decode_error(str, i, "expected ']' or ','") end 303 | end 304 | return res, i 305 | end 306 | 307 | 308 | local function parse_object(str, i) 309 | local res = {} 310 | i = i + 1 311 | while 1 do 312 | local key, val 313 | i = next_char(str, i, space_chars, true) 314 | -- Empty / end of object? 315 | if str:sub(i, i) == "}" then 316 | i = i + 1 317 | break 318 | end 319 | -- Read key 320 | if str:sub(i, i) ~= '"' then 321 | decode_error(str, i, "expected string for key") 322 | end 323 | key, i = parse(str, i) 324 | -- Read ':' delimiter 325 | i = next_char(str, i, space_chars, true) 326 | if str:sub(i, i) ~= ":" then 327 | decode_error(str, i, "expected ':' after key") 328 | end 329 | i = next_char(str, i + 1, space_chars, true) 330 | -- Read value 331 | val, i = parse(str, i) 332 | -- Set 333 | res[key] = val 334 | -- Next token 335 | i = next_char(str, i, space_chars, true) 336 | local chr = str:sub(i, i) 337 | i = i + 1 338 | if chr == "}" then break end 339 | if chr ~= "," then decode_error(str, i, "expected '}' or ','") end 340 | end 341 | return res, i 342 | end 343 | 344 | 345 | local char_func_map = { 346 | [ '"' ] = parse_string, 347 | [ "0" ] = parse_number, 348 | [ "1" ] = parse_number, 349 | [ "2" ] = parse_number, 350 | [ "3" ] = parse_number, 351 | [ "4" ] = parse_number, 352 | [ "5" ] = parse_number, 353 | [ "6" ] = parse_number, 354 | [ "7" ] = parse_number, 355 | [ "8" ] = parse_number, 356 | [ "9" ] = parse_number, 357 | [ "-" ] = parse_number, 358 | [ "t" ] = parse_literal, 359 | [ "f" ] = parse_literal, 360 | [ "n" ] = parse_literal, 361 | [ "[" ] = parse_array, 362 | [ "{" ] = parse_object, 363 | } 364 | 365 | 366 | parse = function(str, idx) 367 | local chr = str:sub(idx, idx) 368 | local f = char_func_map[chr] 369 | if f then 370 | return f(str, idx) 371 | end 372 | decode_error(str, idx, "unexpected character '" .. chr .. "'") 373 | end 374 | 375 | 376 | function json.decode(str) 377 | if type(str) ~= "string" then 378 | error("expected argument of type string, got " .. type(str)) 379 | end 380 | local res, idx = parse(str, next_char(str, 1, space_chars, true)) 381 | idx = next_char(str, idx, space_chars, true) 382 | if idx <= #str then 383 | decode_error(str, idx, "trailing garbage") 384 | end 385 | return res 386 | end 387 | 388 | 389 | return json 390 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/csv" 7 | "encoding/hex" 8 | "encoding/json" 9 | "flag" 10 | "fmt" 11 | "io" 12 | "log" 13 | "math/rand" 14 | "os" 15 | "path/filepath" 16 | "strings" 17 | "text/template" 18 | "time" 19 | 20 | "github.com/clbanning/mxj/v2" 21 | "github.com/mmalcek/mt940" 22 | "github.com/sashabaranov/go-openai" 23 | lua "github.com/yuin/gopher-lua" 24 | "go.mongodb.org/mongo-driver/bson" 25 | "gopkg.in/yaml.v3" 26 | ) 27 | 28 | const version = "1.2.1" 29 | 30 | var ( 31 | luaData *lua.LState 32 | ) 33 | 34 | type tParams struct { 35 | inputFile *string 36 | outputFile *string 37 | textTemplate *string 38 | inputFormat *string 39 | inputDelimiter *string 40 | getVersion *bool 41 | getHelp *bool 42 | chatGPTkey *string 43 | chatGPTmodel *string 44 | chatGPTquery *string 45 | } 46 | 47 | func init() { 48 | rand.Seed(time.Now().UTC().UnixNano()) 49 | if _, err := os.Stat("./lua/functions.lua"); !os.IsNotExist(err) { 50 | luaData = lua.NewState() 51 | if err := luaData.DoFile("./lua/functions.lua"); err != nil { 52 | log.Fatal("loadLuaFunctions", err.Error()) 53 | } 54 | } 55 | } 56 | 57 | func main() { 58 | params := tParams{ 59 | inputFile: flag.String("i", "", `input file 60 | -if not defined read from stdin (pipe mode) 61 | -if prefixed with "?" app will expect yaml file with multiple files description. `), 62 | outputFile: flag.String("o", "", `output file, 63 | -if not defined write to stdout (pipe mode)`), 64 | textTemplate: flag.String("t", "", `template, file or inline. 65 | -Inline template should start with ? e.g. -t "?{{.MyValue}}" `), 66 | inputFormat: flag.String("f", "", "input format: json, bson, yaml, csv, mt940, xml(default)"), 67 | inputDelimiter: flag.String("d", "", "input delimiter: CSV only, default is comma -d ';' or -d 0x09"), 68 | getVersion: flag.Bool("v", false, "show version (Project page: https://github.com/mmalcek/bafi)"), 69 | getHelp: flag.Bool("h", false, "show help"), 70 | chatGPTkey: flag.String("gk", "", "OpenAI API key"), 71 | chatGPTmodel: flag.String("gm", "gpt35", "OpenAI GPT-3 model (gpt35, gpt4)"), 72 | chatGPTquery: flag.String("gq", "", "OpenAI query"), 73 | } 74 | flag.Parse() 75 | 76 | if err := processTemplate(params); err != nil { 77 | log.Fatal(err.Error()) 78 | } 79 | if luaData != nil { 80 | luaData.Close() 81 | } 82 | } 83 | 84 | func processTemplate(params tParams) error { 85 | if *params.getVersion { 86 | fmt.Printf("Version: %s\r\nProject page: https://github.com/mmalcek/bafi\r\n", version) 87 | return nil 88 | } 89 | if *params.getHelp { 90 | fmt.Println("Usage: bafi -i input.json -t template.tmpl -o output.txt") 91 | flag.PrintDefaults() 92 | return nil 93 | } 94 | if *params.textTemplate == "" && *params.chatGPTkey == "" { 95 | fmt.Println("template file must be defined: -t template.tmpl") 96 | return nil 97 | } 98 | data, files, err := getInputData(params.inputFile) 99 | if err != nil { 100 | return err 101 | } 102 | // Try identify file format by extension. Input parameter -f has priority 103 | if *params.inputFormat == "" { 104 | switch strings.ToLower(filepath.Ext(*params.inputFile)) { 105 | case ".json": 106 | *params.inputFormat = "json" 107 | case ".bson": 108 | *params.inputFormat = "bson" 109 | case ".yaml", ".yml": 110 | *params.inputFormat = "yaml" 111 | case ".csv": 112 | *params.inputFormat = "csv" 113 | case ".sta": 114 | *params.inputFormat = "mt940" 115 | case ".xml", ".cdf", ".cdf3": 116 | *params.inputFormat = "xml" 117 | default: 118 | *params.inputFormat = "" 119 | } 120 | } 121 | 122 | // If list of file map them one by one else map incoming []byte to mapData 123 | var mapData interface{} 124 | if data == nil && files != nil { 125 | filesStruct := make(map[string]interface{}) 126 | for _, file := range files { 127 | data, err := os.ReadFile(file["file"].(string)) 128 | if err != nil { 129 | return err 130 | } 131 | *params.inputFormat = file["format"].(string) 132 | if filesStruct[file["label"].(string)], err = mapInputData(data, params); err != nil { 133 | return err 134 | } 135 | } 136 | mapData = &filesStruct 137 | } else { 138 | if mapData, err = mapInputData(data, params); err != nil { 139 | return err 140 | } 141 | } 142 | 143 | if *params.chatGPTkey != "" { 144 | if *params.chatGPTquery == "" { 145 | fmt.Println("OpenAI query must be defined: -gq \"What is the weather like?\"") 146 | return nil 147 | } 148 | response, err := chatGPTprocess(mapData, params) 149 | if err != nil { 150 | return err 151 | } 152 | if *params.outputFile == "" { 153 | fmt.Println(response.Choices[0].Message.Content) 154 | } else { 155 | output, err := os.Create(*params.outputFile) 156 | if err != nil { 157 | return fmt.Errorf("createOutputFile: %s", err.Error()) 158 | } 159 | defer output.Close() 160 | output.WriteString(response.Choices[0].Message.Content) 161 | } 162 | return nil 163 | } 164 | 165 | templateFile, err := readTemplate(*params.textTemplate) 166 | if err != nil { 167 | return err 168 | } 169 | if err := writeOutputData(mapData, params.outputFile, templateFile); err != nil { 170 | return err 171 | } 172 | return nil 173 | } 174 | 175 | // getInputData get the data from stdin/pipe or from file or forward list of multiple input files 176 | func getInputData(input *string) (data []byte, files []map[string]interface{}, errorMsg error) { 177 | var err error 178 | inputFile := *input 179 | switch { 180 | case inputFile == "": 181 | fi, err := os.Stdin.Stat() 182 | if err != nil { 183 | return nil, nil, fmt.Errorf("getStdin: %s", err.Error()) 184 | } 185 | if fi.Mode()&os.ModeNamedPipe == 0 { 186 | return nil, nil, fmt.Errorf("stdin: Error-noPipe") 187 | } 188 | if data, err = io.ReadAll(os.Stdin); err != nil { 189 | return nil, nil, fmt.Errorf("readStdin: %s", err.Error()) 190 | } 191 | case inputFile[:1] == "?": 192 | files = make([]map[string]interface{}, 0) 193 | configFile, err := os.ReadFile(inputFile[1:]) 194 | if err != nil { 195 | return nil, nil, fmt.Errorf("readFileList: %s", err.Error()) 196 | } 197 | if err := yaml.Unmarshal(configFile, &files); err != nil { 198 | return nil, nil, fmt.Errorf("yaml.UnmarshalFileList: %s", err.Error()) 199 | } 200 | return nil, files, nil 201 | default: 202 | if data, err = os.ReadFile(inputFile); err != nil { 203 | return nil, nil, fmt.Errorf("readFile: %s", err.Error()) 204 | } 205 | } 206 | return cleanBOM(data), nil, nil 207 | } 208 | 209 | // mapInputData map input data to map[string]interface{} 210 | func mapInputData(data []byte, params tParams) (interface{}, error) { 211 | switch strings.ToLower(*params.inputFormat) { 212 | case "json": 213 | var mapData map[string]interface{} 214 | if err := json.Unmarshal(data, &mapData); err != nil { 215 | if strings.Contains(err.Error(), "cannot unmarshal array") { 216 | mapDataArray := make([]map[string]interface{}, 0) 217 | if err := json.Unmarshal(data, &mapDataArray); err != nil { 218 | return nil, fmt.Errorf("jsonArray: %s", err.Error()) 219 | } 220 | return mapDataArray, nil 221 | } 222 | return nil, fmt.Errorf("mapJSON: %s", err.Error()) 223 | } 224 | return mapData, nil 225 | case "bson": 226 | var mapData map[string]interface{} 227 | if err := bson.Unmarshal(data, &mapData); err != nil { 228 | // If error try parse as mongoDump 229 | if strings.Contains(err.Error(), "invalid document length") { 230 | var rawData bson.Raw 231 | mapDataArray := make([]map[string]interface{}, 0) 232 | i := 0 233 | for len(data) > 0 { 234 | var x map[string]interface{} 235 | if err := bson.Unmarshal(data, &rawData); err != nil { 236 | return nil, fmt.Errorf("mapBSONArray1: %s", err.Error()) 237 | } 238 | if err := bson.Unmarshal(rawData, &x); err != nil { 239 | return nil, fmt.Errorf("mapBSONArray2: %s", err.Error()) 240 | } 241 | mapDataArray = append(mapDataArray, x) 242 | data = data[len(rawData):] 243 | i++ 244 | } 245 | return mapDataArray, nil 246 | } 247 | return nil, fmt.Errorf("mapBSON: %s", err.Error()) 248 | } 249 | return mapData, nil 250 | case "yaml": 251 | var mapData map[string]interface{} 252 | if err := yaml.Unmarshal(data, &mapData); err != nil { 253 | if strings.Contains(err.Error(), "cannot unmarshal !!") { 254 | mapDataArray := make([]map[string]interface{}, 0) 255 | if err := yaml.Unmarshal(data, &mapDataArray); err != nil { 256 | return nil, fmt.Errorf("yamlArray: %s", err.Error()) 257 | } 258 | return mapDataArray, nil 259 | } 260 | return nil, fmt.Errorf("mapYAML: %s", err.Error()) 261 | } 262 | return mapData, nil 263 | case "csv": 264 | var mapData []map[string]interface{} 265 | r := csv.NewReader(strings.NewReader(string(data))) 266 | r.Comma = prepareDelimiter(*params.inputDelimiter) 267 | lines, err := r.ReadAll() 268 | if err != nil { 269 | return nil, fmt.Errorf("mapCSV: %s", err.Error()) 270 | } 271 | mapData = make([]map[string]interface{}, len(lines[1:])) 272 | headers := make([]string, len(lines[0])) 273 | copy(headers, lines[0]) 274 | for i, line := range lines[1:] { 275 | x := make(map[string]interface{}) 276 | for j, value := range line { 277 | x[headers[j]] = value 278 | } 279 | mapData[i] = x 280 | } 281 | return mapData, nil 282 | case "xml": 283 | mapData, err := mxj.NewMapXml(data) 284 | if err != nil { 285 | return nil, fmt.Errorf("mapXML: %s", err.Error()) 286 | } 287 | return mapData, nil 288 | case "mt940": 289 | if *params.inputDelimiter == "" { 290 | return mt940.Parse(data) 291 | } else { 292 | *params.inputDelimiter = strings.Replace(*params.inputDelimiter, `\r`, "\r", -1) 293 | *params.inputDelimiter = strings.Replace(*params.inputDelimiter, `\n`, "\n", -1) 294 | return mt940.ParseMultimessage(data, *params.inputDelimiter) 295 | } 296 | default: 297 | return nil, fmt.Errorf("unknown input format: use parameter -f to define input format e.g. -f json (accepted values are json, bson, yaml, csv, mt940, xml)") 298 | } 299 | } 300 | 301 | // Delimiter can be defined as string or as HEX value eg. 0x09 302 | func prepareDelimiter(inputString string) rune { 303 | if inputString != "" { 304 | if len(inputString) == 4 && inputString[0:2] == "0x" { 305 | bytes, err := hex.DecodeString(inputString[2:4]) 306 | if err != nil { 307 | log.Fatalf(fmt.Sprintf("error CSV delimiter: %s", err.Error())) 308 | } 309 | return rune(string(bytes)[0]) 310 | } 311 | return rune(inputString[0]) 312 | } 313 | return rune(',') 314 | } 315 | 316 | // readTemplate get template from file or from input 317 | func readTemplate(textTemplate string) ([]byte, error) { 318 | var templateFile []byte 319 | var err error 320 | if textTemplate[:1] == "?" { 321 | templateFile = []byte(textTemplate[1:]) 322 | } else { 323 | templateFile, err = os.ReadFile(textTemplate) 324 | if err != nil { 325 | return nil, fmt.Errorf("readFile: %s", err.Error()) 326 | } 327 | } 328 | return templateFile, nil 329 | } 330 | 331 | // writeOutputData process template and write output 332 | func writeOutputData(mapData interface{}, outputFile *string, templateFile []byte) error { 333 | var err error 334 | template, err := template.New("new").Funcs(templateFunctions()).Parse(string(templateFile)) 335 | if err != nil { 336 | return fmt.Errorf("parseTemplate: %s", err.Error()) 337 | } 338 | if *outputFile == "" { 339 | output := new(bytes.Buffer) 340 | if err = template.Execute(output, mapData); err != nil { 341 | return fmt.Errorf("writeStdout: %s", err.Error()) 342 | } 343 | fmt.Print(output) 344 | } else { 345 | output, err := os.Create(*outputFile) 346 | if err != nil { 347 | return fmt.Errorf("createOutputFile: %s", err.Error()) 348 | } 349 | defer output.Close() 350 | if err = template.Execute(output, mapData); err != nil { 351 | return fmt.Errorf("writeOutputFile: %s", err.Error()) 352 | } 353 | } 354 | return nil 355 | } 356 | 357 | func chatGPTprocess(mapData interface{}, params tParams) (response openai.ChatCompletionResponse, err error) { 358 | jsonData, err := json.Marshal(mapData) 359 | if err != nil { 360 | return response, fmt.Errorf("jsonMarshal: %s", err.Error()) 361 | } 362 | model := openai.GPT3Dot5Turbo 363 | switch *params.chatGPTmodel { 364 | case "gpt35": 365 | model = openai.GPT3Dot5Turbo 366 | case "gpt4": 367 | model = openai.GPT4 368 | case "gpt4o": 369 | model = openai.GPT4o 370 | case "gpt4o-mini": 371 | model = openai.GPT4oMini 372 | default: 373 | model = openai.GPT3Dot5Turbo 374 | } 375 | 376 | client := openai.NewClient(*params.chatGPTkey) 377 | return client.CreateChatCompletion( 378 | context.Background(), 379 | openai.ChatCompletionRequest{ 380 | Model: model, 381 | Messages: []openai.ChatCompletionMessage{ 382 | { 383 | Role: openai.ChatMessageRoleUser, 384 | Content: *params.chatGPTquery + "\n" + string(jsonData), 385 | }, 386 | }, 387 | }, 388 | ) 389 | } 390 | 391 | // cleanBOM remove UTF-8 Byte Order Mark if present 392 | func cleanBOM(b []byte) []byte { 393 | if len(b) >= 3 && 394 | b[0] == 0xef && 395 | b[1] == 0xbb && 396 | b[2] == 0xbf { 397 | return b[3:] 398 | } 399 | return b 400 | } 401 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/clbanning/mxj/v2" 9 | ) 10 | 11 | const bsonDump = `JgAAAAdfaWQAYQAVOXB0lMEH42tuAm5hbWUABgAAAEhlbGxvAAAmAAAAB19pZABhCmAZAkO5o4cVWbsCbmFtZQAGAAAAV29ybGQAAA==` 12 | 13 | func TestProcessTemplate(t *testing.T) { 14 | inputFile := "" 15 | inputFormat := "" 16 | inputDelimiter := "," 17 | outputFile := "" 18 | textTemplate := `?{{define content}}` 19 | getVersion := false 20 | chatGPTkey := "" 21 | chatGPTmodel := "" 22 | chatGPTquery := "" 23 | 24 | params := tParams{ 25 | inputFile: &inputFile, 26 | inputFormat: &inputFormat, 27 | inputDelimiter: &inputDelimiter, 28 | outputFile: &outputFile, 29 | textTemplate: &textTemplate, 30 | getVersion: &getVersion, 31 | chatGPTkey: &chatGPTkey, 32 | chatGPTmodel: &chatGPTmodel, 33 | chatGPTquery: &chatGPTquery, 34 | } 35 | err := processTemplate(params) 36 | if !strings.Contains(err.Error(), "stdin: Error-noPipe") { 37 | t.Errorf("result: %v", err.Error()) 38 | } 39 | inputFile = "testdata.xml" 40 | textTemplate = "hello.tmpl" 41 | err = processTemplate(params) 42 | if !strings.Contains(err.Error(), `readFile: open hello.tmpl:`) { 43 | t.Errorf("result: %v", err.Error()) 44 | } 45 | inputFile = "testdata.xml" 46 | textTemplate = "?{{define content}}" 47 | err = processTemplate(params) 48 | if !strings.Contains(err.Error(), `unexpected "content" in define`) { 49 | t.Errorf("result: %v", err.Error()) 50 | } 51 | inputFormat = "json" 52 | err = processTemplate(params) 53 | if !strings.Contains(err.Error(), `mapJSON: invalid character`) { 54 | t.Errorf("result: %v", err.Error()) 55 | } 56 | textTemplate = `?Output template test: description = {{index .TOP_LEVEL "-description"}} {{print "\r\n"}}` 57 | inputFormat = "" 58 | err = processTemplate(params) 59 | if err != nil { 60 | t.Errorf("result: %v", err.Error()) 61 | } 62 | 63 | textTemplate = `?Output template test: description = {{index .filesTest.TOP_LEVEL "-description"}} {{print "\r\n"}}` 64 | inputFile = "?filesTest.yaml" 65 | err = processTemplate(params) 66 | if err != nil { 67 | t.Errorf("result: %v", err.Error()) 68 | } 69 | 70 | inputFile = "?filesTest.yamlx" 71 | err = processTemplate(params) 72 | if !strings.Contains(err.Error(), "readFileList: open filesTest.yamlx") { 73 | t.Errorf("result: %v", err.Error()) 74 | } 75 | 76 | getVersion = true 77 | err = processTemplate(params) 78 | if err != nil { 79 | t.Errorf("result: %v", err.Error()) 80 | } 81 | textTemplate = "" 82 | getVersion = false 83 | err = processTemplate(params) 84 | if err != nil { 85 | t.Errorf("result: %v", err.Error()) 86 | } 87 | } 88 | 89 | func TestGetInputData(t *testing.T) { 90 | inputFile := "testdata.xml" 91 | data, _, _ := getInputData(&inputFile) 92 | if !strings.Contains(string(data), ``) { 93 | t.Errorf("result: %v", string(data)) 94 | } 95 | inputFile = "Hello.xml" 96 | _, _, err := getInputData(&inputFile) 97 | if !strings.Contains(err.Error(), `readFile: open Hello.xml:`) { 98 | t.Errorf("result: %v", err.Error()) 99 | } 100 | inputFile = "" 101 | _, _, err = getInputData(&inputFile) 102 | if !strings.Contains(err.Error(), `stdin: Error-noPipe`) { 103 | t.Errorf("result: %v", err.Error()) 104 | } 105 | } 106 | 107 | func TestMapInputData(t *testing.T) { 108 | inputFile := "" 109 | inputFormat := "" 110 | inputDelimiter := "," 111 | outputFile := "" 112 | textTemplate := `?{{define content}}` 113 | getVersion := false 114 | params := tParams{ 115 | inputFile: &inputFile, 116 | inputFormat: &inputFormat, 117 | inputDelimiter: &inputDelimiter, 118 | outputFile: &outputFile, 119 | textTemplate: &textTemplate, 120 | getVersion: &getVersion, 121 | } 122 | // Test map json 123 | input := []byte(`{"name": "John","age": 30}`) 124 | inputFormat = "json" 125 | result, _ := mapInputData(input, params) 126 | if result.(map[string]interface{})["name"] != "John" || result.(map[string]interface{})["age"] != float64(30) { 127 | t.Errorf("resultJSON: %v", result) 128 | } 129 | input = []byte(`[{"name": "John","age": 30}, {"name": "Hanz","age": 28}]`) 130 | result, _ = mapInputData(input, params) 131 | if result.([]map[string]interface{})[0]["name"] != "John" { 132 | t.Errorf("resultJSONarray: %v", result.([]map[string]interface{})[0]["name"]) 133 | } 134 | input = []byte(`[{"name": "John","age": 30}, {"name": Hanz","age": 28}]`) 135 | _, err := mapInputData(input, params) 136 | if !strings.Contains(err.Error(), "invalid character 'H'") { 137 | t.Errorf("resultJSONerr: %v", err.Error()) 138 | } 139 | input = []byte(`{"name" John","age": 30}`) 140 | _, err = mapInputData(input, params) 141 | if !strings.Contains(err.Error(), "invalid character 'J'") { 142 | t.Errorf("resultJSONerr: %v", err.Error()) 143 | } 144 | // Test map bson 145 | input = []byte{14, 0, 0, 0, 2, 104, 0, 2, 0, 0, 0, 119, 0, 0} 146 | inputFormat = "bson" 147 | result, _ = mapInputData(input, params) 148 | if result.(map[string]interface{})["h"] != "w" { 149 | t.Errorf("resultBSON: %v", result) 150 | } 151 | input, err = base64.StdEncoding.DecodeString(bsonDump) 152 | if err != nil { 153 | t.Errorf("base64BSONdump: %v", err.Error()) 154 | } 155 | result, _ = mapInputData(input, params) 156 | if result.([]map[string]interface{})[0]["name"] != `Hello` { 157 | t.Errorf("resultBSONdump: %v", result.([]map[string]interface{})[0]["name"]) 158 | } 159 | input = []byte{14, 0, 0, 1, 2, 104, 0, 2, 0, 0, 0, 119, 0, 0} 160 | _, err = mapInputData(input, params) 161 | if !strings.Contains(err.Error(), "EOF") { 162 | t.Errorf("resultBSONerr: %v", err.Error()) 163 | } 164 | // Test map yaml 165 | input = []byte(`name: John`) 166 | inputFormat = "yaml" 167 | result, _ = mapInputData(input, params) 168 | if result.(map[string]interface{})["name"] != "John" { 169 | t.Errorf("resultYAML: %v", result) 170 | } 171 | input = []byte("- name: John\r\n- name: Peter") 172 | result, _ = mapInputData(input, params) 173 | if result.([]map[string]interface{})[0]["name"] != "John" { 174 | t.Errorf("resultYAMLarray: %v", result) 175 | } 176 | input = []byte("- name John\r\n- name: Peter") 177 | _, err = mapInputData(input, params) 178 | if !strings.Contains(err.Error(), "cannot unmarshal !!str `name John`") { 179 | t.Errorf("resultYAMLarrayErr: %v", err.Error()) 180 | } 181 | input = []byte(`name John`) 182 | _, err = mapInputData(input, params) 183 | if !strings.Contains(err.Error(), "cannot unmarshal !!str `name John`") { 184 | t.Errorf("resultYAMLerr: %v", err.Error()) 185 | } 186 | // Test map csv 187 | input = []byte("name,surname\r\nHello,World") 188 | inputFormat = "csv" 189 | result, _ = mapInputData(input, params) 190 | if result.([]map[string]interface{})[0]["name"] != "Hello" { 191 | t.Errorf("result: %v", result.([]map[string]interface{})[0]["name"]) 192 | } 193 | input = []byte("name,surname\r\nHello,World") 194 | inputDelimiter = "0x2C" 195 | result, _ = mapInputData(input, params) 196 | if result.([]map[string]interface{})[0]["name"] != "Hello" { 197 | t.Errorf("result: %v", result.([]map[string]interface{})[0]["name"]) 198 | } 199 | input = []byte("name,surname\r\nHello,World,!!!") 200 | _, err = mapInputData(input, params) 201 | if !strings.Contains(err.Error(), "wrong number of fields") { 202 | t.Errorf("result: %v", err.Error()) 203 | } 204 | 205 | // Test map xml 206 | input = []byte(`John`) 207 | inputFormat = "xml" 208 | result, _ = mapInputData(input, params) 209 | if result.(mxj.Map)["name"] != "John" { 210 | t.Errorf("result: %v", result) 211 | } 212 | input = []byte(`John`) 213 | _, err = mapInputData(input, params) 214 | if !strings.Contains(err.Error(), "xml.Decoder.Token() - XML syntax error on line 1") { 215 | t.Errorf("result: %v", err.Error()) 216 | } 217 | } 218 | 219 | func TestReadTemplate(t *testing.T) { 220 | // Test inline template 221 | result, err := readTemplate("?{{toXML .}}") 222 | if string(result) != "{{toXML .}}" { 223 | t.Errorf("result: %v", string(result)) 224 | } 225 | if err != nil { 226 | t.Errorf("err: %v", err) 227 | } 228 | // Test template file 229 | result, err = readTemplate("template.tmpl") 230 | if !strings.Contains(string(result), "CSV formatted data:") { 231 | t.Errorf("result: %v", string(result)) 232 | } 233 | if err != nil { 234 | t.Errorf("err: %v", err) 235 | } 236 | // Test nonExisting file 237 | _, err = readTemplate("hello.tmpl") 238 | if !strings.Contains(err.Error(), "readFile: open hello.tmpl:") { 239 | t.Errorf("err: %v", err) 240 | } 241 | } 242 | 243 | func TestWriteOutputData(t *testing.T) { 244 | testData := make(map[string]interface{}) 245 | testData["Hello"] = "World" 246 | outputFile := "" 247 | templateFile := []byte(`{{define content}}`) 248 | err := writeOutputData(testData, &outputFile, templateFile) 249 | if !strings.Contains(err.Error(), `new:1: unexpected "content"`) { 250 | t.Errorf("result: %v", err.Error()) 251 | } 252 | templateFile = []byte(`Output test: Hello {{.Hello}} {{print "\r\n"}}`) 253 | if err := writeOutputData(testData, &outputFile, templateFile); err != nil { 254 | t.Errorf("result: %v", err.Error()) 255 | } 256 | outputFile = "output.txt" 257 | if err := writeOutputData(testData, &outputFile, templateFile); err != nil { 258 | t.Errorf("result: %v", err.Error()) 259 | } 260 | testData["Hello"] = make(chan int, 1) 261 | err = writeOutputData(testData, &outputFile, templateFile) 262 | if !strings.Contains(err.Error(), "can't print {{.Hello}} of type chan int") { 263 | t.Errorf("result: %v", err.Error()) 264 | } 265 | outputFile = "out*he\\ll//o/./txt" 266 | err = writeOutputData(testData, &outputFile, templateFile) 267 | if !strings.Contains(err.Error(), "createOutputFile:") { 268 | t.Errorf("result: %v", err.Error()) 269 | } 270 | } 271 | 272 | func TestCleanBOM(t *testing.T) { 273 | input := "\xef\xbb\xbf" + "Hello" 274 | result := string(cleanBOM([]byte(input))) 275 | if result != "Hello" { 276 | t.Errorf("result: %v", result) 277 | } 278 | input = "Hello" 279 | result = string(cleanBOM([]byte(input))) 280 | if result != "Hello" { 281 | t.Errorf("result: %v", result) 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: BaFi 2 | site_url: https://mmalcek.github.io/bafi 3 | nav: 4 | - Application: README.md 5 | - Examples: examples.md 6 | - About: about.md 7 | theme: 8 | name: mkdocs 9 | highlightjs: true 10 | hljs_languages: 11 | - lua 12 | - powershell 13 | navigation_depth: 4 14 | nav_style: light 15 | plugins: 16 | - search 17 | analytics: 18 | gtag: G-GE1PDVJERL 19 | -------------------------------------------------------------------------------- /template.tmpl: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------ 2 | CSV formatted data: 3 | Employee,Date,val1,val2,val3,SUM,LuaMultiply,linkedText 4 | {{- /* {{- ...}} - minus trim whitespace */}} 5 | {{- range (mustArray .TOP_LEVEL.DATA_LINE)}} {{- /* mustArray - force .DATA_LINE to array even if there is just one record (nil if not exists) */}} 6 | {{index .Employee "-ID"}},{{dateFormat .Trans_Date "2006-01-02" "02.01.2006"}},{{.val1}},{{.val2}},{{.val3}},{{add .val1 .val2}},{{lua "mul" .val1 .val2}},"{{index .Linked_Text "-VALUE"}}" 7 | {{- end}} 8 | ------------------------------------------------------------------------------ 9 | Count totals: 10 | {{- /* Use variable and iterate over lines to get SUM */}} 11 | {{- $TotalV1 := 0 }} 12 | {{- range .TOP_LEVEL.DATA_LINE}}{{$TotalV1 = lua "sum" $TotalV1 .val1}}{{end}} 13 | {{- $TotalV3 := 0 }} 14 | {{- /* addf - will provide decimal count */}} 15 | {{- range .TOP_LEVEL.DATA_LINE}}{{$TotalV3 = addf $TotalV3 .val3}}{{end}} 16 | Total: 17 | {{- /* if functions and,or,not,eq(equal),lt(lessThen),gt(greatherThen) */}} 18 | Val1: {{$TotalV1}} - {{if gt (toInt $TotalV1) 50}}Over Budget{{else}}Under Buget{{end}} 19 | Val3: {{$TotalV3}} 20 | Created at: {{now "02.01.2006 - 15:04:05"}} 21 | ------------------------------------------------------------------------------ 22 | JSON formatted data: 23 | {{- $new := "{\"employees\": [" }} 24 | {{- range .TOP_LEVEL.DATA_LINE}} 25 | {{- $new = print $new "{\"employeeID\":\"" (index .Employee "-ID") "\", \"val1\":" .val1 "}," }} 26 | {{- end}} 27 | {{- /* Trim trailing comma, alternatively you can remove last char by "(slice $new 0 (sub (len $new) 1))" */}} 28 | {{- $new = print (trimSuffix $new "," ) "]}"}} 29 | {{$new}} 30 | ------------------------------------------------------------------------------ 31 | JSON Converted to map and marshal to YAML: 32 | {{toYAML (mapJSON $new) -}} 33 | ------------------------------------------------------------------------------ 34 | -------------------------------------------------------------------------------- /testdata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 2021-05-21 7 | 5 8 | 12 9 | 18.3285 10 | 11 | 12 | 13 | 14 | 15 | 16 | 2021-05-23 17 | 43 18 | 9 19 | 5.67343 20 | 21 | 22 | 23 | 24 | 25 | 26 | 2021-05-23 27 | 14 28 | 4 29 | 2.97984 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /workspace.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "name": "BaFi", 5 | "path": "." 6 | }, 7 | ], 8 | "settings": { 9 | "editor.formatOnSave": true, 10 | "editor.minimap.enabled": false, 11 | "files.autoSave": "onFocusChange", 12 | "files.eol": "\r\n", 13 | "git.confirmSync": false, 14 | "git.autofetch": true, 15 | "git.enableSmartCommit": true, 16 | "go.useLanguageServer": true, 17 | } 18 | } --------------------------------------------------------------------------------