├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── testunit ├── a.en.language ├── a.es.language ├── a.fr.language ├── a.template ├── b.template ├── errors.es.txt ├── errors.es.xml └── xcore_xtemplate_test.go ├── v2 ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── interfaces.go ├── testunit │ ├── a.en.language │ ├── a.es.language │ ├── a.fr.language │ ├── a.template │ ├── b.template │ ├── errors.es.txt │ └── errors.es.xml ├── xcache.go ├── xcache_test.go ├── xcore.go ├── xdataset.go ├── xdataset_test.go ├── xdatasetcollection.go ├── xdatasetcollection_test.go ├── xdatasetcollectionts.go ├── xdatasetcollectionts_test.go ├── xdatasetts.go ├── xdatasetts_test.go ├── xlanguage.go ├── xlanguage_test.go ├── xtemplate.go └── xtemplate_test.go ├── xcache.go ├── xcache_test.go ├── xcore.go ├── xdataset.go ├── xdataset_test.go ├── xlanguage.go ├── xlanguage_test.go ├── xtemplate.go └── xtemplate_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Philippe Thomassigny https://github.com/webability-go/xcore 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 | XCore for GO v2 2 | ============================= 3 | 4 | # Please use xcore/v2 5 | 6 | # The version 1 is obsolete. 7 | 8 | [![Go Report Card](https://goreportcard.com/badge/github.com/webability-go/xcore)](https://goreportcard.com/report/github.com/webability-go/xcore) 9 | [![GoDoc](https://godoc.org/github.com/webability-go/xcore/v2?status.png)](https://godoc.org/github.com/webability-go/xcore/v2) 10 | [![GolangCI](https://golangci.com/badges/github.com/webability-go/xcore.svg)](https://golangci.com) 11 | 12 | # Please use xcore/v2 13 | 14 | # The version 1 is obsolete. 15 | 16 | import "github.com/webability-go/xcore/v2" 17 | 18 | The XCore package is used to build basic object for programmation. for the WebAbility compatility code 19 | For GO, the actual existing code includes: 20 | - XCache: Application Memory Caches, thread safe. 21 | - XDataset: Basic nested data structures for any purpose (template injection, configuration files, database records, etc) Support thread safe operations on thread safe structures (XDatasetTS and XDatasetCollectionTS) 22 | - XLanguage: language dependent text tables, thread safe 23 | - XTemplate: template system with meta language, thread safe cloning 24 | 25 | Manuals are available on godoc.org [![GoDoc](https://godoc.org/github.com/webability-go/xcore/v2?status.png)](https://godoc.org/github.com/webability-go/xcore/v2) 26 | 27 | # Please use xcore/v2 28 | 29 | # The version 1 is obsolete. 30 | 31 | Version Changes Control 32 | ======================= 33 | 34 | v1.1.1 - 2022-09-02 35 | ----------------------- 36 | - Bug corrected on XDataset.GetString() and XDatasetCollection.GetDataString(). 37 | If the value is NIL int the dataset, it returns now "" and not "" 38 | 39 | v1.1.0 - 2020-03-01 40 | ----------------------- 41 | - Modularization of XCore 42 | - XLanguage tests and examples are now conform to Go test units 43 | - Implementation of XLanguage.String and XLanguage.GoString, removed Print 44 | - XCache tests and examples are now conform to Go test units 45 | - XDataset tests and examples are now conform to Go test units 46 | - Implementation of XDataset.String and XDataset.GoString, removed Print 47 | - Implementation of XDatasetCollection.String and XDatasetCollection.GoString, removed Print 48 | - XTemplate tests and examples are now conform to Go test units 49 | - Implementation of XTemplate.String and XTemplate.GoString, removed Print 50 | 51 | v1.0.1 - 2020-02-10 52 | ----------------------- 53 | - Documentation corrections 54 | - Bug on String() and GoString() corrected 55 | 56 | v1.0.0 - 2020-02-09 57 | ----------------------- 58 | - Version leveling 59 | - Documentation corrections 60 | - Change functions Stringify() by String() and GoString() for language compatibility 61 | - Tests functions enhanced 62 | 63 | v0.3.1 - 2020-02-09 64 | ----------------------- 65 | - XDatasetDef.Get must accept a path as key (id>id>id) 66 | - XTemplates now resolve {{ fields with path id>id>id 67 | - XTemplates now resolve @@ metalanguage with 1 and 2 Parameters 68 | - XTemplates now resolve && metalanguage with 1,2 and 3 Parameters 69 | - XTemplates now resolve ?? metalanguage with 1, and 2 Parameters 70 | - XTemplates now resolve !! debug orders 71 | - XTemplates now implements sub templates derivation (.none .first .last .(number) ) 72 | - Manuals for XCache, XLanguage and XTemplate written with reference of the metalanguage 73 | - Examples for dataset and xtemplate added (working version) 74 | - XDataset and XDatasetCollection .Stringify now prints also field names. 75 | 76 | v0.3.0 - 2020-02-06 77 | ----------------------- 78 | - The properties of XTemplateParam are now public so the full structure can be used to build other type of code based on the XTemplate rules 79 | - The subtemplates IDs must be lowers, numbers and . - _ in sight of integration with other systems that can mix tags [[]] within the code 80 | 81 | v0.2.3 - 2020-01-23 82 | ----------------------- 83 | - Corrected a bug to avoid null pointer panic error if the array of data for XTemplate.Execute function is nil 84 | 85 | v0.2.2 - 2019-12-21 86 | ----------------------- 87 | - XLanguage now support golang x/text/language instead of direct iso 2 charater language 88 | - godoc manuals for xlanguage, xdataset and xtemplate prepared 89 | 90 | v0.2.1 - 2019-12-13 91 | ----------------------- 92 | - XCache manual enhanced with examples 93 | 94 | v0.2.0 - 2019-12-06 95 | ----------------------- 96 | - XCache Code simplified to expose XCache definition as public, remove not usefull funcion (Get*) 97 | - XCache 0.2.0 is not compatible with XCache 0.1.* , you may need to change your code 98 | - Added more conversions between int-float-bool in XDataset.Get* 99 | 100 | v0.1.2 - 2019-12-05 101 | ----------------------- 102 | - Code cleaned to meet golangci standards, golint checks, more documentation. 103 | 104 | V0.1.1 - 2019-11-05 105 | ----------------------- 106 | - XCore Code comments enhanced to publish in godoc.org as libraries documentation 107 | 108 | V0.1.0 - 2019-10-18 109 | ----------------------- 110 | - Code cleaned to pass 100% of goreportcard.com. Card note added in this document 111 | 112 | V0.0.9 - 2019-07-18 113 | ----------------------- 114 | - Error corrected on XCache: removing an element from a slice when the element is the last one was causing out of bound index. 115 | - XCache.maxitem = 0 (no number of elements limit) is corrected: it was not working 116 | 117 | V0.0.8 - 2019-06-25 118 | ----------------------- 119 | - Added Clone on XDatasetDef and XDataCollectionsetDef 120 | - XDataset testunit added 121 | 122 | V0.0.7 - 2019-03-06 123 | ----------------------- 124 | - Time functions added to XDatasetDef and XDatasetCollectionDef interfaces, and XDataset and XDatasetCollection structures 125 | - Manual for XCache finished 126 | - Manual for XDataset finished 127 | - Preformat for XLanguage manual 128 | - Preformat for XTemplate manual 129 | 130 | V0.0.6 - 2019-02-07 131 | ----------------------- 132 | - Added xcache.GetId(), xcache.GetMax() and xcache.GetExpire() 133 | - XCache Documentation modified 134 | 135 | V0.0.5 - 2019-02-05 136 | ----------------------- 137 | - Added conversion between types con XDataset.Get* functions 138 | - Manuals for XDataSet and XTemplate complemented 139 | 140 | V0.0.4 - 2019-01-06 141 | ----------------------- 142 | - XDataset.Get* functions added to comply with any type of data of a dataset for templates, config, database record etc. 143 | - XCache manual completed. 144 | 145 | V0.0.3 - 2019-01-02 146 | ----------------------- 147 | - Added XCache.Flush function 148 | - Function XCache.Del implemented 149 | - Function XCache.Clean implemented for expiration, and free some space 150 | - Function XCache.Verify created 151 | - Function XCache.SetValidator added, to check cache validity agains a validator function 152 | - Files flags and code removed from XCache. If the cache is a file, the user should controls the files with its own Validator function (original funcions put in examples as a file validator). This lets a lots of flexibility to validate against any source of data (files, database, complex calculations, external streams, etc) 153 | - XCache is ready for candidate release 154 | 155 | V0.0.2 - 2018-12-17 156 | ----------------------- 157 | - Creation of XCache with all set of functions. 158 | - Creation of XLanguage with all set of functions. 159 | - Creation of XTemplate with all set of functions. Basic work done 160 | - Creation of a set of interfaces that XTemplate need to execute and inject the template, 161 | - Creation of a basic XDataset and colection based on interfaces to build a set of data for the template. 162 | - Added xcore.go with version number as constant 163 | 164 | V0.0.1 - 2018-11-14 165 | ----------------------- 166 | - First basic commit with XLanguage object created 167 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/webability-go/xcore 2 | 3 | go 1.15 4 | 5 | require golang.org/x/text v0.3.8 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 2 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 3 | golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= 4 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 5 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 6 | -------------------------------------------------------------------------------- /testunit/a.en.language: -------------------------------------------------------------------------------- 1 | 2 | 3 | Welcome to 4 | XCore 5 | 6 | -------------------------------------------------------------------------------- /testunit/a.es.language: -------------------------------------------------------------------------------- 1 | 2 | 3 | Bienvenido a 4 | XCore 5 | 6 | -------------------------------------------------------------------------------- /testunit/a.fr.language: -------------------------------------------------------------------------------- 1 | 2 | 3 | Bienvenue à 4 | XCore 5 | 6 | -------------------------------------------------------------------------------- /testunit/a.template: -------------------------------------------------------------------------------- 1 | 2 | %-- This is a comment. It will not appear in the final code. This files is LF coded, not CRLF, as the string in test (unix end of line) --% 3 | Let's put your name here: {{clientname}}
4 | And lets put your hobbies here:
5 | %-- note the 1rst id is the entry into the data to inject and the second one is the name of the sub-template to use --% 6 | @@hobbies:hobby@@ 7 | %-- And you need the template for each hobby:--% 8 | [[hobby]] 9 | I love {{name}}
10 | [[]] 11 | -------------------------------------------------------------------------------- /testunit/b.template: -------------------------------------------------------------------------------- 1 | &&header&& 2 | &&body&& 3 | 4 | [[header]] 5 | Sports shop: 6 | [[]] 7 | 8 | [[body]] 9 | {{clientname}} Preferred hobby: 10 | &&:preferredhobby>sport:sport.&& %-- will build sport_ + [yes/no] contained into the sport field. Be sure you have a template for each value ! --% 11 | 12 | ??preferredhobby>sport:sport?? 13 | 14 | [[sport.yes]]{{preferredhobby>name}} - It's a sport, sell him things![[]] 15 | [[sport.no]]{{preferredhobby>name}} - It's not a sport, recommend him next store.[[]] 16 | [[sport]]{{preferredhobby>name}} - We do not know that it is.[[]] 17 | 18 | @@hobbies:hobby@@ 19 | [[hobby.first]] 20 | 1. {{name}} {{sport}} 21 | [[]] 22 | [[hobby.last]] 23 | last. {{name}} {{sport}} 24 | [[]] 25 | [[hobby.even]] 26 | 2x. {{name}} {{sport}} 27 | [[]] 28 | [[hobby.key.3]] 29 | 3. {{name}} {{sport}} 30 | [[]] 31 | [[hobby]] 32 | {{name}} {{sport}} 33 | [[]] 34 | [[hobby.none]] 35 | No hobbies 36 | [[]] 37 | [[]] 38 | -------------------------------------------------------------------------------- /testunit/errors.es.txt: -------------------------------------------------------------------------------- 1 | panicerror=Error crítico del sistema 2 | systemerror=Error del sistema 3 | fileerror=Error leyendo el archivo 4 | -------------------------------------------------------------------------------- /testunit/errors.es.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Error crítico del sistema 4 | Error del sistema 5 | Error leyendo el archivo 6 | 7 | -------------------------------------------------------------------------------- /testunit/xcore_xtemplate_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/webability-go/xcore" 6 | "testing" 7 | // "unsafe" 8 | ) 9 | 10 | /* TEST XTEMPLATE */ 11 | 12 | func TestCommentParam(t *testing.T) { 13 | // Test 1: assign a simple parameter string with some comments 14 | tmpl := xcore.NewXTemplate() 15 | err := tmpl.LoadString("%-- starting comment --%Text %--with a [[]]comment--% here. Also an%----% empty comment %--ending comment--%") 16 | 17 | fmt.Println(err) 18 | // fmt.Println(tmpl.Root) 19 | 20 | result := tmpl.Execute(nil) 21 | 22 | fmt.Println("Result: ", result) 23 | 24 | if result != "Text here. Also an empty comment " { 25 | t.Errorf("The comments have not been removed correctly") 26 | } 27 | } 28 | 29 | func TestLanguageParam(t *testing.T) { 30 | tmpl, _ := xcore.NewXTemplateFromString("Test with ##some## ##languages## here") 31 | 32 | fmt.Println(tmpl) 33 | fmt.Println(tmpl.Root) 34 | 35 | data := &xcore.XDataset{} 36 | l, _ := xcore.NewXLanguageFromString("some=a tiny table\nlanguages=of english language\n") 37 | data.Set("#", l) 38 | 39 | result := tmpl.Execute(data) 40 | 41 | fmt.Println("Result: ", result) 42 | 43 | if result != "Test with a tiny table of english language here" { 44 | t.Errorf("The language table has not been inserted correctly") 45 | } 46 | } 47 | func TestReferenceParam(t *testing.T) { 48 | tmpl, _ := xcore.NewXTemplateFromString(` 49 | The sub template starts here: &&template1&&. End. 50 | [[template1]] 51 | This is the template 1 52 | [[]] 53 | `) 54 | 55 | fmt.Println(tmpl) 56 | fmt.Println(tmpl.Root) 57 | 58 | result := tmpl.Execute(&xcore.XDataset{}) 59 | 60 | fmt.Println("Result: ", result) 61 | 62 | } 63 | 64 | func TestComplexReferenceParam(t *testing.T) { 65 | 66 | tmpl, err := xcore.NewXTemplateFromString(` 67 | The sub template starts here: &&template2&&. End. 68 | [[template1]] 69 | This is the template 1 70 | [[]] 71 | [[template2]] 72 | This is the template 2 73 | [[template3]] 74 | This is the subtemplate 3 75 | [[]] 76 | [[template4|template5]] 77 | These are the subtemplates 4 and 5 78 | [[template6.first]] 79 | This is the subtemplate 6 first element for a loop 80 | [[]] 81 | [[template6]] 82 | This is the subtemplate 6 any element for a loop 83 | [[]] 84 | [[template6.last]] 85 | This is the subtemplate 6 last element for a loop 86 | [[]] 87 | [[]] 88 | [[]] 89 | [[template7|template7.status.false]] 90 | This is the template 7 for field status false and any other values 91 | [[]] 92 | [[template7.status.true]] 93 | This is the template 7 for field status true 94 | [[]] 95 | `) 96 | 97 | if err != nil { 98 | fmt.Println(err) 99 | return 100 | } 101 | 102 | result := tmpl.Execute(&xcore.XDataset{}) 103 | fmt.Println("Result: ", result) 104 | } 105 | 106 | func TestVariableParam(t *testing.T) { 107 | 108 | tmpl, err := xcore.NewXTemplateFromString(` 109 | Some data: 110 | {{data1}} 111 | {{data2}} 112 | {{data3>data31}} 113 | {{data4}} 114 | {{data5}} 115 | {{data6}} 116 | {{data7}} 117 | {{data8}} 118 | @@data8@@ 119 | [[data8]] 120 | * test {{data81}} and {{data82}} and {{data83}} and {{data1}} 121 | [[]] 122 | ??data9?? 123 | [[data9]] 124 | * Data 9 exists and is {{data9}} 125 | [[]] 126 | ??data10?? 127 | [[data10]] 128 | * Data 10 does not exist 129 | [[]] 130 | !!dump!! 131 | `) 132 | 133 | if err != nil { 134 | fmt.Println(err) 135 | return 136 | } 137 | 138 | data := xcore.XDataset{} 139 | data["data1"] = "DATA1" 140 | data["data2"] = "DATA1" 141 | sm := xcore.XDataset{} 142 | sm["data31"] = "DATA31" 143 | data["data3"] = sm 144 | data["data4"] = 123 145 | data["data5"] = 123.432 146 | data["data6"] = true 147 | data["data7"] = func() string { return "ABC" } 148 | 149 | d8r1 := &xcore.XDataset{} 150 | d8r1.Set("data81", "rec 1: Entry 8-1") 151 | d8r1.Set("data82", "rec 1: Entry 8-2") 152 | 153 | d8r2 := &xcore.XDataset{} 154 | d8r2.Set("data81", "rec 2: Entry 8-1") 155 | d8r2.Set("data82", "rec 2: Entry 8-2") 156 | d8r2.Set("data83", "rec 2: Entry 8-3") 157 | 158 | d8r3 := &xcore.XDataset{} 159 | d8r3.Set("data81", "rec 3: Entry 8-1") 160 | d8r3.Set("data82", "rec 3: Entry 8-2") 161 | 162 | d := xcore.XDatasetCollection{} 163 | d.Push(d8r1) 164 | d.Push(d8r2) 165 | d.Push(d8r3) 166 | 167 | data["data8"] = &d 168 | data["data9"] = "I exist" 169 | 170 | fmt.Printf("Data: %v\n", data) 171 | // fmt.Printf("ADDRESS DATA8 / GET R1: %p", data.GetCollection("data8").Get(0)) 172 | 173 | result := tmpl.Execute(&data) 174 | fmt.Println("Result: ", result) 175 | } 176 | -------------------------------------------------------------------------------- /v2/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Philippe Thomassigny https://github.com/webability-go/xcore 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 | -------------------------------------------------------------------------------- /v2/README.md: -------------------------------------------------------------------------------- 1 | XCore v2 for GO 2 | ============================= 3 | 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/webability-go/xcore)](https://goreportcard.com/report/github.com/webability-go/xcore) 5 | [![GoDoc](https://godoc.org/github.com/webability-go/xcore/v2?status.png)](https://godoc.org/github.com/webability-go/xcore/v2) 6 | [![GolangCI](https://golangci.com/badges/github.com/webability-go/xcore.svg)](https://golangci.com) 7 | 8 | Minimum version of GO: 1.17 (for time.Time compatibility) 9 | 10 | The XCore package is used to build basic object for programmation. for the WebAbility compatility code 11 | For GO, the actual existing code includes: 12 | - XCache: Application Memory Caches, thread safe. 13 | - XDataset: Basic nested data structures for any purpose (template injection, configuration files, database records, etc) Support thread safe operations on thread safe structures (XDatasetTS and XDatasetCollectionTS) 14 | - XLanguage: language dependent text tables, thread safe 15 | - XTemplate: template system with meta language, thread safe cloning 16 | 17 | Manuals are available on godoc.org [![GoDoc](https://godoc.org/github.com/webability-go/xcore/v2?status.png)](https://godoc.org/github.com/webability-go/xcore/v2) 18 | 19 | 20 | TO DO, maybe: 21 | ============= 22 | - XDataset.Set should accept path too > > > 23 | - Get*Collection should convert types too 24 | - XTemplate must concatenate strings after compilation 25 | - Implements functions as data entry for template Execute (simple data or loop functions, can get backs anything, creates an interface) 26 | Some improvements to check, later: 27 | - Adds mutex on XTemplate ?? (they should be used locally on every thread, or not ??), maybe adds a flag "thread safe" ? 28 | - XCache: activate persistant cache too (shared memory) ????? maybe not for go itself, but for instance to talk with other memory data used by other languages and apps, or to not loose the caches if the app is restarted. 29 | 30 | 31 | Version Changes Control 32 | ======================= 33 | 34 | v2.2.2 - 2023-10-12 35 | ----------------------- 36 | - Added a security on the sub template .none for ?? meta language into XTemplate, to not try to use an inexistant template and throw a panic error. 37 | 38 | v2.2.1 - 2023-09-29 39 | ----------------------- 40 | - Added the missing sub template .none for ?? meta language into XTemplate. 41 | 42 | v2.2.0 - 2023-08-31 43 | ----------------------- 44 | - Added the parameter status to xlanguage XML and function to get/set the parameter. 45 | - Added the function GetXML() to marshal the structure to an XML file. 46 | 47 | v2.1.7 - 2023-06-15 48 | ----------------------- 49 | - Added the missing counter for @@ meta language. Using {{.counter}} into the loop template, you can add the number of the dataset, 1-based. 50 | 51 | v2.1.6 - 2023-05-25 52 | ----------------------- 53 | - Bug corrected into GetCollection(id) function, if the dataset is not an XDatasetCollection, it should return nil, not a panic error 54 | 55 | v2.1.5 - 2023-05-17 56 | ----------------------- 57 | - Bump golang.org/x/text from 0.3.5 to 0.3.8 for security upgrade 58 | 59 | v2.1.4 - 2023-05-17 60 | ----------------------- 61 | - Bug corrected on @@ loops subtemplates, the .none template was not reach when the array is set but empty 62 | 63 | v2.1.3 - 2022-09-02 64 | ----------------------- 65 | - Bug corrected on XDataset.GetString() and XDatasetCollection.GetDataString(). 66 | If the value is NIL int the dataset, it returns now "" and not "" 67 | 68 | v2.1.2 - 2022-03-02 69 | ----------------------- 70 | - XTemplate: Added = to metalanguage string tags to resolve also the paths (bug corrected). 71 | 72 | v2.1.1 - 2022-03-01 73 | ----------------------- 74 | - XTemplate: Added > to metalanguage string tags to resolve also the paths (bug corrected). 75 | 76 | v2.1.0 - 2022-02-27 77 | ----------------------- 78 | - XLanguage: bug corrected on unlock of stringload and loadFromFile (was blocking the system) 79 | - XTemplate: The metalanguage keywords are now only recognized if they match authorized characters (for instance &&keyword&&), to avoid bugs in JS with && and || and !!. 80 | - Print functions of time.Time corrected (as in go 1.17, the print format changes) into the *test.go test functions 81 | 82 | v2.0.9 - 2021-11-25 83 | ----------------------- 84 | - XCache modified to defer mutex unlocks instead of directly unlock into the code, to avoid dead locks in case of thread panic and crashes. 85 | - XLanguage modified to defer mutex unlocks instead of directly unlock into the code, to avoid dead locks in case of thread panic and crashes. 86 | 87 | v2.0.8 - 2021-05-18 88 | ----------------------- 89 | - XTemplate is now clonable: newtemplate := template.Clone() 90 | 91 | v2.0.7 - 2021-03-23 92 | ----------------------- 93 | - []interface{} added to NewXDataset to be decoded as []map[string]interface{} because of decoded JSON 94 | 95 | 96 | v2.0.5 - 2021-03-23 97 | ----------------------- 98 | - function NewXDataset(data) and NewXDatasetCollection(data) added, to build XDatasets based on a classic map[string]interface{} 99 | - XDatasetTS added to the manual 100 | - XLanguage is now thread safe 101 | - XTemplate: Error corrected in string() function (was saying XLanguage) 102 | - Each cache entry is now able to manage its own TLL if set. New function SetTTL(id, duration) 103 | 104 | v2.0.4 - 2020-04-13 105 | ----------------------- 106 | - XLanguage: Added GetEntries() func (not thread safe yet) 107 | 108 | v2.0.3 - 2020-04-08 109 | ----------------------- 110 | - XLanguage: Error corrected on loadXMLString: the data was not loading correctly into the XLanguage object. 111 | 112 | v2.0.1, v2.0.2 - 2020-03-29 113 | ----------------------- 114 | - Version adjustment for github and go modules v2 115 | 116 | v2.0.0 - 2020-03-29 117 | ----------------------- 118 | - xdataset.go now as a coverage of 100% with xdataset_test.go 119 | - XCache now uses R/W mutex 120 | - New interfaces.go file to keep all the interfaces in it (XDatasetDef, XDatasetCollectionDef) 121 | - New xdatasetts.go for thread safe dataset 122 | - New xdatasetts_test.go for thread safe dataset tests 123 | - New xdatasetcollection.go for the collection of dataset (separation from xdataset.go) 124 | - New xdatasetcollection_test.go for collection tests 125 | - New xdatasetcollectionts.go for thread safe dataset 126 | - New xdatasetcollectionts_test.go for thread safe datasetcollection tests 127 | - XLanguage is now thread safe with R/W mutexes 128 | 129 | v1.1.0 - 2020-03-01 130 | ----------------------- 131 | - Modularization of XCore 132 | - XLanguage tests and examples are now conform to Go test units 133 | - Implementation of XLanguage.String and XLanguage.GoString, removed Print 134 | - XCache tests and examples are now conform to Go test units 135 | - XDataset tests and examples are now conform to Go test units 136 | - Implementation of XDataset.String and XDataset.GoString, removed Print 137 | - Implementation of XDatasetCollection.String and XDatasetCollection.GoString, removed Print 138 | - XTemplate tests and examples are now conform to Go test units 139 | - Implementation of XTemplate.String and XTemplate.GoString, removed Print 140 | 141 | v1.0.1 - 2020-02-10 142 | ----------------------- 143 | - Documentation corrections 144 | - Bug on String() and GoString() corrected 145 | 146 | v1.0.0 - 2020-02-09 147 | ----------------------- 148 | - Version leveling 149 | - Documentation corrections 150 | - Change functions Stringify() by String() and GoString() for language compatibility 151 | - Tests functions enhanced 152 | 153 | v0.3.1 - 2020-02-09 154 | ----------------------- 155 | - XDatasetDef.Get must accept a path as key (id>id>id) 156 | - XTemplates now resolve {{ fields with path id>id>id 157 | - XTemplates now resolve @@ metalanguage with 1 and 2 Parameters 158 | - XTemplates now resolve && metalanguage with 1,2 and 3 Parameters 159 | - XTemplates now resolve ?? metalanguage with 1, and 2 Parameters 160 | - XTemplates now resolve !! debug orders 161 | - XTemplates now implements sub templates derivation (.none .first .last .(number) ) 162 | - Manuals for XCache, XLanguage and XTemplate written with reference of the metalanguage 163 | - Examples for dataset and xtemplate added (working version) 164 | - XDataset and XDatasetCollection .Stringify now prints also field names. 165 | 166 | v0.3.0 - 2020-02-06 167 | ----------------------- 168 | - The properties of XTemplateParam are now public so the full structure can be used to build other type of code based on the XTemplate rules 169 | - The subtemplates IDs must be lowers, numbers and . - _ in sight of integration with other systems that can mix tags [[]] within the code 170 | 171 | v0.2.3 - 2020-01-23 172 | ----------------------- 173 | - Corrected a bug to avoid null pointer panic error if the array of data for XTemplate.Execute function is nil 174 | 175 | v0.2.2 - 2019-12-21 176 | ----------------------- 177 | - XLanguage now support golang x/text/language instead of direct iso 2 charater language 178 | - godoc manuals for xlanguage, xdataset and xtemplate prepared 179 | 180 | v0.2.1 - 2019-12-13 181 | ----------------------- 182 | - XCache manual enhanced with examples 183 | 184 | v0.2.0 - 2019-12-06 185 | ----------------------- 186 | - XCache Code simplified to expose XCache definition as public, remove not usefull funcion (Get*) 187 | - XCache 0.2.0 is not compatible with XCache 0.1.* , you may need to change your code 188 | - Added more conversions between int-float-bool in XDataset.Get* 189 | 190 | v0.1.2 - 2019-12-05 191 | ----------------------- 192 | - Code cleaned to meet golangci standards, golint checks, more documentation. 193 | 194 | V0.1.1 - 2019-11-05 195 | ----------------------- 196 | - XCore Code comments enhanced to publish in godoc.org as libraries documentation 197 | 198 | V0.1.0 - 2019-10-18 199 | ----------------------- 200 | - Code cleaned to pass 100% of goreportcard.com. Card note added in this document 201 | 202 | V0.0.9 - 2019-07-18 203 | ----------------------- 204 | - Error corrected on XCache: removing an element from a slice when the element is the last one was causing out of bound index. 205 | - XCache.maxitem = 0 (no number of elements limit) is corrected: it was not working 206 | 207 | V0.0.8 - 2019-06-25 208 | ----------------------- 209 | - Added Clone on XDatasetDef and XDataCollectionsetDef 210 | - XDataset testunit added 211 | 212 | V0.0.7 - 2019-03-06 213 | ----------------------- 214 | - Time functions added to XDatasetDef and XDatasetCollectionDef interfaces, and XDataset and XDatasetCollection structures 215 | - Manual for XCache finished 216 | - Manual for XDataset finished 217 | - Preformat for XLanguage manual 218 | - Preformat for XTemplate manual 219 | 220 | V0.0.6 - 2019-02-07 221 | ----------------------- 222 | - Added xcache.GetId(), xcache.GetMax() and xcache.GetExpire() 223 | - XCache Documentation modified 224 | 225 | V0.0.5 - 2019-02-05 226 | ----------------------- 227 | - Added conversion between types con XDataset.Get* functions 228 | - Manuals for XDataSet and XTemplate complemented 229 | 230 | V0.0.4 - 2019-01-06 231 | ----------------------- 232 | - XDataset.Get* functions added to comply with any type of data of a dataset for templates, config, database record etc. 233 | - XCache manual completed. 234 | 235 | V0.0.3 - 2019-01-02 236 | ----------------------- 237 | - Added XCache.Flush function 238 | - Function XCache.Del implemented 239 | - Function XCache.Clean implemented for expiration, and free some space 240 | - Function XCache.Verify created 241 | - Function XCache.SetValidator added, to check cache validity agains a validator function 242 | - Files flags and code removed from XCache. If the cache is a file, the user should controls the files with its own Validator function (original funcions put in examples as a file validator). This lets a lots of flexibility to validate against any source of data (files, database, complex calculations, external streams, etc) 243 | - XCache is ready for candidate release 244 | 245 | V0.0.2 - 2018-12-17 246 | ----------------------- 247 | - Creation of XCache with all set of functions. 248 | - Creation of XLanguage with all set of functions. 249 | - Creation of XTemplate with all set of functions. Basic work done 250 | - Creation of a set of interfaces that XTemplate need to execute and inject the template, 251 | - Creation of a basic XDataset and colection based on interfaces to build a set of data for the template. 252 | - Added xcore.go with version number as constant 253 | 254 | V0.0.1 - 2018-11-14 255 | ----------------------- 256 | - First basic commit with XLanguage object created 257 | -------------------------------------------------------------------------------- /v2/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/webability-go/xcore/v2 2 | 3 | go 1.15 4 | 5 | require golang.org/x/text v0.3.8 6 | -------------------------------------------------------------------------------- /v2/go.sum: -------------------------------------------------------------------------------- 1 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 2 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 3 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 4 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 5 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 6 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 7 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 8 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 9 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 10 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 11 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 12 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 13 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 14 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 15 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 16 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 17 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 18 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 19 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 20 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 21 | golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= 22 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 23 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 24 | golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= 25 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 26 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 27 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 28 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 29 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 30 | -------------------------------------------------------------------------------- /v2/interfaces.go: -------------------------------------------------------------------------------- 1 | package xcore 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // XDatasetDef is a special interface to implement a set of data that can be scanned recursively (by XTemplate for instance) 9 | // to search data into it, Stringify it, and set/get/del entries of data 10 | // The get* methods must accept a path id>id>id... 11 | type XDatasetDef interface { 12 | // Stringify will dump the content into a human readable string 13 | fmt.Stringer // please implement String() 14 | fmt.GoStringer // Please implement GoString() 15 | 16 | // Set will associate the data to the key. If it already exists, it will be replaced 17 | Set(key string, data interface{}) 18 | 19 | // Get will return the value associated to the key if it exists, or bool = false 20 | Get(key string) (interface{}, bool) 21 | // Same as Get but will return the value associated to the key as a XDatasetDef if it exists, or bool = false 22 | GetDataset(key string) (XDatasetDef, bool) 23 | // Same as Get but will return the value associated to the key as a XDatasetCollectionDef if it exists, or bool = false 24 | GetCollection(key string) (XDatasetCollectionDef, bool) 25 | 26 | // Same as Get but will return the value associated to the key as a string if it exists, or bool = false 27 | GetString(key string) (string, bool) 28 | // Same as Get but will return the value associated to the key as a bool if it exists, or bool = false 29 | GetBool(key string) (bool, bool) 30 | // Same as Get but will return the value associated to the key as an int if it exists, or bool = false 31 | GetInt(key string) (int, bool) 32 | // Same as Get but will return the value associated to the key as a float64 if it exists, or bool = false 33 | GetFloat(key string) (float64, bool) 34 | // Same as Get but will return the value associated to the key as a Time if it exists, or bool = false 35 | GetTime(key string) (time.Time, bool) 36 | // Same as Get but will return the value associated to the key as a []String if it exists, or bool = false 37 | GetStringCollection(key string) ([]string, bool) 38 | // Same as Get but will return the value associated to the key as a []bool if it exists, or bool = false 39 | GetBoolCollection(key string) ([]bool, bool) 40 | // Same as Get but will return the value associated to the key as a []int if it exists, or bool = false 41 | GetIntCollection(key string) ([]int, bool) 42 | // Same as Get but will return the value associated to the key as a []float64 if it exists, or bool = false 43 | GetFloatCollection(key string) ([]float64, bool) 44 | // Same as Get but will return the value associated to the key as a []Time if it exists, or bool = false 45 | GetTimeCollection(key string) ([]time.Time, bool) 46 | 47 | // Del will delete the data associated to the key and deletes the key entry 48 | Del(key string) 49 | // Clone the object 50 | Clone() XDatasetDef 51 | } 52 | 53 | // XDatasetCollectionDef is the definition of a collection of XDatasetDefs 54 | type XDatasetCollectionDef interface { 55 | // Stringify will dump the content into a human readable string 56 | fmt.Stringer // please implement String() 57 | fmt.GoStringer // Please implement GoString() 58 | 59 | // Will add a datasetdef to the beginning of the collection 60 | Unshift(data XDatasetDef) 61 | // Will remove the first datasetdef of the collection and return it 62 | Shift() XDatasetDef 63 | 64 | // Will add a datasetdef to the end of the collection 65 | Push(data XDatasetDef) 66 | // Will remove the last datasetdef of the collection and return it 67 | Pop() XDatasetDef 68 | 69 | // Will count the quantity of entries 70 | Count() int 71 | 72 | // Will get the entry by the index and let it in the collection 73 | Get(index int) (XDatasetDef, bool) 74 | 75 | // Will search for the data associated to the key by priority (last entry is the most important) 76 | // returns bool = false if nothing has been found 77 | GetData(key string) (interface{}, bool) 78 | 79 | // Same as GetData but will convert the result to a string if possible 80 | // returns bool = false if nothing has been found 81 | GetDataString(key string) (string, bool) 82 | // Same as GetData but will convert the result to an int if possible 83 | // returns bool = false if nothing has been found 84 | GetDataInt(key string) (int, bool) 85 | // Same as GetData but will convert the result to a boolean if possible 86 | // returns second bool = false if nothing has been found 87 | GetDataBool(key string) (bool, bool) 88 | // Same as GetData but will convert the result to a float if possible 89 | // returns bool = false if nothing has been found 90 | GetDataFloat(key string) (float64, bool) 91 | // Same as GetData but will convert the result to a Time if possible 92 | // returns bool = false if nothing has been found 93 | GetDataTime(key string) (time.Time, bool) 94 | // Same as GetData but will convert the result to a XDatasetCollectionDef of data if possible 95 | // returns bool = false if nothing has been found 96 | GetCollection(key string) (XDatasetCollectionDef, bool) 97 | 98 | // Clone the object 99 | Clone() XDatasetCollectionDef 100 | } 101 | -------------------------------------------------------------------------------- /v2/testunit/a.en.language: -------------------------------------------------------------------------------- 1 | 2 | 3 | Welcome to 4 | XCore 5 | 6 | -------------------------------------------------------------------------------- /v2/testunit/a.es.language: -------------------------------------------------------------------------------- 1 | 2 | 3 | Bienvenido a 4 | XCore 5 | 6 | -------------------------------------------------------------------------------- /v2/testunit/a.fr.language: -------------------------------------------------------------------------------- 1 | 2 | 3 | Bienvenue à 4 | XCore 5 | 6 | -------------------------------------------------------------------------------- /v2/testunit/a.template: -------------------------------------------------------------------------------- 1 | 2 | %-- This is a comment. It will not appear in the final code. This files is LF coded, not CRLF, as the string in test (unix end of line) --% 3 | Let's put your name here: {{clientname}}
4 | And lets put your hobbies here:
5 | %-- note the 1rst id is the entry into the data to inject and the second one is the name of the sub-template to use --% 6 | @@hobbies:hobby@@ 7 | %-- And you need the template for each hobby:--% 8 | [[hobby]] 9 | I love {{name}}
10 | [[]] 11 | -------------------------------------------------------------------------------- /v2/testunit/b.template: -------------------------------------------------------------------------------- 1 | &&header&& 2 | &&body&& 3 | 4 | [[header]] 5 | Sports shop: 6 | [[]] 7 | 8 | [[body]] 9 | {{clientname}} Preferred hobby: 10 | &&:preferredhobby>sport:sport.&& %-- will build sport_ + [yes/no] contained into the sport field. Be sure you have a template for each value ! --% 11 | 12 | ??preferredhobby>sport:sport?? 13 | 14 | [[sport.yes]]{{preferredhobby>name}} - It's a sport, sell him things![[]] 15 | [[sport.no]]{{preferredhobby>name}} - It's not a sport, recommend him next store.[[]] 16 | [[sport]]{{preferredhobby>name}} - We do not know that it is.[[]] 17 | 18 | @@hobbies:hobby@@ 19 | [[hobby.first]] 20 | 1. -{{.counter}}- {{name}} {{sport}} 21 | [[]] 22 | [[hobby.last]] 23 | last. -{{.counter}}- {{name}} {{sport}} 24 | [[]] 25 | [[hobby.even]] 26 | 2x. -{{.counter}}- {{name}} {{sport}} 27 | [[]] 28 | [[hobby.key.3]] 29 | 3. -{{.counter}}- {{name}} {{sport}} 30 | [[]] 31 | [[hobby]] 32 | -{{.counter}}- {{name}} {{sport}} 33 | [[]] 34 | [[hobby.none]] 35 | No hobbies 36 | [[]] 37 | [[]] 38 | -------------------------------------------------------------------------------- /v2/testunit/errors.es.txt: -------------------------------------------------------------------------------- 1 | panicerror=Error crítico del sistema 2 | systemerror=Error del sistema 3 | fileerror=Error leyendo el archivo 4 | -------------------------------------------------------------------------------- /v2/testunit/errors.es.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Error crítico del sistema 4 | Error del sistema 5 | Error leyendo el archivo 6 | 7 | -------------------------------------------------------------------------------- /v2/xcache.go: -------------------------------------------------------------------------------- 1 | package xcore 2 | 3 | import ( 4 | "log" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | // XCacheEntry is the cache basic structure to save some data in memory. 10 | type XCacheEntry struct { 11 | // The cache entry has a time to measure expiration if needed, or time of entry in cache: 12 | // - ctime is the creation time (used to validate the object against its source). 13 | ctime time.Time 14 | // - rtime is the last read time (used to clean the cache: the less accessed objects are removed). 15 | rtime time.Time 16 | // - ttl is the max duration of object in cache (ctime + ttl > now = invalid) 17 | ttl time.Duration 18 | // The data as itself is an interface to whatever the user need to cache. 19 | data interface{} 20 | } 21 | 22 | // XCache is the main cache structure, that contains a collection of XCacheEntries and some metadata. 23 | type XCache struct { 24 | // "ID": XCache has a unique id (informative). 25 | ID string 26 | // "Maxitems": The user can creates a cache with a maximum number of elements into it. In this case, when the cache reaches the maximum number of elements stored, then the system makes a clean of 10% of the oldest elements. This type of use is not recommended since is it heavy in CPU use to clean the cache. 27 | Maxitems int 28 | // "Validator" is a function that can be set to check the validity of the data (for instance if the data originates from a file or a database). The validator is called for each Get (and can be heavy for CPU or can wait a long time, for instance if the check is an external database on another cluster). Beware of this. 29 | Validator func(string, time.Time) bool 30 | // "Expire": The user can also create an expiration duration, so every elements in the cache is invalidated after a certain amount of time. It is more recommended to use the cache with an expiration duration. The obsolete objects are destroyed when the user tries to use them and return a "non existence" on Get. (this does not use CPU or extra locks). 31 | Expire time.Duration 32 | // Not available from outside for security, access of data is based on a mutex 33 | // "mutex": The cache owns a mutex to lock access to data to read/write/delete/clean the data, to allow concurrency and multithreading of the cache. 34 | mutex sync.RWMutex 35 | // "pile": The pile keeps the "ordered by date of reading" object keys, so it's fast to clean the data. 36 | items map[string]*XCacheEntry 37 | // "items": The items are a map to cache entries, acceved by the key of entries. 38 | pile []string 39 | } 40 | 41 | // NewXCache function will create a new XCache structure. 42 | // The XCache is resident in memory, supports multithreading and concurrency. 43 | // "id" is the unique id of the XCache. 44 | // "maxitems" is the max authorized quantity of objects into the XCache. If 0, the cache hast no limit in quantity of objects. 45 | // "expire" is a max duration of the objects into the cache. If 0, no limit 46 | // Returns the *XCache created. 47 | func NewXCache(id string, maxitems int, expire time.Duration) *XCache { 48 | if LOG { 49 | log.Printf("Creating cache with data {id: %s, maxitems: %d, expire: %d}", id, maxitems, expire) 50 | } 51 | return &XCache{ 52 | ID: id, 53 | Maxitems: maxitems, 54 | Validator: nil, 55 | Expire: expire, 56 | items: make(map[string]*XCacheEntry), 57 | } 58 | } 59 | 60 | // Set will set an entry in the cache. 61 | // If the entry already exists, just replace it with a new creation date. 62 | // If the entry does not exist, it will insert it in the cache and if the cache if full (maxitems reached), then a clean is called to remove 10%. 63 | // Returns nothing. 64 | func (c *XCache) Set(key string, indata interface{}) { 65 | c.mutex.Lock() 66 | defer c.mutex.Unlock() 67 | // check if the entry already exists 68 | _, ok := c.items[key] 69 | c.items[key] = &XCacheEntry{ctime: time.Now(), rtime: time.Now(), data: indata} 70 | if ok { 71 | c.removeFromPile(key) 72 | } 73 | c.pile = append(c.pile, key) 74 | if c.Maxitems > 0 && len(c.items) >= c.Maxitems { 75 | // We need a cleaning 76 | c.mClean(10) 77 | } 78 | } 79 | 80 | // Set will set a TTL on the entry in the cache. 81 | // If the entry exists, just ads the TTL to the entry 82 | // If the entry does not exist, it does nothing 83 | // Returns nothing. 84 | func (c *XCache) SetTTL(key string, duration time.Duration) { 85 | c.mutex.Lock() 86 | defer c.mutex.Unlock() 87 | _, ok := c.items[key] 88 | if ok { 89 | c.items[key].ttl = duration 90 | } 91 | } 92 | 93 | // removeFromPile will remove an entry key from the ordered pile. 94 | // No lock into this func since it has been set by entry func already 95 | func (c *XCache) removeFromPile(key string) { 96 | // removes the key and append it to the end 97 | for i, x := range c.pile { 98 | if x == key { 99 | if i == len(c.pile)-1 { 100 | c.pile = c.pile[:i] 101 | } else { 102 | c.pile = append(c.pile[:i], c.pile[i+1:]...) 103 | } 104 | break 105 | } 106 | } 107 | } 108 | 109 | // mGet will get the entry of the cache. 110 | func (c *XCache) mGet(key string) (*XCacheEntry, bool) { 111 | c.mutex.RLock() 112 | defer c.mutex.RUnlock() 113 | x, ok := c.items[key] 114 | return x, ok 115 | } 116 | 117 | // Get will get the value of an entry. 118 | // If the entry does not exists, returns nil, false. 119 | // If the entry exists and is invalidated by time or validator function, then returns nil, true. 120 | // If the entry is good, return , false. 121 | func (c *XCache) Get(key string) (interface{}, bool) { 122 | if x, ok := c.mGet(key); ok { 123 | // expired by TTL ? 124 | if x.ttl != 0 { 125 | if x.ctime.Add(x.ttl).Before(time.Now()) { 126 | c.Del(key) 127 | /* 128 | c.mutex.Lock() 129 | delete(c.items, key) 130 | c.removeFromPile(key) 131 | c.mutex.Unlock() 132 | */ 133 | return nil, true 134 | } 135 | } 136 | if c.Validator != nil { 137 | if b := c.Validator(key, x.ctime); !b { 138 | if LOG { 139 | log.Println("Validator invalids entry: " + key) 140 | } 141 | c.Del(key) 142 | /* 143 | c.mutex.Lock() 144 | delete(c.items, key) 145 | c.removeFromPile(key) 146 | c.mutex.Unlock() 147 | */ 148 | return nil, true 149 | } 150 | } 151 | // expired ? 152 | if c.Expire != 0 { 153 | if x.ctime.Add(c.Expire).Before(time.Now()) { 154 | if LOG { 155 | log.Println("Cache timeout Expired: " + key) 156 | } 157 | c.Del(key) 158 | /* 159 | c.mutex.Lock() 160 | delete(c.items, key) 161 | c.removeFromPile(key) 162 | c.mutex.Unlock() 163 | */ 164 | return nil, true 165 | } 166 | } 167 | x.rtime = time.Now() 168 | c.mutex.Lock() 169 | defer c.mutex.Unlock() 170 | c.removeFromPile(key) 171 | c.pile = append(c.pile, key) 172 | return x.data, false 173 | } 174 | return nil, false 175 | } 176 | 177 | // Del will delete the entry of the cache if it exists. 178 | func (c *XCache) Del(key string) { 179 | c.mutex.Lock() 180 | defer c.mutex.Unlock() 181 | delete(c.items, key) 182 | // we should check if the entry exists before trying to removing 183 | c.removeFromPile(key) 184 | } 185 | 186 | // Count will return the quantity of entries in the cache. 187 | func (c *XCache) Count() int { 188 | c.mutex.RLock() 189 | defer c.mutex.RUnlock() 190 | x := len(c.items) 191 | return x 192 | } 193 | 194 | // mClean will delete expired entries, and free perc% of max items based on time. It does not set locks. The caller must do it 195 | // perc = 0 to 100 (percentage to clean). 196 | // Returns quantity of removed entries. 197 | // It Will **not** verify the cache against its source (if Validator is set). If you want to scan that, use the Verify function. 198 | func (c *XCache) mClean(perc int) int { 199 | if LOG { 200 | log.Println("Cleaning cache") 201 | } 202 | i := 0 203 | // 1. clean all expired items 204 | if c.Expire != 0 { 205 | for k, x := range c.items { 206 | if x.ctime.Add(c.Expire).Before(time.Now()) { 207 | if LOG { 208 | log.Println("Cache timeout Expired: " + k) 209 | } 210 | delete(c.items, k) 211 | i++ 212 | } 213 | } 214 | } 215 | // 2. clean perc% of olders 216 | // How many do we have to clean ? 217 | total := len(c.items) 218 | num := total * perc / 100 219 | if LOG { 220 | log.Println("Quantity of elements to remove from cache:", num) 221 | } 222 | for i = 0; i < num; i++ { 223 | delete(c.items, c.pile[i]) 224 | } 225 | c.pile = c.pile[i:] 226 | return i 227 | } 228 | 229 | // Clean will delete expired entries, and free perc% of max items based on time. 230 | // perc = 0 to 100 (percentage to clean). 231 | // Returns quantity of removed entries. 232 | // It Will **not** verify the cache against its source (if Validator is set). If you want to scan that, use the Verify function. 233 | func (c *XCache) Clean(perc int) int { 234 | c.mutex.Lock() 235 | defer c.mutex.Unlock() 236 | return c.mClean(perc) 237 | } 238 | 239 | // Verify will first, Clean(0) keeping all the entries, then will delete expired entries using the Validator function. 240 | // Returns the quantity of removed entries. 241 | // Based on what the validator function does, calling Verify can be **very** slow and cpu dependant. Be very careful. 242 | func (c *XCache) Verify() int { 243 | // 1. clean all expired items, do not touch others 244 | i := c.Clean(0) 245 | // 2. If there is a validator, verifies anything 246 | if c.Validator != nil { 247 | for k, x := range c.items { 248 | if b := c.Validator(k, x.ctime); !b { 249 | if LOG { 250 | log.Println("Validator invalids entry: " + k) 251 | } 252 | c.Del(k) 253 | /* 254 | c.mutex.Lock() 255 | delete(c.items, k) 256 | c.removeFromPile(k) 257 | c.mutex.Unlock() 258 | */ 259 | i++ 260 | } 261 | } 262 | } 263 | return i 264 | } 265 | 266 | // Flush will empty the whole cache and free all the memory of it. 267 | // Returns nothing. 268 | func (c *XCache) Flush() { 269 | c.mutex.Lock() 270 | defer c.mutex.Unlock() 271 | // how to really deletes the data ? ( to free memory) 272 | c.items = make(map[string]*XCacheEntry) 273 | } 274 | -------------------------------------------------------------------------------- /v2/xcache_test.go: -------------------------------------------------------------------------------- 1 | package xcore 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func ExampleNewXCache() { 10 | cache1 := NewXCache("cacheid1", 0, 0) 11 | cache2 := NewXCache("cacheid2", 100, 0) 12 | cache3 := NewXCache("cacheid3", 0, 100*time.Minute) 13 | 14 | fmt.Println("All caches empty:", cache1.Count(), cache2.Count(), cache3.Count()) 15 | // Output: All caches empty: 0 0 0 16 | } 17 | 18 | func TestNewXCache(t *testing.T) { 19 | cache1 := NewXCache("cacheid1", 0, 0) 20 | cache2 := NewXCache("cacheid2", 100, 0) 21 | cache3 := NewXCache("cacheid3", 0, 100*time.Minute) 22 | 23 | if cache1 == nil || cache1.Count() != 0 { 24 | t.Error("Error creating NewXCache cache1") 25 | return 26 | } 27 | if cache2 == nil || cache2.Count() != 0 { 28 | t.Error("Error creating NewXCache cache2") 29 | return 30 | } 31 | if cache3 == nil || cache3.Count() != 0 { 32 | t.Error("Error creating NewXCache cache3") 33 | return 34 | } 35 | } 36 | 37 | func TestXCache(t *testing.T) { 38 | cache := NewXCache("cacheid", 0, 0) 39 | cache.Set("id1", "Data for id1") 40 | cache.Set("id2", "Data for id2") 41 | cache.Set("id3", "Data for id3") 42 | if cache.Count() != 3 { 43 | t.Error("Error counting cache") 44 | return 45 | } 46 | data1, deleted1 := cache.Get("id1") 47 | if deleted1 || data1 != "Data for id1" { 48 | t.Error("Error getting id1 in cache") 49 | return 50 | } 51 | cache.Del("id1") 52 | if cache.Count() != 2 { 53 | t.Error("Error counting cache after delete") 54 | return 55 | } 56 | data2, deleted2 := cache.Get("id1") 57 | if deleted2 || data2 != nil { 58 | t.Error("Error getting id2 in cache") 59 | return 60 | } 61 | } 62 | 63 | func TestXCache_invalidated(t *testing.T) { 64 | cache := NewXCache("cacheid", 0, 1*time.Second) 65 | cache.Set("id1", "Data for id1") 66 | cache.Set("id2", "Data for id2") 67 | cache.Set("id3", "Data for id3") 68 | if cache.Count() != 3 { 69 | t.Error("Error counting cache") 70 | return 71 | } 72 | t.Log("Wait for 2 seconds to invalidate all 3 entries") 73 | time.Sleep(2 * time.Second) 74 | if cache.Count() != 3 { 75 | t.Error("Error counting cache after invalidating") 76 | return 77 | } 78 | data1, deleted1 := cache.Get("id1") 79 | if !deleted1 || data1 != nil { 80 | t.Error("Error getting invalidated id1 in cache") 81 | return 82 | } 83 | if cache.Count() != 2 { 84 | t.Error("Error counting cache after invalidating") 85 | return 86 | } 87 | 88 | data2, deleted2 := cache.Get("id2") 89 | if !deleted2 || data2 != nil { 90 | t.Error("Error getting invalidated id2 in cache") 91 | return 92 | } 93 | if cache.Count() != 1 { 94 | t.Error("Error counting cache after invalidating") 95 | return 96 | } 97 | } 98 | 99 | func TestXCache_cleaning(t *testing.T) { 100 | cache := NewXCache("cacheid", 110, 0) 101 | for i := 0; i < 100; i++ { 102 | cache.Set(fmt.Sprintf("id%d", i), i) 103 | } 104 | if cache.Count() != 100 { 105 | t.Error("Error counting cache") 106 | return 107 | } 108 | // Clean 30% 109 | cache.Clean(30) 110 | if cache.Count() != 70 { 111 | t.Error("Error counting cache after cleaning") 112 | return 113 | } 114 | cache.Flush() 115 | if cache.Count() != 0 { 116 | t.Error("Error counting cache after flushing") 117 | return 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /v2/xdataset.go: -------------------------------------------------------------------------------- 1 | package xcore 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // ===================== 12 | // XDataset 13 | // ===================== 14 | 15 | // XDataset is the basic dataset type that is xdatasetdef inferface compatible 16 | // XDataset IS NOT thread safe 17 | type XDataset map[string]interface{} 18 | 19 | // NewXDataset is used to build an XDataset from a standard map 20 | func NewXDataset(data map[string]interface{}) XDatasetDef { 21 | // Scan data and encapsulate it into the XDataset 22 | ds := &XDataset{} 23 | for i, v := range data { 24 | switch v.(type) { 25 | case []interface{}: 26 | dsc := &XDatasetCollection{} 27 | for _, it := range v.([]interface{}) { 28 | dscit, ok := it.(map[string]interface{}) 29 | if ok { 30 | dscc := NewXDataset(dscit) 31 | dsc.Push(dscc) 32 | } 33 | } 34 | ds.Set(i, dsc) 35 | case []map[string]interface{}: 36 | ds.Set(i, NewXDatasetCollection(v.([]map[string]interface{}))) 37 | case map[string]interface{}: 38 | ds.Set(i, NewXDataset(v.(map[string]interface{}))) 39 | default: 40 | ds.Set(i, v) 41 | } 42 | } 43 | return ds 44 | } 45 | 46 | // String will transform the XDataset into a readable string for humans 47 | func (d *XDataset) String() string { 48 | sdata := []string{} 49 | for key, val := range *d { 50 | sdata = append(sdata, key+":"+fmt.Sprintf("%v", val)) 51 | } 52 | sort.Strings(sdata) // Lets be sure the print is always the same presentation 53 | return "xcore.XDataset{" + strings.Join(sdata, " ") + "}" 54 | } 55 | 56 | // GoString will transform the XDataset into a readable string for humans 57 | func (d *XDataset) GoString() string { 58 | sdata := []string{} 59 | for key, val := range *d { 60 | sdata = append(sdata, key+":"+fmt.Sprintf("%#v", val)) 61 | } 62 | sort.Strings(sdata) // Lets be sure the print is always the same presentation 63 | return "#xcore.XDataset{" + strings.Join(sdata, " ") + "}" 64 | } 65 | 66 | // Set will add a variable key with value data to the XDataset 67 | func (d *XDataset) Set(key string, data interface{}) { 68 | (*d)[key] = data 69 | } 70 | 71 | // Get will read the value of the key variable 72 | func (d *XDataset) Get(key string) (interface{}, bool) { 73 | xid := strings.Split(key, ">") 74 | if len(xid) > 1 { 75 | subset, ok := (*d)[xid[0]] 76 | if !ok { 77 | return nil, false 78 | } 79 | if ds, ok := subset.(XDatasetDef); ok { 80 | return ds.Get(strings.Join(xid[1:], ">")) 81 | } 82 | if dsc, ok := subset.(XDatasetCollectionDef); ok { 83 | entry, err := strconv.Atoi(xid[1]) 84 | if err != nil { 85 | return nil, false 86 | } 87 | ds, _ := dsc.Get(entry) 88 | if ds == nil { 89 | return nil, false 90 | } 91 | if len(xid) == 2 { 92 | return ds, true 93 | } 94 | return ds.Get(strings.Join(xid[2:], ">")) 95 | } 96 | } 97 | data, ok := (*d)[key] 98 | if ok { 99 | return data, true 100 | } 101 | return nil, false 102 | } 103 | 104 | // GetDataset will read the value of the key variable as a XDatasetDef cast type 105 | func (d *XDataset) GetDataset(key string) (XDatasetDef, bool) { 106 | if val, ok := d.Get(key); ok { 107 | if val2, ok2 := val.(XDatasetDef); ok2 { 108 | return val2, true 109 | } 110 | } 111 | return nil, false 112 | } 113 | 114 | // GetCollection will read the value of the key variable as a XDatasetCollection cast type 115 | func (d *XDataset) GetCollection(key string) (XDatasetCollectionDef, bool) { 116 | if val, ok := d.Get(key); ok { 117 | if val2, ok2 := val.(XDatasetCollectionDef); ok2 { 118 | return val2, true 119 | } 120 | } 121 | return nil, false 122 | } 123 | 124 | // GetString will read the value of the key variable as a string cast type 125 | func (d *XDataset) GetString(key string) (string, bool) { 126 | if val, ok := d.Get(key); ok { 127 | if val == nil { 128 | return "", true 129 | } 130 | return fmt.Sprint(val), true 131 | } 132 | return "", false 133 | } 134 | 135 | // GetBool will read the value of the key variable as a boolean cast type 136 | // If the value is int, float, it will be convert with the rule 0: false, != 0: true 137 | // If the value is anything else and it exists, it will return true if it's not nil 138 | func (d *XDataset) GetBool(key string) (bool, bool) { 139 | if val, ok := d.Get(key); ok { 140 | if val2, ok2 := val.(bool); ok2 { 141 | return val2, true 142 | } 143 | if val2, ok2 := val.(time.Time); ok2 { 144 | return !val2.Equal(time.Time{}), true 145 | } 146 | if val2, ok2 := val.(int); ok2 { 147 | return val2 != 0, true 148 | } 149 | if val2, ok2 := val.(int8); ok2 { 150 | return val2 != 0, true 151 | } 152 | if val2, ok2 := val.(int16); ok2 { 153 | return val2 != 0, true 154 | } 155 | if val2, ok2 := val.(int32); ok2 { 156 | return val2 != 0, true 157 | } 158 | if val2, ok2 := val.(int64); ok2 { 159 | return val2 != 0, true 160 | } 161 | if val2, ok2 := val.(uint); ok2 { 162 | return val2 != 0, true 163 | } 164 | if val2, ok2 := val.(uint8); ok2 { 165 | return val2 != 0, true 166 | } 167 | if val2, ok2 := val.(uint16); ok2 { 168 | return val2 != 0, true 169 | } 170 | if val2, ok2 := val.(uint32); ok2 { 171 | return val2 != 0, true 172 | } 173 | if val2, ok2 := val.(uint64); ok2 { 174 | return val2 != 0, true 175 | } 176 | // byte is alias of uint8, no need for it 177 | // if val2, ok2 := val.(byte); ok2 { 178 | // return val2 != 0, true 179 | // } 180 | if val2, ok2 := val.(float32); ok2 { 181 | return val2 != 0, true 182 | } 183 | if val2, ok2 := val.(float64); ok2 { 184 | return val2 != 0, true 185 | } 186 | return val != nil, true 187 | } 188 | return false, false 189 | } 190 | 191 | // GetInt will read the value of the key variable as an integer cast type 192 | // If the value is bool, will return 0/1 193 | // If the value is float, will return integer part of value 194 | func (d *XDataset) GetInt(key string) (int, bool) { 195 | if val, ok := d.Get(key); ok { 196 | if val2, ok2 := val.(bool); ok2 { 197 | if val2 { 198 | return 1, true 199 | } 200 | return 0, true 201 | } 202 | if val2, ok2 := val.(int); ok2 { 203 | return val2, true 204 | } 205 | if val2, ok2 := val.(int8); ok2 { 206 | return int(val2), true 207 | } 208 | if val2, ok2 := val.(int16); ok2 { 209 | return int(val2), true 210 | } 211 | if val2, ok2 := val.(int32); ok2 { 212 | return int(val2), true 213 | } 214 | if val2, ok2 := val.(int64); ok2 { 215 | return int(val2), true 216 | } 217 | if val2, ok2 := val.(uint); ok2 { 218 | return int(val2), true 219 | } 220 | if val2, ok2 := val.(uint8); ok2 { 221 | return int(val2), true 222 | } 223 | if val2, ok2 := val.(uint16); ok2 { 224 | return int(val2), true 225 | } 226 | if val2, ok2 := val.(uint32); ok2 { 227 | return int(val2), true 228 | } 229 | if val2, ok2 := val.(uint64); ok2 { 230 | return int(val2), true 231 | } 232 | // if val2, ok2 := val.(byte); ok2 { 233 | // return int(val2), true 234 | // } 235 | if val2, ok2 := val.(float32); ok2 { 236 | return int(val2), true 237 | } 238 | if val2, ok2 := val.(float64); ok2 { 239 | return int(val2), true 240 | } 241 | if val2, ok2 := val.(time.Time); ok2 { 242 | if val2.Equal(time.Time{}) { 243 | return 0, true 244 | } 245 | return int(val2.Unix()), true 246 | } 247 | } 248 | return 0, false 249 | } 250 | 251 | // GetFloat will read the value of the key variable as a float64 cast type 252 | func (d *XDataset) GetFloat(key string) (float64, bool) { 253 | if val, ok := d.Get(key); ok { 254 | if val2, ok2 := val.(bool); ok2 { 255 | if val2 { 256 | return 1.0, true 257 | } 258 | return 0.0, true 259 | } 260 | if val2, ok2 := val.(int); ok2 { 261 | return float64(val2), true 262 | } 263 | if val2, ok2 := val.(int8); ok2 { 264 | return float64(val2), true 265 | } 266 | if val2, ok2 := val.(int16); ok2 { 267 | return float64(val2), true 268 | } 269 | if val2, ok2 := val.(int32); ok2 { 270 | return float64(val2), true 271 | } 272 | if val2, ok2 := val.(int64); ok2 { 273 | return float64(val2), true 274 | } 275 | if val2, ok2 := val.(uint); ok2 { 276 | return float64(val2), true 277 | } 278 | if val2, ok2 := val.(uint8); ok2 { 279 | return float64(val2), true 280 | } 281 | if val2, ok2 := val.(uint16); ok2 { 282 | return float64(val2), true 283 | } 284 | if val2, ok2 := val.(uint32); ok2 { 285 | return float64(val2), true 286 | } 287 | if val2, ok2 := val.(uint64); ok2 { 288 | return float64(val2), true 289 | } 290 | // if val2, ok2 := val.(byte); ok2 { 291 | // return float64(val2), true 292 | // } 293 | if val2, ok2 := val.(float32); ok2 { 294 | return float64(val2), true 295 | } 296 | if val2, ok2 := val.(float64); ok2 { 297 | return val2, true 298 | } 299 | if val2, ok2 := val.(time.Time); ok2 { 300 | if val2.Equal(time.Time{}) { 301 | return 0, true 302 | } 303 | return float64(val2.Unix()), true 304 | } 305 | } 306 | return 0.0, false 307 | } 308 | 309 | // GetTime will read the value of the key variable as a time cast type 310 | func (d *XDataset) GetTime(key string) (time.Time, bool) { 311 | if val, ok := d.Get(key); ok { 312 | if val2, ok2 := val.(time.Time); ok2 { 313 | return val2, true 314 | } 315 | // int, float, conversion ? 316 | } 317 | return time.Time{}, false 318 | } 319 | 320 | // GetStringCollection will read the value of the key variable as a collection of strings cast type 321 | func (d *XDataset) GetStringCollection(key string) ([]string, bool) { 322 | if val, ok := d.Get(key); ok { 323 | if val2, ok2 := val.([]string); ok2 { 324 | return val2, true 325 | } 326 | // other types: conversion? 327 | } 328 | return nil, false 329 | } 330 | 331 | // GetBoolCollection will read the value of the key variable as a collection of bool cast type 332 | func (d *XDataset) GetBoolCollection(key string) ([]bool, bool) { 333 | if val, ok := d.Get(key); ok { 334 | if val2, ok2 := val.([]bool); ok2 { 335 | return val2, true 336 | } 337 | // other types: conversion? 338 | } 339 | return nil, false 340 | } 341 | 342 | // GetIntCollection will read the value of the key variable as a collection of int cast type 343 | func (d *XDataset) GetIntCollection(key string) ([]int, bool) { 344 | if val, ok := d.Get(key); ok { 345 | if val2, ok2 := val.([]int); ok2 { 346 | return val2, true 347 | } 348 | // other types: conversion? 349 | } 350 | return nil, false 351 | } 352 | 353 | // GetFloatCollection will read the value of the key variable as a collection of float cast type 354 | // If the field is not an array it will be converted to an array. 355 | // If the field is an array of another type, it will be converted. 356 | func (d *XDataset) GetFloatCollection(key string) ([]float64, bool) { 357 | if val, ok := d.Get(key); ok { 358 | if val2, ok2 := val.([]float64); ok2 { 359 | return val2, true 360 | } 361 | // other types: conversion? 362 | 363 | } 364 | return nil, false 365 | } 366 | 367 | // GetTimeCollection will read the value of the key variable as a collection of time cast type 368 | func (d *XDataset) GetTimeCollection(key string) ([]time.Time, bool) { 369 | if val, ok := d.Get(key); ok { 370 | if val2, ok2 := val.([]time.Time); ok2 { 371 | return val2, true 372 | } 373 | } 374 | return nil, false 375 | } 376 | 377 | // Del will deletes the variable 378 | func (d *XDataset) Del(key string) { 379 | delete(*d, key) 380 | } 381 | 382 | // Clone will creates a totally new data memory cloned from this object 383 | func (d *XDataset) Clone() XDatasetDef { 384 | cloned := &XDataset{} 385 | for id, val := range *d { 386 | clonedval := val 387 | // If the object is also cloneable, we clone it 388 | if cloneable1, ok := val.(interface{ Clone() XDatasetDef }); ok { 389 | clonedval = cloneable1.Clone() 390 | } 391 | if cloneable2, ok := val.(interface{ Clone() XDatasetCollectionDef }); ok { 392 | clonedval = cloneable2.Clone() 393 | } 394 | cloned.Set(id, clonedval) 395 | } 396 | return cloned 397 | } 398 | -------------------------------------------------------------------------------- /v2/xdatasetcollection.go: -------------------------------------------------------------------------------- 1 | package xcore 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | // ===================== 10 | // XDatasetCollection 11 | // ===================== 12 | 13 | // XDatasetCollection is the basic collection of XDatasetDefs 14 | type XDatasetCollection []XDatasetDef 15 | 16 | // NewXDatasetCollection is used to build an XDatasetCollection from a standard []map 17 | func NewXDatasetCollection(data []map[string]interface{}) XDatasetCollectionDef { 18 | // Scan data and encapsulate it into the XDataset 19 | dsc := &XDatasetCollection{} 20 | for _, v := range data { 21 | dsc.Push(NewXDataset(v)) 22 | } 23 | return dsc 24 | } 25 | 26 | // String will transform the XDataset into a readable string 27 | func (d *XDatasetCollection) String() string { 28 | str := "XDatasetCollection[" 29 | for key, val := range *d { 30 | str += strconv.Itoa(key) + ":" + fmt.Sprint(val) + " " 31 | } 32 | str += "]" 33 | return str 34 | } 35 | 36 | // GoString will transform the XDataset into a readable string for humans 37 | func (d *XDatasetCollection) GoString() string { 38 | return d.String() 39 | } 40 | 41 | // Unshift will adds a XDatasetDef at the beginning of the collection 42 | func (d *XDatasetCollection) Unshift(data XDatasetDef) { 43 | *d = append([]XDatasetDef{data}, (*d)...) 44 | } 45 | 46 | // Shift will remove the element at the beginning of the collection 47 | func (d *XDatasetCollection) Shift() XDatasetDef { 48 | data := (*d)[0] 49 | *d = (*d)[1:] 50 | return data 51 | } 52 | 53 | // Push will adds a XDatasetDef at the end of the collection 54 | func (d *XDatasetCollection) Push(data XDatasetDef) { 55 | *d = append(*d, data) 56 | } 57 | 58 | // Pop will remove the element at the end of the collection 59 | func (d *XDatasetCollection) Pop() XDatasetDef { 60 | data := (*d)[len(*d)-1] 61 | *d = (*d)[:len(*d)-1] 62 | return data 63 | } 64 | 65 | // Count will return the quantity of elements into the collection 66 | func (d *XDatasetCollection) Count() int { 67 | return len(*d) 68 | } 69 | 70 | // Get will retrieve an element by index from the collection 71 | func (d *XDatasetCollection) Get(index int) (XDatasetDef, bool) { 72 | if index < 0 || index >= len(*d) { 73 | return nil, false 74 | } 75 | return (*d)[index], true 76 | } 77 | 78 | // GetData will retrieve the first available data identified by key from the collection ordered by index 79 | func (d *XDatasetCollection) GetData(key string) (interface{}, bool) { 80 | for i := len(*d) - 1; i >= 0; i-- { 81 | val, ok := (*d)[i].Get(key) 82 | if ok { 83 | return val, true 84 | } 85 | } 86 | return nil, false 87 | } 88 | 89 | // GetDataString will retrieve the first available data identified by key from the collection ordered by index and return it as a string 90 | func (d *XDatasetCollection) GetDataString(key string) (string, bool) { 91 | if val, ok := d.GetData(key); ok { 92 | if val == nil { 93 | return "", true 94 | } 95 | return fmt.Sprint(val), true 96 | } 97 | return "", false 98 | } 99 | 100 | // GetDataBool will retrieve the first available data identified by key from the collection ordered by index and return it as a boolean 101 | func (d *XDatasetCollection) GetDataBool(key string) (bool, bool) { 102 | if val, ok := d.GetData(key); ok { 103 | if val2, ok2 := val.(bool); ok2 { 104 | return val2, true 105 | } 106 | } 107 | return false, false 108 | } 109 | 110 | // GetDataInt will retrieve the first available data identified by key from the collection ordered by index and return it as an integer 111 | func (d *XDatasetCollection) GetDataInt(key string) (int, bool) { 112 | if val, ok := d.GetData(key); ok { 113 | if val2, ok2 := val.(int); ok2 { 114 | return val2, true 115 | } 116 | } 117 | return 0, false 118 | } 119 | 120 | // GetDataFloat will retrieve the first available data identified by key from the collection ordered by index and return it as a float 121 | func (d *XDatasetCollection) GetDataFloat(key string) (float64, bool) { 122 | if val, ok := d.GetData(key); ok { 123 | if val2, ok2 := val.(float64); ok2 { 124 | return val2, true 125 | } 126 | } 127 | return 0, false 128 | } 129 | 130 | // GetDataTime will retrieve the first available data identified by key from the collection ordered by index and return it as a time 131 | func (d *XDatasetCollection) GetDataTime(key string) (time.Time, bool) { 132 | if val, ok := d.GetData(key); ok { 133 | if val2, ok2 := val.(time.Time); ok2 { 134 | return val2, true 135 | } 136 | } 137 | return time.Time{}, false 138 | } 139 | 140 | // GetCollection will retrieve a collection from the XDatasetCollection 141 | func (d *XDatasetCollection) GetCollection(key string) (XDatasetCollectionDef, bool) { 142 | v, ok := d.GetData(key) 143 | // Verify v IS actually a XDatasetCollectionDef to avoid the error 144 | if ok { 145 | dsd, ok2 := v.(XDatasetCollectionDef) 146 | if ok2 { 147 | return dsd, true 148 | } 149 | } 150 | return nil, false 151 | } 152 | 153 | // Clone will make a full copy of the object into memory 154 | func (d *XDatasetCollection) Clone() XDatasetCollectionDef { 155 | cloned := &XDatasetCollection{} 156 | for _, val := range *d { 157 | *cloned = append(*cloned, val.Clone()) 158 | } 159 | return cloned 160 | } 161 | -------------------------------------------------------------------------------- /v2/xdatasetcollection_test.go: -------------------------------------------------------------------------------- 1 | package xcore 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func ExampleXDatasetCollection() { 10 | 11 | tmp, _ := time.Parse(time.RFC3339, "2020-01-01T12:00:00.0Z") 12 | ds := &XDataset{ 13 | "v1": 123, 14 | "v2": "abc", 15 | "vt": tmp, 16 | "v3": true, 17 | "vpi": 3.1415927, 18 | } 19 | fmt.Println(ds) 20 | 21 | data := &XDataset{ 22 | "clientname": "Fred", 23 | "clientpicture": "face.jpg", 24 | "hobbies": &XDatasetCollection{ 25 | &XDataset{"name": "Football", "sport": "yes"}, 26 | &XDataset{"name": "Ping-pong", "sport": "yes"}, 27 | &XDataset{"name": "Swimming", "sport": "yes"}, 28 | &XDataset{"name": "Videogames", "sport": "no"}, 29 | }, 30 | "preferredhobby": &XDataset{ 31 | "name": "Baseball", 32 | "sport": "yes", 33 | }, 34 | "metadata": &XDataset{ 35 | "preferred-color": "blue", 36 | "Salary": 3568.65, 37 | "hiredate": tmp, 38 | }, 39 | } 40 | 41 | fmt.Println(data) 42 | // Output: 43 | // xcore.XDataset{v1:123 v2:abc v3:true vpi:3.1415927 vt:2020-01-01 12:00:00 +0000 UTC} 44 | // xcore.XDataset{clientname:Fred clientpicture:face.jpg hobbies:XDatasetCollection[0:xcore.XDataset{name:Football sport:yes} 1:xcore.XDataset{name:Ping-pong sport:yes} 2:xcore.XDataset{name:Swimming sport:yes} 3:xcore.XDataset{name:Videogames sport:no} ] metadata:xcore.XDataset{Salary:3568.65 hiredate:2020-01-01 12:00:00 +0000 UTC preferred-color:blue} preferredhobby:xcore.XDataset{name:Baseball sport:yes}} 45 | } 46 | 47 | func TestLoadJSONInXDatasetCollectionTS(t *testing.T) { 48 | 49 | } 50 | -------------------------------------------------------------------------------- /v2/xdatasetcollectionts.go: -------------------------------------------------------------------------------- 1 | package xcore 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | // XDatasetCollectionTS is the basic collection of XDatasetDefs 11 | type XDatasetCollectionTS struct { 12 | mutex sync.RWMutex 13 | data []XDatasetDef 14 | } 15 | 16 | // String will transform the XDataset into a readable string 17 | func (dc *XDatasetCollectionTS) String() string { 18 | str := "XDatasetCollectionTS[" 19 | dc.mutex.RLock() 20 | for key, val := range dc.data { 21 | str += strconv.Itoa(key) + ":" + fmt.Sprint(val) + " " 22 | } 23 | dc.mutex.RUnlock() 24 | str += "]" 25 | return str 26 | } 27 | 28 | // GoString will transform the XDataset into a readable string for humans 29 | func (dc *XDatasetCollectionTS) GoString() string { 30 | return dc.String() 31 | } 32 | 33 | // Unshift will adds a XDatasetDef at the beginning of the collection 34 | func (dc *XDatasetCollectionTS) Unshift(data XDatasetDef) { 35 | dc.mutex.Lock() 36 | dc.data = append([]XDatasetDef{data}, dc.data...) 37 | dc.mutex.Unlock() 38 | } 39 | 40 | // Shift will remove the element at the beginning of the collection 41 | func (dc *XDatasetCollectionTS) Shift() XDatasetDef { 42 | dc.mutex.Lock() 43 | data := dc.data[0] 44 | dc.data = dc.data[1:] 45 | dc.mutex.Unlock() 46 | return data 47 | } 48 | 49 | // Push will adds a XDatasetDef at the end of the collection 50 | func (dc *XDatasetCollectionTS) Push(data XDatasetDef) { 51 | dc.mutex.Lock() 52 | dc.data = append(dc.data, data) 53 | dc.mutex.Unlock() 54 | } 55 | 56 | // Pop will remove the element at the end of the collection 57 | func (dc *XDatasetCollectionTS) Pop() XDatasetDef { 58 | dc.mutex.Lock() 59 | data := dc.data[len(dc.data)-1] 60 | dc.data = dc.data[:len(dc.data)-1] 61 | dc.mutex.Unlock() 62 | return data 63 | } 64 | 65 | // Count will return the quantity of elements into the collection 66 | func (dc *XDatasetCollectionTS) Count() int { 67 | dc.mutex.RLock() 68 | defer dc.mutex.RUnlock() 69 | return len(dc.data) 70 | } 71 | 72 | // Get will retrieve an element by index from the collection 73 | func (dc *XDatasetCollectionTS) Get(index int) (XDatasetDef, bool) { 74 | dc.mutex.RLock() 75 | defer dc.mutex.RUnlock() 76 | if index < 0 || index >= len(dc.data) { 77 | return nil, false 78 | } 79 | return dc.data[index], true 80 | } 81 | 82 | // GetData will retrieve the first available data identified by key from the collection ordered by index 83 | func (dc *XDatasetCollectionTS) GetData(key string) (interface{}, bool) { 84 | dc.mutex.RLock() 85 | l := len(dc.data) - 1 86 | dc.mutex.RUnlock() 87 | for i := l; i >= 0; i-- { 88 | dc.mutex.RLock() 89 | dcc := dc.data[i] 90 | dc.mutex.RUnlock() 91 | val, ok := dcc.Get(key) 92 | if ok { 93 | return val, true 94 | } 95 | } 96 | return nil, false 97 | } 98 | 99 | // GetDataString will retrieve the first available data identified by key from the collection ordered by index and return it as a string 100 | func (dc *XDatasetCollectionTS) GetDataString(key string) (string, bool) { 101 | v, ok := dc.GetData(key) 102 | if ok { 103 | return fmt.Sprint(v), true 104 | } 105 | return "", false 106 | } 107 | 108 | // GetDataBool will retrieve the first available data identified by key from the collection ordered by index and return it as a boolean 109 | func (dc *XDatasetCollectionTS) GetDataBool(key string) (bool, bool) { 110 | if val, ok := dc.GetData(key); ok { 111 | if val2, ok2 := val.(bool); ok2 { 112 | return val2, true 113 | } 114 | } 115 | return false, false 116 | } 117 | 118 | // GetDataInt will retrieve the first available data identified by key from the collection ordered by index and return it as an integer 119 | func (dc *XDatasetCollectionTS) GetDataInt(key string) (int, bool) { 120 | if val, ok := dc.GetData(key); ok { 121 | if val2, ok2 := val.(int); ok2 { 122 | return val2, true 123 | } 124 | } 125 | return 0, false 126 | } 127 | 128 | // GetDataFloat will retrieve the first available data identified by key from the collection ordered by index and return it as a float 129 | func (dc *XDatasetCollectionTS) GetDataFloat(key string) (float64, bool) { 130 | if val, ok := dc.GetData(key); ok { 131 | if val2, ok2 := val.(float64); ok2 { 132 | return val2, true 133 | } 134 | } 135 | return 0, false 136 | } 137 | 138 | // GetDataTime will retrieve the first available data identified by key from the collection ordered by index and return it as a time 139 | func (dc *XDatasetCollectionTS) GetDataTime(key string) (time.Time, bool) { 140 | if val, ok := dc.GetData(key); ok { 141 | if val2, ok2 := val.(time.Time); ok2 { 142 | return val2, true 143 | } 144 | } 145 | return time.Time{}, false 146 | } 147 | 148 | // GetCollection will retrieve a collection from the XDatasetCollectionTS 149 | func (dc *XDatasetCollectionTS) GetCollection(key string) (XDatasetCollectionDef, bool) { 150 | v, ok := dc.GetData(key) 151 | // Verify v IS actually a XDatasetCollectionDef to avoid the error 152 | if ok { 153 | return v.(XDatasetCollectionDef), true 154 | } 155 | return nil, false 156 | } 157 | 158 | // Clone will make a full copy of the object into memory 159 | func (dc *XDatasetCollectionTS) Clone() XDatasetCollectionDef { 160 | cloned := &XDatasetCollectionTS{} 161 | for _, val := range dc.data { 162 | cloned.data = append(cloned.data, val.Clone()) 163 | } 164 | return cloned 165 | } 166 | -------------------------------------------------------------------------------- /v2/xdatasetcollectionts_test.go: -------------------------------------------------------------------------------- 1 | package xcore 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func ExampleXDatasetCollectionTS() { 10 | 11 | tmp, _ := time.Parse(time.RFC3339, "2020-01-01T12:00:00.0Z") 12 | ds := &XDataset{ 13 | "v1": 123, 14 | "v2": "abc", 15 | "vt": tmp, 16 | "v3": true, 17 | "vpi": 3.1415927, 18 | } 19 | fmt.Println(ds) 20 | 21 | data := &XDataset{ 22 | "clientname": "Fred", 23 | "clientpicture": "face.jpg", 24 | "hobbies": &XDatasetCollection{ 25 | &XDataset{"name": "Football", "sport": "yes"}, 26 | &XDataset{"name": "Ping-pong", "sport": "yes"}, 27 | &XDataset{"name": "Swimming", "sport": "yes"}, 28 | &XDataset{"name": "Videogames", "sport": "no"}, 29 | }, 30 | "preferredhobby": &XDataset{ 31 | "name": "Baseball", 32 | "sport": "yes", 33 | }, 34 | "metadata": &XDataset{ 35 | "preferred-color": "blue", 36 | "Salary": 3568.65, 37 | "hiredate": tmp, 38 | }, 39 | } 40 | 41 | fmt.Println(data) 42 | // Output: 43 | // xcore.XDataset{v1:123 v2:abc v3:true vpi:3.1415927 vt:2020-01-01 12:00:00 +0000 UTC} 44 | // xcore.XDataset{clientname:Fred clientpicture:face.jpg hobbies:XDatasetCollection[0:xcore.XDataset{name:Football sport:yes} 1:xcore.XDataset{name:Ping-pong sport:yes} 2:xcore.XDataset{name:Swimming sport:yes} 3:xcore.XDataset{name:Videogames sport:no} ] metadata:xcore.XDataset{Salary:3568.65 hiredate:2020-01-01 12:00:00 +0000 UTC preferred-color:blue} preferredhobby:xcore.XDataset{name:Baseball sport:yes}} 45 | } 46 | 47 | func TestXDatasetCollectionTS_simple_print(t *testing.T) { 48 | 49 | // 1. Create a simple XDataset 50 | tmp, _ := time.Parse(time.RFC3339, "2020-01-01T12:00:00.0Z") 51 | ds := &XDataset{ 52 | "v1": 123, 53 | "v2": "abc", 54 | "vt": tmp, 55 | "v3": true, 56 | "vpi": 3.1415927, 57 | } 58 | 59 | // 2. print 60 | str := fmt.Sprintf("%v", ds) 61 | if str != "xcore.XDataset{v1:123 v2:abc v3:true vpi:3.1415927 vt:2020-01-01 12:00:00 +0000 UTC}" { 62 | t.Error("Error creating and printing simple XDataset " + str) 63 | return 64 | } 65 | 66 | str = fmt.Sprintf("%#v", ds) 67 | if str != "#xcore.XDataset{v1:123 v2:\"abc\" v3:true vpi:3.1415927 vt:time.Date(2020, time.January, 1, 12, 0, 0, 0, time.UTC)}" { 68 | t.Error("Error creating and #printing simple XDataset " + str) 69 | return 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /v2/xdatasetts.go: -------------------------------------------------------------------------------- 1 | package xcore 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | // XDatasetTS is a thread safe xdataset (not thread safe) encapsulator 9 | // XDatasetTS IS thread safe 10 | type XDatasetTS struct { 11 | mutex sync.RWMutex 12 | data XDatasetDef 13 | } 14 | 15 | // NewXDatasetTS builds a thread safe encapsulator on a XDataset compatible structure 16 | func NewXDatasetTS(maindata XDatasetDef) *XDatasetTS { 17 | ds := &XDatasetTS{ 18 | data: maindata, 19 | } 20 | return ds 21 | } 22 | 23 | // String will transform the XDataset into a readable string for humans 24 | func (ds *XDatasetTS) String() string { 25 | ds.mutex.RLock() 26 | defer ds.mutex.RUnlock() 27 | return ds.data.String() 28 | } 29 | 30 | // GoString will transform the XDataset into a readable string for humans 31 | func (ds *XDatasetTS) GoString() string { 32 | ds.mutex.RLock() 33 | defer ds.mutex.RUnlock() 34 | return ds.data.GoString() 35 | } 36 | 37 | // Set will add a variable key with value data to the XDatasetTS 38 | func (ds *XDatasetTS) Set(key string, data interface{}) { 39 | ds.mutex.Lock() 40 | ds.data.Set(key, data) 41 | ds.mutex.Unlock() 42 | } 43 | 44 | // Get will read the value of the key variable 45 | func (ds *XDatasetTS) Get(key string) (interface{}, bool) { 46 | ds.mutex.RLock() 47 | defer ds.mutex.RUnlock() 48 | return ds.data.Get(key) 49 | } 50 | 51 | // GetDataset will read the value of the key variable as a XDatasetDef cast type 52 | func (ds *XDatasetTS) GetDataset(key string) (XDatasetDef, bool) { 53 | ds.mutex.RLock() 54 | defer ds.mutex.RUnlock() 55 | return ds.data.GetDataset(key) 56 | } 57 | 58 | // GetCollection will read the value of the key variable as a XDatasetCollection cast type 59 | func (ds *XDatasetTS) GetCollection(key string) (XDatasetCollectionDef, bool) { 60 | ds.mutex.RLock() 61 | defer ds.mutex.RUnlock() 62 | return ds.data.GetCollection(key) 63 | } 64 | 65 | // GetString will read the value of the key variable as a string cast type 66 | func (ds *XDatasetTS) GetString(key string) (string, bool) { 67 | ds.mutex.RLock() 68 | defer ds.mutex.RUnlock() 69 | return ds.data.GetString(key) 70 | } 71 | 72 | // GetBool will read the value of the key variable as a boolean cast type 73 | // If the value is int, float, it will be convert with the rule 0: false, != 0: true 74 | // If the value is anything else and it exists, it will return true if it's not nil 75 | func (ds *XDatasetTS) GetBool(key string) (bool, bool) { 76 | ds.mutex.RLock() 77 | defer ds.mutex.RUnlock() 78 | return ds.data.GetBool(key) 79 | } 80 | 81 | // GetInt will read the value of the key variable as an integer cast type 82 | // If the value is bool, will return 0/1 83 | // If the value is float, will return integer part of value 84 | func (ds *XDatasetTS) GetInt(key string) (int, bool) { 85 | ds.mutex.RLock() 86 | defer ds.mutex.RUnlock() 87 | return ds.data.GetInt(key) 88 | } 89 | 90 | // GetFloat will read the value of the key variable as a float64 cast type 91 | func (ds *XDatasetTS) GetFloat(key string) (float64, bool) { 92 | ds.mutex.RLock() 93 | defer ds.mutex.RUnlock() 94 | return ds.data.GetFloat(key) 95 | } 96 | 97 | // GetTime will read the value of the key variable as a time cast type 98 | func (ds *XDatasetTS) GetTime(key string) (time.Time, bool) { 99 | ds.mutex.RLock() 100 | defer ds.mutex.RUnlock() 101 | return ds.data.GetTime(key) 102 | } 103 | 104 | // GetStringCollection will read the value of the key variable as a collection of strings cast type 105 | func (ds *XDatasetTS) GetStringCollection(key string) ([]string, bool) { 106 | ds.mutex.RLock() 107 | defer ds.mutex.RUnlock() 108 | return ds.data.GetStringCollection(key) 109 | } 110 | 111 | // GetBoolCollection will read the value of the key variable as a collection of bool cast type 112 | func (ds *XDatasetTS) GetBoolCollection(key string) ([]bool, bool) { 113 | ds.mutex.RLock() 114 | defer ds.mutex.RUnlock() 115 | return ds.data.GetBoolCollection(key) 116 | } 117 | 118 | // GetIntCollection will read the value of the key variable as a collection of int cast type 119 | func (ds *XDatasetTS) GetIntCollection(key string) ([]int, bool) { 120 | ds.mutex.RLock() 121 | defer ds.mutex.RUnlock() 122 | return ds.data.GetIntCollection(key) 123 | } 124 | 125 | // GetFloatCollection will read the value of the key variable as a collection of float cast type 126 | func (ds *XDatasetTS) GetFloatCollection(key string) ([]float64, bool) { 127 | ds.mutex.RLock() 128 | defer ds.mutex.RUnlock() 129 | return ds.data.GetFloatCollection(key) 130 | } 131 | 132 | // GetTimeCollection will read the value of the key variable as a collection of time cast type 133 | func (ds *XDatasetTS) GetTimeCollection(key string) ([]time.Time, bool) { 134 | ds.mutex.RLock() 135 | defer ds.mutex.RUnlock() 136 | return ds.data.GetTimeCollection(key) 137 | } 138 | 139 | // Del will deletes the variable 140 | func (ds *XDatasetTS) Del(key string) { 141 | ds.mutex.Lock() 142 | ds.data.Del(key) 143 | ds.mutex.Unlock() 144 | } 145 | 146 | // Clone will creates a totally new data memory cloned from this object 147 | func (ds *XDatasetTS) Clone() XDatasetDef { 148 | cloned := &XDatasetTS{} 149 | ds.mutex.RLock() 150 | cloned.data = ds.data.Clone() 151 | ds.mutex.RUnlock() 152 | return cloned 153 | } 154 | -------------------------------------------------------------------------------- /v2/xlanguage.go: -------------------------------------------------------------------------------- 1 | package xcore 2 | 3 | import ( 4 | "bufio" 5 | "encoding/xml" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "sort" 10 | "strings" 11 | "sync" 12 | 13 | "golang.org/x/text/language" 14 | ) 15 | 16 | // XLanguage is the oficial structure for the user 17 | type XLanguage struct { 18 | Name string 19 | Language language.Tag 20 | entries map[string]string 21 | status map[string]string 22 | mutex sync.RWMutex 23 | } 24 | 25 | // NewXLanguage will create an empty Language structure with a name and a language 26 | func NewXLanguage(name string, lang language.Tag) *XLanguage { 27 | return &XLanguage{Name: name, Language: lang, entries: make(map[string]string), status: make(map[string]string)} 28 | } 29 | 30 | // NewXLanguageFromXMLFile will create an XLanguage structure with the data into the XML file 31 | // Returns nil if there is an error 32 | func NewXLanguageFromXMLFile(file string) (*XLanguage, error) { 33 | lang := &XLanguage{entries: make(map[string]string), status: make(map[string]string)} 34 | err := lang.LoadXMLFile(file) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return lang, nil 39 | } 40 | 41 | // NewXLanguageFromXMLString will create an XLanguage structure with the data into the XML String 42 | // Returns nil if there is an error 43 | func NewXLanguageFromXMLString(xml string) (*XLanguage, error) { 44 | lang := &XLanguage{entries: make(map[string]string), status: make(map[string]string)} 45 | err := lang.LoadXMLString(xml) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return lang, nil 50 | } 51 | 52 | // NewXLanguageFromFile will create an XLanguage structure with the data into the text file 53 | // Returns nil if there is an error 54 | func NewXLanguageFromFile(file string) (*XLanguage, error) { 55 | l := &XLanguage{entries: make(map[string]string), status: make(map[string]string)} 56 | err := l.LoadFile(file) 57 | if err != nil { 58 | return nil, err 59 | } 60 | return l, nil 61 | } 62 | 63 | // NewXLanguageFromString will create an XLanguage structure with the data into the string 64 | // Returns nil if there is an error 65 | func NewXLanguageFromString(data string) (*XLanguage, error) { 66 | l := &XLanguage{entries: make(map[string]string), status: make(map[string]string)} 67 | err := l.LoadString(data) 68 | if err != nil { 69 | return nil, err 70 | } 71 | return l, nil 72 | } 73 | 74 | // LoadXMLFile will Load a language from an XML file and replace the content of the XLanguage structure with the new data 75 | // Returns nil if there is an error 76 | func (l *XLanguage) LoadXMLFile(file string) error { 77 | data, err := ioutil.ReadFile(file) 78 | if err != nil { 79 | return err 80 | } 81 | return l.LoadXMLString(string(data)) 82 | } 83 | 84 | // LoadXMLString will Load a language from an XML file and replace the content of the XLanguage structure with the new data 85 | // Returns nil if there is an error 86 | func (l *XLanguage) LoadXMLString(data string) error { 87 | // Temporal structures for XML loading 88 | type xentry struct { 89 | ID string `xml:"id,attr"` 90 | Entry string `xml:",chardata"` 91 | Status string `xml:"status,attr"` 92 | } 93 | 94 | type xlang struct { 95 | Name string `xml:"id,attr"` 96 | Language string `xml:"lang,attr"` 97 | Entries []xentry `xml:"entry"` 98 | } 99 | 100 | // Unmarshal 101 | temp := &xlang{} 102 | err := xml.Unmarshal([]byte(data), temp) 103 | 104 | if err != nil { 105 | return err 106 | } 107 | 108 | // Scan to our XLanguage Object 109 | l.Name = temp.Name 110 | l.Language, _ = language.Parse(temp.Language) 111 | l.mutex.Lock() 112 | defer l.mutex.Unlock() 113 | for _, e := range temp.Entries { 114 | l.entries[e.ID] = e.Entry 115 | l.status[e.ID] = e.Status 116 | } 117 | return nil 118 | } 119 | 120 | // LoadFile will Load a language from a file and replace the content of the XLanguage structure with the new data 121 | // Returns nil if there is an error 122 | func (l *XLanguage) LoadFile(file string) error { 123 | flatFile, err := os.Open(file) 124 | if err != nil { 125 | return err 126 | } 127 | data, err := ioutil.ReadAll(flatFile) 128 | if err != nil { 129 | return err 130 | } 131 | err = flatFile.Close() 132 | if err != nil { 133 | return err 134 | } 135 | return l.LoadString(string(data)) 136 | } 137 | 138 | // LoadString will Load a language from a string and replace the content of the XLanguage structure with the new data 139 | // Returns nil if there is an error 140 | func (l *XLanguage) LoadString(data string) error { 141 | scanner := bufio.NewScanner(strings.NewReader(data)) 142 | for scanner.Scan() { 143 | line := scanner.Text() 144 | posequal := strings.Index(line, "=") 145 | 146 | // we ignore empty and comments lines, no key=value lines too 147 | if len(line) == 0 || line[0] == '#' || line[0] == ';' || posequal < 0 { 148 | continue 149 | } 150 | 151 | // we separate the key. if there is no key, we ignore the data 152 | key := strings.TrimSpace(line[:posequal]) 153 | if len(key) == 0 { 154 | continue 155 | } 156 | 157 | // we capture the value if it exists. If not, the key entry is initialized with a nil value 158 | value := "" 159 | if len(line) > posequal { 160 | value = strings.TrimSpace(line[posequal+1:]) 161 | } 162 | l.mutex.Lock() 163 | l.entries[key] = value 164 | l.mutex.Unlock() 165 | } 166 | if err := scanner.Err(); err != nil { 167 | return err 168 | } 169 | return nil 170 | } 171 | 172 | // SetName will set the name of the language table 173 | func (l *XLanguage) SetName(name string) { 174 | l.Name = name 175 | } 176 | 177 | // SetLanguage will set the language ISO code (2 letters) of the language table 178 | func (l *XLanguage) SetLanguage(lang language.Tag) { 179 | l.Language = lang 180 | } 181 | 182 | // GetName will return the name of the language table 183 | func (l *XLanguage) GetName() string { 184 | return l.Name 185 | } 186 | 187 | // GetLanguage will return the language of the language table 188 | func (l *XLanguage) GetLanguage() language.Tag { 189 | return l.Language 190 | } 191 | 192 | // Set will add an entry id-value into the language table 193 | func (l *XLanguage) Set(entry string, value string) { 194 | l.mutex.Lock() 195 | defer l.mutex.Unlock() 196 | l.entries[entry] = value 197 | } 198 | 199 | // Get will read an entry id-value from the language table 200 | func (l *XLanguage) Get(entry string) string { 201 | l.mutex.RLock() 202 | defer l.mutex.RUnlock() 203 | v, ok := l.entries[entry] 204 | if ok { 205 | return v 206 | } 207 | return "" 208 | } 209 | 210 | // Set will add an entry id-value into the language table 211 | func (l *XLanguage) SetStatus(entry string, value string) { 212 | l.mutex.Lock() 213 | defer l.mutex.Unlock() 214 | l.status[entry] = value 215 | } 216 | 217 | // Get will read an entry id-value from the language table 218 | func (l *XLanguage) GetStatus(entry string) string { 219 | l.mutex.RLock() 220 | defer l.mutex.RUnlock() 221 | v, ok := l.status[entry] 222 | if ok { 223 | return v 224 | } 225 | return "" 226 | } 227 | 228 | // Del will remove an entry id-value from the language table 229 | func (l *XLanguage) Del(entry string) { 230 | l.mutex.Lock() 231 | defer l.mutex.Unlock() 232 | delete(l.entries, entry) 233 | delete(l.status, entry) 234 | } 235 | 236 | // GetEntries will return a COPY of the key-values pairs of the language. 237 | func (l *XLanguage) GetEntries() map[string]string { 238 | clone := map[string]string{} 239 | l.mutex.RLock() 240 | defer l.mutex.RUnlock() 241 | for k, v := range l.entries { 242 | clone[k] = v 243 | } 244 | return clone 245 | } 246 | 247 | // GetXML will generate the XML to save the file 248 | func (l *XLanguage) GetXML() string { 249 | sdata := []string{} 250 | l.mutex.RLock() 251 | defer l.mutex.RUnlock() 252 | for key, val := range l.entries { 253 | sdata = append(sdata, " ") 254 | } 255 | sort.Strings(sdata) // Lets be sure the print is always the same presentation 256 | return "\n\n" + 257 | strings.Join(sdata, "\n") + "\n" 258 | } 259 | 260 | // String will transform the XDataset into a readable string for humans 261 | func (l *XLanguage) String() string { 262 | sdata := []string{} 263 | l.mutex.RLock() 264 | defer l.mutex.RUnlock() 265 | for key, val := range l.entries { 266 | sdata = append(sdata, key+":"+fmt.Sprintf("%v", val)) 267 | } 268 | sort.Strings(sdata) // Lets be sure the print is always the same presentation 269 | return "xcore.XLanguage{" + strings.Join(sdata, " ") + "}" 270 | } 271 | 272 | // GoString will transform the XDataset into a readable string for humans 273 | func (l *XLanguage) GoString() string { 274 | sdata := []string{} 275 | l.mutex.RLock() 276 | defer l.mutex.RUnlock() 277 | for key, val := range l.entries { 278 | sdata = append(sdata, key+":"+fmt.Sprintf("%#v", val)) 279 | } 280 | sort.Strings(sdata) // Lets be sure the print is always the same presentation 281 | return "#xcore.XLanguage{" + strings.Join(sdata, " ") + "}" 282 | } 283 | -------------------------------------------------------------------------------- /v2/xlanguage_test.go: -------------------------------------------------------------------------------- 1 | package xcore 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "testing" 7 | 8 | "golang.org/x/text/language" 9 | ) 10 | 11 | func ExampleNewXLanguage() { 12 | 13 | langES := NewXLanguage("messages", language.Spanish) 14 | langEN := NewXLanguage("messages", language.English) 15 | langFR := NewXLanguage("messages", language.French) 16 | 17 | // You can load this from your system files for instance and keep your translation tables apart 18 | langES.Set("panicerror", "Error crítico del sistema") 19 | langEN.Set("panicerror", "System panic error") 20 | langFR.Set("panicerror", "Erreur grave dans le système") 21 | 22 | // pick a random general system language (for instance the client's OS language) 23 | lang := langFR 24 | 25 | // launch a panic errors 26 | fmt.Println("Launch a panic error message in the selected language:", lang.Get("panicerror")) 27 | // Output: 28 | // Launch a panic error message in the selected language: Erreur grave dans le système 29 | } 30 | 31 | func ExampleNewXLanguageFromXMLFile() { 32 | 33 | langES, _ := NewXLanguageFromXMLFile("./testunit/a.es.language") 34 | langEN, _ := NewXLanguageFromXMLFile("./testunit/a.en.language") 35 | langFR, _ := NewXLanguageFromXMLFile("./testunit/a.fr.language") 36 | 37 | // pick a random general system language (for instance the client's OS language) 38 | lang := langES 39 | 40 | // Say hello main language 41 | fmt.Println(lang.Get("entry1") + " " + lang.Get("entry2")) 42 | // Say hello in other languages 43 | fmt.Println(langEN.Get("entry1") + " " + langEN.Get("entry2")) 44 | fmt.Println(langFR.Get("entry1") + " " + langFR.Get("entry2")) 45 | // Output: 46 | 47 | // Bienvenido a XCore 48 | // Welcome to XCore 49 | // Bienvenue à XCore 50 | } 51 | 52 | func TestXLanguage(t *testing.T) { 53 | 54 | // Those strings are the same in errors.es.txt and errors.es.xml to make the tests 55 | teststrings := map[string]string{ 56 | "panicerror": "Error crítico del sistema", 57 | "systemerror": "Error del sistema", 58 | "fileerror": "Error leyendo el archivo", 59 | } 60 | 61 | manualES := NewXLanguage("messages", language.Spanish) 62 | for id, val := range teststrings { 63 | manualES.Set(id, val) 64 | } 65 | 66 | // Load from xml file 67 | loadxmlES, err := NewXLanguageFromXMLFile("./testunit/errors.es.xml") 68 | if err != nil { 69 | t.Errorf("Error loading XML File into language %s", err) 70 | return 71 | } 72 | 73 | // Load from text file 74 | loadtextES, err := NewXLanguageFromFile("./testunit/errors.es.txt") 75 | if err != nil { 76 | t.Errorf("Error loading Text File into language %s", err) 77 | return 78 | } 79 | 80 | return 81 | 82 | // from xml string 83 | xmlstr, err := ioutil.ReadFile("./testunit/errors.es.xml") 84 | if err != nil { 85 | t.Errorf("Error loading XML File errors into string %s", err) 86 | return 87 | } 88 | loadstringxmlES, err := NewXLanguageFromXMLString(string(xmlstr)) 89 | if err != nil { 90 | t.Errorf("Error loading XML File into language %s", err) 91 | return 92 | } 93 | 94 | // from text string 95 | textstr, err := ioutil.ReadFile("./testunit/errors.es.txt") 96 | if err != nil { 97 | t.Errorf("Error loading Text File errors into string %s", err) 98 | return 99 | } 100 | loadstringtxtES, err := NewXLanguageFromString(string(textstr)) 101 | if err != nil { 102 | t.Errorf("Error loading Text File into language %s", err) 103 | return 104 | } 105 | 106 | // verify all 107 | for id, val := range teststrings { 108 | v1 := manualES.Get(id) 109 | if v1 != val { 110 | t.Errorf("Error reading value of manualES::%s", v1) 111 | return 112 | } 113 | v2 := loadxmlES.Get(id) 114 | if v2 != val { 115 | t.Errorf("Error reading value of loadxmlES::%s", v2) 116 | return 117 | } 118 | v3 := loadtextES.Get(id) 119 | if v3 != val { 120 | t.Errorf("Error reading value of loadtextES::%s", v3) 121 | return 122 | } 123 | v4 := loadstringxmlES.Get(id) 124 | if v4 != val { 125 | t.Errorf("Error reading value of loadstringxmlES::%s", v4) 126 | return 127 | } 128 | v5 := loadstringtxtES.Get(id) 129 | if v5 != val { 130 | t.Errorf("Error reading value of loadstringtxtES::%s", v5) 131 | return 132 | } 133 | } 134 | } 135 | 136 | func TestXLanguageAssign(t *testing.T) { 137 | 138 | // Those strings are the same in errors.es.txt and errors.es.xml to make the tests 139 | teststrings := map[string]string{ 140 | "panicerror": "System critical error", 141 | "systemerror": "System error", 142 | "fileerror": "File error", 143 | } 144 | 145 | manualES := NewXLanguage("messages", language.Spanish) 146 | for id, val := range teststrings { 147 | manualES.Set(id, val) 148 | } 149 | 150 | manualES.SetName("errors") 151 | manualES.SetLanguage(language.Spanish) 152 | manualES.Set("noerror", "There is no error") 153 | 154 | // Overload the spanish entries 155 | err := manualES.LoadFile("./testunit/errors.es.txt") 156 | if err != nil { 157 | t.Errorf("Error loading Text File into language %s", err) 158 | return 159 | } 160 | 161 | v1 := manualES.Get("panicerror") 162 | if v1 != "Error crítico del sistema" { 163 | t.Errorf("The value of panicerror is not correct %s", v1) 164 | return 165 | } 166 | 167 | v2 := manualES.Get("noerror") // should still be in english 168 | if v2 != "There is no error" { 169 | t.Errorf("The value of noerror is not correct %s", v2) 170 | return 171 | } 172 | 173 | manualES.Del("noerror") 174 | v3 := manualES.Get("noerror") // should be empty 175 | if v3 != "" { 176 | t.Errorf("The value of noerror is not correct (empty) %s", v3) 177 | return 178 | } 179 | 180 | name := manualES.GetName() 181 | if name != "errors" { 182 | t.Errorf("The value of language name is not correct %s", name) 183 | return 184 | } 185 | 186 | lang := manualES.GetLanguage() 187 | if lang != language.Spanish { 188 | t.Errorf("The value of language is not correct %s", lang) 189 | return 190 | } 191 | 192 | // String 193 | str := fmt.Sprint(manualES) 194 | if str != "xcore.XLanguage{fileerror:Error leyendo el archivo panicerror:Error crítico del sistema systemerror:Error del sistema}" { 195 | t.Errorf("The print value language is not correct %s", str) 196 | return 197 | } 198 | 199 | // Gostring 200 | str = fmt.Sprintf("%#v", manualES) 201 | if str != "#xcore.XLanguage{fileerror:\"Error leyendo el archivo\" panicerror:\"Error crítico del sistema\" systemerror:\"Error del sistema\"}" { 202 | t.Errorf("The print #value language is not correct %s", str) 203 | return 204 | } 205 | 206 | str = manualES.GetXML() 207 | fmt.Println(str) 208 | } 209 | -------------------------------------------------------------------------------- /v2/xtemplate.go: -------------------------------------------------------------------------------- 1 | package xcore 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "regexp" 9 | "sort" 10 | "strconv" 11 | "strings" 12 | // "sync" 13 | ) 14 | 15 | /* 16 | class to compile and keep a Template string 17 | A template is a set of HTML/XML (or any other language) set of: 18 | 19 | Comments: 20 | %-- comments --% 21 | Fields: 22 | {{field}} 23 | {{field>Subfield>Subfield}} 24 | Language injection 25 | ##entry## 26 | Subtemplates: 27 | xml/html code 28 | [[id]] 29 | xml/html code 30 | [[id]] 31 | xml/html code indented 32 | [[]] 33 | xml/html code 34 | [[]] 35 | Meta elements: 36 | ??xx?? if/then/else 37 | @@xx@@ loops 38 | &&xx&& references 39 | !!xx!! debug (dump) 40 | */ 41 | 42 | // MetaString and other consts: 43 | // type of elements present in the template 44 | const ( 45 | MetaString = 0 // a simple string to integrate into the code 46 | MetaComment = 1 // Comment, ignore it 47 | 48 | MetaLanguage = 2 // one param of the URL parameters list, index-1 based [page]/value1/value2... 49 | MetaReference = 3 // an URL variable coming through a query ?variable=value 50 | MetaRange = 4 // Parameter passed to the page Run by code 51 | MetaCondition = 5 // System (site) parameter 52 | MetaDump = 6 // Main page called parameters (into .page file) 53 | MetaVariable = 7 // this page parameters (into .page file), same as Main page parameters if it's the external called page 54 | 55 | MetaTemplateStart = 101 // Temporal nested box start tag 56 | MetaTemplateEnd = 102 // Temporal nested box end tag 57 | 58 | MetaUnused = -1 // a "not used anymore" param to be freed 59 | ) 60 | 61 | // XTemplateParam is a parameter definition into the template 62 | type XTemplateParam struct { 63 | ParamType int 64 | Data string 65 | // children *XTemplateData 66 | } 67 | 68 | // Clone will make a copy of the param 69 | func (tp *XTemplateParam) Clone() *XTemplateParam { 70 | return &XTemplateParam{ParamType: tp.ParamType, Data: tp.Data} 71 | } 72 | 73 | // XTemplateData is an Array of all the parameters into the template 74 | type XTemplateData []XTemplateParam 75 | 76 | // XTemplate is the plain template structure 77 | type XTemplate struct { 78 | Name string 79 | Root *XTemplateData 80 | SubTemplates map[string]*XTemplate 81 | // mutex sync.RWMutex 82 | } 83 | 84 | // NewXTemplate will create a new empty template 85 | func NewXTemplate() *XTemplate { 86 | return &XTemplate{} 87 | } 88 | 89 | // NewXTemplateFromFile will create a new template from a file containing the template code 90 | func NewXTemplateFromFile(file string) (*XTemplate, error) { 91 | t := &XTemplate{} 92 | err := t.LoadFile(file) 93 | if err != nil { 94 | return nil, err 95 | } 96 | return t, nil 97 | } 98 | 99 | // NewXTemplateFromString will create a new template from a string containing the template code 100 | func NewXTemplateFromString(data string) (*XTemplate, error) { 101 | t := &XTemplate{} 102 | err := t.LoadString(data) 103 | if err != nil { 104 | return nil, err 105 | } 106 | return t, nil 107 | } 108 | 109 | // LoadFile will load a file into the template 110 | func (t *XTemplate) LoadFile(file string) error { 111 | tFile, err := os.Open(file) 112 | if err != nil { 113 | return err 114 | } 115 | data, err := ioutil.ReadAll(tFile) 116 | if err != nil { 117 | return err 118 | } 119 | err = tFile.Close() 120 | if err != nil { 121 | return err 122 | } 123 | return t.LoadString(string(data)) 124 | } 125 | 126 | // LoadString will load a string into the template 127 | func (t *XTemplate) LoadString(data string) error { 128 | return t.compile(data) 129 | } 130 | 131 | // compile will interprete the template code into objects 132 | func (t *XTemplate) compile(data string) error { 133 | // build, compile return result 134 | code := 135 | `(?s)` + // . is multiline 136 | 137 | // ==== COMENTS 138 | `(%)--(.*?)--%(\n|\r|\r\n|\n\r)?` + // index based 1 139 | 140 | // ==== LANGUAGE INJECTION 141 | `|(#)#([a-zA-Z0-9-_\.]+?)##` + // index based 4 142 | 143 | // ==== ELEMENTS 144 | `|(&)&([a-zA-Z0-9-_\=\>\:\|\.]+?)&&` + // index based 6 145 | `|(@)@([a-zA-Z0-9-_\=\>\:\|\.]+?)@@` + // index based 8 146 | `|(\?)\?([a-zA-Z0-9-_\=\>\:\|\.]+?)\?\?` + // index based 10 147 | `|(\!)\!([a-zA-Z0-9-_\=\>\:\|\.]+?)\!\!` + // index based 12 148 | `|(\{)\{([a-zA-Z0-9-_\=\>\:\|\.]+?)\}\}` + // index based 14 149 | 150 | // ==== NESTED ELEMENTS (SUB TEMPLATES) 151 | `|\[\[(\])\](\n|\r|\r\n|\n\r)?` + // index based 16 152 | `|(\[)\[([a-z0-9\|\.\-_]+?)\]\](\n|\r|\r\n|\n\r)?` // index based 18 153 | 154 | codex := regexp.MustCompile(code) 155 | indexes := codex.FindAllStringIndex(data, -1) 156 | matches := codex.FindAllStringSubmatch(data, -1) 157 | 158 | var compiled XTemplateData 159 | pointer := 0 160 | for i, x := range indexes { 161 | if pointer != x[0] { 162 | compiled = append(compiled, *(&XTemplateParam{ParamType: MetaString, Data: data[pointer:x[0]]})) 163 | } 164 | 165 | param := &XTemplateParam{} 166 | if matches[i][1] == "%" { 167 | param.ParamType = MetaComment // comment 168 | param.Data = matches[i][2] 169 | } else if matches[i][4] == "#" { 170 | param.ParamType = MetaLanguage // Language entry 171 | param.Data = matches[i][5] 172 | } else if matches[i][6] == "&" { 173 | param.ParamType = MetaReference // Reference to template 174 | param.Data = matches[i][7] 175 | } else if matches[i][8] == "@" { 176 | param.ParamType = MetaRange // Loop on data 177 | param.Data = matches[i][9] 178 | } else if matches[i][10] == "?" { 179 | param.ParamType = MetaCondition // Conditional on data 180 | param.Data = matches[i][11] 181 | } else if matches[i][12] == "!" { 182 | param.ParamType = MetaDump // Debug 183 | param.Data = matches[i][13] 184 | } else if matches[i][14] == "{" { 185 | param.ParamType = MetaVariable // Simple element 186 | param.Data = matches[i][15] 187 | } else if matches[i][18] == "[" { 188 | param.ParamType = MetaTemplateStart // Template start 189 | param.Data = matches[i][19] 190 | } else if matches[i][16] == "]" { 191 | param.ParamType = MetaTemplateEnd // Template end 192 | } else { 193 | param.ParamType = MetaUnused // unknown, will be removed 194 | } 195 | compiled = append(compiled, *param) 196 | pointer = x[1] 197 | } 198 | // end of Data 199 | if pointer != len(data) { 200 | compiled = append(compiled, *(&XTemplateParam{ParamType: MetaString, Data: data[pointer:]})) 201 | } 202 | 203 | // second pass: all the sub templates into the Subtemplates 204 | startpointers := []int{} 205 | subtemplates := []*XTemplate{} 206 | actualtemplate := t 207 | for i, x := range compiled { 208 | if x.ParamType == MetaTemplateStart { 209 | startpointers = append(startpointers, i) 210 | subtemplates = append(subtemplates, actualtemplate) 211 | actualtemplate = &XTemplate{Name: x.Data, Root: nil} 212 | } else if x.ParamType == MetaTemplateEnd { 213 | // we found the end of the nested box, lets create a nested param array from stacked startpointer up to i 214 | last := len(startpointers) - 1 215 | if last < 0 { 216 | return errors.New("Error: template mismatch Start/End") 217 | } 218 | startpointer := startpointers[last] 219 | startpointers = startpointers[:last] 220 | 221 | var subset XTemplateData 222 | for ptr := startpointer + 1; ptr < i; ptr++ { // we ignore the BOX]] end param (we dont need it in the hierarchic structure) 223 | if compiled[ptr].ParamType != MetaUnused { // we just ignore params marked to be deleted 224 | subset = append(subset, compiled[ptr]) 225 | compiled[ptr].ParamType = MetaUnused // marked to be deleted, traslated to a substructure 226 | } 227 | } 228 | actualtemplate.Root = &subset 229 | 230 | uppertemplate := subtemplates[last] 231 | subtemplates = subtemplates[:last] 232 | 233 | // If there are |, we separate the templates and add every one to the list with same pointer 234 | pospipe := strings.Index(actualtemplate.Name, "|") 235 | if pospipe >= 0 { 236 | vals := strings.Split(actualtemplate.Name, "|") 237 | for _, v := range vals { 238 | if len(v) > 0 { 239 | uppertemplate.AddTemplate(v, actualtemplate) 240 | } 241 | } 242 | } else { 243 | uppertemplate.AddTemplate(actualtemplate.Name, actualtemplate) 244 | } 245 | 246 | // pop actualtemplate 247 | actualtemplate = uppertemplate 248 | 249 | compiled[startpointer].ParamType = MetaUnused // marked to be deleted, no need of start template 250 | compiled[i].ParamType = MetaUnused // marked to be deleted, no need of end template 251 | } 252 | } 253 | if len(startpointers) > 0 { 254 | return errors.New("Error: template mismatch Start/End") 255 | } 256 | 257 | // last pass: delete params marked to be deleted and concatenate strings 258 | currentpointer := 0 259 | for i, x := range compiled { 260 | if x.ParamType != MetaUnused { 261 | if currentpointer != i { 262 | compiled[currentpointer] = x 263 | } 264 | currentpointer++ 265 | } 266 | } 267 | 268 | compiled = compiled[:currentpointer] 269 | t.Root = &compiled 270 | return nil 271 | } 272 | 273 | // AddTemplate will add a sub template to this template 274 | func (t *XTemplate) AddTemplate(name string, tmpl *XTemplate) { 275 | if t.SubTemplates == nil { 276 | t.SubTemplates = make(map[string]*XTemplate) 277 | } 278 | t.SubTemplates[name] = tmpl 279 | } 280 | 281 | // GetTemplate gets a sub template existing into this template 282 | func (t *XTemplate) GetTemplate(name string) *XTemplate { 283 | if t.SubTemplates == nil { 284 | return nil 285 | } 286 | return t.SubTemplates[name] 287 | } 288 | 289 | // Execute will inject the Data into the template and creates the final string 290 | func (t *XTemplate) Execute(data XDatasetDef) string { 291 | // Does data has a language ? 292 | if data != nil { 293 | var language *XLanguage 294 | lang, _ := data.Get("#") 295 | if lang != nil { 296 | language, _ = lang.(*XLanguage) // language is nil if it-s not a *XLanguage 297 | } 298 | stack := &XDatasetCollection{} 299 | stack.Push(data) 300 | return t.injector(stack, language) 301 | } 302 | return t.injector(nil, nil) 303 | } 304 | 305 | // injector will injects the data into this template 306 | func (t *XTemplate) injector(datacol XDatasetCollectionDef, language *XLanguage) string { 307 | var injected []string 308 | if t.Root == nil { 309 | return "Error, no template.Root compiled" 310 | } 311 | for _, v := range *t.Root { 312 | switch v.ParamType { 313 | case MetaString: // included string from original code 314 | injected = append(injected, v.Data) 315 | case MetaComment: 316 | // nothing to do: comment ignored 317 | case MetaLanguage: 318 | if language != nil { 319 | injected = append(injected, language.Get(v.Data)) 320 | } 321 | case MetaReference: // Reference && 322 | xid := strings.Split(v.Data, ":") 323 | if len(xid) == 3 { 324 | field := xid[1] 325 | prefix := xid[2] 326 | value, _ := datacol.GetDataString(field) 327 | subt := t.GetTemplate(prefix + value) 328 | if subt != nil { 329 | substr := subt.injector(datacol, language) 330 | injected = append(injected, substr) 331 | } else { 332 | subt := t.GetTemplate(prefix) 333 | if subt != nil { 334 | substr := subt.injector(datacol, language) 335 | injected = append(injected, substr) 336 | } 337 | } 338 | } else { 339 | template := "" 340 | if len(xid) >= 1 { 341 | template = xid[0] 342 | } 343 | subt := t.GetTemplate(template) 344 | if subt != nil { 345 | withds := false 346 | if len(xid) == 2 { 347 | dcl, _ := datacol.GetData(xid[1]) 348 | ds, ok := dcl.(XDatasetDef) 349 | if ok { 350 | withds = true 351 | datacol.Push(ds) 352 | } 353 | } 354 | substr := subt.injector(datacol, language) 355 | if withds { 356 | datacol.Pop() 357 | } 358 | injected = append(injected, substr) 359 | } 360 | } 361 | case MetaVariable: // {{id>id>id...}} 362 | if datacol != nil { 363 | d, _ := datacol.GetDataString(v.Data) 364 | injected = append(injected, d) 365 | } 366 | case MetaRange: // Range (loop over subset) @@id:id@@ 367 | xdata := strings.Split(v.Data, ":") 368 | subdataid := xdata[0] 369 | subtemplateid := xdata[0] 370 | if len(xdata) > 1 { 371 | subtemplateid = xdata[1] 372 | } 373 | 374 | subt := t.GetTemplate(subtemplateid) 375 | if subt != nil { 376 | if datacol != nil { 377 | cl, _ := datacol.GetCollection(subdataid) 378 | if cl != nil && cl.Count() > 0 { 379 | for i := 0; i < cl.Count(); i++ { 380 | var tmp *XTemplate 381 | tmp = t.GetTemplate(subtemplateid + ".key." + strconv.Itoa(i)) 382 | // if tmp == nil { 383 | // tmp = t.GetTemplate(subtemplateid + ".field." + field + "." + value) 384 | // } 385 | if tmp == nil && i == 0 { 386 | tmp = t.GetTemplate(subtemplateid + ".first") 387 | } 388 | if tmp == nil && i == cl.Count()-1 { 389 | tmp = t.GetTemplate(subtemplateid + ".last") 390 | } 391 | if tmp == nil && i%2 == 0 { 392 | tmp = t.GetTemplate(subtemplateid + ".even") 393 | } 394 | if tmp == nil { 395 | tmp = subt 396 | } 397 | dcl, _ := cl.Get(i) 398 | dcl.Set(".counter", i+1) 399 | datacol.Push(dcl) 400 | substr := tmp.injector(datacol, language) 401 | injected = append(injected, substr) 402 | // unstack extra data 403 | datacol.Pop() 404 | } 405 | } else { 406 | var tmp *XTemplate 407 | tmp = t.GetTemplate(subtemplateid + ".none") 408 | if tmp == nil { 409 | tmp = subt 410 | } 411 | substr := tmp.injector(datacol, language) 412 | injected = append(injected, substr) 413 | } 414 | } 415 | } 416 | case MetaCondition: // ??id?? 417 | xdata := strings.Split(v.Data, ":") 418 | subdataid := xdata[0] 419 | subtemplateid := xdata[0] 420 | if len(xdata) > 1 { 421 | subtemplateid = xdata[1] 422 | } 423 | 424 | subt := t.GetTemplate(subtemplateid) 425 | var value interface{} 426 | if datacol != nil { 427 | value, _ = datacol.GetData(subdataid) 428 | } 429 | if subt != nil && value != nil { 430 | withds := false 431 | svalue := "" 432 | ds, ok := value.(XDatasetDef) 433 | if ok { 434 | withds = true 435 | datacol.Push(ds) 436 | } else { 437 | svalue = fmt.Sprint(value) 438 | } 439 | if svalue != "" { 440 | // subtemplate with .value? 441 | tmp := t.GetTemplate(subtemplateid + "." + svalue) 442 | if tmp != nil { 443 | subt = tmp 444 | } 445 | substr := subt.injector(datacol, language) 446 | injected = append(injected, substr) 447 | } 448 | if withds { 449 | datacol.Pop() 450 | } 451 | } 452 | if value == nil || fmt.Sprint(value) == "" { 453 | tmp := t.GetTemplate(subtemplateid + ".none") 454 | if tmp != nil { 455 | subt = tmp 456 | } 457 | if subt != nil { 458 | substr := subt.injector(datacol, language) 459 | injected = append(injected, substr) 460 | } 461 | } 462 | case MetaDump: 463 | if datacol != nil { 464 | if v.Data == "dump" || v.Data == "list" { 465 | dsubstr, _ := datacol.Get(0) 466 | if dsubstr != nil { 467 | substr := dsubstr.GoString() 468 | injected = append(injected, substr) 469 | } 470 | } 471 | } 472 | default: 473 | injected = append(injected, "THE METALANGUAGE FROM OUTERSPACE IS NOT SUPPORTED: "+fmt.Sprint(v.ParamType)) 474 | } 475 | } 476 | // return the page string 477 | return strings.Join(injected, "") 478 | } 479 | 480 | // String will transform the XDataset into a readable string for humans 481 | func (t *XTemplate) String() string { 482 | sdata := []string{} 483 | for _, val := range *t.Root { 484 | sdata = append(sdata, fmt.Sprintf("%v", val)) 485 | } 486 | sort.Strings(sdata) // Lets be sure the print is always the same presentation 487 | return "xcore.XTemplate{" + strings.Join(sdata, " ") + "}" 488 | } 489 | 490 | // GoString will transform the XDataset into a readable string for humans with values indexes 491 | func (t *XTemplate) GoString() string { 492 | sdata := []string{} 493 | for _, val := range *t.Root { 494 | sdata = append(sdata, fmt.Sprintf("%#v", val)) 495 | } 496 | sort.Strings(sdata) // Lets be sure the print is always the same presentation 497 | return "#xcore.XTemplate{" + strings.Join(sdata, " ") + "}" 498 | } 499 | 500 | // Clone will make a full new copy of the template into a new memory space 501 | func (t *XTemplate) Clone() *XTemplate { 502 | cloned := &XTemplate{Name: t.Name} 503 | if t.Root != nil { 504 | var newroot XTemplateData 505 | for _, td := range *t.Root { 506 | newroot = append(newroot, *td.Clone()) 507 | } 508 | cloned.Root = &newroot 509 | } 510 | if t.SubTemplates != nil { 511 | newsubtemplates := map[string]*XTemplate{} 512 | for id, xt := range t.SubTemplates { 513 | newsubtemplates[id] = xt.Clone() 514 | } 515 | cloned.SubTemplates = newsubtemplates 516 | } 517 | 518 | return cloned 519 | } 520 | -------------------------------------------------------------------------------- /v2/xtemplate_test.go: -------------------------------------------------------------------------------- 1 | package xcore 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "golang.org/x/text/language" 9 | ) 10 | 11 | func ExampleNewXTemplateFromString() { 12 | tmpl, _ := NewXTemplateFromString(` 13 | %-- This is a comment. It will not appear in the final code. --% 14 | Let's put your name here: {{clientname}}
15 | And lets put your hobbies here:
16 | %-- note the 1rst id is the entry into the data to inject and the second one is the name of the sub-template to use --% 17 | @@hobbies:hobby@@ 18 | %-- And you need the template for each hobby:--% 19 | [[hobby]] 20 | I love {{name}}
21 | [[]] 22 | `) 23 | // The creation of the data is obviously tedious here, in real life it should come from a JSON, a Database, etc 24 | data := XDataset{ 25 | "clientname": "Fred", 26 | "hobbies": &XDatasetCollection{ 27 | &XDataset{"name": "Football"}, 28 | &XDataset{"name": "Ping-pong"}, 29 | &XDataset{"name": "Swimming"}, 30 | &XDataset{"name": "Videogames"}, 31 | }, 32 | } 33 | 34 | fmt.Println(tmpl.Execute(&data)) 35 | // Output: 36 | // Let's put your name here: Fred
37 | // And lets put your hobbies here:
38 | // I love Football
39 | // I love Ping-pong
40 | // I love Swimming
41 | // I love Videogames
42 | } 43 | 44 | func TestNewXTemplateEmptyLoop(t *testing.T) { 45 | tmpl, _ := NewXTemplateFromString(` 46 | %-- This is a comment. It will not appear in the final code. --% 47 | Let's put your name here: {{clientname}}
48 | And lets put your hobbies here:
49 | %-- note the 1rst id is the entry into the data to inject and the second one is the name of the sub-template to use --% 50 | @@hobbies@@ 51 | %-- And you need the template for each hobby:--% 52 | [[hobbies]] 53 | I love {{name}}
54 | [[]] 55 | [[hobbies.none]] 56 | I love nothing
57 | [[]] 58 | `) 59 | // The creation of the data is obviously tedious here, in real life it should come from a JSON, a Database, etc 60 | data := XDataset{ 61 | "clientname": "Fred", 62 | "hobbies": &XDatasetCollection{}, 63 | } 64 | 65 | fmt.Println(tmpl.Execute(&data)) 66 | // Output: 67 | // Let's put your name here: Fred
68 | // And lets put your hobbies here:
69 | // I love nothing
70 | } 71 | 72 | func TestNewXTemplateEmptyCondition(t *testing.T) { 73 | tmpl, _ := NewXTemplateFromString(` 74 | TAGADA 75 | ??familyname?? 76 | [[familyname]] 77 | Hello {{familyname}}
78 | [[]] 79 | [[familyname.none]] 80 | I don't know you
81 | [[]] 82 | `) 83 | // The creation of the data is obviously tedious here, in real life it should come from a JSON, a Database, etc 84 | data := XDataset{ 85 | "clientname": "Fred", 86 | } 87 | 88 | fmt.Println(tmpl.Execute(&data)) 89 | // Output: 90 | // I don't know you
91 | } 92 | 93 | func TestNewXTemplateNilCollection(t *testing.T) { 94 | 95 | tmpl, _ := NewXTemplateFromString(` 96 | %-- This is a comment. It will not appear in the final code. --% 97 | Let's put your name here: {{clientname}}
98 | And lets put your hobbies here:
99 | %-- note the 1rst id is the entry into the data to inject and the second one is the name of the sub-template to use --% 100 | @@hobbies@@ 101 | %-- And you need the template for each hobby:--% 102 | [[hobbies]] 103 | I love {{name}}
104 | [[]] 105 | [[hobbies.none]] 106 | I love nothing
107 | [[]] 108 | `) 109 | // The creation of the data is obviously tedious here, in real life it should come from a JSON, a Database, etc 110 | var col XDatasetCollection 111 | data := XDataset{ 112 | "clientname": "Fred", 113 | "hobbies": col, 114 | } 115 | 116 | fmt.Println(tmpl.Execute(&data)) 117 | // Output: 118 | // Let's put your name here: Fred
119 | // And lets put your hobbies here:
120 | // I love nothing
121 | } 122 | 123 | func TestNewXTemplateFromString(t *testing.T) { 124 | tmpl, _ := NewXTemplateFromString(` 125 | %-- This is a comment. It will not appear in the final code. --% 126 | Let's put your name here: {{clientname}}
127 | And lets put your hobbies here:
128 | %-- note the 1rst id is the entry into the data to inject and the second one is the name of the sub-template to use --% 129 | @@hobbies:hobby@@ 130 | %-- And you need the template for each hobby:--% 131 | [[hobby]] 132 | I love {{name}}
133 | [[]] 134 | `) 135 | // The creation of the data is obviously tedious here, in real life it should come from a JSON, a Database, etc 136 | data := XDataset{ 137 | "clientname": "Fred", 138 | "hobbies": &XDatasetCollection{ 139 | &XDataset{"name": "Football"}, 140 | &XDataset{"name": "Ping-pong"}, 141 | &XDataset{"name": "Swimming"}, 142 | &XDataset{"name": "Videogames"}, 143 | }, 144 | } 145 | 146 | str := tmpl.Execute(&data) 147 | if str != ` 148 | Let's put your name here: Fred
149 | And lets put your hobbies here:
150 | I love Football
151 | I love Ping-pong
152 | I love Swimming
153 | I love Videogames
154 | 155 | ` { 156 | t.Error("Error loading and running the template from string " + str) 157 | return 158 | } 159 | } 160 | 161 | func TestNewXTemplateFromFile(t *testing.T) { 162 | tmpl, _ := NewXTemplateFromFile("testunit/a.template") 163 | 164 | // The creation of the data is obviously tedious here, in real life it should come from a JSON, a Database, etc 165 | data := XDataset{ 166 | "clientname": "Fred", 167 | "hobbies": &XDatasetCollection{ 168 | &XDataset{"name": "Football"}, 169 | &XDataset{"name": "Ping-pong"}, 170 | &XDataset{"name": "Swimming"}, 171 | &XDataset{"name": "Videogames"}, 172 | }, 173 | } 174 | 175 | str := tmpl.Execute(&data) 176 | if str != ` 177 | Let's put your name here: Fred
178 | And lets put your hobbies here:
179 | I love Football
180 | I love Ping-pong
181 | I love Swimming
182 | I love Videogames
183 | 184 | ` { 185 | t.Error("Error loading and running the template from file " + str) 186 | return 187 | } 188 | } 189 | 190 | func TestXTemplateErrors(t *testing.T) { 191 | tmpl1, _ := NewXTemplateFromString("template [[subtemplate]]") 192 | 193 | if tmpl1 != nil { 194 | t.Error("Error compiling a template with error, the template should be nil") 195 | return 196 | } 197 | } 198 | 199 | func TestXTemplateComments(t *testing.T) { 200 | tmpl1, _ := NewXTemplateFromString("abcdefg") 201 | tmpl2, _ := NewXTemplateFromString(`a%--comment1--%b%--comment 2 202 | [[]] 203 | [[subtemplate]] 204 | --%c%--comment3 --% 205 | defg%-- ending @@subtemplate@@ comment --% 206 | `) 207 | 208 | if tmpl1.Execute(nil) != tmpl2.Execute(nil) { 209 | t.Error("Error comparing templates for comments") 210 | return 211 | } 212 | } 213 | 214 | func TestXTemplateLanguageParam(t *testing.T) { 215 | tmpl, _ := NewXTemplateFromString("Test with ##some## ##languages## here") 216 | 217 | data := &XDataset{} 218 | l, _ := NewXLanguageFromString("some=a tiny table\nlanguages=of english language\n") 219 | data.Set("#", l) 220 | 221 | result := tmpl.Execute(data) 222 | 223 | if result != "Test with a tiny table of english language here" { 224 | t.Errorf("The language table has not been inserted correctly") 225 | } 226 | } 227 | 228 | func TestXTemplateSimple(t *testing.T) { 229 | tmpl, err := NewXTemplateFromFile("testunit/b.template") 230 | if err != nil { 231 | t.Error(err) 232 | return 233 | } 234 | 235 | tmp, _ := time.Parse(time.RFC3339, "2020-01-01T12:00:00") 236 | lang := NewXLanguage("mainpage", language.English) 237 | lang.Set("welcome", "Welcome to you") 238 | data := XDataset{ 239 | "clientname": "Fred", 240 | "clientpicture": "face.jpg", 241 | "hobbies": &XDatasetCollection{ 242 | &XDataset{"name": "Football", "sport": "yes"}, 243 | &XDataset{"name": "Ping-pong", "sport": "yes"}, 244 | &XDataset{"name": "Swimming", "sport": "yes"}, 245 | &XDataset{"name": "Videogames", "sport": "no"}, 246 | &XDataset{"name": "other 1", "sport": "no"}, 247 | &XDataset{"name": "other 2", "sport": "no"}, 248 | &XDataset{"name": "other 3", "sport": "yes"}, 249 | &XDataset{"name": "other 4", "sport": "no"}, 250 | &XDataset{"name": "other 5", "sport": nil}, 251 | }, 252 | "preferredhobby": &XDataset{ 253 | "name": "Baseball", 254 | "sport": "yes", 255 | }, 256 | "metadata": &XDataset{ 257 | "preferred-color": "blue", 258 | "Salary": 3568.65, 259 | "hiredate": tmp, 260 | }, 261 | "#": lang, 262 | } 263 | 264 | str := tmpl.Execute(&data) 265 | if str == "" { 266 | t.Errorf("Error build complex template") 267 | } 268 | } 269 | 270 | func TestXTemplateClone(t *testing.T) { 271 | tmpl, err := NewXTemplateFromFile("testunit/b.template") 272 | if err != nil { 273 | t.Error(err) 274 | return 275 | } 276 | 277 | newtmpl := tmpl.Clone() 278 | 279 | tmp, _ := time.Parse(time.RFC3339, "2020-01-01T12:00:00") 280 | lang := NewXLanguage("mainpage", language.English) 281 | lang.Set("welcome", "Welcome to you") 282 | data := XDataset{ 283 | "clientname": "Fred", 284 | "clientpicture": "face.jpg", 285 | "hobbies": &XDatasetCollection{ 286 | &XDataset{"name": "Football", "sport": "yes"}, 287 | &XDataset{"name": "Ping-pong", "sport": "yes"}, 288 | &XDataset{"name": "Swimming", "sport": "yes"}, 289 | &XDataset{"name": "Videogames", "sport": "no"}, 290 | &XDataset{"name": "other 1", "sport": "no"}, 291 | &XDataset{"name": "other 2", "sport": "no"}, 292 | &XDataset{"name": "other 3", "sport": "yes"}, 293 | &XDataset{"name": "other 4", "sport": "no"}, 294 | &XDataset{"name": "other 5", "sport": nil}, 295 | }, 296 | "preferredhobby": &XDataset{ 297 | "name": "Baseball", 298 | "sport": "yes", 299 | }, 300 | "metadata": &XDataset{ 301 | "preferred-color": "blue", 302 | "Salary": 3568.65, 303 | "hiredate": tmp, 304 | }, 305 | "#": lang, 306 | } 307 | 308 | str1 := tmpl.Execute(&data) 309 | if str1 == "" { 310 | t.Errorf("Error build complex template") 311 | } 312 | str2 := newtmpl.Execute(&data) 313 | if str2 == "" { 314 | t.Errorf("Error build complex template cloned") 315 | } 316 | if str2 != str1 { 317 | t.Errorf("Error comparing template cloned") 318 | } 319 | fmt.Println(str1) 320 | } 321 | 322 | /* 323 | package main 324 | 325 | import ( 326 | "fmt" 327 | "github.com/webability-go/xcore" 328 | "testing" 329 | // "unsafe" 330 | ) 331 | 332 | // TEST XTEMPLATE 333 | 334 | func TestReferenceParam(t *testing.T) { 335 | tmpl, _ := xcore.NewXTemplateFromString(` 336 | The sub template starts here: &&template1&&. End. 337 | [[template1]] 338 | This is the template 1 339 | [[]] 340 | `) 341 | 342 | fmt.Println(tmpl) 343 | fmt.Println(tmpl.Root) 344 | 345 | result := tmpl.Execute(&xcore.XDataset{}) 346 | 347 | fmt.Println("Result: ", result) 348 | 349 | } 350 | 351 | func TestComplexReferenceParam(t *testing.T) { 352 | 353 | tmpl, err := xcore.NewXTemplateFromString(` 354 | The sub template starts here: &&template2&&. End. 355 | [[template1]] 356 | This is the template 1 357 | [[]] 358 | [[template2]] 359 | This is the template 2 360 | [[template3]] 361 | This is the subtemplate 3 362 | [[]] 363 | [[template4|template5]] 364 | These are the subtemplates 4 and 5 365 | [[template6.first]] 366 | This is the subtemplate 6 first element for a loop 367 | [[]] 368 | [[template6]] 369 | This is the subtemplate 6 any element for a loop 370 | [[]] 371 | [[template6.last]] 372 | This is the subtemplate 6 last element for a loop 373 | [[]] 374 | [[]] 375 | [[]] 376 | [[template7|template7.status.false]] 377 | This is the template 7 for field status false and any other values 378 | [[]] 379 | [[template7.status.true]] 380 | This is the template 7 for field status true 381 | [[]] 382 | `) 383 | 384 | if err != nil { 385 | fmt.Println(err) 386 | return 387 | } 388 | 389 | result := tmpl.Execute(&xcore.XDataset{}) 390 | fmt.Println("Result: ", result) 391 | } 392 | 393 | func TestVariableParam(t *testing.T) { 394 | 395 | tmpl, err := xcore.NewXTemplateFromString(` 396 | Some data: 397 | {{data1}} 398 | {{data2}} 399 | {{data3>data31}} 400 | {{data4}} 401 | {{data5}} 402 | {{data6}} 403 | {{data7}} 404 | {{data8}} 405 | @@data8@@ 406 | [[data8]] 407 | * test {{data81}} and {{data82}} and {{data83}} and {{data1}} 408 | [[]] 409 | ??data9?? 410 | [[data9]] 411 | * Data 9 exists and is {{data9}} 412 | [[]] 413 | ??data10?? 414 | [[data10]] 415 | * Data 10 does not exist 416 | [[]] 417 | !!dump!! 418 | `) 419 | 420 | if err != nil { 421 | fmt.Println(err) 422 | return 423 | } 424 | 425 | data := xcore.XDataset{} 426 | data["data1"] = "DATA1" 427 | data["data2"] = "DATA1" 428 | sm := xcore.XDataset{} 429 | sm["data31"] = "DATA31" 430 | data["data3"] = sm 431 | data["data4"] = 123 432 | data["data5"] = 123.432 433 | data["data6"] = true 434 | data["data7"] = func() string { return "ABC" } 435 | 436 | d8r1 := &xcore.XDataset{} 437 | d8r1.Set("data81", "rec 1: Entry 8-1") 438 | d8r1.Set("data82", "rec 1: Entry 8-2") 439 | 440 | d8r2 := &xcore.XDataset{} 441 | d8r2.Set("data81", "rec 2: Entry 8-1") 442 | d8r2.Set("data82", "rec 2: Entry 8-2") 443 | d8r2.Set("data83", "rec 2: Entry 8-3") 444 | 445 | d8r3 := &xcore.XDataset{} 446 | d8r3.Set("data81", "rec 3: Entry 8-1") 447 | d8r3.Set("data82", "rec 3: Entry 8-2") 448 | 449 | d := xcore.XDatasetCollection{} 450 | d.Push(d8r1) 451 | d.Push(d8r2) 452 | d.Push(d8r3) 453 | 454 | data["data8"] = &d 455 | data["data9"] = "I exist" 456 | 457 | fmt.Printf("Data: %v\n", data) 458 | // fmt.Printf("ADDRESS DATA8 / GET R1: %p", data.GetCollection("data8").Get(0)) 459 | 460 | result := tmpl.Execute(&data) 461 | fmt.Println("Result: ", result) 462 | } 463 | */ 464 | -------------------------------------------------------------------------------- /xcache.go: -------------------------------------------------------------------------------- 1 | package xcore 2 | 3 | import ( 4 | "log" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | // XCacheEntry is the cache basic structure to save some data in memory. 10 | type XCacheEntry struct { 11 | // The cache entry has a time to measure expiration if needed, or time of entry in cache: 12 | // - ctime is the creation time (used to validate the object against its source). 13 | ctime time.Time 14 | // - rtime is the last read time (used to clean the cache: the less accessed objects are removed). 15 | rtime time.Time 16 | // The data as itself is an interface to whatever the user need to cache. 17 | data interface{} 18 | } 19 | 20 | // XCache is the main cache structure, that contains a collection of XCacheEntries and some metadata. 21 | type XCache struct { 22 | // "ID": XCache has a unique id (informative). 23 | ID string 24 | // "Maxitems": The user can creates a cache with a maximum number of elements into it. In this case, when the cache reaches the maximum number of elements stored, then the system makes a clean of 10% of the oldest elements. This type of use is not recommended since is it heavy in CPU use to clean the cache. 25 | Maxitems int 26 | // "Validator" is a function that can be set to check the validity of the data (for instance if the data originates from a file or a database). The validator is called for each Get (and can be heavy for CPU or can wait a long time, for instance if the check is an external database on another cluster). Beware of this. 27 | Validator func(string, time.Time) bool 28 | // "Expire": The user can also create an expiration duration, so every elements in the cache is invalidated after a certain amount of time. It is more recommended to use the cache with an expiration duration. The obsolete objects are destroyed when the user tries to use them and return a "non existence" on Get. (this does not use CPU or extra locks). 29 | Expire time.Duration 30 | // Not available from outside for security, access of data is based on a mutex 31 | // "mutex": The cache owns a mutex to lock access to data to read/write/delete/clean the data, to allow concurrency and multithreading of the cache. 32 | mutex sync.Mutex 33 | // "pile": The pile keeps the "ordered by date of reading" object keys, so it's fast to clean the data. 34 | items map[string]*XCacheEntry 35 | // "items": The items are a map to cache entries, acceved by the key of entries. 36 | pile []string 37 | } 38 | 39 | // NewXCache function will create a new XCache structure. 40 | // The XCache is resident in memory, supports multithreading and concurrency. 41 | // "id" is the unique id of the XCache. 42 | // "maxitems" is the max authorized quantity of objects into the XCache. If 0, the cache hast no limit in quantity of objects. 43 | // "expire" is a max duration of the objects into the cache. If 0, no limit 44 | // Returns the *XCache created. 45 | func NewXCache(id string, maxitems int, expire time.Duration) *XCache { 46 | if LOG { 47 | log.Printf("Creating cache with data {id: %s, maxitems: %d, expire: %d}", id, maxitems, expire) 48 | } 49 | return &XCache{ 50 | ID: id, 51 | Maxitems: maxitems, 52 | Validator: nil, 53 | Expire: expire, 54 | items: make(map[string]*XCacheEntry), 55 | } 56 | } 57 | 58 | // Set will set an entry in the cache. 59 | // If the entry already exists, just replace it with a new creation date. 60 | // If the entry does not exist, it will insert it in the cache and if the cache if full (maxitems reached), then a clean is called to remove 10%. 61 | // Returns nothing. 62 | func (c *XCache) Set(key string, indata interface{}) { 63 | c.mutex.Lock() 64 | // check if the entry already exists 65 | _, ok := c.items[key] 66 | c.items[key] = &XCacheEntry{ctime: time.Now(), rtime: time.Now(), data: indata} 67 | if ok { 68 | c.removeFromPile(key) 69 | } 70 | c.pile = append(c.pile, key) 71 | c.mutex.Unlock() 72 | if c.Maxitems > 0 && len(c.items) >= c.Maxitems { 73 | // We need a cleaning 74 | c.Clean(10) 75 | } 76 | } 77 | 78 | // removeFromPile will remove an entry key from the ordered pile. 79 | func (c *XCache) removeFromPile(key string) { 80 | // removes the key and append it to the end 81 | for i, x := range c.pile { 82 | if x == key { 83 | if i == len(c.pile)-1 { 84 | c.pile = c.pile[:i] 85 | } else { 86 | c.pile = append(c.pile[:i], c.pile[i+1:]...) 87 | } 88 | break 89 | } 90 | } 91 | } 92 | 93 | // Get will get the value of an entry. 94 | // If the entry does not exists, returns nil, false. 95 | // If the entry exists and is invalidated by time or validator function, then returns nil, true. 96 | // If the entry is good, return , false. 97 | func (c *XCache) Get(key string) (interface{}, bool) { 98 | c.mutex.Lock() 99 | if x, ok := c.items[key]; ok { 100 | c.mutex.Unlock() 101 | if c.Validator != nil { 102 | if b := c.Validator(key, x.ctime); !b { 103 | if LOG { 104 | log.Println("Validator invalids entry: " + key) 105 | } 106 | c.mutex.Lock() 107 | delete(c.items, key) 108 | c.removeFromPile(key) 109 | c.mutex.Unlock() 110 | return nil, true 111 | } 112 | } 113 | // expired ? 114 | if c.Expire != 0 { 115 | if x.ctime.Add(c.Expire).Before(time.Now()) { 116 | if LOG { 117 | log.Println("Cache timeout Expired: " + key) 118 | } 119 | c.mutex.Lock() 120 | delete(c.items, key) 121 | c.removeFromPile(key) 122 | c.mutex.Unlock() 123 | return nil, true 124 | } 125 | } 126 | x.rtime = time.Now() 127 | c.removeFromPile(key) 128 | c.pile = append(c.pile, key) 129 | return x.data, false 130 | } 131 | c.mutex.Unlock() 132 | return nil, false 133 | } 134 | 135 | // Del will delete the entry of the cache if it exists. 136 | func (c *XCache) Del(key string) { 137 | c.mutex.Lock() 138 | delete(c.items, key) 139 | // we should check if the entry exists before trying to removing 140 | c.removeFromPile(key) 141 | c.mutex.Unlock() 142 | } 143 | 144 | // Count will return the quantity of entries in the cache. 145 | func (c *XCache) Count() int { 146 | c.mutex.Lock() 147 | x := len(c.items) 148 | c.mutex.Unlock() 149 | return x 150 | } 151 | 152 | // Clean will delete expired entries, and free perc% of max items based on time. 153 | // perc = 0 to 100 (percentage to clean). 154 | // Returns quantity of removed entries. 155 | // It Will **not** verify the cache against its source (if Validator is set). If you want to scan that, use the Verify function. 156 | func (c *XCache) Clean(perc int) int { 157 | if LOG { 158 | log.Println("Cleaning cache") 159 | } 160 | i := 0 161 | c.mutex.Lock() 162 | // 1. clean all expired items 163 | if c.Expire != 0 { 164 | for k, x := range c.items { 165 | if x.ctime.Add(c.Expire).Before(time.Now()) { 166 | if LOG { 167 | log.Println("Cache timeout Expired: " + k) 168 | } 169 | delete(c.items, k) 170 | i++ 171 | } 172 | } 173 | } 174 | // 2. clean perc% of olders 175 | // How many do we have to clean ? 176 | total := len(c.items) 177 | num := total * perc / 100 178 | if LOG { 179 | log.Println("Quantity of elements to remove from cache:", num) 180 | } 181 | for i = 0; i < num; i++ { 182 | delete(c.items, c.pile[i]) 183 | } 184 | c.pile = c.pile[i:] 185 | c.mutex.Unlock() 186 | return i 187 | } 188 | 189 | // Verify will first, Clean(0) keeping all the entries, then will delete expired entries using the Validator function. 190 | // Returns the quantity of removed entries. 191 | // Based on what the validator function does, calling Verify can be **very** slow and cpu dependant. Be very careful. 192 | func (c *XCache) Verify() int { 193 | // 1. clean all expired items, do not touch others 194 | i := c.Clean(0) 195 | // 2. If there is a validator, verifies anything 196 | if c.Validator != nil { 197 | for k, x := range c.items { 198 | if b := c.Validator(k, x.ctime); !b { 199 | if LOG { 200 | log.Println("Validator invalids entry: " + k) 201 | } 202 | c.mutex.Lock() 203 | delete(c.items, k) 204 | c.mutex.Unlock() 205 | i++ 206 | } 207 | } 208 | } 209 | return i 210 | } 211 | 212 | // Flush will empty the whole cache and free all the memory of it. 213 | // Returns nothing. 214 | func (c *XCache) Flush() { 215 | c.mutex.Lock() 216 | // how to really deletes the data ? ( to free memory) 217 | c.items = make(map[string]*XCacheEntry) 218 | c.mutex.Unlock() 219 | } 220 | -------------------------------------------------------------------------------- /xcache_test.go: -------------------------------------------------------------------------------- 1 | package xcore 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func ExampleNewXCache() { 10 | cache1 := NewXCache("cacheid1", 0, 0) 11 | cache2 := NewXCache("cacheid2", 100, 0) 12 | cache3 := NewXCache("cacheid3", 0, 100*time.Minute) 13 | 14 | fmt.Println("All caches empty:", cache1.Count(), cache2.Count(), cache3.Count()) 15 | // Output: All caches empty: 0 0 0 16 | } 17 | 18 | func TestNewXCache(t *testing.T) { 19 | cache1 := NewXCache("cacheid1", 0, 0) 20 | cache2 := NewXCache("cacheid2", 100, 0) 21 | cache3 := NewXCache("cacheid3", 0, 100*time.Minute) 22 | 23 | if cache1 == nil || cache1.Count() != 0 { 24 | t.Error("Error creating NewXCache cache1") 25 | return 26 | } 27 | if cache2 == nil || cache2.Count() != 0 { 28 | t.Error("Error creating NewXCache cache2") 29 | return 30 | } 31 | if cache3 == nil || cache3.Count() != 0 { 32 | t.Error("Error creating NewXCache cache3") 33 | return 34 | } 35 | } 36 | 37 | func TestXCache(t *testing.T) { 38 | cache := NewXCache("cacheid", 0, 0) 39 | cache.Set("id1", "Data for id1") 40 | cache.Set("id2", "Data for id2") 41 | cache.Set("id3", "Data for id3") 42 | if cache.Count() != 3 { 43 | t.Error("Error counting cache") 44 | return 45 | } 46 | data1, deleted1 := cache.Get("id1") 47 | if deleted1 || data1 != "Data for id1" { 48 | t.Error("Error getting id1 in cache") 49 | return 50 | } 51 | cache.Del("id1") 52 | if cache.Count() != 2 { 53 | t.Error("Error counting cache after delete") 54 | return 55 | } 56 | data2, deleted2 := cache.Get("id1") 57 | if deleted2 || data2 != nil { 58 | t.Error("Error getting id2 in cache") 59 | return 60 | } 61 | } 62 | 63 | func TestXCache_invalidated(t *testing.T) { 64 | cache := NewXCache("cacheid", 0, 1*time.Second) 65 | cache.Set("id1", "Data for id1") 66 | cache.Set("id2", "Data for id2") 67 | cache.Set("id3", "Data for id3") 68 | if cache.Count() != 3 { 69 | t.Error("Error counting cache") 70 | return 71 | } 72 | t.Log("Wait for 2 seconds to invalidate all 3 entries") 73 | time.Sleep(2 * time.Second) 74 | if cache.Count() != 3 { 75 | t.Error("Error counting cache after invalidating") 76 | return 77 | } 78 | data1, deleted1 := cache.Get("id1") 79 | if !deleted1 || data1 != nil { 80 | t.Error("Error getting invalidated id1 in cache") 81 | return 82 | } 83 | if cache.Count() != 2 { 84 | t.Error("Error counting cache after invalidating") 85 | return 86 | } 87 | 88 | data2, deleted2 := cache.Get("id2") 89 | if !deleted2 || data2 != nil { 90 | t.Error("Error getting invalidated id2 in cache") 91 | return 92 | } 93 | if cache.Count() != 1 { 94 | t.Error("Error counting cache after invalidating") 95 | return 96 | } 97 | } 98 | 99 | func TestXCache_cleaning(t *testing.T) { 100 | cache := NewXCache("cacheid", 110, 0) 101 | for i := 0; i < 100; i++ { 102 | cache.Set(fmt.Sprintf("id%d", i), i) 103 | } 104 | if cache.Count() != 100 { 105 | t.Error("Error counting cache") 106 | return 107 | } 108 | // Clean 30% 109 | cache.Clean(30) 110 | if cache.Count() != 70 { 111 | t.Error("Error counting cache after cleaning") 112 | return 113 | } 114 | cache.Flush() 115 | if cache.Count() != 0 { 116 | t.Error("Error counting cache after flushing") 117 | return 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /xdataset.go: -------------------------------------------------------------------------------- 1 | package xcore 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // XDatasetDef is a special interface to implement a set of data that can be scanned recursively (by XTemplate for instance) 12 | // to search data into it, Stringify it, and set/get/del entries of data 13 | // The get* methods must accept a path id>id>id... 14 | type XDatasetDef interface { 15 | // Stringify will dump the content into a human readable string 16 | fmt.Stringer // please implement String() 17 | fmt.GoStringer // Please implement GoString() 18 | 19 | // Set will associate the data to the key. If it already exists, it will be replaced 20 | Set(key string, data interface{}) 21 | 22 | // Get will return the value associated to the key if it exists, or bool = false 23 | Get(key string) (interface{}, bool) 24 | // Same as Get but will return the value associated to the key as a XDatasetDef if it exists, or bool = false 25 | GetDataset(key string) (XDatasetDef, bool) 26 | // Same as Get but will return the value associated to the key as a XDatasetCollectionDef if it exists, or bool = false 27 | GetCollection(key string) (XDatasetCollectionDef, bool) 28 | 29 | // Same as Get but will return the value associated to the key as a string if it exists, or bool = false 30 | GetString(key string) (string, bool) 31 | // Same as Get but will return the value associated to the key as a bool if it exists, or bool = false 32 | GetBool(key string) (bool, bool) 33 | // Same as Get but will return the value associated to the key as an int if it exists, or bool = false 34 | GetInt(key string) (int, bool) 35 | // Same as Get but will return the value associated to the key as a float64 if it exists, or bool = false 36 | GetFloat(key string) (float64, bool) 37 | // Same as Get but will return the value associated to the key as a Time if it exists, or bool = false 38 | GetTime(key string) (time.Time, bool) 39 | // Same as Get but will return the value associated to the key as a []String if it exists, or bool = false 40 | GetStringCollection(key string) ([]string, bool) 41 | // Same as Get but will return the value associated to the key as a []bool if it exists, or bool = false 42 | GetBoolCollection(key string) ([]bool, bool) 43 | // Same as Get but will return the value associated to the key as a []int if it exists, or bool = false 44 | GetIntCollection(key string) ([]int, bool) 45 | // Same as Get but will return the value associated to the key as a []float64 if it exists, or bool = false 46 | GetFloatCollection(key string) ([]float64, bool) 47 | // Same as Get but will return the value associated to the key as a []Time if it exists, or bool = false 48 | GetTimeCollection(key string) ([]time.Time, bool) 49 | 50 | // Del will delete the data associated to the key and deletes the key entry 51 | Del(key string) 52 | // Clone the object 53 | Clone() XDatasetDef 54 | } 55 | 56 | // ===================== 57 | // XDataset 58 | // ===================== 59 | 60 | // XDataset is the basic interface dataset interface 61 | type XDataset map[string]interface{} 62 | 63 | // String will transform the XDataset into a readable string for humans 64 | func (d *XDataset) String() string { 65 | sdata := []string{} 66 | for key, val := range *d { 67 | sdata = append(sdata, key+":"+fmt.Sprintf("%v", val)) 68 | } 69 | sort.Strings(sdata) // Lets be sure the print is always the same presentation 70 | return "xcore.XDataset{" + strings.Join(sdata, " ") + "}" 71 | } 72 | 73 | // GoString will transform the XDataset into a readable string for humans 74 | func (d *XDataset) GoString() string { 75 | sdata := []string{} 76 | for key, val := range *d { 77 | sdata = append(sdata, key+":"+fmt.Sprintf("%#v", val)) 78 | } 79 | sort.Strings(sdata) // Lets be sure the print is always the same presentation 80 | return "#xcore.XDataset{" + strings.Join(sdata, " ") + "}" 81 | } 82 | 83 | // Set will add a variable key with value data to the XDataset 84 | func (d *XDataset) Set(key string, data interface{}) { 85 | (*d)[key] = data 86 | } 87 | 88 | // Get will read the value of the key variable 89 | func (d *XDataset) Get(key string) (interface{}, bool) { 90 | xid := strings.Split(key, ">") 91 | if len(xid) > 1 { 92 | subset, ok := (*d)[xid[0]] 93 | if !ok { 94 | return nil, false 95 | } 96 | if ds, ok := subset.(XDatasetDef); ok { 97 | return ds.Get(strings.Join(xid[1:], ">")) 98 | } 99 | if dsc, ok := subset.(XDatasetCollectionDef); ok { 100 | entry, err := strconv.Atoi(xid[1]) 101 | if err != nil { 102 | return nil, false 103 | } 104 | ds, _ := dsc.Get(entry) 105 | if ds == nil { 106 | return nil, false 107 | } 108 | if len(xid) == 2 { 109 | return ds, true 110 | } 111 | return ds.Get(strings.Join(xid[2:], ">")) 112 | } 113 | } 114 | data, ok := (*d)[key] 115 | if ok { 116 | return data, true 117 | } 118 | return nil, false 119 | } 120 | 121 | // GetDataset will read the value of the key variable as a XDatasetDef cast type 122 | func (d *XDataset) GetDataset(key string) (XDatasetDef, bool) { 123 | if val, ok := d.Get(key); ok { 124 | if val2, ok2 := val.(XDatasetDef); ok2 { 125 | return val2, true 126 | } 127 | } 128 | return nil, false 129 | } 130 | 131 | // GetCollection will read the value of the key variable as a XDatasetCollection cast type 132 | func (d *XDataset) GetCollection(key string) (XDatasetCollectionDef, bool) { 133 | if val, ok := d.Get(key); ok { 134 | if val2, ok2 := val.(XDatasetCollectionDef); ok2 { 135 | return val2, true 136 | } 137 | } 138 | return nil, false 139 | } 140 | 141 | // GetString will read the value of the key variable as a string cast type 142 | func (d *XDataset) GetString(key string) (string, bool) { 143 | if val, ok := d.Get(key); ok { 144 | if val == nil { 145 | return "", true 146 | } 147 | return fmt.Sprint(val), true 148 | } 149 | return "", false 150 | } 151 | 152 | // GetBool will read the value of the key variable as a boolean cast type 153 | // If the value is int, float, it will be convert with the rule 0: false, != 0: true 154 | // If the value is anything else and it exists, it will return true if it's not nil 155 | func (d *XDataset) GetBool(key string) (bool, bool) { 156 | if val, ok := d.Get(key); ok { 157 | if val2, ok2 := val.(bool); ok2 { 158 | return val2, true 159 | } 160 | if val2, ok2 := val.(int); ok2 { 161 | return val2 != 0, true 162 | } 163 | if val2, ok2 := val.(int8); ok2 { 164 | return val2 != 0, true 165 | } 166 | if val2, ok2 := val.(int16); ok2 { 167 | return val2 != 0, true 168 | } 169 | if val2, ok2 := val.(int32); ok2 { 170 | return val2 != 0, true 171 | } 172 | if val2, ok2 := val.(int64); ok2 { 173 | return val2 != 0, true 174 | } 175 | if val2, ok2 := val.(uint); ok2 { 176 | return val2 != 0, true 177 | } 178 | if val2, ok2 := val.(uint8); ok2 { 179 | return val2 != 0, true 180 | } 181 | if val2, ok2 := val.(uint16); ok2 { 182 | return val2 != 0, true 183 | } 184 | if val2, ok2 := val.(uint32); ok2 { 185 | return val2 != 0, true 186 | } 187 | if val2, ok2 := val.(uint64); ok2 { 188 | return val2 != 0, true 189 | } 190 | if val2, ok2 := val.(byte); ok2 { 191 | return val2 != 0, true 192 | } 193 | if val2, ok2 := val.(float32); ok2 { 194 | return val2 != 0, true 195 | } 196 | if val2, ok2 := val.(float64); ok2 { 197 | return val2 != 0, true 198 | } 199 | if val != nil { 200 | return true, true 201 | } 202 | } 203 | return false, false 204 | } 205 | 206 | // GetInt will read the value of the key variable as an integer cast type 207 | // If the value is bool, will return 0/1 208 | // If the value is float, will return integer part of value 209 | func (d *XDataset) GetInt(key string) (int, bool) { 210 | if val, ok := d.Get(key); ok { 211 | if val2, ok2 := val.(bool); ok2 { 212 | if val2 { 213 | return 1, true 214 | } 215 | return 0, true 216 | } 217 | if val2, ok2 := val.(int); ok2 { 218 | return val2, true 219 | } 220 | if val2, ok2 := val.(int8); ok2 { 221 | return int(val2), true 222 | } 223 | if val2, ok2 := val.(int16); ok2 { 224 | return int(val2), true 225 | } 226 | if val2, ok2 := val.(int32); ok2 { 227 | return int(val2), true 228 | } 229 | if val2, ok2 := val.(int64); ok2 { 230 | return int(val2), true 231 | } 232 | if val2, ok2 := val.(uint); ok2 { 233 | return int(val2), true 234 | } 235 | if val2, ok2 := val.(uint8); ok2 { 236 | return int(val2), true 237 | } 238 | if val2, ok2 := val.(uint16); ok2 { 239 | return int(val2), true 240 | } 241 | if val2, ok2 := val.(uint32); ok2 { 242 | return int(val2), true 243 | } 244 | if val2, ok2 := val.(uint64); ok2 { 245 | return int(val2), true 246 | } 247 | if val2, ok2 := val.(byte); ok2 { 248 | return int(val2), true 249 | } 250 | if val2, ok2 := val.(float32); ok2 { 251 | return int(val2), true 252 | } 253 | if val2, ok2 := val.(float64); ok2 { 254 | return int(val2), true 255 | } 256 | } 257 | return 0, false 258 | } 259 | 260 | // GetFloat will read the value of the key variable as a float64 cast type 261 | func (d *XDataset) GetFloat(key string) (float64, bool) { 262 | if val, ok := d.Get(key); ok { 263 | if val2, ok2 := val.(bool); ok2 { 264 | if val2 { 265 | return 1.0, true 266 | } 267 | return 0.0, true 268 | } 269 | if val2, ok2 := val.(int); ok2 { 270 | return float64(val2), true 271 | } 272 | if val2, ok2 := val.(int8); ok2 { 273 | return float64(val2), true 274 | } 275 | if val2, ok2 := val.(int16); ok2 { 276 | return float64(val2), true 277 | } 278 | if val2, ok2 := val.(int32); ok2 { 279 | return float64(val2), true 280 | } 281 | if val2, ok2 := val.(int64); ok2 { 282 | return float64(val2), true 283 | } 284 | if val2, ok2 := val.(uint); ok2 { 285 | return float64(val2), true 286 | } 287 | if val2, ok2 := val.(uint8); ok2 { 288 | return float64(val2), true 289 | } 290 | if val2, ok2 := val.(uint16); ok2 { 291 | return float64(val2), true 292 | } 293 | if val2, ok2 := val.(uint32); ok2 { 294 | return float64(val2), true 295 | } 296 | if val2, ok2 := val.(uint64); ok2 { 297 | return float64(val2), true 298 | } 299 | if val2, ok2 := val.(byte); ok2 { 300 | return float64(val2), true 301 | } 302 | if val2, ok2 := val.(float32); ok2 { 303 | return float64(val2), true 304 | } 305 | if val2, ok2 := val.(float64); ok2 { 306 | return val2, true 307 | } 308 | } 309 | return 0.0, false 310 | } 311 | 312 | // GetTime will read the value of the key variable as a time cast type 313 | func (d *XDataset) GetTime(key string) (time.Time, bool) { 314 | if val, ok := d.Get(key); ok { 315 | if val2, ok2 := val.(time.Time); ok2 { 316 | return val2, true 317 | } 318 | } 319 | return time.Time{}, false 320 | } 321 | 322 | // GetStringCollection will read the value of the key variable as a collection of strings cast type 323 | func (d *XDataset) GetStringCollection(key string) ([]string, bool) { 324 | if val, ok := d.Get(key); ok { 325 | if val2, ok2 := val.([]string); ok2 { 326 | return val2, true 327 | } 328 | } 329 | return nil, false 330 | } 331 | 332 | // GetBoolCollection will read the value of the key variable as a collection of bool cast type 333 | func (d *XDataset) GetBoolCollection(key string) ([]bool, bool) { 334 | if val, ok := d.Get(key); ok { 335 | if val2, ok2 := val.([]bool); ok2 { 336 | return val2, true 337 | } 338 | } 339 | return nil, false 340 | } 341 | 342 | // GetIntCollection will read the value of the key variable as a collection of int cast type 343 | func (d *XDataset) GetIntCollection(key string) ([]int, bool) { 344 | if val, ok := d.Get(key); ok { 345 | if val2, ok2 := val.([]int); ok2 { 346 | return val2, true 347 | } 348 | } 349 | return nil, false 350 | } 351 | 352 | // GetFloatCollection will read the value of the key variable as a collection of float cast type 353 | func (d *XDataset) GetFloatCollection(key string) ([]float64, bool) { 354 | if val, ok := d.Get(key); ok { 355 | if val2, ok2 := val.([]float64); ok2 { 356 | return val2, true 357 | } 358 | } 359 | return nil, false 360 | } 361 | 362 | // GetTimeCollection will read the value of the key variable as a collection of time cast type 363 | func (d *XDataset) GetTimeCollection(key string) ([]time.Time, bool) { 364 | if val, ok := d.Get(key); ok { 365 | if val2, ok2 := val.([]time.Time); ok2 { 366 | return val2, true 367 | } 368 | } 369 | return nil, false 370 | } 371 | 372 | // Del will deletes the variable 373 | func (d *XDataset) Del(key string) { 374 | delete(*d, key) 375 | } 376 | 377 | // Clone will creates a totally new data memory cloned from this object 378 | func (d *XDataset) Clone() XDatasetDef { 379 | cloned := &XDataset{} 380 | for id, val := range *d { 381 | clonedval := val 382 | // If the object is also cloneable, we clone it 383 | if cloneable1, ok := val.(interface{ Clone() XDatasetDef }); ok { 384 | clonedval = cloneable1.Clone() 385 | } 386 | if cloneable2, ok := val.(interface{ Clone() XDatasetCollectionDef }); ok { 387 | clonedval = cloneable2.Clone() 388 | } 389 | cloned.Set(id, clonedval) 390 | } 391 | return cloned 392 | } 393 | 394 | // XDatasetCollectionDef is the definition of a collection of XDatasetDefs 395 | type XDatasetCollectionDef interface { 396 | // Stringify will dump the content into a human readable string 397 | fmt.Stringer // please implement String() 398 | fmt.GoStringer // Please implement GoString() 399 | 400 | // Will add a datasetdef to the beginning of the collection 401 | Unshift(data XDatasetDef) 402 | // Will remove the first datasetdef of the collection and return it 403 | Shift() XDatasetDef 404 | 405 | // Will add a datasetdef to the end of the collection 406 | Push(data XDatasetDef) 407 | // Will remove the last datasetdef of the collection and return it 408 | Pop() XDatasetDef 409 | 410 | // Will count the quantity of entries 411 | Count() int 412 | 413 | // Will get the entry by the index and let it in the collection 414 | Get(index int) (XDatasetDef, bool) 415 | 416 | // Will search for the data associated to the key by priority (last entry is the most important) 417 | // returns bool = false if nothing has been found 418 | GetData(key string) (interface{}, bool) 419 | 420 | // Same as GetData but will convert the result to a string if possible 421 | // returns bool = false if nothing has been found 422 | GetDataString(key string) (string, bool) 423 | // Same as GetData but will convert the result to an int if possible 424 | // returns bool = false if nothing has been found 425 | GetDataInt(key string) (int, bool) 426 | // Same as GetData but will convert the result to a boolean if possible 427 | // returns second bool = false if nothing has been found 428 | GetDataBool(key string) (bool, bool) 429 | // Same as GetData but will convert the result to a float if possible 430 | // returns bool = false if nothing has been found 431 | GetDataFloat(key string) (float64, bool) 432 | // Same as GetData but will convert the result to a Time if possible 433 | // returns bool = false if nothing has been found 434 | GetDataTime(key string) (time.Time, bool) 435 | // Same as GetData but will convert the result to a XDatasetCollectionDef of data if possible 436 | // returns bool = false if nothing has been found 437 | GetCollection(key string) (XDatasetCollectionDef, bool) 438 | 439 | // Clone the object 440 | Clone() XDatasetCollectionDef 441 | } 442 | 443 | // ===================== 444 | // XDatasetConnection 445 | // ===================== 446 | 447 | // XDatasetCollection is the basic collection of XDatasetDefs 448 | type XDatasetCollection []XDatasetDef 449 | 450 | // String will transform the XDataset into a readable string 451 | func (d *XDatasetCollection) String() string { 452 | str := "XDatasetCollection[" 453 | for key, val := range *d { 454 | str += strconv.Itoa(key) + ":" + fmt.Sprint(val) + " " 455 | } 456 | str += "]" 457 | return str 458 | } 459 | 460 | // GoString will transform the XDataset into a readable string for humans 461 | func (d *XDatasetCollection) GoString() string { 462 | return d.String() 463 | } 464 | 465 | // Unshift will adds a XDatasetDef at the beginning of the collection 466 | func (d *XDatasetCollection) Unshift(data XDatasetDef) { 467 | *d = append([]XDatasetDef{data}, (*d)...) 468 | } 469 | 470 | // Shift will remove the element at the beginning of the collection 471 | func (d *XDatasetCollection) Shift() XDatasetDef { 472 | data := (*d)[0] 473 | *d = (*d)[1:] 474 | return data 475 | } 476 | 477 | // Push will adds a XDatasetDef at the end of the collection 478 | func (d *XDatasetCollection) Push(data XDatasetDef) { 479 | *d = append(*d, data) 480 | } 481 | 482 | // Pop will remove the element at the end of the collection 483 | func (d *XDatasetCollection) Pop() XDatasetDef { 484 | data := (*d)[len(*d)-1] 485 | *d = (*d)[:len(*d)-1] 486 | return data 487 | } 488 | 489 | // Count will return the quantity of elements into the collection 490 | func (d *XDatasetCollection) Count() int { 491 | return len(*d) 492 | } 493 | 494 | // Get will retrieve an element by index from the collection 495 | func (d *XDatasetCollection) Get(index int) (XDatasetDef, bool) { 496 | if index < 0 || index >= len(*d) { 497 | return nil, false 498 | } 499 | return (*d)[index], true 500 | } 501 | 502 | // GetData will retrieve the first available data identified by key from the collection ordered by index 503 | func (d *XDatasetCollection) GetData(key string) (interface{}, bool) { 504 | for i := len(*d) - 1; i >= 0; i-- { 505 | val, ok := (*d)[i].Get(key) 506 | if ok { 507 | return val, true 508 | } 509 | } 510 | return nil, false 511 | } 512 | 513 | // GetDataString will retrieve the first available data identified by key from the collection ordered by index and return it as a string 514 | func (d *XDatasetCollection) GetDataString(key string) (string, bool) { 515 | v, ok := d.GetData(key) 516 | if ok { 517 | if v == nil { 518 | return "", true 519 | } 520 | return fmt.Sprint(v), true 521 | } 522 | return "", false 523 | } 524 | 525 | // GetDataBool will retrieve the first available data identified by key from the collection ordered by index and return it as a boolean 526 | func (d *XDatasetCollection) GetDataBool(key string) (bool, bool) { 527 | if val, ok := d.GetData(key); ok { 528 | if val2, ok2 := val.(bool); ok2 { 529 | return val2, true 530 | } 531 | } 532 | return false, false 533 | } 534 | 535 | // GetDataInt will retrieve the first available data identified by key from the collection ordered by index and return it as an integer 536 | func (d *XDatasetCollection) GetDataInt(key string) (int, bool) { 537 | if val, ok := d.GetData(key); ok { 538 | if val2, ok2 := val.(int); ok2 { 539 | return val2, true 540 | } 541 | } 542 | return 0, false 543 | } 544 | 545 | // GetDataFloat will retrieve the first available data identified by key from the collection ordered by index and return it as a float 546 | func (d *XDatasetCollection) GetDataFloat(key string) (float64, bool) { 547 | if val, ok := d.GetData(key); ok { 548 | if val2, ok2 := val.(float64); ok2 { 549 | return val2, true 550 | } 551 | } 552 | return 0, false 553 | } 554 | 555 | // GetDataTime will retrieve the first available data identified by key from the collection ordered by index and return it as a time 556 | func (d *XDatasetCollection) GetDataTime(key string) (time.Time, bool) { 557 | if val, ok := d.GetData(key); ok { 558 | if val2, ok2 := val.(time.Time); ok2 { 559 | return val2, true 560 | } 561 | } 562 | return time.Time{}, false 563 | } 564 | 565 | // GetCollection will retrieve a collection from the XDatasetCollection 566 | func (d *XDatasetCollection) GetCollection(key string) (XDatasetCollectionDef, bool) { 567 | v, ok := d.GetData(key) 568 | // Verify v IS actually a XDatasetCollectionDef to avoid the error 569 | if ok { 570 | return v.(XDatasetCollectionDef), true 571 | } 572 | return nil, false 573 | } 574 | 575 | // Clone will make a full copy of the object into memory 576 | func (d *XDatasetCollection) Clone() XDatasetCollectionDef { 577 | cloned := &XDatasetCollection{} 578 | for _, val := range *d { 579 | *cloned = append(*cloned, val.Clone()) 580 | } 581 | return cloned 582 | } 583 | -------------------------------------------------------------------------------- /xdataset_test.go: -------------------------------------------------------------------------------- 1 | package xcore 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func ExampleXDataset() { 10 | 11 | tmp, _ := time.Parse(time.RFC3339, "2020-01-01T12:00:00.0Z") 12 | ds := &XDataset{ 13 | "v1": 123, 14 | "v2": "abc", 15 | "vt": tmp, 16 | "v3": true, 17 | "vpi": 3.1415927, 18 | } 19 | fmt.Println(ds) 20 | 21 | data := &XDataset{ 22 | "clientname": "Fred", 23 | "clientpicture": "face.jpg", 24 | "hobbies": &XDatasetCollection{ 25 | &XDataset{"name": "Football", "sport": "yes"}, 26 | &XDataset{"name": "Ping-pong", "sport": "yes"}, 27 | &XDataset{"name": "Swimming", "sport": "yes"}, 28 | &XDataset{"name": "Videogames", "sport": "no"}, 29 | }, 30 | "preferredhobby": &XDataset{ 31 | "name": "Baseball", 32 | "sport": "yes", 33 | }, 34 | "metadata": &XDataset{ 35 | "preferred-color": "blue", 36 | "Salary": 3568.65, 37 | "hiredate": tmp, 38 | }, 39 | } 40 | 41 | fmt.Println(data) 42 | // Output: 43 | // xcore.XDataset{v1:123 v2:abc v3:true vpi:3.1415927 vt:2020-01-01 12:00:00 +0000 UTC} 44 | // xcore.XDataset{clientname:Fred clientpicture:face.jpg hobbies:XDatasetCollection[0:xcore.XDataset{name:Football sport:yes} 1:xcore.XDataset{name:Ping-pong sport:yes} 2:xcore.XDataset{name:Swimming sport:yes} 3:xcore.XDataset{name:Videogames sport:no} ] metadata:xcore.XDataset{Salary:3568.65 hiredate:2020-01-01 12:00:00 +0000 UTC preferred-color:blue} preferredhobby:xcore.XDataset{name:Baseball sport:yes}} 45 | } 46 | 47 | func TestXDataset_simple_print(t *testing.T) { 48 | 49 | // 1. Create a simple XDataset 50 | tmp, _ := time.Parse(time.RFC3339, "2020-01-01T12:00:00.0Z") 51 | ds := &XDataset{ 52 | "v1": 123, 53 | "v2": "abc", 54 | "vt": tmp, 55 | "v3": true, 56 | "vpi": 3.1415927, 57 | } 58 | 59 | // 2. print 60 | str := fmt.Sprintf("%v", ds) 61 | if str != "xcore.XDataset{v1:123 v2:abc v3:true vpi:3.1415927 vt:2020-01-01 12:00:00 +0000 UTC}" { 62 | t.Error("Error creating and printing simple XDataset " + str) 63 | return 64 | } 65 | 66 | str = fmt.Sprintf("%#v", ds) 67 | if str != "#xcore.XDataset{v1:123 v2:\"abc\" v3:true vpi:3.1415927 vt:time.Date(2020, time.January, 1, 12, 0, 0, 0, time.UTC)}" { 68 | t.Error("Error creating and #printing simple XDataset " + str) 69 | return 70 | } 71 | } 72 | 73 | func getComplexDataset() *XDataset { 74 | 75 | // Create a complex XDataset with subsets 76 | tmp, _ := time.Parse(time.RFC3339, "2020-01-01T12:00:00") 77 | data := &XDataset{ 78 | "clientname": "Fred", 79 | "clientpicture": "face.jpg", 80 | "hobbies": &XDatasetCollection{ 81 | &XDataset{"name": "Football", "sport": "yes"}, 82 | &XDataset{"name": "Ping-pong", "sport": "yes"}, 83 | &XDataset{"name": "Swimming", "sport": "yes"}, 84 | &XDataset{"name": "Videogames", "sport": "no"}, 85 | }, 86 | "preferredhobby": &XDataset{ 87 | "name": "Baseball", 88 | "sport": "yes", 89 | }, 90 | "metadata": &XDataset{ 91 | "preferred-color": "blue", 92 | "Salary": 3568.65, 93 | "hiredate": tmp, 94 | }, 95 | } 96 | return data 97 | } 98 | 99 | func TestCreateXDataset_complex_print(t *testing.T) { 100 | 101 | data := getComplexDataset() 102 | 103 | str := fmt.Sprintf("%v", data) 104 | if str != "xcore.XDataset{clientname:Fred clientpicture:face.jpg hobbies:XDatasetCollection[0:xcore.XDataset{name:Football sport:yes} 1:xcore.XDataset{name:Ping-pong sport:yes} 2:xcore.XDataset{name:Swimming sport:yes} 3:xcore.XDataset{name:Videogames sport:no} ] metadata:xcore.XDataset{Salary:3568.65 hiredate:0001-01-01 00:00:00 +0000 UTC preferred-color:blue} preferredhobby:xcore.XDataset{name:Baseball sport:yes}}" { 105 | t.Error("Error creating and printing complex XDataset " + str) 106 | return 107 | } 108 | 109 | str = fmt.Sprintf("%#v", data) 110 | if str != "#xcore.XDataset{clientname:\"Fred\" clientpicture:\"face.jpg\" hobbies:XDatasetCollection[0:xcore.XDataset{name:Football sport:yes} 1:xcore.XDataset{name:Ping-pong sport:yes} 2:xcore.XDataset{name:Swimming sport:yes} 3:xcore.XDataset{name:Videogames sport:no} ] metadata:#xcore.XDataset{Salary:3568.65 hiredate:time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC) preferred-color:\"blue\"} preferredhobby:#xcore.XDataset{name:\"Baseball\" sport:\"yes\"}}" { 111 | t.Error("Error creating and #printing complex XDataset " + str) 112 | return 113 | } 114 | } 115 | 116 | func TestCreateXDataset_Get(t *testing.T) { 117 | 118 | data := getComplexDataset() 119 | 120 | // 2. Gets and paths 121 | v1, _ := data.GetString("clientname") 122 | if v1 != "Fred" { 123 | t.Error("Error getting clientname from XDataset " + v1) 124 | return 125 | } 126 | 127 | v2, _ := data.Get("hobbies") 128 | strv2 := fmt.Sprintf("%v", v2) 129 | if strv2 != "XDatasetCollection[0:xcore.XDataset{name:Football sport:yes} 1:xcore.XDataset{name:Ping-pong sport:yes} 2:xcore.XDataset{name:Swimming sport:yes} 3:xcore.XDataset{name:Videogames sport:no} ]" { 130 | t.Error("Error getting hobbies from XDataset " + strv2) 131 | return 132 | } 133 | 134 | v3, _ := data.Get("hobbies>2") 135 | strv3 := fmt.Sprintf("%v", v3) 136 | if strv3 != "xcore.XDataset{name:Swimming sport:yes}" { 137 | t.Error("Error getting hobbies>2 from XDataset " + strv3) 138 | return 139 | } 140 | 141 | v4, _ := data.GetString("hobbies>2>name") 142 | if v4 != "Swimming" { 143 | t.Error("Error getting hobbies>2>name from XDataset " + v4) 144 | return 145 | } 146 | } 147 | 148 | func TestLoadJSONInXDataset(t *testing.T) { 149 | 150 | } 151 | 152 | func TestXDataset_Clone(t *testing.T) { 153 | 154 | ds := &XDataset{ 155 | "v1": 123, 156 | "v2": "abc", 157 | "v3": true, 158 | "v4": &XDataset{ 159 | "v4_p1": 456, 160 | "v4_p2": "def", 161 | "v4_p3": false, 162 | }, 163 | } 164 | original := fmt.Sprintf("%v", ds) 165 | 166 | cds := ds.Clone() 167 | cloned := fmt.Sprintf("%v", cds) 168 | 169 | if original != cloned { 170 | t.Error("Error during clonation of XDataset " + cloned) 171 | return 172 | } 173 | 174 | // Verify it has been really cloned 175 | (*(*ds)["v4"].(*XDataset))["p5"] = "val5" 176 | val5o, _ := ds.GetString("v4>p5") // should be "val5" 177 | val5c, _ := cds.GetString("v4>p5") // should be "" 178 | if val5o == val5c { 179 | t.Error("Error during clonation of XDataset " + val5c) 180 | return 181 | } 182 | } 183 | 184 | func TestXDatasetCollection_Clone(t *testing.T) { 185 | 186 | dsc := &XDatasetCollection{ 187 | &XDataset{"v1": 123, 188 | "v2": "abc", 189 | "v3": true, 190 | "v4": &XDataset{ 191 | "v4_p1": 456, 192 | "v4_p2": "def", 193 | "v4_p3": false, 194 | }, 195 | }, 196 | &XDataset{"v11": 123, 197 | "v12": "abc", 198 | "v13": true, 199 | "v14": &XDataset{ 200 | "v14_p1": 456, 201 | "v14_p2": "def", 202 | "v14_p3": false, 203 | }, 204 | }, 205 | } 206 | 207 | original := fmt.Sprintf("%v", dsc) 208 | 209 | cdsc := dsc.Clone().(*XDatasetCollection) 210 | cloned := fmt.Sprintf("%v", cdsc) 211 | 212 | if original != cloned { 213 | t.Error("Error during clonation of XDatasetCollection " + cloned) 214 | return 215 | } 216 | 217 | // Verify it has been really cloned 218 | (*((*(*dsc)[0].(*XDataset))["v4"].(*XDataset)))["p5"] = "val5" 219 | 220 | ro0, _ := dsc.Get(0) 221 | val5o, _ := ro0.GetString("v4>p5") // should be "val5" 222 | 223 | r0, _ := cdsc.Get(0) 224 | val5c, _ := r0.GetString("v4>p5") // should be "" 225 | 226 | if val5c != "" || val5o != "val5" { 227 | t.Error("Error during clonation of XDatasetCollection " + val5c) 228 | return 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /xlanguage.go: -------------------------------------------------------------------------------- 1 | package xcore 2 | 3 | import ( 4 | "bufio" 5 | "encoding/xml" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "sort" 10 | "strings" 11 | 12 | "golang.org/x/text/language" 13 | ) 14 | 15 | // XLanguage is the oficial structure for the user 16 | type XLanguage struct { 17 | Name string 18 | Language language.Tag 19 | Entries map[string]string 20 | } 21 | 22 | // NewXLanguage will create an empty Language structure with a name and a language 23 | func NewXLanguage(name string, lang language.Tag) *XLanguage { 24 | return &XLanguage{Name: name, Language: lang, Entries: make(map[string]string)} 25 | } 26 | 27 | // NewXLanguageFromXMLFile will create an XLanguage structure with the data into the XML file 28 | // Returns nil if there is an error 29 | func NewXLanguageFromXMLFile(file string) (*XLanguage, error) { 30 | lang := &XLanguage{Entries: make(map[string]string)} 31 | err := lang.LoadXMLFile(file) 32 | if err != nil { 33 | return nil, err 34 | } 35 | return lang, nil 36 | } 37 | 38 | // NewXLanguageFromXMLString will create an XLanguage structure with the data into the XML String 39 | // Returns nil if there is an error 40 | func NewXLanguageFromXMLString(xml string) (*XLanguage, error) { 41 | lang := &XLanguage{Entries: make(map[string]string)} 42 | err := lang.LoadXMLString(xml) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return lang, nil 47 | } 48 | 49 | // NewXLanguageFromFile will create an XLanguage structure with the data into the text file 50 | // Returns nil if there is an error 51 | func NewXLanguageFromFile(file string) (*XLanguage, error) { 52 | l := &XLanguage{Entries: make(map[string]string)} 53 | err := l.LoadFile(file) 54 | if err != nil { 55 | return nil, err 56 | } 57 | return l, nil 58 | } 59 | 60 | // NewXLanguageFromString will create an XLanguage structure with the data into the string 61 | // Returns nil if there is an error 62 | func NewXLanguageFromString(data string) (*XLanguage, error) { 63 | l := &XLanguage{Entries: make(map[string]string)} 64 | err := l.LoadString(data) 65 | if err != nil { 66 | return nil, err 67 | } 68 | return l, nil 69 | } 70 | 71 | // LoadXMLFile will Load a language from an XML file and replace the content of the XLanguage structure with the new data 72 | // Returns nil if there is an error 73 | func (l *XLanguage) LoadXMLFile(file string) error { 74 | xmlFile, err := os.Open(file) 75 | if err != nil { 76 | return err 77 | } 78 | data, err := ioutil.ReadAll(xmlFile) 79 | if err != nil { 80 | return err 81 | } 82 | err = xmlFile.Close() 83 | if err != nil { 84 | return err 85 | } 86 | return l.LoadXMLString(string(data)) 87 | } 88 | 89 | // LoadXMLString will Load a language from an XML file and replace the content of the XLanguage structure with the new data 90 | // Returns nil if there is an error 91 | func (l *XLanguage) LoadXMLString(data string) error { 92 | // Temporal structures for XML loading 93 | type xentry struct { 94 | ID string `xml:"id,attr"` 95 | Entry string `xml:",chardata"` 96 | } 97 | 98 | type xlang struct { 99 | Name string `xml:"id,attr"` 100 | Language string `xml:"lang,attr"` 101 | Entries []xentry `xml:"entry"` 102 | } 103 | 104 | // Unmarshal 105 | temp := &xlang{} 106 | err := xml.Unmarshal([]byte(data), temp) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | // Scan to our XLanguage Object 112 | l.Name = temp.Name 113 | l.Language, _ = language.Parse(temp.Language) 114 | for _, e := range temp.Entries { 115 | l.Entries[e.ID] = e.Entry 116 | } 117 | return nil 118 | } 119 | 120 | // LoadFile will Load a language from a file and replace the content of the XLanguage structure with the new data 121 | // Returns nil if there is an error 122 | func (l *XLanguage) LoadFile(file string) error { 123 | flatFile, err := os.Open(file) 124 | if err != nil { 125 | return err 126 | } 127 | data, err := ioutil.ReadAll(flatFile) 128 | if err != nil { 129 | return err 130 | } 131 | err = flatFile.Close() 132 | if err != nil { 133 | return err 134 | } 135 | return l.LoadString(string(data)) 136 | } 137 | 138 | // LoadString will Load a language from a string and replace the content of the XLanguage structure with the new data 139 | // Returns nil if there is an error 140 | func (l *XLanguage) LoadString(data string) error { 141 | scanner := bufio.NewScanner(strings.NewReader(data)) 142 | for scanner.Scan() { 143 | line := scanner.Text() 144 | posequal := strings.Index(line, "=") 145 | 146 | // we ignore empty and comments lines, no key=value lines too 147 | if len(line) == 0 || line[0] == '#' || line[0] == ';' || posequal < 0 { 148 | continue 149 | } 150 | 151 | // we separate the key. if there is no key, we ignore the data 152 | key := strings.TrimSpace(line[:posequal]) 153 | if len(key) == 0 { 154 | continue 155 | } 156 | 157 | // we capture the value if it exists. If not, the key entry is initialized with a nil value 158 | value := "" 159 | if len(line) > posequal { 160 | value = strings.TrimSpace(line[posequal+1:]) 161 | } 162 | l.Entries[key] = value 163 | } 164 | if err := scanner.Err(); err != nil { 165 | return err 166 | } 167 | return nil 168 | } 169 | 170 | // SetName will set the name of the language table 171 | func (l *XLanguage) SetName(name string) { 172 | l.Name = name 173 | } 174 | 175 | // SetLanguage will set the language ISO code (2 letters) of the language table 176 | func (l *XLanguage) SetLanguage(lang language.Tag) { 177 | l.Language = lang 178 | } 179 | 180 | // GetName will return the name of the language table 181 | func (l *XLanguage) GetName() string { 182 | return l.Name 183 | } 184 | 185 | // GetLanguage will return the language of the language table 186 | func (l *XLanguage) GetLanguage() language.Tag { 187 | return l.Language 188 | } 189 | 190 | // Set will add an entry id-value into the language table 191 | func (l *XLanguage) Set(entry string, value string) { 192 | l.Entries[entry] = value 193 | } 194 | 195 | // Get will read an entry id-value from the language table 196 | func (l *XLanguage) Get(entry string) string { 197 | v, ok := l.Entries[entry] 198 | if ok { 199 | return v 200 | } 201 | return "" 202 | } 203 | 204 | // Del will remove an entry id-value from the language table 205 | func (l *XLanguage) Del(entry string) { 206 | delete(l.Entries, entry) 207 | } 208 | 209 | // String will transform the XDataset into a readable string for humans 210 | func (l *XLanguage) String() string { 211 | sdata := []string{} 212 | for key, val := range l.Entries { 213 | sdata = append(sdata, key+":"+fmt.Sprintf("%v", val)) 214 | } 215 | sort.Strings(sdata) // Lets be sure the print is always the same presentation 216 | return "xcore.XLanguage{" + strings.Join(sdata, " ") + "}" 217 | } 218 | 219 | // GoString will transform the XDataset into a readable string for humans 220 | func (l *XLanguage) GoString() string { 221 | sdata := []string{} 222 | for key, val := range l.Entries { 223 | sdata = append(sdata, key+":"+fmt.Sprintf("%#v", val)) 224 | } 225 | sort.Strings(sdata) // Lets be sure the print is always the same presentation 226 | return "#xcore.XLanguage{" + strings.Join(sdata, " ") + "}" 227 | } 228 | -------------------------------------------------------------------------------- /xlanguage_test.go: -------------------------------------------------------------------------------- 1 | package xcore 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "testing" 7 | 8 | "golang.org/x/text/language" 9 | ) 10 | 11 | func ExampleNewXLanguage() { 12 | 13 | langES := NewXLanguage("messages", language.Spanish) 14 | langEN := NewXLanguage("messages", language.English) 15 | langFR := NewXLanguage("messages", language.French) 16 | 17 | // You can load this from your system files for instance and keep your translation tables apart 18 | langES.Set("panicerror", "Error crítico del sistema") 19 | langEN.Set("panicerror", "System panic error") 20 | langFR.Set("panicerror", "Erreur grave dans le système") 21 | 22 | // pick a random general system language (for instance the client's OS language) 23 | lang := langFR 24 | 25 | // launch a panic errors 26 | fmt.Println("Launch a panic error message in the selected language:", lang.Get("panicerror")) 27 | // Output: 28 | // Launch a panic error message in the selected language: Erreur grave dans le système 29 | } 30 | 31 | func ExampleNewXLanguageFromXMLFile() { 32 | 33 | langES, _ := NewXLanguageFromXMLFile("./testunit/a.es.language") 34 | langEN, _ := NewXLanguageFromXMLFile("./testunit/a.en.language") 35 | langFR, _ := NewXLanguageFromXMLFile("./testunit/a.fr.language") 36 | 37 | // pick a random general system language (for instance the client's OS language) 38 | lang := langES 39 | 40 | // Say hello main language 41 | fmt.Println(lang.Get("entry1") + " " + lang.Get("entry2")) 42 | // Say hello in other languages 43 | fmt.Println(langEN.Get("entry1") + " " + langEN.Get("entry2")) 44 | fmt.Println(langFR.Get("entry1") + " " + langFR.Get("entry2")) 45 | // Output: 46 | // Bienvenido a XCore 47 | // Welcome to XCore 48 | // Bienvenue à XCore 49 | } 50 | 51 | func TestXLanguage(t *testing.T) { 52 | 53 | // Those strings are the same in errors.es.txt and errors.es.xml to make the tests 54 | teststrings := map[string]string{ 55 | "panicerror": "Error crítico del sistema", 56 | "systemerror": "Error del sistema", 57 | "fileerror": "Error leyendo el archivo", 58 | } 59 | 60 | manualES := NewXLanguage("messages", language.Spanish) 61 | for id, val := range teststrings { 62 | manualES.Set(id, val) 63 | } 64 | 65 | // Load from xml file 66 | loadxmlES, err := NewXLanguageFromXMLFile("./testunit/errors.es.xml") 67 | if err != nil { 68 | t.Errorf("Error loading XML File into language %s", err) 69 | return 70 | } 71 | 72 | // Load from text file 73 | loadtextES, err := NewXLanguageFromFile("./testunit/errors.es.txt") 74 | if err != nil { 75 | t.Errorf("Error loading Text File into language %s", err) 76 | return 77 | } 78 | 79 | // from xml string 80 | xmlstr, err := ioutil.ReadFile("./testunit/errors.es.xml") 81 | if err != nil { 82 | t.Errorf("Error loading XML File errors into string %s", err) 83 | return 84 | } 85 | loadstringxmlES, err := NewXLanguageFromXMLString(string(xmlstr)) 86 | if err != nil { 87 | t.Errorf("Error loading XML File into language %s", err) 88 | return 89 | } 90 | 91 | // from text string 92 | textstr, err := ioutil.ReadFile("./testunit/errors.es.txt") 93 | if err != nil { 94 | t.Errorf("Error loading Text File errors into string %s", err) 95 | return 96 | } 97 | loadstringtxtES, err := NewXLanguageFromString(string(textstr)) 98 | if err != nil { 99 | t.Errorf("Error loading Text File into language %s", err) 100 | return 101 | } 102 | 103 | // verify all 104 | for id, val := range teststrings { 105 | v1 := manualES.Get(id) 106 | if v1 != val { 107 | t.Errorf("Error reading value of manualES::%s", v1) 108 | return 109 | } 110 | v2 := loadxmlES.Get(id) 111 | if v2 != val { 112 | t.Errorf("Error reading value of loadxmlES::%s", v2) 113 | return 114 | } 115 | v3 := loadtextES.Get(id) 116 | if v3 != val { 117 | t.Errorf("Error reading value of loadtextES::%s", v3) 118 | return 119 | } 120 | v4 := loadstringxmlES.Get(id) 121 | if v4 != val { 122 | t.Errorf("Error reading value of loadstringxmlES::%s", v4) 123 | return 124 | } 125 | v5 := loadstringtxtES.Get(id) 126 | if v5 != val { 127 | t.Errorf("Error reading value of loadstringtxtES::%s", v5) 128 | return 129 | } 130 | } 131 | } 132 | 133 | func TestXLanguageAssign(t *testing.T) { 134 | 135 | // Those strings are the same in errors.es.txt and errors.es.xml to make the tests 136 | teststrings := map[string]string{ 137 | "panicerror": "System critical error", 138 | "systemerror": "System error", 139 | "fileerror": "File error", 140 | } 141 | 142 | manualES := NewXLanguage("messages", language.Spanish) 143 | for id, val := range teststrings { 144 | manualES.Set(id, val) 145 | } 146 | 147 | manualES.SetName("errors") 148 | manualES.SetLanguage(language.Spanish) 149 | manualES.Set("noerror", "There is no error") 150 | 151 | // Overload the spanish entries 152 | err := manualES.LoadFile("./testunit/errors.es.txt") 153 | if err != nil { 154 | t.Errorf("Error loading Text File into language %s", err) 155 | return 156 | } 157 | 158 | v1 := manualES.Get("panicerror") 159 | if v1 != "Error crítico del sistema" { 160 | t.Errorf("The value of panicerror is not correct %s", v1) 161 | return 162 | } 163 | 164 | v2 := manualES.Get("noerror") // should still be in english 165 | if v2 != "There is no error" { 166 | t.Errorf("The value of noerror is not correct %s", v2) 167 | return 168 | } 169 | 170 | manualES.Del("noerror") 171 | v3 := manualES.Get("noerror") // should be empty 172 | if v3 != "" { 173 | t.Errorf("The value of noerror is not correct (empty) %s", v3) 174 | return 175 | } 176 | 177 | name := manualES.GetName() 178 | if name != "errors" { 179 | t.Errorf("The value of language name is not correct %s", name) 180 | return 181 | } 182 | 183 | lang := manualES.GetLanguage() 184 | if lang != language.Spanish { 185 | t.Errorf("The value of language is not correct %s", lang) 186 | return 187 | } 188 | 189 | // String 190 | str := fmt.Sprint(manualES) 191 | if str != "xcore.XLanguage{fileerror:Error leyendo el archivo panicerror:Error crítico del sistema systemerror:Error del sistema}" { 192 | t.Errorf("The print value language is not correct %s", str) 193 | return 194 | } 195 | 196 | // Gostring 197 | str = fmt.Sprintf("%#v", manualES) 198 | if str != "#xcore.XLanguage{fileerror:\"Error leyendo el archivo\" panicerror:\"Error crítico del sistema\" systemerror:\"Error del sistema\"}" { 199 | t.Errorf("The print #value language is not correct %s", str) 200 | return 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /xtemplate.go: -------------------------------------------------------------------------------- 1 | package xcore 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "regexp" 9 | "sort" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | /* 15 | class to compile and keep a Template string 16 | A template is a set of HTML/XML (or any other language) set of: 17 | 18 | Comments: 19 | %-- comments --% 20 | Fields: 21 | {{field}} 22 | {{field>Subfield>Subfield}} 23 | Language injection 24 | ##entry## 25 | Subtemplates: 26 | xml/html code 27 | [[id]] 28 | xml/html code 29 | [[id]] 30 | xml/html code indented 31 | [[]] 32 | xml/html code 33 | [[]] 34 | Meta elements: 35 | ??xx?? if/then/else 36 | @@xx@@ loops 37 | &&xx&& references 38 | !!xx!! debug (dump) 39 | */ 40 | 41 | // MetaString and other consts: 42 | // type of elements present in the template 43 | const ( 44 | MetaString = 0 // a simple string to integrate into the code 45 | MetaComment = 1 // Comment, ignore it 46 | 47 | MetaLanguage = 2 // one param of the URL parameters list, index-1 based [page]/value1/value2... 48 | MetaReference = 3 // an URL variable coming through a query ?variable=value 49 | MetaRange = 4 // Parameter passed to the page Run by code 50 | MetaCondition = 5 // System (site) parameter 51 | MetaDump = 6 // Main page called parameters (into .page file) 52 | MetaVariable = 7 // this page parameters (into .page file), same as Main page parameters if it's the external called page 53 | 54 | MetaTemplateStart = 101 // Temporal nested box start tag 55 | MetaTemplateEnd = 102 // Temporal nested box end tag 56 | 57 | MetaUnused = -1 // a "not used anymore" param to be freed 58 | ) 59 | 60 | // XTemplateParam is a parameter definition into the template 61 | type XTemplateParam struct { 62 | ParamType int 63 | Data string 64 | // children *XTemplateData 65 | } 66 | 67 | // XTemplateData is an Array of all the parameters into the template 68 | type XTemplateData []XTemplateParam 69 | 70 | // XTemplate is the plain template structure 71 | type XTemplate struct { 72 | Name string 73 | Root *XTemplateData 74 | SubTemplates map[string]*XTemplate 75 | } 76 | 77 | // NewXTemplate will create a new empty template 78 | func NewXTemplate() *XTemplate { 79 | return &XTemplate{} 80 | } 81 | 82 | // NewXTemplateFromFile will create a new template from a file containing the template code 83 | func NewXTemplateFromFile(file string) (*XTemplate, error) { 84 | t := &XTemplate{} 85 | err := t.LoadFile(file) 86 | if err != nil { 87 | return nil, err 88 | } 89 | return t, nil 90 | } 91 | 92 | // NewXTemplateFromString will create a new template from a string containing the template code 93 | func NewXTemplateFromString(data string) (*XTemplate, error) { 94 | t := &XTemplate{} 95 | err := t.LoadString(data) 96 | if err != nil { 97 | return nil, err 98 | } 99 | return t, nil 100 | } 101 | 102 | // LoadFile will load a file into the template 103 | func (t *XTemplate) LoadFile(file string) error { 104 | tFile, err := os.Open(file) 105 | if err != nil { 106 | return err 107 | } 108 | data, err := ioutil.ReadAll(tFile) 109 | if err != nil { 110 | return err 111 | } 112 | err = tFile.Close() 113 | if err != nil { 114 | return err 115 | } 116 | return t.LoadString(string(data)) 117 | } 118 | 119 | // LoadString will load a string into the template 120 | func (t *XTemplate) LoadString(data string) error { 121 | return t.compile(data) 122 | } 123 | 124 | // compile will interprete the template code into objects 125 | func (t *XTemplate) compile(data string) error { 126 | // build, compile return result 127 | code := 128 | `(?s)` + // . is multiline 129 | 130 | // ==== COMENTS 131 | `(%)--(.*?)--%(\n|\r|\r\n|\n\r)?` + // index based 1 132 | 133 | // ==== LANGUAGE INJECTION 134 | `|(#)#(.+?)##` + // index based 4 135 | 136 | // ==== ELEMENTS 137 | `|(&)&(.+?)&&` + // index based 6 138 | `|(@)@(.+?)@@` + // index based 8 139 | `|(\?)\?(.+?)\?\?` + // index based 10 140 | `|(\!)\!(.+?)\!\!` + // index based 12 141 | `|(\{)\{(.+?)\}\}` + // index based 14 142 | 143 | // ==== NESTED ELEMENTS (SUB TEMPLATES) 144 | `|\[\[(\])\](\n|\r|\r\n|\n\r)?` + // index based 16 145 | `|(\[)\[([a-z0-9\|\.\-_]+?)\]\](\n|\r|\r\n|\n\r)?` // index based 18 146 | 147 | codex := regexp.MustCompile(code) 148 | indexes := codex.FindAllStringIndex(data, -1) 149 | matches := codex.FindAllStringSubmatch(data, -1) 150 | 151 | var compiled XTemplateData 152 | pointer := 0 153 | for i, x := range indexes { 154 | if pointer != x[0] { 155 | compiled = append(compiled, *(&XTemplateParam{ParamType: MetaString, Data: data[pointer:x[0]]})) 156 | } 157 | 158 | param := &XTemplateParam{} 159 | if matches[i][1] == "%" { 160 | param.ParamType = MetaComment // comment 161 | param.Data = matches[i][2] 162 | } else if matches[i][4] == "#" { 163 | param.ParamType = MetaLanguage // Language entry 164 | param.Data = matches[i][5] 165 | } else if matches[i][6] == "&" { 166 | param.ParamType = MetaReference // Reference to template 167 | param.Data = matches[i][7] 168 | } else if matches[i][8] == "@" { 169 | param.ParamType = MetaRange // Loop on data 170 | param.Data = matches[i][9] 171 | } else if matches[i][10] == "?" { 172 | param.ParamType = MetaCondition // Conditional on data 173 | param.Data = matches[i][11] 174 | } else if matches[i][12] == "!" { 175 | param.ParamType = MetaDump // Debug 176 | param.Data = matches[i][13] 177 | } else if matches[i][14] == "{" { 178 | param.ParamType = MetaVariable // Simple element 179 | param.Data = matches[i][15] 180 | } else if matches[i][18] == "[" { 181 | param.ParamType = MetaTemplateStart // Template start 182 | param.Data = matches[i][19] 183 | } else if matches[i][16] == "]" { 184 | param.ParamType = MetaTemplateEnd // Template end 185 | } else { 186 | param.ParamType = MetaUnused // unknown, will be removed 187 | } 188 | compiled = append(compiled, *param) 189 | pointer = x[1] 190 | } 191 | // end of Data 192 | if pointer != len(data) { 193 | compiled = append(compiled, *(&XTemplateParam{ParamType: MetaString, Data: data[pointer:]})) 194 | } 195 | 196 | // second pass: all the sub templates into the Subtemplates 197 | startpointers := []int{} 198 | subtemplates := []*XTemplate{} 199 | actualtemplate := t 200 | for i, x := range compiled { 201 | if x.ParamType == MetaTemplateStart { 202 | startpointers = append(startpointers, i) 203 | subtemplates = append(subtemplates, actualtemplate) 204 | actualtemplate = &XTemplate{Name: x.Data, Root: nil} 205 | } else if x.ParamType == MetaTemplateEnd { 206 | // we found the end of the nested box, lets create a nested param array from stacked startpointer up to i 207 | last := len(startpointers) - 1 208 | if last < 0 { 209 | return errors.New("Error: template mismatch Start/End") 210 | } 211 | startpointer := startpointers[last] 212 | startpointers = startpointers[:last] 213 | 214 | var subset XTemplateData 215 | for ptr := startpointer + 1; ptr < i; ptr++ { // we ignore the BOX]] end param (we dont need it in the hierarchic structure) 216 | if compiled[ptr].ParamType != MetaUnused { // we just ignore params marked to be deleted 217 | subset = append(subset, compiled[ptr]) 218 | compiled[ptr].ParamType = MetaUnused // marked to be deleted, traslated to a substructure 219 | } 220 | } 221 | actualtemplate.Root = &subset 222 | 223 | uppertemplate := subtemplates[last] 224 | subtemplates = subtemplates[:last] 225 | 226 | // If there are |, we separate the templates and add every one to the list with same pointer 227 | pospipe := strings.Index(actualtemplate.Name, "|") 228 | if pospipe >= 0 { 229 | vals := strings.Split(actualtemplate.Name, "|") 230 | for _, v := range vals { 231 | if len(v) > 0 { 232 | uppertemplate.AddTemplate(v, actualtemplate) 233 | } 234 | } 235 | } else { 236 | uppertemplate.AddTemplate(actualtemplate.Name, actualtemplate) 237 | } 238 | 239 | // pop actualtemplate 240 | actualtemplate = uppertemplate 241 | 242 | compiled[startpointer].ParamType = MetaUnused // marked to be deleted, no need of start template 243 | compiled[i].ParamType = MetaUnused // marked to be deleted, no need of end template 244 | } 245 | } 246 | if len(startpointers) > 0 { 247 | return errors.New("Error: template mismatch Start/End") 248 | } 249 | 250 | // last pass: delete params marked to be deleted and concatenate strings 251 | currentpointer := 0 252 | for i, x := range compiled { 253 | if x.ParamType != MetaUnused { 254 | if currentpointer != i { 255 | compiled[currentpointer] = x 256 | } 257 | currentpointer++ 258 | } 259 | } 260 | 261 | compiled = compiled[:currentpointer] 262 | t.Root = &compiled 263 | return nil 264 | } 265 | 266 | // AddTemplate will add a sub template to this template 267 | func (t *XTemplate) AddTemplate(name string, tmpl *XTemplate) { 268 | if t.SubTemplates == nil { 269 | t.SubTemplates = make(map[string]*XTemplate) 270 | } 271 | t.SubTemplates[name] = tmpl 272 | } 273 | 274 | // GetTemplate gets a sub template existing into this template 275 | func (t *XTemplate) GetTemplate(name string) *XTemplate { 276 | if t.SubTemplates == nil { 277 | return nil 278 | } 279 | return t.SubTemplates[name] 280 | } 281 | 282 | // Execute will inject the Data into the template and creates the final string 283 | func (t *XTemplate) Execute(data XDatasetDef) string { 284 | // Does data has a language ? 285 | if data != nil { 286 | var language *XLanguage 287 | lang, _ := data.Get("#") 288 | if lang != nil { 289 | language, _ = lang.(*XLanguage) // language is nil if it-s not a *XLanguage 290 | } 291 | stack := &XDatasetCollection{} 292 | stack.Push(data) 293 | return t.injector(stack, language) 294 | } 295 | return t.injector(nil, nil) 296 | } 297 | 298 | // injector will injects the data into this template 299 | func (t *XTemplate) injector(datacol XDatasetCollectionDef, language *XLanguage) string { 300 | var injected []string 301 | if t.Root == nil { 302 | return "Error, no template.Root compiled" 303 | } 304 | for _, v := range *t.Root { 305 | switch v.ParamType { 306 | case MetaString: // included string from original code 307 | injected = append(injected, v.Data) 308 | case MetaComment: 309 | // nothing to do: comment ignored 310 | case MetaLanguage: 311 | if language != nil { 312 | injected = append(injected, language.Get(v.Data)) 313 | } 314 | case MetaReference: // Reference && 315 | xid := strings.Split(v.Data, ":") 316 | if len(xid) == 3 { 317 | field := xid[1] 318 | prefix := xid[2] 319 | value, _ := datacol.GetDataString(field) 320 | subt := t.GetTemplate(prefix + value) 321 | if subt != nil { 322 | substr := subt.injector(datacol, language) 323 | injected = append(injected, substr) 324 | } else { 325 | subt := t.GetTemplate(prefix) 326 | if subt != nil { 327 | substr := subt.injector(datacol, language) 328 | injected = append(injected, substr) 329 | } 330 | } 331 | } else { 332 | template := "" 333 | if len(xid) >= 1 { 334 | template = xid[0] 335 | } 336 | subt := t.GetTemplate(template) 337 | if subt != nil { 338 | withds := false 339 | if len(xid) == 2 { 340 | dcl, _ := datacol.GetData(xid[1]) 341 | ds, ok := dcl.(XDatasetDef) 342 | if ok { 343 | withds = true 344 | datacol.Push(ds) 345 | } 346 | } 347 | substr := subt.injector(datacol, language) 348 | if withds { 349 | datacol.Pop() 350 | } 351 | injected = append(injected, substr) 352 | } 353 | } 354 | case MetaVariable: // {{id>id>id...}} 355 | if datacol != nil { 356 | d, _ := datacol.GetDataString(v.Data) 357 | injected = append(injected, d) 358 | } 359 | case MetaRange: // Range (loop over subset) @@id:id@@ 360 | xdata := strings.Split(v.Data, ":") 361 | subdataid := xdata[0] 362 | subtemplateid := xdata[0] 363 | if len(xdata) > 1 { 364 | subtemplateid = xdata[1] 365 | } 366 | 367 | subt := t.GetTemplate(subtemplateid) 368 | if subt != nil { 369 | if datacol != nil { 370 | cl, _ := datacol.GetCollection(subdataid) 371 | if cl != nil { 372 | for i := 0; i < cl.Count(); i++ { 373 | var tmp *XTemplate 374 | tmp = t.GetTemplate(subtemplateid + ".key." + strconv.Itoa(i)) 375 | // if tmp == nil { 376 | // tmp = t.GetTemplate(subtemplateid + ".field." + field + "." + value) 377 | // } 378 | if tmp == nil && i == 0 { 379 | tmp = t.GetTemplate(subtemplateid + ".first") 380 | } 381 | if tmp == nil && i == cl.Count()-1 { 382 | tmp = t.GetTemplate(subtemplateid + ".last") 383 | } 384 | if tmp == nil && i%2 == 0 { 385 | tmp = t.GetTemplate(subtemplateid + ".even") 386 | } 387 | if tmp == nil { 388 | tmp = subt 389 | } 390 | dcl, _ := cl.Get(i) 391 | datacol.Push(dcl) 392 | substr := tmp.injector(datacol, language) 393 | injected = append(injected, substr) 394 | // unstack extra data 395 | datacol.Pop() 396 | } 397 | } else { 398 | var tmp *XTemplate 399 | tmp = t.GetTemplate(subtemplateid + ".none") 400 | if tmp == nil { 401 | tmp = subt 402 | } 403 | substr := tmp.injector(datacol, language) 404 | injected = append(injected, substr) 405 | } 406 | } 407 | } 408 | case MetaCondition: // ??id?? 409 | xdata := strings.Split(v.Data, ":") 410 | subdataid := xdata[0] 411 | subtemplateid := xdata[0] 412 | if len(xdata) > 1 { 413 | subtemplateid = xdata[1] 414 | } 415 | 416 | subt := t.GetTemplate(subtemplateid) 417 | var value interface{} 418 | if datacol != nil { 419 | value, _ = datacol.GetData(subdataid) 420 | } 421 | if subt != nil && value != nil { 422 | withds := false 423 | svalue := "" 424 | ds, ok := value.(XDatasetDef) 425 | if ok { 426 | withds = true 427 | datacol.Push(ds) 428 | } else { 429 | svalue = fmt.Sprint(value) 430 | } 431 | if svalue != "" { 432 | // subtemplate with .value? 433 | tmp := t.GetTemplate(subtemplateid + "." + svalue) 434 | if tmp != nil { 435 | subt = tmp 436 | } 437 | substr := subt.injector(datacol, language) 438 | injected = append(injected, substr) 439 | } 440 | if withds { 441 | datacol.Pop() 442 | } 443 | } 444 | case MetaDump: 445 | if datacol != nil { 446 | if v.Data == "dump" || v.Data == "list" { 447 | dsubstr, _ := datacol.Get(0) 448 | if dsubstr != nil { 449 | substr := dsubstr.GoString() 450 | injected = append(injected, substr) 451 | } 452 | } 453 | } 454 | default: 455 | injected = append(injected, "THE METALANGUAGE FROM OUTERSPACE IS NOT SUPPORTED: "+fmt.Sprint(v.ParamType)) 456 | } 457 | } 458 | // return the page string 459 | return strings.Join(injected, "") 460 | } 461 | 462 | // String will transform the XDataset into a readable string for humans 463 | func (t *XTemplate) String() string { 464 | sdata := []string{} 465 | for _, val := range *t.Root { 466 | sdata = append(sdata, fmt.Sprintf("%v", val)) 467 | } 468 | sort.Strings(sdata) // Lets be sure the print is always the same presentation 469 | return "xcore.XLanguage{" + strings.Join(sdata, " ") + "}" 470 | } 471 | 472 | // GoString will transform the XDataset into a readable string for humans 473 | func (t *XTemplate) GoString() string { 474 | return t.String() 475 | } 476 | -------------------------------------------------------------------------------- /xtemplate_test.go: -------------------------------------------------------------------------------- 1 | package xcore 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "golang.org/x/text/language" 9 | ) 10 | 11 | func ExampleNewXTemplateFromString() { 12 | tmpl, _ := NewXTemplateFromString(` 13 | %-- This is a comment. It will not appear in the final code. --% 14 | Let's put your name here: {{clientname}}
15 | And lets put your hobbies here:
16 | %-- note the 1rst id is the entry into the data to inject and the second one is the name of the sub-template to use --% 17 | @@hobbies:hobby@@ 18 | %-- And you need the template for each hobby:--% 19 | [[hobby]] 20 | I love {{name}}
21 | [[]] 22 | `) 23 | // The creation of the data is obviously tedious here, in real life it should come from a JSON, a Database, etc 24 | data := XDataset{ 25 | "clientname": "Fred", 26 | "hobbies": &XDatasetCollection{ 27 | &XDataset{"name": "Football"}, 28 | &XDataset{"name": "Ping-pong"}, 29 | &XDataset{"name": "Swimming"}, 30 | &XDataset{"name": "Videogames"}, 31 | }, 32 | } 33 | 34 | fmt.Println(tmpl.Execute(&data)) 35 | // Output: 36 | // Let's put your name here: Fred
37 | // And lets put your hobbies here:
38 | // I love Football
39 | // I love Ping-pong
40 | // I love Swimming
41 | // I love Videogames
42 | } 43 | 44 | func TestNewXTemplateFromString(t *testing.T) { 45 | tmpl, _ := NewXTemplateFromString(` 46 | %-- This is a comment. It will not appear in the final code. --% 47 | Let's put your name here: {{clientname}}
48 | And lets put your hobbies here:
49 | %-- note the 1rst id is the entry into the data to inject and the second one is the name of the sub-template to use --% 50 | @@hobbies:hobby@@ 51 | %-- And you need the template for each hobby:--% 52 | [[hobby]] 53 | I love {{name}}
54 | [[]] 55 | `) 56 | // The creation of the data is obviously tedious here, in real life it should come from a JSON, a Database, etc 57 | data := XDataset{ 58 | "clientname": "Fred", 59 | "hobbies": &XDatasetCollection{ 60 | &XDataset{"name": "Football"}, 61 | &XDataset{"name": "Ping-pong"}, 62 | &XDataset{"name": "Swimming"}, 63 | &XDataset{"name": "Videogames"}, 64 | }, 65 | } 66 | 67 | str := tmpl.Execute(&data) 68 | if str != ` 69 | Let's put your name here: Fred
70 | And lets put your hobbies here:
71 | I love Football
72 | I love Ping-pong
73 | I love Swimming
74 | I love Videogames
75 | 76 | ` { 77 | t.Error("Error loading and running the template from string " + str) 78 | return 79 | } 80 | } 81 | 82 | func TestNewXTemplateFromFile(t *testing.T) { 83 | tmpl, _ := NewXTemplateFromFile("testunit/a.template") 84 | 85 | // The creation of the data is obviously tedious here, in real life it should come from a JSON, a Database, etc 86 | data := XDataset{ 87 | "clientname": "Fred", 88 | "hobbies": &XDatasetCollection{ 89 | &XDataset{"name": "Football"}, 90 | &XDataset{"name": "Ping-pong"}, 91 | &XDataset{"name": "Swimming"}, 92 | &XDataset{"name": "Videogames"}, 93 | }, 94 | } 95 | 96 | str := tmpl.Execute(&data) 97 | if str != ` 98 | Let's put your name here: Fred
99 | And lets put your hobbies here:
100 | I love Football
101 | I love Ping-pong
102 | I love Swimming
103 | I love Videogames
104 | 105 | ` { 106 | t.Error("Error loading and running the template from file " + str) 107 | return 108 | } 109 | } 110 | 111 | func TestXTemplateErrors(t *testing.T) { 112 | tmpl1, _ := NewXTemplateFromString("template [[subtemplate]]") 113 | 114 | if tmpl1 != nil { 115 | t.Error("Error compiling a template with error, the template should be nil") 116 | return 117 | } 118 | } 119 | 120 | func TestXTemplateComments(t *testing.T) { 121 | tmpl1, _ := NewXTemplateFromString("abcdefg") 122 | tmpl2, _ := NewXTemplateFromString(`a%--comment1--%b%--comment 2 123 | [[]] 124 | [[subtemplate]] 125 | --%c%--comment3 --% 126 | defg%-- ending @@subtemplate@@ comment --% 127 | `) 128 | 129 | if tmpl1.Execute(nil) != tmpl2.Execute(nil) { 130 | t.Error("Error comparing templates for comments") 131 | return 132 | } 133 | } 134 | 135 | func TestXTemplateLanguageParam(t *testing.T) { 136 | tmpl, _ := NewXTemplateFromString("Test with ##some## ##languages## here") 137 | 138 | data := &XDataset{} 139 | l, _ := NewXLanguageFromString("some=a tiny table\nlanguages=of english language\n") 140 | data.Set("#", l) 141 | 142 | result := tmpl.Execute(data) 143 | 144 | if result != "Test with a tiny table of english language here" { 145 | t.Errorf("The language table has not been inserted correctly") 146 | } 147 | } 148 | 149 | func TestXTemplateSimple(t *testing.T) { 150 | tmpl, err := NewXTemplateFromFile("testunit/b.template") 151 | if err != nil { 152 | t.Error(err) 153 | return 154 | } 155 | 156 | tmp, _ := time.Parse(time.RFC3339, "2020-01-01T12:00:00") 157 | lang := NewXLanguage("mainpage", language.English) 158 | lang.Set("welcome", "Welcome to you") 159 | data := XDataset{ 160 | "clientname": "Fred", 161 | "clientpicture": "face.jpg", 162 | "hobbies": &XDatasetCollection{ 163 | &XDataset{"name": "Football", "sport": "yes"}, 164 | &XDataset{"name": "Ping-pong", "sport": "yes"}, 165 | &XDataset{"name": "Swimming", "sport": "yes"}, 166 | &XDataset{"name": "Videogames", "sport": "no"}, 167 | &XDataset{"name": "other 1", "sport": "no"}, 168 | &XDataset{"name": "other 2", "sport": "no"}, 169 | &XDataset{"name": "other 3", "sport": "yes"}, 170 | &XDataset{"name": "other 4", "sport": "no"}, 171 | &XDataset{"name": "other 5", "sport": nil}, 172 | }, 173 | "preferredhobby": &XDataset{ 174 | "name": "Baseball", 175 | "sport": "yes", 176 | }, 177 | "metadata": &XDataset{ 178 | "preferred-color": "blue", 179 | "Salary": 3568.65, 180 | "hiredate": tmp, 181 | }, 182 | "#": lang, 183 | } 184 | 185 | // fmt.Println(data) 186 | str := tmpl.Execute(&data) 187 | if str == "" { 188 | t.Errorf("Error build complex template") 189 | } 190 | // fmt.Println(str) 191 | } 192 | 193 | /* 194 | package main 195 | 196 | import ( 197 | "fmt" 198 | "github.com/webability-go/xcore" 199 | "testing" 200 | // "unsafe" 201 | ) 202 | 203 | // TEST XTEMPLATE 204 | 205 | func TestReferenceParam(t *testing.T) { 206 | tmpl, _ := xcore.NewXTemplateFromString(` 207 | The sub template starts here: &&template1&&. End. 208 | [[template1]] 209 | This is the template 1 210 | [[]] 211 | `) 212 | 213 | fmt.Println(tmpl) 214 | fmt.Println(tmpl.Root) 215 | 216 | result := tmpl.Execute(&xcore.XDataset{}) 217 | 218 | fmt.Println("Result: ", result) 219 | 220 | } 221 | 222 | func TestComplexReferenceParam(t *testing.T) { 223 | 224 | tmpl, err := xcore.NewXTemplateFromString(` 225 | The sub template starts here: &&template2&&. End. 226 | [[template1]] 227 | This is the template 1 228 | [[]] 229 | [[template2]] 230 | This is the template 2 231 | [[template3]] 232 | This is the subtemplate 3 233 | [[]] 234 | [[template4|template5]] 235 | These are the subtemplates 4 and 5 236 | [[template6.first]] 237 | This is the subtemplate 6 first element for a loop 238 | [[]] 239 | [[template6]] 240 | This is the subtemplate 6 any element for a loop 241 | [[]] 242 | [[template6.last]] 243 | This is the subtemplate 6 last element for a loop 244 | [[]] 245 | [[]] 246 | [[]] 247 | [[template7|template7.status.false]] 248 | This is the template 7 for field status false and any other values 249 | [[]] 250 | [[template7.status.true]] 251 | This is the template 7 for field status true 252 | [[]] 253 | `) 254 | 255 | if err != nil { 256 | fmt.Println(err) 257 | return 258 | } 259 | 260 | result := tmpl.Execute(&xcore.XDataset{}) 261 | fmt.Println("Result: ", result) 262 | } 263 | 264 | func TestVariableParam(t *testing.T) { 265 | 266 | tmpl, err := xcore.NewXTemplateFromString(` 267 | Some data: 268 | {{data1}} 269 | {{data2}} 270 | {{data3>data31}} 271 | {{data4}} 272 | {{data5}} 273 | {{data6}} 274 | {{data7}} 275 | {{data8}} 276 | @@data8@@ 277 | [[data8]] 278 | * test {{data81}} and {{data82}} and {{data83}} and {{data1}} 279 | [[]] 280 | ??data9?? 281 | [[data9]] 282 | * Data 9 exists and is {{data9}} 283 | [[]] 284 | ??data10?? 285 | [[data10]] 286 | * Data 10 does not exist 287 | [[]] 288 | !!dump!! 289 | `) 290 | 291 | if err != nil { 292 | fmt.Println(err) 293 | return 294 | } 295 | 296 | data := xcore.XDataset{} 297 | data["data1"] = "DATA1" 298 | data["data2"] = "DATA1" 299 | sm := xcore.XDataset{} 300 | sm["data31"] = "DATA31" 301 | data["data3"] = sm 302 | data["data4"] = 123 303 | data["data5"] = 123.432 304 | data["data6"] = true 305 | data["data7"] = func() string { return "ABC" } 306 | 307 | d8r1 := &xcore.XDataset{} 308 | d8r1.Set("data81", "rec 1: Entry 8-1") 309 | d8r1.Set("data82", "rec 1: Entry 8-2") 310 | 311 | d8r2 := &xcore.XDataset{} 312 | d8r2.Set("data81", "rec 2: Entry 8-1") 313 | d8r2.Set("data82", "rec 2: Entry 8-2") 314 | d8r2.Set("data83", "rec 2: Entry 8-3") 315 | 316 | d8r3 := &xcore.XDataset{} 317 | d8r3.Set("data81", "rec 3: Entry 8-1") 318 | d8r3.Set("data82", "rec 3: Entry 8-2") 319 | 320 | d := xcore.XDatasetCollection{} 321 | d.Push(d8r1) 322 | d.Push(d8r2) 323 | d.Push(d8r3) 324 | 325 | data["data8"] = &d 326 | data["data9"] = "I exist" 327 | 328 | fmt.Printf("Data: %v\n", data) 329 | // fmt.Printf("ADDRESS DATA8 / GET R1: %p", data.GetCollection("data8").Get(0)) 330 | 331 | result := tmpl.Execute(&data) 332 | fmt.Println("Result: ", result) 333 | } 334 | */ 335 | --------------------------------------------------------------------------------