├── .gitignore ├── buf.go ├── buf_test.go ├── cache.go ├── coll.go ├── coll_test.go ├── comparison.go ├── comparison_test.go ├── conc.go ├── conc_test.go ├── constraints.go ├── err.go ├── err_test.go ├── file_graph.go ├── file_graph_test.go ├── flag.go ├── flag_test.go ├── go.mod ├── grepr ├── grepr.go ├── grepr_internal.go ├── grepr_test.go └── readme.md ├── gsql ├── gsql_arr.go ├── gsql_arr_test.go ├── gsql_constraints.go ├── gsql_internal.go ├── gsql_internal_test.go ├── gsql_like.go ├── gsql_like_test.go ├── gsql_misc.go ├── gsql_rune.go ├── gsql_rune_test.go ├── gsql_scan.go ├── gsql_scan_test.go └── readme.md ├── gtest ├── gtest.go ├── gtest_internal.go ├── gtest_msg.go └── readme.md ├── internal.go ├── io.go ├── json.go ├── json_test.go ├── lazy.go ├── lazy_coll.go ├── lazy_coll_test.go ├── lazy_initer.go ├── lazy_initer_test.go ├── lazy_test.go ├── log.go ├── main_test.go ├── makefile ├── map.go ├── map_test.go ├── math.go ├── math_conv.go ├── math_conv_32_bit.go ├── math_conv_64_bit.go ├── math_conv_test.go ├── math_test.go ├── maybe.go ├── mem.go ├── mem_test.go ├── misc.go ├── misc_test.go ├── opt.go ├── opt_test.go ├── ord_map.go ├── ord_set.go ├── ord_set_test.go ├── ptr.go ├── ptr_test.go ├── readme.md ├── reflect.go ├── reflect_internal.go ├── reflect_test.go ├── rnd.go ├── rnd_test.go ├── set.go ├── set_test.go ├── slice.go ├── slice_test.go ├── sync.go ├── sync_test.go ├── testdata ├── graph_file_long ├── graph_invalid_cyclic_direct │ ├── one.pgsql │ └── two.pgsql ├── graph_invalid_cyclic_indirect │ ├── four.pgsql │ ├── main.pgsql │ ├── one.pgsql │ ├── three.pgsql │ └── two.pgsql ├── graph_invalid_cyclic_self │ └── one.pgsql ├── graph_invalid_imports │ └── one.pgsql ├── graph_invalid_missing_deps │ └── one.pgsql ├── graph_invalid_multiple_entries │ ├── one.pgsql │ └── two.pgsql ├── graph_valid_non_empty │ ├── four.pgsql │ ├── main.pgsql │ ├── one.pgsql │ ├── three.pgsql │ └── two.pgsql └── graph_valid_with_skip │ ├── four.pgsql │ ├── main.pgsql │ ├── one.pgsql │ ├── three.pgsql │ └── two.pgsql ├── text.go ├── text_decode.go ├── text_decode_test.go ├── text_encode.go ├── text_encode_test.go ├── text_test.go ├── time_micro.go ├── time_micro_test.go ├── time_milli.go ├── time_milli_test.go ├── trace.go ├── trace_test.go ├── try.go ├── try_test.go ├── unlicense ├── unsafe.go ├── unsafe_test.go ├── zop.go └── zop_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /* 3 | 4 | !/*ignore 5 | !/go.mod 6 | !/*.go 7 | !/readme.md 8 | !/makefile 9 | !/unlicense 10 | 11 | !testdata 12 | !/grepr 13 | !/gsql 14 | !/gtest 15 | 16 | stash_* 17 | *_stash.go 18 | -------------------------------------------------------------------------------- /buf_test.go: -------------------------------------------------------------------------------- 1 | package gg_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mitranim/gg" 7 | "github.com/mitranim/gg/gtest" 8 | ) 9 | 10 | // TODO dedup with `TestToString`. 11 | func TestBuf_String(t *testing.T) { 12 | defer gtest.Catch(t) 13 | 14 | gtest.Zero(gg.Buf(nil).String()) 15 | 16 | test := func(src string) { 17 | buf := gg.Buf(src) 18 | tar := buf.String() 19 | 20 | gtest.Eq(tar, src) 21 | gtest.Eq(gg.TextDat(buf), gg.TextDat(tar)) 22 | } 23 | 24 | test(``) 25 | test(`a`) 26 | test(`ab`) 27 | test(`abc`) 28 | 29 | t.Run(`mutation`, func(t *testing.T) { 30 | defer gtest.Catch(t) 31 | 32 | buf := gg.Buf(`abc`) 33 | tar := buf.String() 34 | gtest.Eq(tar, `abc`) 35 | 36 | buf[0] = 'd' 37 | gtest.Eq(tar, `dbc`) 38 | }) 39 | } 40 | 41 | func TestBuf_AppendAnys(t *testing.T) { 42 | defer gtest.Catch(t) 43 | 44 | var buf gg.Buf 45 | gtest.Zero(buf) 46 | 47 | buf.AppendAnys() 48 | gtest.Zero(buf) 49 | 50 | buf.AppendAnys(nil) 51 | gtest.Zero(buf) 52 | 53 | buf.AppendAnys(``, nil, ``) 54 | gtest.Zero(buf) 55 | 56 | buf.AppendAnys(10) 57 | gtest.Str(buf, `10`) 58 | 59 | buf.AppendAnys(` `, 20, ` `, 30) 60 | gtest.Str(buf, `10 20 30`) 61 | } 62 | 63 | func TestBuf_AppendAnysln(t *testing.T) { 64 | defer gtest.Catch(t) 65 | 66 | { 67 | var buf gg.Buf 68 | gtest.Zero(buf) 69 | } 70 | 71 | { 72 | var buf gg.Buf 73 | buf.AppendAnysln() 74 | gtest.Zero(buf) 75 | } 76 | 77 | { 78 | var buf gg.Buf 79 | buf.AppendAnysln(nil) 80 | gtest.Zero(buf) 81 | } 82 | 83 | { 84 | var buf gg.Buf 85 | buf.AppendAnysln(nil, ``, nil) 86 | gtest.Zero(buf) 87 | 88 | buf.AppendAnysln() 89 | gtest.Zero(buf) 90 | } 91 | 92 | { 93 | var buf gg.Buf 94 | buf.AppendAnysln(`one`, `two`+"\n") 95 | gtest.Str(buf, `onetwo`+"\n") 96 | } 97 | 98 | { 99 | var buf gg.Buf 100 | buf.AppendAnysln(`one`+"\n", `two`+"\n") 101 | gtest.Str(buf, `one`+"\n"+`two`+"\n") 102 | } 103 | 104 | { 105 | var buf gg.Buf 106 | buf.AppendAnysln(`one`+"\n", `two`) 107 | gtest.Str(buf, `one`+"\n"+`two`+"\n") 108 | } 109 | 110 | { 111 | var buf gg.Buf 112 | buf.AppendAnysln(`one`) 113 | gtest.Str(buf, `one`+"\n") 114 | 115 | buf.AppendAnysln() 116 | gtest.Str(buf, `one`+"\n\n") 117 | 118 | buf.AppendAnysln(`two`) 119 | gtest.Str(buf, `one`+"\n\n"+`two`+"\n") 120 | 121 | buf.AppendAnysln() 122 | gtest.Str(buf, `one`+"\n\n"+`two`+"\n\n") 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import ( 4 | r "reflect" 5 | "sync" 6 | ) 7 | 8 | // Type-inferring shortcut for creating a `Cache` of the given type. 9 | func CacheOf[ 10 | Key comparable, 11 | Val any, 12 | Ptr Initer1Ptr[Val, Key], 13 | ]() *Cache[Key, Val, Ptr] { 14 | return new(Cache[Key, Val, Ptr]) 15 | } 16 | 17 | /* 18 | Concurrency-safe cache that creates and initializes values on demand, using keys 19 | as inputs. Does not support eviction or expiration. Suitable when values are 20 | unambiguously related to keys and don't need to be evicted or refreshed. 21 | */ 22 | type Cache[ 23 | Key comparable, 24 | Val any, 25 | Ptr Initer1Ptr[Val, Key], 26 | ] struct { 27 | Map map[Key]Ptr 28 | Lock sync.RWMutex 29 | } 30 | 31 | /* 32 | Shortcut for using `.Ptr` and dereferencing the result. May be invalid if the 33 | resulting value is non-copyable, for example when it contains a mutex. 34 | */ 35 | func (self *Cache[Key, Val, Ptr]) Get(key Key) Val { return *self.Ptr(key) } 36 | 37 | /* 38 | Returns the cached value for the given key, by pointer. If the value did not 39 | previously exist, idempotently initializes it by calling `.Init` and caches the 40 | result. For any given key, the value is initialized exactly once, even if 41 | multiple goroutines are trying to access it simultaneously. 42 | */ 43 | func (self *Cache[Key, Val, Ptr]) Ptr(key Key) Ptr { 44 | ptr := self.get(key) 45 | if ptr != nil { 46 | return ptr 47 | } 48 | 49 | defer Lock(&self.Lock).Unlock() 50 | 51 | ptr = self.Map[key] 52 | if ptr != nil { 53 | return ptr 54 | } 55 | 56 | ptr = new(Val) 57 | ptr.Init(key) 58 | MapInit(&self.Map)[key] = ptr 59 | return ptr 60 | } 61 | 62 | func (self *Cache[Key, _, Ptr]) get(key Key) Ptr { 63 | defer Lock(self.Lock.RLocker()).Unlock() 64 | return self.Map[key] 65 | } 66 | 67 | // Deletes the value for the given key. 68 | func (self *Cache[Key, _, _]) Del(key Key) { 69 | defer Lock(&self.Lock).Unlock() 70 | delete(self.Map, key) 71 | } 72 | 73 | // Type-inferring shortcut for creating a `TypeCache` of the given type. 74 | func TypeCacheOf[Val any, Ptr Initer1Ptr[Val, r.Type]]() *TypeCache[Val, Ptr] { 75 | return new(TypeCache[Val, Ptr]) 76 | } 77 | 78 | /* 79 | Tool for storing information derived from `reflect.Type` that can be generated 80 | once and then cached. Used internally. All methods are concurrency-safe. 81 | */ 82 | type TypeCache[Val any, Ptr Initer1Ptr[Val, r.Type]] struct { 83 | Map map[r.Type]Ptr 84 | Lock sync.RWMutex 85 | } 86 | 87 | /* 88 | Shortcut for using `.Ptr` and dereferencing the result. May be invalid if the 89 | resulting value is non-copyable, for example when it contains a mutex. 90 | */ 91 | func (self *TypeCache[Val, Ptr]) Get(key r.Type) Val { return *self.Ptr(key) } 92 | 93 | /* 94 | Returns the cached value for the given key. If the value did not previously 95 | exist, idempotently initializes it by calling `.Init` (by pointer) and caches 96 | the result. For any given key, the value is initialized exactly once, even if 97 | multiple goroutines are trying to access it simultaneously. 98 | */ 99 | func (self *TypeCache[Val, Ptr]) Ptr(key r.Type) Ptr { 100 | ptr := self.get(key) 101 | if ptr != nil { 102 | return ptr 103 | } 104 | 105 | defer Lock(&self.Lock).Unlock() 106 | 107 | ptr = self.Map[key] 108 | if ptr != nil { 109 | return ptr 110 | } 111 | 112 | ptr = new(Val) 113 | ptr.Init(key) 114 | 115 | if self.Map == nil { 116 | self.Map = map[r.Type]Ptr{} 117 | } 118 | self.Map[key] = ptr 119 | 120 | return ptr 121 | } 122 | 123 | func (self *TypeCache[Val, Ptr]) get(key r.Type) Ptr { 124 | defer Lock(self.Lock.RLocker()).Unlock() 125 | return self.Map[key] 126 | } 127 | -------------------------------------------------------------------------------- /coll.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import "encoding/json" 4 | 5 | /* 6 | Short for "valid primary key". Returns the primary key generated by the given 7 | input, asserts that the key is non-zero, and returns the resulting key. 8 | Used internally by `Coll` and `LazyColl`. 9 | */ 10 | func ValidPk[Key comparable, Val Pker[Key]](val Val) Key { 11 | key := val.Pk() 12 | if IsZero(key) { 13 | panic(Errf(`unexpected empty key %v in %v`, Type[Key](), Type[Val]())) 14 | } 15 | return key 16 | } 17 | 18 | /* 19 | Syntactic shortcut for making a `Coll` of the given arguments. Reuses the given 20 | slice as-is with no reallocation. 21 | */ 22 | func CollOf[Key comparable, Val Pker[Key]](src ...Val) Coll[Key, Val] { 23 | var tar Coll[Key, Val] 24 | tar.Reset(src...) 25 | return tar 26 | } 27 | 28 | /* 29 | Syntactic shortcut for making a `Coll` from any number of source slices. When 30 | called with exactly one argument, this reuses the given slice as-is with no 31 | reallocation. 32 | */ 33 | func CollFrom[Key comparable, Val Pker[Key], Slice ~[]Val](src ...Slice) Coll[Key, Val] { 34 | var tar Coll[Key, Val] 35 | 36 | switch len(src) { 37 | case 1: 38 | tar.Reset(src[0]...) 39 | default: 40 | for _, src := range src { 41 | tar.Add(src...) 42 | } 43 | } 44 | 45 | return tar 46 | } 47 | 48 | /* 49 | Short for "collection". Represents an ordered map where keys are automatically 50 | derived from values. Compare `OrdMap` where keys are provided externally. Keys 51 | must be non-zero. Similarly to a map, this ensures value uniqueness by primary 52 | key, and allows efficient access by key. Unlike a map, values in this type are 53 | ordered and can be iterated cheaply, because they are stored in a 54 | publicly-accessible slice. However, as a tradeoff, this type does not support 55 | deletion. 56 | */ 57 | type Coll[Key comparable, Val Pker[Key]] OrdMap[Key, Val] 58 | 59 | // Same as `OrdMap.Len`. 60 | func (self Coll[_, _]) Len() int { return self.OrdMap().Len() } 61 | 62 | // Same as `OrdMap.IsEmpty`. 63 | func (self Coll[_, _]) IsEmpty() bool { return self.OrdMap().IsEmpty() } 64 | 65 | // Same as `OrdMap.IsNotEmpty`. 66 | func (self Coll[_, _]) IsNotEmpty() bool { return self.OrdMap().IsNotEmpty() } 67 | 68 | // Same as `OrdMap.Has`. 69 | func (self Coll[Key, _]) Has(key Key) bool { return self.OrdMap().Has(key) } 70 | 71 | // Same as `OrdMap.Get`. 72 | func (self Coll[Key, Val]) Get(key Key) Val { return self.OrdMap().Get(key) } 73 | 74 | // Same as `OrdMap.GetReq`. 75 | func (self Coll[Key, Val]) GetReq(key Key) Val { return self.OrdMap().GetReq(key) } 76 | 77 | // Same as `OrdMap.Got`. 78 | func (self Coll[Key, Val]) Got(key Key) (Val, bool) { return self.OrdMap().Got(key) } 79 | 80 | // Same as `OrdMap.Ptr`. 81 | func (self Coll[Key, Val]) Ptr(key Key) *Val { return self.OrdMap().Ptr(key) } 82 | 83 | // Same as `OrdMap.PtrReq`. 84 | func (self Coll[Key, Val]) PtrReq(key Key) *Val { return self.OrdMap().PtrReq(key) } 85 | 86 | /* 87 | Idempotently adds each given value to both the inner slice and the inner index. 88 | Every value whose key already exists in the index is replaced at the existing 89 | position in the slice. 90 | */ 91 | func (self *Coll[Key, Val]) Add(src ...Val) *Coll[Key, Val] { 92 | for _, src := range src { 93 | self.OrdMap().Set(ValidPk[Key](src), src) 94 | } 95 | return self 96 | } 97 | 98 | /* 99 | Same as `Coll.Add`, but panics if any inputs are redundant, as in, their primary 100 | keys are already present in the index. 101 | */ 102 | func (self *Coll[Key, Val]) AddUniq(src ...Val) *Coll[Key, Val] { 103 | for _, src := range src { 104 | self.OrdMap().Add(ValidPk[Key](src), src) 105 | } 106 | return self 107 | } 108 | 109 | // Same as `OrdMap.Clear`. 110 | func (self *Coll[Key, Val]) Clear() *Coll[Key, Val] { 111 | self.OrdMap().Clear() 112 | return self 113 | } 114 | 115 | /* 116 | Replaces `.Slice` with the given slice and rebuilds `.Index`. Uses the slice 117 | as-is with no reallocation. Callers must be careful to avoid modifying the 118 | source data, which may invalidate the collection's index. 119 | */ 120 | func (self *Coll[Key, Val]) Reset(src ...Val) *Coll[Key, Val] { 121 | self.Slice = src 122 | self.Reindex() 123 | return self 124 | } 125 | 126 | /* 127 | Rebuilds the inner index from the inner slice, without checking the validity of 128 | the existing index. Can be useful for external code that directly modifies the 129 | inner `.Slice`, for example by sorting it. This is NOT used when adding items 130 | via `.Add`, which modifies the index incrementally rather than all-at-once. 131 | */ 132 | func (self *Coll[Key, Val]) Reindex() *Coll[Key, Val] { 133 | slice := self.Slice 134 | if len(slice) <= 0 { 135 | self.Index = nil 136 | return self 137 | } 138 | 139 | index := make(map[Key]int, len(slice)) 140 | for ind, val := range slice { 141 | index[ValidPk[Key](val)] = ind 142 | } 143 | self.Index = index 144 | 145 | return self 146 | } 147 | 148 | /* 149 | Swaps two elements both in `.Slice` and in `.Index`. Useful for sorting. 150 | `.Index` may be nil, in which case it's unaffected. Slice indices must be 151 | either equal or valid. 152 | */ 153 | func (self Coll[Key, _]) Swap(ind0, ind1 int) { 154 | if ind0 == ind1 { 155 | return 156 | } 157 | 158 | slice := self.Slice 159 | val0, val1 := slice[ind0], slice[ind1] 160 | slice[ind0], slice[ind1] = val1, val0 161 | 162 | index := self.Index 163 | if index != nil { 164 | index[ValidPk[Key](val0)], index[ValidPk[Key](val1)] = ind1, ind0 165 | } 166 | } 167 | 168 | // Implement `json.Marshaler`. Encodes the inner slice, ignoring the index. 169 | func (self Coll[_, _]) MarshalJSON() ([]byte, error) { 170 | return json.Marshal(self.Slice) 171 | } 172 | 173 | /* 174 | Implement `json.Unmarshaler`. Decodes the input into the inner slice and 175 | rebuilds the index. 176 | */ 177 | func (self *Coll[_, _]) UnmarshalJSON(src []byte) error { 178 | err := json.Unmarshal(src, &self.Slice) 179 | self.Reindex() 180 | return err 181 | } 182 | 183 | /* 184 | Free cast into the equivalent `*OrdMap`. Note that mutating the resulting 185 | `OrdMap` via methods such as `OrdMap.Add` may violate guarantees of the `Coll` 186 | type, mainly that each value is stored under the key returned by its `.Pk` 187 | method. 188 | */ 189 | func (self *Coll[Key, Val]) OrdMap() *OrdMap[Key, Val] { 190 | return (*OrdMap[Key, Val])(self) 191 | } 192 | 193 | // Free cast to equivalent `LazyColl`. 194 | func (self *Coll[Key, Val]) LazyColl() *LazyColl[Key, Val] { 195 | return (*LazyColl[Key, Val])(self) 196 | } 197 | -------------------------------------------------------------------------------- /file_graph_test.go: -------------------------------------------------------------------------------- 1 | package gg_test 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/mitranim/gg" 9 | "github.com/mitranim/gg/gtest" 10 | ) 11 | 12 | func TestGraphDir_invalid_imports(t *testing.T) { 13 | defer gtest.Catch(t) 14 | 15 | gtest.PanicStr( 16 | `unable to build dependency graph for "testdata/graph_invalid_imports": invalid imports in "one.pgsql", every import must be a base name, found ["dir/base_name.pgsql"]`, 17 | func() { 18 | gg.GraphDirInit(`testdata/graph_invalid_imports`) 19 | }, 20 | ) 21 | } 22 | 23 | func TestGraphDir_invalid_missing_deps(t *testing.T) { 24 | defer gtest.Catch(t) 25 | 26 | gtest.PanicStr( 27 | `unable to build dependency graph for "testdata/graph_invalid_missing_deps": dependency error for "one.pgsql": missing file "missing.pgsql"`, 28 | func() { 29 | gg.GraphDirInit(`testdata/graph_invalid_missing_deps`) 30 | }, 31 | ) 32 | } 33 | 34 | func TestGraphDir_invalid_multiple_entries(t *testing.T) { 35 | defer gtest.Catch(t) 36 | 37 | gtest.PanicStr( 38 | `unable to build dependency graph for "testdata/graph_invalid_multiple_entries": expected to find exactly one dependency-free entry file, found multiple: ["one.pgsql" "two.pgsql"]`, 39 | func() { 40 | gg.GraphDirInit(`testdata/graph_invalid_multiple_entries`) 41 | }, 42 | ) 43 | } 44 | 45 | func TestGraphDir_invalid_cyclic_self(t *testing.T) { 46 | defer gtest.Catch(t) 47 | 48 | gtest.PanicStr( 49 | `unable to build dependency graph for "testdata/graph_invalid_cyclic_self": dependency cycle: ["one.pgsql" "one.pgsql"]`, 50 | func() { 51 | gg.GraphDirInit(`testdata/graph_invalid_cyclic_self`) 52 | }, 53 | ) 54 | } 55 | 56 | func TestGraphDir_invalid_cyclic_direct(t *testing.T) { 57 | defer gtest.Catch(t) 58 | 59 | gtest.PanicStr( 60 | `unable to build dependency graph for "testdata/graph_invalid_cyclic_direct": dependency cycle: ["one.pgsql" "two.pgsql" "one.pgsql"]`, 61 | func() { 62 | gg.GraphDirInit(`testdata/graph_invalid_cyclic_direct`) 63 | }, 64 | ) 65 | } 66 | 67 | func TestGraphDir_invalid_cyclic_indirect(t *testing.T) { 68 | defer gtest.Catch(t) 69 | 70 | gtest.PanicStr( 71 | `unable to build dependency graph for "testdata/graph_invalid_cyclic_indirect": dependency cycle: ["four.pgsql" "one.pgsql" "two.pgsql" "three.pgsql" "four.pgsql"]`, 72 | func() { 73 | gg.GraphDirInit(`testdata/graph_invalid_cyclic_indirect`) 74 | }, 75 | ) 76 | } 77 | 78 | func TestGraphDir_valid_empty(t *testing.T) { 79 | defer gtest.Catch(t) 80 | 81 | const DIR = `testdata/empty` 82 | gg.MkdirAll(DIR) 83 | testGraphDir(DIR, nil) 84 | } 85 | 86 | func TestGraphDir_valid_non_empty(t *testing.T) { 87 | defer gtest.Catch(t) 88 | 89 | testGraphDir(`testdata/graph_valid_non_empty`, []string{ 90 | `main.pgsql`, 91 | `one.pgsql`, 92 | `two.pgsql`, 93 | `three.pgsql`, 94 | `four.pgsql`, 95 | }) 96 | } 97 | 98 | func TestGraphDir_valid_with_skip(t *testing.T) { 99 | defer gtest.Catch(t) 100 | 101 | testGraphDir(`testdata/graph_valid_with_skip`, []string{ 102 | `main.pgsql`, 103 | `one.pgsql`, 104 | `three.pgsql`, 105 | }) 106 | } 107 | 108 | func testGraphDir(dir string, exp []string) { 109 | gtest.Equal(gg.GraphDirInit(dir).Names(), exp) 110 | } 111 | 112 | var graphFileSrc = gg.ReadFile[string](`testdata/graph_file_long`) 113 | 114 | var graphFileOut = []string{ 115 | `aaa7c30c9fe6494db244df541a415b8f`, 116 | `ed33e824fe574a2f91712c1a1609df8c`, 117 | `ebe9816ee8b14ce9bba478c8e0853581`, 118 | `b6728a7d157e4984afb430ed2bf750b7`, 119 | `f4f68f8f00dd45fcba1b2a97c1eafc94`, 120 | `5acde9df2bb348d1aeb55dbc8f06565c`, 121 | `e6a34f990e2c4bbd85b13f46d96ed708`, 122 | `889b367cd42d42189a1b7d9d3f177e84`, 123 | `00ef58a6eca448c799d744ba5630fc48`, 124 | `b737450984cd4daea11170364773e98c`, 125 | `fb37e2f97f3f469080eacd08e29e99ad`, 126 | `09c3e5a78bf14e69b61b5c8b10db0bec`, 127 | `e9dd168029cd441296ac6d918c8a95b5`, 128 | `a83e48bad3eb414c89479bb6666b1e76`, 129 | `d3316aeb511a4d9295f4b78a3e330bdc`, 130 | `dac680dcf3fd4f0b99d0789cf396f777`, 131 | `42d2a4fb764445818d07e5fee726448d`, 132 | } 133 | 134 | func Test_graph_file_parse_regexp(t *testing.T) { 135 | defer gtest.Catch(t) 136 | 137 | gtest.Equal(graphFileParseRegexp(graphFileSrc), graphFileOut) 138 | } 139 | 140 | func Benchmark_graph_file_parse_regexp(b *testing.B) { 141 | defer gtest.Catch(b) 142 | 143 | for ind := 0; ind < b.N; ind++ { 144 | gg.Nop1(graphFileParseRegexp(graphFileSrc)) 145 | } 146 | } 147 | 148 | // Copied from `file_graph.go` because it's private. 149 | func graphFileParseRegexp(src string) []string { 150 | return firstSubmatches(graphFileImportRegexp, src) 151 | } 152 | 153 | var graphFileImportRegexp = regexp.MustCompile(`(?m)^@import\s+(.*)$`) 154 | 155 | func firstSubmatches(reg *regexp.Regexp, src string) []string { 156 | return gg.Map(reg.FindAllStringSubmatch(src, -1), get1) 157 | } 158 | 159 | func get1(src []string) string { return src[1] } 160 | 161 | func Test_graph_file_parse_lines(t *testing.T) { 162 | defer gtest.Catch(t) 163 | 164 | gtest.Equal(graphFileParseLines(graphFileSrc), graphFileOut) 165 | } 166 | 167 | /* 168 | On the author's machine in Go 1.20.2: 169 | 170 | Benchmark_graph_file_parse_regexp 56996 ns/op 37 allocs/op 171 | Benchmark_graph_file_parse_lines 7026 ns/op 14 allocs/op 172 | Benchmark_graph_file_parse_custom 7020 ns/op 6 allocs/op 173 | 174 | (The last one involves a custom parser that got deleted.) 175 | */ 176 | func Benchmark_graph_file_parse_lines(b *testing.B) { 177 | defer gtest.Catch(b) 178 | 179 | for ind := 0; ind < b.N; ind++ { 180 | gg.Nop1(graphFileParseLines(graphFileSrc)) 181 | } 182 | } 183 | 184 | func graphFileParseLines(src string) (out []string) { 185 | for _, line := range gg.SplitLines(src) { 186 | rest := strings.TrimPrefix(line, `@import `) 187 | if rest != line { 188 | out = append(out, strings.TrimSpace(rest)) 189 | } 190 | } 191 | return 192 | } 193 | 194 | func BenchmarkGraphFile_Pk(b *testing.B) { 195 | defer gtest.Catch(b) 196 | var file gg.GraphFile 197 | file.Path = `one/two/three/four.pgsql` 198 | 199 | for ind := 0; ind < b.N; ind++ { 200 | gg.Nop1(file.Pk()) 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mitranim/gg 2 | 3 | go 1.20 4 | -------------------------------------------------------------------------------- /grepr/grepr.go: -------------------------------------------------------------------------------- 1 | /* 2 | Missing feature of the standard library: printing arbitrary inputs as Go code, 3 | with proper spacing and support for multi-line output with indentation. The 4 | name "repr" stands for "representation" and alludes to the Python function with 5 | the same name. 6 | */ 7 | package grepr 8 | 9 | import ( 10 | "fmt" 11 | r "reflect" 12 | "regexp" 13 | u "unsafe" 14 | 15 | "github.com/mitranim/gg" 16 | ) 17 | 18 | // Default config used by top-level formatting functions in this package. 19 | var ConfDefault = Conf{Indent: gg.Indent} 20 | 21 | // Config that allows formatting of struct zero fields. 22 | var ConfFull = Conf{Indent: gg.Indent, ZeroFields: true} 23 | 24 | /* 25 | Formatting config. 26 | 27 | - `.Indent` controls indentation. If empty, output is single line. 28 | - `.ZeroFields`, if set, forces printing of zero fields in structs. 29 | By default zero fields are skipped. 30 | - `.Pkg`, if set, indicates the package name to strip from type names. 31 | */ 32 | type Conf struct { 33 | Indent string 34 | ZeroFields bool 35 | Pkg string 36 | } 37 | 38 | /* 39 | Short for "is single line". If `.Indent` is empty, this is true, and output 40 | is single-line. Otherwise output is multi-line. 41 | */ 42 | func (self Conf) IsSingle() bool { return self.Indent == `` } 43 | 44 | // Short for "is multi line". Inverse of `.IsSingle`. 45 | func (self Conf) IsMulti() bool { return self.Indent != `` } 46 | 47 | // Inverse of `.ZeroFields`. 48 | func (self Conf) SkipZeroFields() bool { return !self.ZeroFields } 49 | 50 | // Shortcut for creating a pretty-formatter with this config. 51 | func (self Conf) Fmt() Fmt { 52 | var buf Fmt 53 | buf.Conf = self 54 | return buf 55 | } 56 | 57 | // Short for "formatter". 58 | type Fmt struct { 59 | Conf 60 | gg.Buf 61 | Lvl int 62 | ElideType bool 63 | Visited gg.Set[u.Pointer] 64 | PkgReg *regexp.Regexp 65 | } 66 | 67 | /* 68 | Similar to `fmt.Sprintf("%#v")` or `gg.GoString`, but more advanced. 69 | Formats the input as Go code, using the config `ConfDefault`, returning 70 | the resulting string. 71 | */ 72 | func String[A any](src A) string { return StringIndent(src, 0) } 73 | 74 | /* 75 | Similar to `fmt.Sprintf("%#v")` or `gg.GoString`, but more advanced. 76 | Formats the input as Go code, using the config `ConfDefault`, returning 77 | the resulting bytes. 78 | */ 79 | func Bytes[A any](src A) []byte { return BytesIndent(src, 0) } 80 | 81 | /* 82 | Formats the input as Go code, using the given config, returning the 83 | resulting string. 84 | */ 85 | func StringC[A any](conf Conf, src A) string { return StringIndentC(conf, src, 0) } 86 | 87 | /* 88 | Formats the input as Go code, using the given config, returning the 89 | resulting bytes. 90 | */ 91 | func BytesC[A any](conf Conf, src A) []byte { return BytesIndentC(conf, src, 0) } 92 | 93 | /* 94 | Formats the input as Go code, using the default config with the given 95 | indentation level, returning the resulting string. 96 | */ 97 | func StringIndent[A any](src A, lvl int) string { 98 | return StringIndentC(ConfDefault, src, lvl) 99 | } 100 | 101 | /* 102 | Formats the input as Go code, using the default config with the given 103 | indentation level, returning the resulting bytes. 104 | */ 105 | func BytesIndent[A any](src A, lvl int) []byte { 106 | return BytesIndentC(ConfDefault, src, lvl) 107 | } 108 | 109 | /* 110 | Formats the input as Go code, using the given config with the given indentation 111 | level, returning the resulting string. 112 | */ 113 | func StringIndentC[A any](conf Conf, src A, lvl int) string { 114 | return gg.ToString(BytesIndentC(conf, src, lvl)) 115 | } 116 | 117 | /* 118 | Formats the input as Go code, using the given config with the given indentation 119 | level, returning the resulting bytes. 120 | */ 121 | func BytesIndentC[A any](conf Conf, src A, lvl int) []byte { 122 | buf := conf.Fmt() 123 | buf.Lvl += lvl 124 | buf.fmtAny(gg.Type[A](), r.ValueOf(gg.AnyNoEscUnsafe(src))) 125 | return buf.Buf 126 | } 127 | 128 | /* 129 | Shortcut for printing the input as Go code, prefixed with the given description, 130 | using the default config. Handy for debug-printing. 131 | */ 132 | func Prn[A any](desc string, src A) { fmt.Println(desc, String(src)) } 133 | 134 | // Shortcut for printing the input as Go code, using the default config. 135 | func Println[A any](src A) { fmt.Println(String(src)) } 136 | 137 | // Shortcut for printing the input as Go code, using the given config. 138 | func PrintlnC[A any](conf Conf, src A) { fmt.Println(StringC(conf, src)) } 139 | -------------------------------------------------------------------------------- /grepr/readme.md: -------------------------------------------------------------------------------- 1 | Missing feature of the standard library: printing arbitrary inputs as Go code, with proper spacing and support for multi-line output with indentation. The name "repr" stands for "representation" and alludes to the Python function with the same name. 2 | 3 | API doc: https://pkg.go.dev/github.com/mitranim/gg/grepr 4 | 5 | Example: 6 | 7 | ```go 8 | package mock 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/mitranim/gg" 14 | "github.com/mitranim/gg/grepr" 15 | ) 16 | 17 | type Outer struct { 18 | OuterId int 19 | OuterName string 20 | Embed 21 | Inner *Inner 22 | } 23 | 24 | type Embed struct { 25 | EmbedId int 26 | EmbedName string 27 | } 28 | 29 | type Inner struct { 30 | InnerId *int 31 | InnerName *string 32 | } 33 | 34 | func main() { 35 | fmt.Println(grepr.String(Outer{ 36 | OuterName: `outer`, 37 | Embed: Embed{EmbedId: 20}, 38 | Inner: &Inner{ 39 | InnerId: gg.Ptr(30), 40 | InnerName: gg.Ptr(`inner`), 41 | }, 42 | })) 43 | 44 | /** 45 | mock.Outer{ 46 | OuterName: `outer`, 47 | Embed: mock.Embed{EmbedId: 20}, 48 | Inner: &mock.Inner{ 49 | InnerId: gg.Ptr(30), 50 | InnerName: gg.Ptr(`inner`), 51 | }, 52 | } 53 | */ 54 | } 55 | ``` 56 | -------------------------------------------------------------------------------- /gsql/gsql_arr.go: -------------------------------------------------------------------------------- 1 | package gsql 2 | 3 | import ( 4 | "database/sql/driver" 5 | 6 | "github.com/mitranim/gg" 7 | ) 8 | 9 | /* 10 | Shortcut for converting to `Arr`. Workaround for the lack of type inference in 11 | type literals and type conversions. This is a free cast with no reallocation. 12 | */ 13 | func ToArr[A any](val []A) Arr[A] { return val } 14 | 15 | // Shortcut for creating `Arr` from the arguments. 16 | func ArrOf[A any](val ...A) Arr[A] { return val } 17 | 18 | /* 19 | Short for "array". A slice type that supports SQL array encoding and decoding, 20 | using the `{}` format. Examples: 21 | 22 | Arr[int]{10, 20} <-> '{10,20}' 23 | Arr[Arr[int]]{{10, 20}, {30, 40}} <-> '{{10,20},{30,40}}' 24 | */ 25 | type Arr[A any] []A 26 | 27 | var ( 28 | _ = gg.Encoder(gg.Zero[Arr[any]]()) 29 | _ = gg.Decoder(gg.Zero[*Arr[any]]()) 30 | ) 31 | 32 | // Implement `gg.Nullable`. True if the slice is nil. 33 | func (self Arr[A]) IsNull() bool { return self == nil } 34 | 35 | // Implement `fmt.Stringer`. Returns an SQL encoding of the array. 36 | func (self Arr[A]) String() string { return gg.AppenderString(self) } 37 | 38 | /* 39 | Implement `AppenderTo`, appending the array's SQL encoding to the buffer. 40 | If the slice is nil, appends nothing. 41 | */ 42 | func (self Arr[A]) AppendTo(buf []byte) []byte { 43 | if self != nil { 44 | buf = append(buf, '{') 45 | buf = self.AppendInner(buf) 46 | buf = append(buf, '}') 47 | } 48 | return buf 49 | } 50 | 51 | // Same as `.AppenderTo` but without the enclosing `{}`. 52 | func (self Arr[A]) AppendInner(buf []byte) []byte { 53 | var found bool 54 | for _, val := range self { 55 | if found { 56 | buf = append(buf, ',') 57 | } 58 | found = true 59 | /** 60 | Technical note. We're not bothering to validate that the appended value is 61 | well-formed. That's because we expect `Arr` to be passed to SQL via an SQL 62 | parameter, which already prevents SQL injection. This saves us effort and 63 | performance. 64 | */ 65 | buf = gg.AppendTo(buf, val) 66 | } 67 | return buf 68 | } 69 | 70 | // Decodes from an SQL array literal string. Supports nested arrays. 71 | func (self *Arr[A]) Parse(src string) (err error) { 72 | defer gg.Rec(&err) 73 | defer gg.Detailf(`unable to decode %q into %T`, src, self) 74 | 75 | self.Clear() 76 | 77 | if len(src) <= 0 { 78 | return nil 79 | } 80 | 81 | if src == `{}` { 82 | if *self == nil { 83 | *self = Arr[A]{} 84 | } 85 | return nil 86 | } 87 | 88 | if !(gg.TextHeadByte(src) == '{' && gg.TextLastByte(src) == '}') { 89 | panic(gg.ErrInvalidInput) 90 | } 91 | src = src[1 : len(src)-1] 92 | 93 | for len(src) > 0 { 94 | end, size := popSqlArrSegment(src, 0, 0, ',') 95 | gg.Append(self, gg.ParseTo[A](unquoteOpt(src[:end]))) 96 | src = src[end+size:] 97 | } 98 | return nil 99 | } 100 | 101 | // Truncates the length, keeping the capacity. 102 | func (self *Arr[A]) Clear() { gg.Trunc(self) } 103 | 104 | // Implement `driver.Valuer`. 105 | func (self Arr[A]) Value() (driver.Value, error) { 106 | if self.IsNull() { 107 | return nil, nil 108 | } 109 | return self.String(), nil 110 | } 111 | 112 | // Implement `sql.Scanner`. 113 | func (self *Arr[A]) Scan(src any) error { 114 | // Known inefficiency: when the source is `[]byte`, this may allocate, which 115 | // is wasted when the output is transient, but correct when parts of the 116 | // output are stored in the result. 117 | str, ok := gg.AnyToText[string](src) 118 | if ok { 119 | return self.Parse(str) 120 | } 121 | 122 | switch src := src.(type) { 123 | case Arr[A]: 124 | *self = src 125 | return nil 126 | 127 | default: 128 | return gg.ErrConv(src, gg.Type[Arr[A]]()) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /gsql/gsql_arr_test.go: -------------------------------------------------------------------------------- 1 | package gsql_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mitranim/gg" 7 | "github.com/mitranim/gg/gsql" 8 | "github.com/mitranim/gg/gtest" 9 | ) 10 | 11 | func TestArrOf(t *testing.T) { 12 | defer gtest.Catch(t) 13 | 14 | gtest.Zero(gsql.Arr[int](nil)) 15 | gtest.Equal(gsql.ArrOf[int](), gsql.Arr[int](nil)) 16 | gtest.Equal(gsql.ArrOf(10), gsql.Arr[int]{10}) 17 | gtest.Equal(gsql.ArrOf(10, 20), gsql.Arr[int]{10, 20}) 18 | gtest.Equal(gsql.ArrOf(10, 20, 30), gsql.Arr[int]{10, 20, 30}) 19 | } 20 | 21 | func TestArr(t *testing.T) { 22 | t.Run(`String`, func(t *testing.T) { 23 | defer gtest.Catch(t) 24 | 25 | gtest.Str(gsql.Arr[int](nil), ``) 26 | gtest.Str(gsql.Arr[int]{}, `{}`) 27 | gtest.Str(gsql.Arr[int]{10}, `{10}`) 28 | gtest.Str(gsql.Arr[int]{10, 20}, `{10,20}`) 29 | gtest.Str(gsql.Arr[int]{10, 20, 30}, `{10,20,30}`) 30 | gtest.Str(gsql.Arr[gsql.Arr[int]]{{}, {}}, `{{},{}}`) 31 | gtest.Str(gsql.Arr[gsql.Arr[int]]{{10, 20}, {30, 40}}, `{{10,20},{30,40}}`) 32 | }) 33 | 34 | t.Run(`Parse`, func(t *testing.T) { 35 | defer gtest.Catch(t) 36 | 37 | testParser(``, gsql.Arr[int](nil)) 38 | testParser(`{}`, gsql.Arr[int]{}) 39 | testParser(`{10}`, gsql.Arr[int]{10}) 40 | testParser(`{10,20}`, gsql.Arr[int]{10, 20}) 41 | testParser(`{10,20,30}`, gsql.Arr[int]{10, 20, 30}) 42 | testParser(`{{},{}}`, gsql.Arr[gsql.Arr[int]]{{}, {}}) 43 | testParser(`{{10},{20},{30,40}}`, gsql.Arr[gsql.Arr[int]]{{10}, {20}, {30, 40}}) 44 | testParser(`{"10","20","30"}`, gsql.Arr[string]{`10`, `20`, `30`}) 45 | testParser(`{("10","20"),("30","40")}`, gsql.Arr[string]{`("10","20")`, `("30","40")`}) 46 | testParser(`{"(10,20)","(30,40)"}`, gsql.Arr[string]{`(10,20)`, `(30,40)`}) 47 | testParser(`{"(\"10\",20)","(\"30\",40)"}`, gsql.Arr[string]{`("10",20)`, `("30",40)`}) 48 | testParser(`{"(\"[\\\"one\\\"]\")","{\"(\\\"two\\\")\"}"}`, gsql.Arr[string]{`("[\"one\"]")`, `{"(\"two\")"}`}) 49 | }) 50 | } 51 | 52 | // TODO consider moving to `gtest`. 53 | func testParser[ 54 | A any, 55 | B interface { 56 | *A 57 | gg.Parser 58 | }, 59 | ](src string, exp A) { 60 | var tar A 61 | gtest.NoErr(B(&tar).Parse(src)) 62 | gtest.Equal(tar, exp) 63 | } 64 | 65 | func BenchmarkArr_Append(b *testing.B) { 66 | buf := make([]byte, 0, 4096) 67 | arr := gsql.ArrOf(10, 20, 30, 40, 50, 60, 70, 80) 68 | b.ResetTimer() 69 | 70 | for ind := 0; ind < b.N; ind++ { 71 | gg.Nop1(arr.AppendTo(buf)) 72 | } 73 | } 74 | 75 | func BenchmarkArr_String(b *testing.B) { 76 | arr := gsql.ArrOf(10, 20, 30, 40, 50, 60, 70, 80) 77 | b.ResetTimer() 78 | 79 | for ind := 0; ind < b.N; ind++ { 80 | gg.Nop1(arr.String()) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /gsql/gsql_constraints.go: -------------------------------------------------------------------------------- 1 | package gsql 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "io" 7 | 8 | "github.com/mitranim/gg" 9 | ) 10 | 11 | // Implemented by stdlib types such as `sql.DB`. 12 | type Db interface { 13 | DbConn 14 | DbTxer 15 | } 16 | 17 | // Implemented by stdlib types such as `sql.DB`. 18 | type DbTxer interface { 19 | BeginTx(context.Context, *sql.TxOptions) (*sql.Tx, error) 20 | } 21 | 22 | // Implemented by stdlib types such as `sql.Conn` and `sql.Tx`. 23 | type DbConn interface { 24 | QueryContext(context.Context, string, ...any) (*sql.Rows, error) 25 | ExecContext(context.Context, string, ...any) (sql.Result, error) 26 | } 27 | 28 | // Implemented by stdlib types such as `sql.Tx`. 29 | type DbTx interface { 30 | DbConn 31 | Commit() error 32 | Rollback() error 33 | } 34 | 35 | // Interface of `sql.Rows`. Used by various scanning tools. 36 | type Rows interface { 37 | io.Closer 38 | gg.Errer 39 | gg.Nexter 40 | ColumnerScanner 41 | } 42 | 43 | // Sub-interface of `Rows` used by `ScanNext`. 44 | type ColumnerScanner interface { 45 | Columns() ([]string, error) 46 | Scan(...any) error 47 | } 48 | -------------------------------------------------------------------------------- /gsql/gsql_internal_test.go: -------------------------------------------------------------------------------- 1 | package gsql 2 | 3 | import ( 4 | r "reflect" 5 | "testing" 6 | 7 | "github.com/mitranim/gg" 8 | "github.com/mitranim/gg/gtest" 9 | ) 10 | 11 | type Inner struct { 12 | InnerId string `db:"inner_id"` 13 | InnerName *string `db:"inner_name"` 14 | } 15 | 16 | type Outer struct { 17 | OuterId int64 `db:"outer_id"` 18 | OuterName string `db:"outer_name"` 19 | InnerZop gg.Zop[Inner] `db:"inner_zop"` 20 | Inner Inner `db:"inner"` 21 | } 22 | 23 | func Test_structMetaCache(t *testing.T) { 24 | defer gtest.Catch(t) 25 | 26 | gtest.Equal( 27 | typeMetaCache.Get(gg.Type[Outer]()), 28 | typeMeta{ 29 | typ: gg.Type[Outer](), 30 | dict: map[string][]int{ 31 | `outer_id`: []int{0}, 32 | `outer_name`: []int{1}, 33 | `inner_zop.inner_id`: []int{2, 0, 0}, 34 | `inner_zop.inner_name`: []int{2, 0, 1}, 35 | `inner.inner_id`: []int{3, 0}, 36 | `inner.inner_name`: []int{3, 1}, 37 | }, 38 | }, 39 | ) 40 | } 41 | 42 | /* 43 | Used by `scanValsReflect`. This demonstrates doubling behavior of 44 | `reflect.Value.Grow`. Our choice of implementation relies on this behavior. If 45 | `reflect.Value.Grow` allocated precisely the requested amount of additional 46 | capacity, which in our case is 1, we would have to change our strategy. 47 | 48 | Compare our `gg.GrowCap`, which behaves like `reflect.Value.Grow`, and 49 | `gg.GrowCapExact`, which behaves in a way that would be detrimental for 50 | the kind of algorithm we use here. 51 | */ 52 | func Test_reflect_slice_grow_alloc(t *testing.T) { 53 | defer gtest.Catch(t) 54 | 55 | var tar []int 56 | 57 | val := r.ValueOf(&tar).Elem() 58 | 59 | test := func(diff, total int) { 60 | prevLen := len(tar) 61 | 62 | val.Grow(diff) 63 | gtest.Eq(val.Len(), len(tar)) 64 | gtest.Eq(val.Cap(), cap(tar)) 65 | gtest.Eq(cap(tar), total) 66 | gtest.Eq(len(tar), prevLen) 67 | 68 | val.SetLen(total) 69 | gtest.Eq(val.Len(), len(tar)) 70 | gtest.Eq(len(tar), total) 71 | } 72 | 73 | test(0, 0) 74 | test(1, 1) 75 | test(1, 2) 76 | test(1, 4) 77 | test(1, 8) 78 | test(1, 16) 79 | test(1, 32) 80 | } 81 | -------------------------------------------------------------------------------- /gsql/gsql_like.go: -------------------------------------------------------------------------------- 1 | package gsql 2 | 3 | import ( 4 | "database/sql/driver" 5 | "strings" 6 | 7 | "github.com/mitranim/gg" 8 | ) 9 | 10 | /* 11 | Variant of `string` intended as an operand for SQL "like" and "ilike" operators. 12 | When generating an SQL argument via `.Value`, the string is wrapped in `%` to 13 | ensure partial match, escaping any pre-existing `%` and `_`. As a special case, 14 | an empty string is used as-is, and doesn't match anything when used with 15 | `like` or `ilike`. 16 | */ 17 | type Like string 18 | 19 | // Implement `fmt.Stringer`. Returns the underlying string unchanged. 20 | func (self Like) String() string { return string(self) } 21 | 22 | // Implement `driver.Valuer`, returning the escaped string from `.Esc`. 23 | func (self Like) Value() (driver.Value, error) { return self.Esc(), nil } 24 | 25 | // Implement `sql.Scanner`. 26 | func (self *Like) Scan(src any) error { 27 | str, ok := gg.AnyToText[string](src) 28 | if ok { 29 | *self = Like(str) 30 | return nil 31 | } 32 | return gg.ErrConv(src, gg.Type[Like]()) 33 | } 34 | 35 | /* 36 | Returns an escaped string suitable as an operand for SQL "like" or "ilike". 37 | As a special case, an empty string is returned as-is. 38 | */ 39 | func (self Like) Esc() string { 40 | if self == `` { 41 | return `` 42 | } 43 | return `%` + replaceLike.Replace(string(self)) + `%` 44 | } 45 | 46 | var replaceLike = strings.NewReplacer(`%`, `\%`, `_`, `\_`) 47 | -------------------------------------------------------------------------------- /gsql/gsql_like_test.go: -------------------------------------------------------------------------------- 1 | package gsql_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mitranim/gg" 7 | "github.com/mitranim/gg/gsql" 8 | "github.com/mitranim/gg/gtest" 9 | ) 10 | 11 | func TestLike(t *testing.T) { 12 | defer gtest.Catch(t) 13 | 14 | test := func(src, esc string) { 15 | tar := gsql.Like(src) 16 | gtest.Eq(tar.String(), src) 17 | gtest.Eq(tar.Esc(), esc) 18 | gtest.Eq(gg.Try1(tar.Value()).(string), esc) 19 | } 20 | 21 | test(``, ``) 22 | test(` `, `% %`) 23 | test(`str`, `%str%`) 24 | test(`%`, `%\%%`) 25 | test(`_`, `%\_%`) 26 | test(`%str%`, `%\%str\%%`) 27 | test(`_str_`, `%\_str\_%`) 28 | } 29 | -------------------------------------------------------------------------------- /gsql/gsql_misc.go: -------------------------------------------------------------------------------- 1 | package gsql 2 | 3 | import "github.com/mitranim/gg" 4 | 5 | /* 6 | Must be deferred. Commit if there was no panic, rollback if there was a 7 | panic. Usage: 8 | 9 | defer DbTxDone(conn) 10 | */ 11 | func DbTxDone[A DbTx](val A) { 12 | DbTxDoneWith(val, gg.AnyErrTracedAt(recover(), 1)) 13 | } 14 | 15 | /* 16 | Commit if there was no error, rollback if there was an error. 17 | Used internally by `DbTxDone`. 18 | */ 19 | func DbTxDoneWith[A DbTx](val A, err error) { 20 | if err != nil { 21 | _ = val.Rollback() 22 | panic(err) 23 | } 24 | 25 | defer gg.Detail(`failed to commit DB transaction`) 26 | gg.Try(val.Commit()) 27 | } 28 | -------------------------------------------------------------------------------- /gsql/gsql_rune.go: -------------------------------------------------------------------------------- 1 | package gsql 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | r "reflect" 7 | 8 | "github.com/mitranim/gg" 9 | ) 10 | 11 | /* 12 | Shortcut for converting an arbitrary rune-like to `Rune`. 13 | Useful for higher-order functions such as `gg.Map`. 14 | */ 15 | func RuneFrom[A ~rune](val A) Rune { return Rune(val) } 16 | 17 | /* 18 | Variant of Go `rune` compatible with text, JSON, and SQL (with caveats). In text 19 | and JSON, behaves like `string`. In Go and SQL, behaves like `rune`/`int32`. As 20 | a special case, zero value is considered empty in text, and null in JSON and 21 | SQL. When parsing, input must be empty or single char. 22 | 23 | Some databases, or their Go drivers, may not support representing chars as 24 | int32. For example, Postgres doesn't have an analog of Go `rune`. Its "char" 25 | type is a variable-sized string. This type is not compatible with such 26 | databases. 27 | */ 28 | type Rune rune 29 | 30 | // Implement `gg.Nullable`. True if zero value. 31 | func (self Rune) IsNull() bool { return self == 0 } 32 | 33 | // Inverse of `.IsNull`. 34 | func (self Rune) IsNotNull() bool { return !self.IsNull() } 35 | 36 | // Implement `Clearer`. Zeroes the receiver. 37 | func (self *Rune) Clear() { *self = 0 } 38 | 39 | /* 40 | Implement `fmt.Stringer`. If zero, returns an empty string. Otherwise returns 41 | a string containing exactly one character. 42 | */ 43 | func (self Rune) String() string { 44 | if self.IsNull() { 45 | return `` 46 | } 47 | return string(self) 48 | } 49 | 50 | // Implement `AppenderTo`, appending the same representation as `.String`. 51 | func (self Rune) AppendTo(buf []byte) []byte { 52 | if self.IsNull() { 53 | return buf 54 | } 55 | return append(buf, self.String()...) 56 | } 57 | 58 | /* 59 | Implement `Parser`. If the input is empty, clears the receiver via `.Clear`. If 60 | the input has more than one character, returns an error. Otherwise uses the 61 | first and only character from the input. 62 | */ 63 | func (self *Rune) Parse(src string) error { 64 | chars := []rune(src) 65 | if len(chars) == 0 { 66 | self.Clear() 67 | return nil 68 | } 69 | 70 | if len(chars) > 1 { 71 | return gg.Errf(`unable to parse %q as char: too many chars`, src) 72 | } 73 | 74 | *self = Rune(chars[0]) 75 | return nil 76 | } 77 | 78 | // Implement `encoding.TextMarshaler`, returning the same representation as `.String`. 79 | func (self Rune) MarshalText() ([]byte, error) { 80 | return gg.ToBytes(self.String()), nil 81 | } 82 | 83 | // Implement `encoding.TextUnmarshaler`, using the same logic as `.Parse`. 84 | func (self *Rune) UnmarshalText(src []byte) error { 85 | return self.Parse(gg.ToString(src)) 86 | } 87 | 88 | /* 89 | Implement `json.Marshaler`. If `.IsNull`, returns a representation of JSON null. 90 | Otherwise uses an equivalent of `json.Marshal(self.String())`. 91 | */ 92 | func (self Rune) MarshalJSON() ([]byte, error) { 93 | if self.IsNull() { 94 | return gg.ToBytes(`null`), nil 95 | } 96 | 97 | if self == '"' { 98 | return gg.ToBytes(`"\""`), nil 99 | } 100 | return gg.ToBytes(`"` + string(rune(self)) + `"`), nil 101 | } 102 | 103 | /* 104 | Implement `json.Unmarshaler`. If the input is empty or represents JSON null, 105 | clears the receiver via `.Clear`. Otherwise requires the input to be a JSON 106 | string and decodes it via `.Parse`. 107 | */ 108 | func (self *Rune) UnmarshalJSON(src []byte) error { 109 | if gg.IsJsonEmpty(src) { 110 | self.Clear() 111 | return nil 112 | } 113 | 114 | // Inefficient, TODO tune. 115 | var tar string 116 | err := json.Unmarshal(src, gg.AnyNoEscUnsafe(&tar)) 117 | if err != nil { 118 | return err 119 | } 120 | 121 | return self.Parse(tar) 122 | } 123 | 124 | /* 125 | Implement SQL `driver.Valuer`. If `.IsNull`, returns nil. Otherwise returns 126 | rune. 127 | */ 128 | func (self Rune) Value() (driver.Value, error) { 129 | if self.IsNull() { 130 | return nil, nil 131 | } 132 | return rune(self), nil 133 | } 134 | 135 | /* 136 | Implement SQL `Scanner`, decoding arbitrary input, which must be one of: 137 | 138 | * Nil -> use `.Clear`. 139 | * Text -> use `.Parse`. 140 | * Rune -> assign as-is. 141 | */ 142 | func (self *Rune) Scan(src any) error { 143 | if src == nil { 144 | self.Clear() 145 | return nil 146 | } 147 | 148 | str, ok := gg.AnyToText[string](src) 149 | if ok { 150 | return self.Parse(str) 151 | } 152 | 153 | val := r.ValueOf(gg.AnyNoEscUnsafe(src)) 154 | if val.Kind() == r.Int32 { 155 | *self = Rune(val.Int()) 156 | return nil 157 | } 158 | 159 | return gg.ErrConv(src, gg.Type[Rune]()) 160 | } 161 | -------------------------------------------------------------------------------- /gsql/gsql_rune_test.go: -------------------------------------------------------------------------------- 1 | package gsql_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mitranim/gg" 7 | s "github.com/mitranim/gg/gsql" 8 | gtest "github.com/mitranim/gg/gtest" 9 | ) 10 | 11 | func TestRune_IsNull(t *testing.T) { 12 | defer gtest.Catch(t) 13 | 14 | gtest.True(s.Rune(0).IsNull()) 15 | gtest.False(s.Rune(1).IsNull()) 16 | } 17 | 18 | func TestRune_IsNotNull(t *testing.T) { 19 | defer gtest.Catch(t) 20 | 21 | gtest.False(s.Rune(0).IsNotNull()) 22 | gtest.True(s.Rune(1).IsNotNull()) 23 | } 24 | 25 | func TestRune_Clear(t *testing.T) { 26 | defer gtest.Catch(t) 27 | 28 | var tar s.Rune 29 | gtest.Zero(tar) 30 | 31 | tar = '👍' 32 | gtest.NotZero(tar) 33 | 34 | tar.Clear() 35 | gtest.Zero(tar) 36 | 37 | tar.Clear() 38 | gtest.Zero(tar) 39 | } 40 | 41 | func TestRune_String(t *testing.T) { 42 | defer gtest.Catch(t) 43 | 44 | gtest.Str(s.Rune(0), ``) 45 | gtest.Str(s.Rune('👍'), `👍`) 46 | } 47 | 48 | func BenchmarkRune_String(b *testing.B) { 49 | for ind := 0; ind < b.N; ind++ { 50 | gg.Nop1(s.Rune('👍')) 51 | } 52 | } 53 | 54 | func TestRune_Append(t *testing.T) { 55 | defer gtest.Catch(t) 56 | 57 | buf := gg.ToBytes(`init_`) 58 | 59 | gtest.Equal(s.Rune(0).AppendTo(buf), buf) 60 | gtest.Equal(s.Rune(0).AppendTo(nil), nil) 61 | 62 | gtest.Equal(s.Rune('👍').AppendTo(buf), gg.ToBytes(`init_👍`)) 63 | gtest.Equal(s.Rune('👍').AppendTo(nil), gg.ToBytes(`👍`)) 64 | } 65 | 66 | func TestRune_Parse(t *testing.T) { 67 | defer gtest.Catch(t) 68 | testRuneParse((*s.Rune).Parse) 69 | } 70 | 71 | func testRuneParse(fun func(*s.Rune, string) error) { 72 | gtest.ErrStr(`unable to parse "ab" as char: too many chars`, fun(new(s.Rune), `ab`)) 73 | gtest.ErrStr(`unable to parse "abc" as char: too many chars`, fun(new(s.Rune), `abc`)) 74 | gtest.ErrStr(`unable to parse "👍👎" as char: too many chars`, fun(new(s.Rune), `👍👎`)) 75 | 76 | var tar s.Rune 77 | 78 | gtest.NoErr(fun(&tar, `🙂`)) 79 | gtest.Eq(tar, '🙂') 80 | 81 | gtest.NoErr(fun(&tar, ``)) 82 | gtest.Zero(tar) 83 | } 84 | 85 | func BenchmarkRune_Parse_empty(b *testing.B) { 86 | var tar s.Rune 87 | 88 | for ind := 0; ind < b.N; ind++ { 89 | gg.Try(tar.Parse(``)) 90 | } 91 | } 92 | 93 | func BenchmarkRune_Parse_non_empty(b *testing.B) { 94 | var tar s.Rune 95 | 96 | for ind := 0; ind < b.N; ind++ { 97 | gg.Try(tar.Parse(`🙂`)) 98 | } 99 | } 100 | 101 | func TestRune_MarshalText(t *testing.T) { 102 | defer gtest.Catch(t) 103 | 104 | encode := func(src s.Rune) string { 105 | return gg.ToString(gg.Try1(src.MarshalText())) 106 | } 107 | 108 | gtest.Eq(encode(0), ``) 109 | gtest.Eq(encode('👍'), `👍`) 110 | } 111 | 112 | func BenchmarkRune_MarshalText(b *testing.B) { 113 | for ind := 0; ind < b.N; ind++ { 114 | gg.Nop2(s.Rune('👍').MarshalText()) 115 | } 116 | } 117 | 118 | func TestRune_UnmarshalText(t *testing.T) { 119 | defer gtest.Catch(t) 120 | testRuneParse(charUnmarshalText) 121 | } 122 | 123 | func charUnmarshalText(tar *s.Rune, src string) error { 124 | return tar.UnmarshalText(gg.ToBytes(src)) 125 | } 126 | 127 | func BenchmarkRune_UnmarshalText(b *testing.B) { 128 | var tar s.Rune 129 | 130 | for ind := 0; ind < b.N; ind++ { 131 | gg.Try(tar.UnmarshalText(gg.ToBytes(`👍`))) 132 | } 133 | } 134 | 135 | func TestRune_MarshalJSON(t *testing.T) { 136 | defer gtest.Catch(t) 137 | 138 | encode := func(src s.Rune) string { 139 | return gg.ToString(gg.Try1(src.MarshalJSON())) 140 | } 141 | 142 | gtest.Eq(encode(0), `null`) 143 | gtest.Eq(encode('👍'), `"👍"`) 144 | } 145 | 146 | func BenchmarkRune_MarshalJSON_empty(b *testing.B) { 147 | for ind := 0; ind < b.N; ind++ { 148 | gg.Nop2(s.Rune(0).MarshalJSON()) 149 | } 150 | } 151 | 152 | func BenchmarkRune_MarshalJSON_non_empty(b *testing.B) { 153 | for ind := 0; ind < b.N; ind++ { 154 | gg.Nop2(s.Rune('👍').MarshalJSON()) 155 | } 156 | } 157 | 158 | func TestRune_UnmarshalJSON(t *testing.T) { 159 | defer gtest.Catch(t) 160 | 161 | testRuneParse(charUnmarshalJson) 162 | 163 | gtest.ErrStr( 164 | `cannot unmarshal number into Go value of type string`, 165 | new(s.Rune).UnmarshalJSON(gg.ToBytes(`123`)), 166 | ) 167 | 168 | { 169 | tar := s.Rune('👍') 170 | gtest.NoErr(tar.UnmarshalJSON(nil)) 171 | gtest.Zero(tar) 172 | } 173 | 174 | { 175 | tar := s.Rune('👍') 176 | gtest.NoErr(tar.UnmarshalJSON(gg.ToBytes(`null`))) 177 | gtest.Zero(tar) 178 | } 179 | } 180 | 181 | func charUnmarshalJson(tar *s.Rune, src string) error { 182 | return tar.UnmarshalJSON(gg.JsonBytes(src)) 183 | } 184 | 185 | func BenchmarkRune_UnmarshalJSON_empty(b *testing.B) { 186 | var tar s.Rune 187 | 188 | for ind := 0; ind < b.N; ind++ { 189 | gg.Try(tar.UnmarshalJSON(gg.ToBytes(`null`))) 190 | } 191 | } 192 | 193 | func BenchmarkRune_UnmarshalJSON_non_empty(b *testing.B) { 194 | var tar s.Rune 195 | 196 | for ind := 0; ind < b.N; ind++ { 197 | gg.Try(tar.UnmarshalJSON(gg.ToBytes(`"👍"`))) 198 | } 199 | } 200 | 201 | func TestRune_Value(t *testing.T) { 202 | defer gtest.Catch(t) 203 | 204 | gtest.Zero(gg.Try1(s.Rune(0).Value())) 205 | gtest.Equal(gg.Try1(s.Rune('👍').Value()), any(rune('👍'))) 206 | } 207 | 208 | func TestRune_Scan(t *testing.T) { 209 | t.Run(`clear`, func(t *testing.T) { 210 | defer gtest.Catch(t) 211 | 212 | test := func(src any) { 213 | tar := s.Rune('👍') 214 | gtest.NoErr(tar.Scan(src)) 215 | gtest.Zero(tar) 216 | } 217 | 218 | test(nil) 219 | test(string(``)) 220 | test([]byte(nil)) 221 | test([]byte{}) 222 | test(rune(0)) 223 | test(s.Rune(0)) 224 | }) 225 | 226 | t.Run(`unclear`, func(t *testing.T) { 227 | defer gtest.Catch(t) 228 | 229 | test := func(src any, exp s.Rune) { 230 | var tar s.Rune 231 | gtest.NoErr(tar.Scan(src)) 232 | gtest.Eq(tar, exp) 233 | } 234 | 235 | test(string(`👍`), '👍') 236 | test([]byte(`👍`), '👍') 237 | test(rune('👍'), '👍') 238 | test(s.Rune('👍'), '👍') 239 | }) 240 | } 241 | 242 | func BenchmarkRune_Scan_empty(b *testing.B) { 243 | var tar s.Rune 244 | var src []byte 245 | 246 | for ind := 0; ind < b.N; ind++ { 247 | gg.Try(tar.Scan(src)) 248 | } 249 | } 250 | 251 | func BenchmarkRune_Scan_non_empty(b *testing.B) { 252 | var tar s.Rune 253 | src := string(`👍`) 254 | 255 | for ind := 0; ind < b.N; ind++ { 256 | gg.Try(tar.Scan(src)) 257 | } 258 | } 259 | 260 | func Test_string_to_char_slice(t *testing.T) { 261 | defer gtest.Catch(t) 262 | 263 | gtest.Equal( 264 | []rune(`👍👎🙂😄`), 265 | []rune{'👍', '👎', '🙂', '😄'}, 266 | ) 267 | 268 | gtest.Equal( 269 | []s.Rune(`👍👎🙂😄`), 270 | []s.Rune{'👍', '👎', '🙂', '😄'}, 271 | ) 272 | } 273 | -------------------------------------------------------------------------------- /gsql/gsql_scan.go: -------------------------------------------------------------------------------- 1 | package gsql 2 | 3 | import ( 4 | "database/sql" 5 | r "reflect" 6 | 7 | "github.com/mitranim/gg" 8 | ) 9 | 10 | // Returned by `ScanVal` and `ScanAny` when there are too many rows. 11 | const ErrMultipleRows gg.ErrStr = `expected one row, got multiple` 12 | 13 | /* 14 | Takes `Rows` and decodes them into a slice of the given type, using `ScanNext` 15 | for each row. Output type must be either scalar or struct. Always closes the 16 | rows. 17 | */ 18 | func ScanVals[Row any, Src Rows](src Src) (out []Row) { 19 | defer gg.Close(src) 20 | for src.Next() { 21 | out = append(out, ScanNext[Row](src)) 22 | } 23 | gg.ErrOk(src) 24 | return 25 | } 26 | 27 | /* 28 | Takes `Rows` and decodes the first row into a value of the given type, using 29 | `ScanNext` once. The rows must consist of EXACTLY one row, otherwise this 30 | panics. Output type must be either scalar or struct. Always closes the rows. 31 | */ 32 | func ScanVal[Row any, Src Rows](src Src) Row { 33 | defer gg.Close(src) 34 | 35 | if !src.Next() { 36 | panic(gg.AnyErrTraced(sql.ErrNoRows)) 37 | } 38 | 39 | out := ScanNext[Row](src) 40 | gg.ErrOk(src) 41 | 42 | if src.Next() { 43 | panic(gg.AnyErrTraced(ErrMultipleRows)) 44 | } 45 | return out 46 | } 47 | 48 | /* 49 | Takes `Rows` and decodes the next row into a value of the given type. Output 50 | type must be either scalar or struct. Panics on errors. Must be called only 51 | after `Rows.Next`. 52 | */ 53 | func ScanNext[Row any, Src ColumnerScanner](src Src) Row { 54 | if isScalar[Row]() { 55 | return scanNextScalar[Row](src) 56 | } 57 | return scanNextStruct[Row](src) 58 | } 59 | 60 | /* 61 | Decodes `Rows` into the given dynamically typed output. Counterpart to 62 | `ScanVals` and `ScanVal` which are statically typed. The output must be 63 | a non-nil pointer, any amount of levels deep, to one of the following: 64 | 65 | - Slice of scalars. 66 | - Slice of structs. 67 | - Single scalar. 68 | - Single struct. 69 | - Interface value hosting a concrete type. 70 | 71 | Always closes the rows. If the output is not a slice, verifies that there is 72 | EXACTLY one row in total, otherwise panics. 73 | */ 74 | func ScanAny[Src Rows](src Src, out any) { 75 | ScanReflect(src, r.ValueOf(out)) 76 | } 77 | 78 | // Variant of `ScanAny` that takes `reflect.Value` rather than `any`. 79 | func ScanReflect[Src Rows](src Src, out r.Value) { 80 | tar, iface := derefAlloc(out) 81 | 82 | if tar.Kind() == r.Slice { 83 | scanValsReflect(src, tar) 84 | } else { 85 | scanValReflect(src, tar, true) 86 | } 87 | 88 | if iface.CanSet() { 89 | iface.Set(tar.Convert(iface.Type())) 90 | } 91 | } 92 | 93 | /* 94 | Similar to `ScanAny`, but when scanning into a single value (not a slice), 95 | doesn't panic if there are zero rows, leaving the destination unchanged. 96 | When scanning into a slice, behaves exactly like `ScanAny`. 97 | */ 98 | func ScanAnyOpt[Src Rows](src Src, out any) { 99 | ScanReflectOpt(src, r.ValueOf(out)) 100 | } 101 | 102 | // Variant of `ScanAnyOpt` that takes `reflect.Value` rather than `any`. 103 | func ScanReflectOpt[Src Rows](src Src, out r.Value) { 104 | tar, iface := derefAlloc(out) 105 | 106 | if tar.Kind() == r.Slice { 107 | scanValsReflect(src, tar) 108 | } else { 109 | scanValReflect(src, tar, false) 110 | } 111 | 112 | if iface.CanSet() { 113 | iface.Set(tar.Convert(iface.Type())) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /gsql/readme.md: -------------------------------------------------------------------------------- 1 | Missing features of the Go standard library related to SQL: 2 | 3 | * Support for scanning SQL rows into Go structs. 4 | 5 | * Usable support for SQL arrays. Single-dimensional arrays are simple slices. Multi-dimensional arrays are nested slices. Everything is automatically compatible with other formats such as JSON. 6 | 7 | * Various utility types compatible with both SQL and JSON. 8 | 9 | API doc: https://pkg.go.dev/github.com/mitranim/gg/gsql 10 | -------------------------------------------------------------------------------- /gtest/gtest_internal.go: -------------------------------------------------------------------------------- 1 | package gtest 2 | 3 | import ( 4 | "fmt" 5 | r "reflect" 6 | "strings" 7 | 8 | "github.com/mitranim/gg" 9 | "github.com/mitranim/gg/grepr" 10 | ) 11 | 12 | // Suboptimal, TODO revise. 13 | func reindent(src string) string { 14 | return gg.JoinLines(gg.Map(gg.SplitLines(src), indent)...) 15 | } 16 | 17 | func indent(src string) string { 18 | if src == `` { 19 | return src 20 | } 21 | return gg.Indent + src 22 | } 23 | 24 | // TODO rename and make public. 25 | func goStringIndent[A any](val A) string { return grepr.StringIndent(val, 1) } 26 | 27 | func errTrace(err error) string { 28 | return strings.TrimSpace(gg.ErrTrace(err).StringIndent(1)) 29 | } 30 | 31 | /* 32 | Should return `true` when stringifying the given value via `fmt.Sprint` produces 33 | basically the same representation as pretty-printing it via `grepr`, with no 34 | significant difference in information. We "discount" the string quotes in this 35 | case. TODO rename and move to `grepr`. This test for `fmt.Stringer` but ignores 36 | other text-encoding interfaces such as `gg.Appender` or `encoding.TextMarshaler` 37 | because `gtest` produces the "simple" representation by calling `fmt.Sprint`, 38 | which does not support any of those additional interfaces. 39 | */ 40 | func isSimple(src any) bool { 41 | return src == nil || (!gg.AnyIs[fmt.Stringer](src) && 42 | !gg.AnyIs[fmt.GoStringer](src) && 43 | isPrim(src)) 44 | } 45 | 46 | // TODO should probably move to `gg` and make public. 47 | func isPrim(src any) bool { 48 | val := r.ValueOf(src) 49 | 50 | switch val.Kind() { 51 | case r.Bool, 52 | r.Int8, r.Int16, r.Int32, r.Int64, r.Int, 53 | r.Uint8, r.Uint16, r.Uint32, r.Uint64, r.Uint, r.Uintptr, 54 | r.Float32, r.Float64, 55 | r.Complex64, r.Complex128, 56 | r.String: 57 | return true 58 | default: 59 | return false 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /gtest/gtest_msg.go: -------------------------------------------------------------------------------- 1 | package gtest 2 | 3 | import "github.com/mitranim/gg" 4 | 5 | // Internal shortcut for generating parts of an error message. 6 | func Msg(msg, det string) string { return gg.JoinLinesOpt(msg, reindent(det)) } 7 | 8 | // Internal shortcut for generating parts of an error message. 9 | func MsgOpt(msg, det string) string { 10 | if det == `` { 11 | return `` 12 | } 13 | return Msg(msg, det) 14 | } 15 | 16 | // Internal shortcut for generating parts of an error message. 17 | func MsgExtra(src ...any) string { 18 | return MsgOpt(`extra:`, gg.SpacedOpt(src...)) 19 | } 20 | 21 | // Internal shortcut for generating parts of an error message. 22 | func MsgExp[A any](val A) string { return Msg(`expected:`, gg.StringAny(val)) } 23 | 24 | // Internal shortcut for generating parts of an error message. 25 | func MsgSingle[A any](val A) string { 26 | if isSimple(val) { 27 | return Msg(`value:`, gg.StringAny(val)) 28 | } 29 | 30 | return gg.JoinLinesOpt( 31 | Msg(`detailed:`, goStringIndent(val)), 32 | Msg(`simple:`, gg.StringAny(val)), 33 | ) 34 | } 35 | 36 | // Internal shortcut for generating parts of an error message. 37 | func MsgEq(act, exp any) string { 38 | return gg.JoinLinesOpt(`unexpected difference`, MsgEqInner(act, exp)) 39 | } 40 | 41 | // Internal shortcut for generating parts of an error message. 42 | func MsgEqInner(act, exp any) string { 43 | if isSimple(act) && isSimple(exp) { 44 | return gg.JoinLinesOpt( 45 | Msg(`actual:`, gg.StringAny(act)), 46 | Msg(`expected:`, gg.StringAny(exp)), 47 | ) 48 | } 49 | 50 | return gg.JoinLinesOpt( 51 | MsgEqDetailed(act, exp), 52 | MsgEqSimple(act, exp), 53 | ) 54 | } 55 | 56 | // Internal shortcut for generating parts of an error message. 57 | func MsgEqDetailed(act, exp any) string { 58 | return gg.JoinLinesOpt( 59 | Msg(`actual detailed:`, goStringIndent(act)), 60 | Msg(`expected detailed:`, goStringIndent(exp)), 61 | ) 62 | } 63 | 64 | // Internal shortcut for generating parts of an error message. 65 | func MsgEqSimple(act, exp any) string { 66 | return gg.JoinLinesOpt( 67 | Msg(`actual simple:`, gg.StringAny(act)), 68 | Msg(`expected simple:`, gg.StringAny(exp)), 69 | ) 70 | } 71 | 72 | // Internal shortcut for generating parts of an error message. 73 | func MsgNotEq[A any](act A) string { 74 | return gg.JoinLinesOpt(`unexpected equality`, MsgSingle(act)) 75 | } 76 | 77 | // Internal shortcut for generating parts of an error message. 78 | func MsgErr(err error) string { 79 | return gg.JoinLinesOpt( 80 | Msg(`error trace:`, errTrace(err)), 81 | Msg(`error string:`, gg.StringAny(err)), 82 | ) 83 | } 84 | 85 | // Internal shortcut for generating parts of an error message. 86 | func MsgErrNone(test func(error) bool) string { 87 | return gg.JoinLinesOpt(`unexpected lack of error`, MsgErrTest(test)) 88 | } 89 | 90 | // Internal shortcut for generating parts of an error message. 91 | func MsgErrMismatch(fun func(), test func(error) bool, err error) string { 92 | return gg.JoinLinesOpt( 93 | `unexpected error mismatch`, 94 | MsgErrFunTest(fun, test), 95 | Msg(`error trace:`, errTrace(err)), 96 | Msg(`error string:`, gg.StringAny(err)), 97 | ) 98 | } 99 | 100 | // Internal shortcut for generating parts of an error message. 101 | func MsgErrMsgMismatch(fun func(), exp, act string) string { 102 | return gg.JoinLinesOpt( 103 | `unexpected error string mismatch`, 104 | MsgFun(fun), 105 | Msg(`actual error string:`, act), 106 | Msg(`expected error string substring:`, exp), 107 | ) 108 | } 109 | 110 | // Internal shortcut for generating parts of an error message. 111 | func MsgErrIsMismatch(err, exp error) string { 112 | return gg.JoinLinesOpt( 113 | `unexpected error mismatch`, 114 | Msg(`actual error trace:`, errTrace(err)), 115 | Msg(`actual error string:`, gg.StringAny(err)), 116 | Msg(`expected error via errors.Is:`, gg.StringAny(exp)), 117 | ) 118 | } 119 | 120 | // Internal shortcut for generating parts of an error message. 121 | func MsgErrTest(val func(error) bool) string { 122 | if val == nil { 123 | return `` 124 | } 125 | return Msg(`error test:`, gg.FuncName(val)) 126 | } 127 | 128 | // Internal shortcut for generating parts of an error message. 129 | func MsgErrFunTest(fun func(), test func(error) bool) string { 130 | return gg.JoinLinesOpt(MsgFun(fun), MsgErrTest(test)) 131 | } 132 | 133 | // Internal shortcut for generating parts of an error message. 134 | func MsgFunErr(fun func(), err error) string { 135 | return gg.JoinLinesOpt(MsgFun(fun), MsgErr(err)) 136 | } 137 | 138 | // Internal shortcut for generating parts of an error message. 139 | func MsgFun(val func()) string { 140 | if val == nil { 141 | return `` 142 | } 143 | return Msg(`function:`, gg.FuncName(val)) 144 | } 145 | 146 | // Internal shortcut for generating parts of an error message. 147 | func MsgNotPanic() string { return `unexpected lack of panic` } 148 | 149 | // Internal shortcut for generating parts of an error message. 150 | func MsgPanicNoneWithTest(fun func(), test func(error) bool) string { 151 | return gg.JoinLinesOpt(MsgNotPanic(), MsgErrFunTest(fun, test)) 152 | } 153 | 154 | // Internal shortcut for generating parts of an error message. 155 | func MsgPanicNoneWithStr(fun func(), exp string) string { 156 | return gg.JoinLinesOpt(MsgNotPanic(), MsgFun(fun), MsgExp(exp)) 157 | } 158 | 159 | // Internal shortcut for generating parts of an error message. 160 | func MsgPanicNoneWithErr(fun func(), exp error) string { 161 | return gg.JoinLinesOpt(MsgNotPanic(), MsgFun(fun), MsgExp(exp)) 162 | } 163 | 164 | // Internal shortcut for generating parts of an error message. 165 | func MsgSliceElemMissing[A ~[]B, B any](src A, val B) string { 166 | return gg.JoinLinesOpt(`missing element in slice`, MsgSliceElem(src, val)) 167 | } 168 | 169 | // Internal shortcut for generating parts of an error message. 170 | func MsgSliceElemUnexpected[A ~[]B, B any](src A, val B) string { 171 | return gg.JoinLinesOpt(`unexpected element in slice`, MsgSliceElem(src, val)) 172 | } 173 | 174 | // Internal shortcut for generating parts of an error message. 175 | func MsgSliceElem[A ~[]B, B any](src A, val B) string { 176 | // TODO avoid detailed view when it's unnecessary. 177 | return gg.JoinLinesOpt( 178 | Msg(`slice detailed:`, goStringIndent(src)), 179 | Msg(`element detailed:`, goStringIndent(val)), 180 | Msg(`slice simple:`, gg.StringAny(src)), 181 | Msg(`element simple:`, gg.StringAny(val)), 182 | ) 183 | } 184 | 185 | // Internal shortcut for generating parts of an error message. 186 | func MsgLess[A any](one, two A) string { 187 | return gg.JoinLinesOpt(`expected A < B`, MsgAB(one, two)) 188 | } 189 | 190 | // Internal shortcut for generating parts of an error message. 191 | func MsgLessEq[A any](one, two A) string { 192 | return gg.JoinLinesOpt(`expected A <= B`, MsgAB(one, two)) 193 | } 194 | 195 | // Internal shortcut for generating parts of an error message. 196 | func MsgAB[A any](one, two A) string { 197 | return gg.JoinLinesOpt( 198 | // TODO avoid detailed view when it's unnecessary. 199 | Msg(`A detailed:`, goStringIndent(one)), 200 | Msg(`B detailed:`, goStringIndent(two)), 201 | Msg(`A simple:`, gg.StringAny(one)), 202 | Msg(`B simple:`, gg.StringAny(two)), 203 | ) 204 | } 205 | -------------------------------------------------------------------------------- /gtest/readme.md: -------------------------------------------------------------------------------- 1 | Missing feature of the standard library: test assertions. Similar to the popular package `testify`, but better. Uses generics for proper static typing. Unobtrusively uses panic and recovery, allowing terse assertions without explicitly threading `t` through your function calls. Prints nice, readable stack traces. 2 | 3 | API doc: https://pkg.go.dev/github.com/mitranim/gg/gtest 4 | -------------------------------------------------------------------------------- /internal.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import ( 4 | "io/fs" 5 | "math" 6 | "path/filepath" 7 | r "reflect" 8 | "regexp" 9 | "strings" 10 | u "unsafe" 11 | ) 12 | 13 | func typeBitSize(typ r.Type) int { return int(typ.Size() * 8) } 14 | 15 | // Borrowed from the standard library. Requires caution. 16 | func noescape(src u.Pointer) u.Pointer { 17 | out := uintptr(src) 18 | //nolint:staticcheck 19 | return u.Pointer(out ^ 0) 20 | } 21 | 22 | func errAppendInner(buf Buf, err error) Buf { 23 | if err != nil { 24 | buf.AppendString(`: `) 25 | buf.AppendError(err) 26 | } 27 | return buf 28 | } 29 | 30 | func errAppendTraceIndentWithNewline(buf Buf, trace Trace) Buf { 31 | if trace.IsNotEmpty() { 32 | buf.AppendNewline() 33 | return errAppendTraceIndent(buf, trace) 34 | } 35 | return buf 36 | } 37 | 38 | func errAppendTraceIndent(buf Buf, trace Trace) Buf { 39 | if trace.IsNotEmpty() { 40 | buf.AppendString(`trace:`) 41 | buf = trace.AppendIndentTo(buf, 1) 42 | } 43 | return buf 44 | } 45 | 46 | func isFuncNameAnon(val string) bool { 47 | const pre = `func` 48 | return strings.HasPrefix(val, pre) && hasPrefixDigit(val[len(pre):]) 49 | } 50 | 51 | func hasPrefixDigit(val string) bool { return isDigit(TextHeadByte(val)) } 52 | 53 | func isDigit(val byte) bool { return val >= '0' && val <= '9' } 54 | 55 | func validateLenMatch(one, two int) { 56 | if one != two { 57 | panic(Errf( 58 | `unable to iterate pairwise: length mismatch: %v and %v`, 59 | one, two, 60 | )) 61 | } 62 | } 63 | 64 | // Note: `strconv.ParseBool` is too permissive for our taste. 65 | func parseBool(src string, out r.Value) error { 66 | switch src { 67 | case `true`: 68 | out.SetBool(true) 69 | return nil 70 | 71 | case `false`: 72 | out.SetBool(false) 73 | return nil 74 | 75 | default: 76 | return ErrParse(ErrInvalidInput, src, Type[bool]()) 77 | } 78 | } 79 | 80 | /* 81 | Somewhat similar to `filepath.Rel`, but doesn't support `..`, performs 82 | significantly better, and returns the path as-is when it doesn't start 83 | with the given base. 84 | */ 85 | func relOpt(base, src string) string { 86 | if strings.HasPrefix(src, base) { 87 | rem := src[len(base):] 88 | if len(rem) > 0 && rem[0] == filepath.Separator { 89 | return rem[1:] 90 | } 91 | } 92 | return src 93 | } 94 | 95 | func isIntString(val string) bool { 96 | if len(val) <= 0 { 97 | return false 98 | } 99 | 100 | if len(val) > 0 && (val[0] == '+' || val[0] == '-') { 101 | val = val[1:] 102 | } 103 | 104 | if len(val) <= 0 { 105 | return false 106 | } 107 | 108 | // Note: here we iterate bytes rather than UTF-8 characters because digits 109 | // are always single byte and we abort on the first mismatch. This may be 110 | // slightly more efficient than iterating characters. 111 | for ind := 0; ind < len(val); ind++ { 112 | if !isDigit(val[ind]) { 113 | return false 114 | } 115 | } 116 | return true 117 | } 118 | 119 | func isCliFlag(val string) bool { return TextHeadByte(val) == '-' } 120 | 121 | func isCliFlagValid(val string) bool { return reCliFlag.Get().MatchString(val) } 122 | 123 | /* 124 | Must begin with `-` and consist of alphanumeric characters, optionally 125 | containing `-` between those characters. 126 | 127 | TODO test. 128 | */ 129 | var reCliFlag = NewLazy(func() *regexp.Regexp { 130 | return regexp.MustCompile(`^-+[\p{L}\d]+(?:[\p{L}\d-]*[\p{L}\d])?$`) 131 | }) 132 | 133 | func cliFlagSplit(src string) (_ string, _ string, _ bool) { 134 | if !isCliFlag(src) { 135 | return 136 | } 137 | 138 | ind := strings.IndexRune(src, '=') 139 | if ind >= 0 { 140 | return src[:ind], src[ind+1:], true 141 | } 142 | 143 | return src, ``, false 144 | } 145 | 146 | /* 147 | Represents nodes in a linked list. Normally in Go, linked lists tend to be an 148 | anti-pattern; slices perform better in most scenarios, and don't require an 149 | additional abstraction. However, there is one valid scenario for linked lists: 150 | when nodes are pointers to local variables, when those local variables don't 151 | escape, and when they represent addresses to actual memory regions in stack 152 | frames. In this case, this may provide us with a resizable data structure 153 | allocated entirely on the stack, which is useful for book-keeping in recursive 154 | tree-walking or graph-walking algorithms. We currently do not verify if the 155 | trick has the expected efficiency, as the overheads are minimal. 156 | */ 157 | type node[A comparable] struct { 158 | tail *node[A] 159 | val A 160 | } 161 | 162 | func (self node[A]) has(val A) bool { 163 | return self.val == val || (self.tail != nil && self.tail.has(val)) 164 | } 165 | 166 | func (self *node[A]) cons(val A) (out node[A]) { 167 | out.tail = self 168 | out.val = val 169 | return 170 | } 171 | 172 | /* 173 | Suboptimal: doesn't preallocate capacity. We only call this in case of errors, 174 | so the overhead should be negligible. 175 | */ 176 | func (self node[A]) vals() (out []A) { 177 | out = append(out, self.val) 178 | node := self.tail 179 | for node != nil { 180 | out = append(out, node.val) 181 | node = node.tail 182 | } 183 | return 184 | } 185 | 186 | func safeUintToInt(src uint) int { 187 | if src > math.MaxInt { 188 | return math.MaxInt 189 | } 190 | return int(src) 191 | } 192 | 193 | func isByteNewline(val byte) bool { return val == '\n' || val == '\r' } 194 | 195 | func errCollMissing[Val, Key any](key Key) Err { 196 | return Errf(`missing value of type %v for key %v`, Type[Val](), key) 197 | } 198 | 199 | func dirEntryToFileName(src fs.DirEntry) (_ string) { 200 | if src == nil || src.IsDir() { 201 | return 202 | } 203 | return src.Name() 204 | } 205 | -------------------------------------------------------------------------------- /json.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | // Uses `json.Marshal` to encode the given value as JSON, panicking on error. 12 | func JsonEncode[Out Text, Src any](src Src) Out { 13 | return Try1(JsonEncodeCatch[Out](src)) 14 | } 15 | 16 | /* 17 | Uses `json.MarshalIndent` to encode the given value as JSON with indentation 18 | controlled by the `Indent` variable, panicking on error. 19 | */ 20 | func JsonEncodeIndent[Out Text, Src any](src Src) Out { 21 | return Try1(JsonEncodeIndentCatch[Out](src)) 22 | } 23 | 24 | /* 25 | Same as `json.Marshal` but sometimes marginally more efficient. Avoids spurious 26 | heap escape of the input. May be redundant in later Go versions. 27 | */ 28 | func JsonEncodeCatch[Out Text, Src any](src Src) (Out, error) { 29 | out, err := json.Marshal(AnyNoEscUnsafe(src)) 30 | return ToText[Out](out), err 31 | } 32 | 33 | /* 34 | Same as `json.MarshalIndent`, but uses the default indentation controlled by the 35 | `Indent` variable. Also sometimes marginally more efficient. Avoids spurious 36 | heap escape of the input. 37 | */ 38 | func JsonEncodeIndentCatch[Out Text, Src any](src Src) (Out, error) { 39 | out, err := json.MarshalIndent(AnyNoEscUnsafe(src), ``, Indent) 40 | return ToText[Out](out), err 41 | } 42 | 43 | // Shortcut for `JsonEncode` for `[]byte`. 44 | func JsonBytes[A any](src A) []byte { return JsonEncode[[]byte](src) } 45 | 46 | // Shortcut for `JsonEncodeIndent` for `[]byte`. 47 | func JsonBytesIndent[A any](src A) []byte { return JsonEncodeIndent[[]byte](src) } 48 | 49 | // Shortcut for `JsonEncodeCatch` for `[]byte`. 50 | func JsonBytesCatch[A any](src A) ([]byte, error) { return JsonEncodeCatch[[]byte](src) } 51 | 52 | // Shortcut for `JsonEncodeIndentCatch` for `[]byte`. 53 | func JsonBytesIndentCatch[A any](src A) ([]byte, error) { return JsonEncodeIndentCatch[[]byte](src) } 54 | 55 | /* 56 | Shortcut for implementing JSON encoding of `Nullable` types. 57 | Mostly for internal use. 58 | */ 59 | func JsonBytesNullCatch[A any, B NullableValGetter[A]](val B) ([]byte, error) { 60 | if val.IsNull() { 61 | return ToBytes(`null`), nil 62 | } 63 | return JsonBytesCatch(val.Get()) 64 | } 65 | 66 | // Shortcut for `JsonEncode` for `string`. 67 | func JsonString[A any](src A) string { return JsonEncode[string](src) } 68 | 69 | // Shortcut for `JsonEncodeIndent` for `string`. 70 | func JsonStringIndent[A any](src A) string { return JsonEncodeIndent[string](src) } 71 | 72 | // Shortcut for `JsonEncodeCatch` for `string`. 73 | func JsonStringCatch[A any](src A) (string, error) { return JsonEncodeCatch[string](src) } 74 | 75 | // Shortcut for `JsonEncodeIndentCatch` for `string`. 76 | func JsonStringIndentCatch[A any](src A) (string, error) { return JsonEncodeIndentCatch[string](src) } 77 | 78 | /* 79 | Shortcut for parsing arbitrary text into the given output, panicking on errors. 80 | If the output pointer is nil, does nothing. 81 | */ 82 | func JsonDecode[Out any, Src Text](src Src, out *Out) { Try(JsonDecodeCatch(src, out)) } 83 | 84 | /* 85 | Shortcut for parsing the given text into the given output, ignoring errors. 86 | If the output pointer is nil, does nothing. 87 | */ 88 | func JsonDecodeOpt[Out any, Src Text](src Src, out *Out) { Nop1(JsonDecodeCatch(src, out)) } 89 | 90 | /* 91 | Shortcut for parsing the given text into a value of the given type, panicking 92 | on errors. 93 | */ 94 | func JsonDecodeTo[Out any, Src Text](src Src) (out Out) { 95 | Try(JsonDecodeCatch(src, &out)) 96 | return 97 | } 98 | 99 | /* 100 | Shortcut for parsing the given text into the given output, ignoring errors. 101 | If the output pointer is nil, does nothing. 102 | */ 103 | func JsonDecodeOptTo[Out any, Src Text](src Src) (out Out) { 104 | Nop1(JsonDecodeCatch(src, &out)) 105 | return 106 | } 107 | 108 | /* 109 | Parses the given text into the given output. Similar to `json.Unmarshal`, but 110 | avoids the overhead of byte-string conversion and spurious escapes. If the 111 | output pointer is nil, does nothing. 112 | */ 113 | func JsonDecodeCatch[Out any, Src Text](src Src, out *Out) error { 114 | if out != nil { 115 | return json.Unmarshal(ToBytes(src), AnyNoEscUnsafe(out)) 116 | } 117 | return nil 118 | } 119 | 120 | /* 121 | Shortcut for decoding the content of the given file into a value of the given 122 | type. Panics on error. 123 | */ 124 | func JsonDecodeFileTo[A any](path string) (out A) { 125 | JsonDecodeFile(path, &out) 126 | return 127 | } 128 | 129 | /* 130 | Shortcut for decoding the content of the given file into a pointer of the given 131 | type. Panics on error. 132 | */ 133 | func JsonDecodeFile[A any](path string, out *A) { 134 | if out != nil { 135 | JsonDecodeClose(Try1(os.Open(path)), NoEscUnsafe(out)) 136 | } 137 | } 138 | 139 | /* 140 | Shortcut for writing the JSON encoding of the given value to a file at the given 141 | path. Intermediary directories are created automatically. Any existing file is 142 | truncated. 143 | */ 144 | func JsonEncodeFile[A any](path string, src A) { 145 | MkdirAll(filepath.Dir(path)) 146 | 147 | file := Try1(os.Create(path)) 148 | defer file.Close() 149 | 150 | Try(json.NewEncoder(file).Encode(src)) 151 | Try(file.Close()) 152 | } 153 | 154 | /* 155 | Uses `json.Decoder` to decode one JSON entry/line from the reader, writing to 156 | the given output. Always closes the reader. Panics on errors. 157 | */ 158 | func JsonDecodeClose[A any](src io.ReadCloser, out *A) { 159 | defer Close(src) 160 | if out != nil { 161 | Try(json.NewDecoder(NoEscUnsafe(src)).Decode(AnyNoEscUnsafe(out))) 162 | } 163 | } 164 | 165 | // True if the input is "null" or blank. Ignores whitespace. 166 | func IsJsonEmpty[A Text](val A) bool { 167 | src := strings.TrimSpace(ToString(val)) 168 | return src == `` || src == `null` 169 | } 170 | -------------------------------------------------------------------------------- /json_test.go: -------------------------------------------------------------------------------- 1 | package gg_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/mitranim/gg" 8 | "github.com/mitranim/gg/gtest" 9 | ) 10 | 11 | func Benchmark_json_Marshal(b *testing.B) { 12 | var val SomeModel 13 | 14 | for ind := 0; ind < b.N; ind++ { 15 | gg.Nop1(gg.Try1(json.Marshal(val))) 16 | } 17 | } 18 | 19 | func BenchmarkJsonBytes(b *testing.B) { 20 | var val SomeModel 21 | 22 | for ind := 0; ind < b.N; ind++ { 23 | gg.Nop1(gg.JsonBytes(val)) 24 | } 25 | } 26 | 27 | func Benchmark_json_Marshal_string(b *testing.B) { 28 | var val SomeModel 29 | 30 | for ind := 0; ind < b.N; ind++ { 31 | gg.Nop1(string(gg.Try1(json.Marshal(val)))) 32 | } 33 | } 34 | 35 | func BenchmarkJsonString(b *testing.B) { 36 | var val SomeModel 37 | 38 | for ind := 0; ind < b.N; ind++ { 39 | gg.Nop1(gg.JsonString(val)) 40 | } 41 | } 42 | 43 | func Benchmark_json_Unmarshal(b *testing.B) { 44 | var val int 45 | 46 | for ind := 0; ind < b.N; ind++ { 47 | gg.Try(json.Unmarshal(gg.ToBytes(`123`), &val)) 48 | } 49 | } 50 | 51 | func BenchmarkJsonDecodeTo(b *testing.B) { 52 | for ind := 0; ind < b.N; ind++ { 53 | gg.Nop1(gg.JsonDecodeTo[int](`123`)) 54 | } 55 | } 56 | 57 | func BenchmarkJsonDecode(b *testing.B) { 58 | var val int 59 | 60 | for ind := 0; ind < b.N; ind++ { 61 | gg.JsonDecode(`123`, &val) 62 | } 63 | } 64 | 65 | func TestJsonDecodeTo(t *testing.T) { 66 | gtest.Catch(t) 67 | 68 | gtest.Eq( 69 | gg.JsonDecodeTo[SomeModel](`{"id":10}`), 70 | SomeModel{Id: 10}, 71 | ) 72 | 73 | gtest.Eq( 74 | gg.JsonDecodeTo[SomeModel]([]byte(`{"id":10}`)), 75 | SomeModel{Id: 10}, 76 | ) 77 | } 78 | -------------------------------------------------------------------------------- /lazy.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import "sync" 4 | 5 | /* 6 | Creates `Lazy` with the given function. See the type's description for details. 7 | Similar to `sync.OnceValue` added in Go 1.21. 8 | */ 9 | func NewLazy[A any](fun func() A) *Lazy[A] { return &Lazy[A]{fun: fun} } 10 | 11 | /* 12 | Similar to `sync.Once`, but specialized for creating and caching one value, 13 | instead of relying on nullary functions and side effects. Created via `NewLazy`. 14 | Calling `.Get` on the resulting object will idempotently call the given function 15 | and cache the result, and discard the function. Uses `sync.Once` internally for 16 | synchronization. 17 | 18 | Go 1.21 introduced `sync.OnceValue`, arguably making this redundant. 19 | */ 20 | type Lazy[A any] struct { 21 | val A 22 | fun func() A 23 | once sync.Once 24 | } 25 | 26 | /* 27 | Returns the inner value after idempotently creating it. 28 | This method is synchronized and safe for concurrent use. 29 | */ 30 | func (self *Lazy[A]) Get() A { 31 | self.once.Do(self.init) 32 | return self.val 33 | } 34 | 35 | func (self *Lazy[_]) init() { 36 | fun := self.fun 37 | self.fun = nil 38 | if fun != nil { 39 | self.val = fun() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lazy_coll.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import "encoding/json" 4 | 5 | /* 6 | Same as `CollOf` but for `LazyColl`. Note that while the return type is a 7 | non-pointer for easy assignment, callers must always access `LazyColl` by 8 | pointer to avoid redundant reindexing. 9 | */ 10 | func LazyCollOf[Key comparable, Val Pker[Key]](src ...Val) LazyColl[Key, Val] { 11 | var tar LazyColl[Key, Val] 12 | tar.Reset(src...) 13 | return tar 14 | } 15 | 16 | /* 17 | Same as `CollFrom` but for `LazyColl`. Note that while the return type is a 18 | non-pointer for easy assignment, callers must always access `LazyColl` by 19 | pointer to avoid redundant reindexing. 20 | */ 21 | func LazyCollFrom[Key comparable, Val Pker[Key], Slice ~[]Val](src ...Slice) LazyColl[Key, Val] { 22 | var tar LazyColl[Key, Val] 23 | 24 | switch len(src) { 25 | case 1: 26 | tar.Reset(src[0]...) 27 | default: 28 | for _, src := range src { 29 | tar.Add(src...) 30 | } 31 | } 32 | 33 | return tar 34 | } 35 | 36 | /* 37 | Short for "lazy collection". Variant of `Coll` where the index is built lazily 38 | rather than immediately. This is not the default behavior in `Coll` because it 39 | requires various access methods such as `.Has` and `.Get` to be defined on the 40 | pointer type rather than value type, and more importantly, it's more error 41 | prone: the caller is responsible for making sure that the collection is always 42 | accessed by pointer, never by value, to avoid redundant reindexing. 43 | */ 44 | type LazyColl[Key comparable, Val Pker[Key]] Coll[Key, Val] 45 | 46 | // Same as `Coll.Len`. 47 | func (self LazyColl[_, _]) Len() int { return self.coll().Len() } 48 | 49 | // Same as `Coll.IsEmpty`. 50 | func (self LazyColl[_, _]) IsEmpty() bool { return self.coll().IsEmpty() } 51 | 52 | // Same as `Coll.IsNotEmpty`. 53 | func (self LazyColl[_, _]) IsNotEmpty() bool { return self.coll().IsNotEmpty() } 54 | 55 | // Same as `Coll.Has`. Lazily rebuilds the index if necessary. 56 | func (self *LazyColl[Key, _]) Has(key Key) bool { 57 | self.ReindexOpt() 58 | return self.coll().Has(key) 59 | } 60 | 61 | // Same as `Coll.Get`. Lazily rebuilds the index if necessary. 62 | func (self *LazyColl[Key, Val]) Get(key Key) Val { 63 | self.ReindexOpt() 64 | return self.coll().Get(key) 65 | } 66 | 67 | // Same as `Coll.GetReq`. Lazily rebuilds the index if necessary. 68 | func (self *LazyColl[Key, Val]) GetReq(key Key) Val { 69 | self.ReindexOpt() 70 | return self.coll().GetReq(key) 71 | } 72 | 73 | // Same as `Coll.Got`. Lazily rebuilds the index if necessary. 74 | func (self *LazyColl[Key, Val]) Got(key Key) (Val, bool) { 75 | self.ReindexOpt() 76 | return self.coll().Got(key) 77 | } 78 | 79 | // Same as `Coll.Ptr`. Lazily rebuilds the index if necessary. 80 | func (self *LazyColl[Key, Val]) Ptr(key Key) *Val { 81 | self.ReindexOpt() 82 | return self.coll().Ptr(key) 83 | } 84 | 85 | // Same as `Coll.PtrReq`. Lazily rebuilds the index if necessary. 86 | func (self *LazyColl[Key, Val]) PtrReq(key Key) *Val { 87 | self.ReindexOpt() 88 | return self.coll().PtrReq(key) 89 | } 90 | 91 | // Similar to `Coll.Add`, but does not add new entries to the index. 92 | func (self *LazyColl[Key, Val]) Add(src ...Val) *LazyColl[Key, Val] { 93 | for _, val := range src { 94 | key := ValidPk[Key](val) 95 | ind, ok := self.Index[key] 96 | if ok { 97 | self.Slice[ind] = val 98 | continue 99 | } 100 | Append(&self.Slice, val) 101 | } 102 | return self 103 | } 104 | 105 | // Same as `Coll.AddUniq`. Lazily rebuilds the index. 106 | func (self *LazyColl[Key, Val]) AddUniq(src ...Val) *LazyColl[Key, Val] { 107 | self.ReindexOpt() 108 | self.coll().AddUniq(src...) 109 | return self 110 | } 111 | 112 | // Same as `Coll.Reset` but deletes the index instead of rebuilding it. 113 | func (self *LazyColl[Key, Val]) Reset(src ...Val) *LazyColl[Key, Val] { 114 | self.Index = nil 115 | self.Slice = src 116 | return self 117 | } 118 | 119 | // Same as `Coll.Clear`. 120 | func (self *LazyColl[Key, Val]) Clear() *LazyColl[Key, Val] { 121 | self.coll().Clear() 122 | return self 123 | } 124 | 125 | // Same as `Coll.Reindex`. 126 | func (self *LazyColl[Key, Val]) Reindex() *LazyColl[Key, Val] { 127 | self.coll().Reindex() 128 | return self 129 | } 130 | 131 | /* 132 | Rebuilds the index if the length of inner slice and index doesn't match. 133 | This is used internally by all "read" methods on this type. 134 | */ 135 | func (self *LazyColl[Key, _]) ReindexOpt() { 136 | if len(self.Slice) != len(self.Index) { 137 | self.Reindex() 138 | } 139 | } 140 | 141 | // Same as `Coll.Swap` but deletes the index instead of modifying it. 142 | func (self *LazyColl[Key, _]) Swap(ind0, ind1 int) { 143 | self.Index = nil 144 | self.coll().Swap(ind0, ind1) 145 | } 146 | 147 | /* 148 | Implement `json.Marshaler`. Same as `Coll.MarshalJSON`. Encodes the inner slice, 149 | ignoring the index. 150 | */ 151 | func (self LazyColl[_, _]) MarshalJSON() ([]byte, error) { 152 | return self.coll().MarshalJSON() 153 | } 154 | 155 | /* 156 | Implement `json.Unmarshaler`. Similar to `Coll.UnmarshalJSON`, but after 157 | decoding the input into the inner slice, deletes the index instead of 158 | rebuilding it. 159 | */ 160 | func (self *LazyColl[_, _]) UnmarshalJSON(src []byte) error { 161 | self.Index = nil 162 | return json.Unmarshal(src, &self.Slice) 163 | } 164 | 165 | // Converts to equivalent `Coll`. Lazily rebuilds the index if necessary. 166 | func (self *LazyColl[Key, Val]) Coll() *Coll[Key, Val] { 167 | self.ReindexOpt() 168 | return (*Coll[Key, Val])(self) 169 | } 170 | 171 | /* 172 | Free cast to equivalent `Coll`. Private because careless use produces invalid 173 | behavior. Incomplete index in `LazyColl` violates assumptions made by `Coll`. 174 | */ 175 | func (self *LazyColl[Key, Val]) coll() *Coll[Key, Val] { 176 | return (*Coll[Key, Val])(self) 177 | } 178 | -------------------------------------------------------------------------------- /lazy_coll_test.go: -------------------------------------------------------------------------------- 1 | package gg_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mitranim/gg" 7 | "github.com/mitranim/gg/gtest" 8 | ) 9 | 10 | func TestLazyColl(t *testing.T) { 11 | defer gtest.Catch(t) 12 | testColl[*gg.LazyColl[SomeKey, SomeModel]]() 13 | } 14 | 15 | func TestLazyCollOf(t *testing.T) { 16 | defer gtest.Catch(t) 17 | 18 | gtest.Zero(gg.LazyCollOf[SomeKey, SomeModel]()) 19 | 20 | testLazyCollMake(func(src ...SomeModel) SomeLazyColl { 21 | return gg.LazyCollOf[SomeKey, SomeModel](src...) 22 | }) 23 | } 24 | 25 | func testLazyCollMake[Coll AnyColl](fun func(...SomeModel) Coll) { 26 | test := func(slice []SomeModel, index map[SomeKey]int) { 27 | tar := fun(slice...) 28 | testCollEqual(tar, SomeColl{Slice: slice, Index: index}) 29 | gtest.Is(getCollSlice(tar), slice) 30 | } 31 | 32 | test( 33 | []SomeModel{SomeModel{10, `one`}}, 34 | nil, 35 | ) 36 | 37 | test( 38 | []SomeModel{ 39 | SomeModel{10, `one`}, 40 | SomeModel{20, `two`}, 41 | }, 42 | nil, 43 | ) 44 | } 45 | 46 | func TestLazyCollFrom(t *testing.T) { 47 | defer gtest.Catch(t) 48 | 49 | gtest.Zero(gg.LazyCollFrom[SomeKey, SomeModel, []SomeModel]()) 50 | 51 | testLazyCollMake(func(src ...SomeModel) SomeLazyColl { 52 | return gg.LazyCollFrom[SomeKey, SomeModel](src) 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /lazy_initer.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import "sync" 4 | 5 | /* 6 | Shortcut for type inference. The following is equivalent: 7 | 8 | NewLazyIniter[Val]() 9 | new(LazyIniter[Val, Ptr]) 10 | */ 11 | func NewLazyIniter[Val any, Ptr IniterPtr[Val]]() *LazyIniter[Val, Ptr] { 12 | return new(LazyIniter[Val, Ptr]) 13 | } 14 | 15 | /* 16 | Encapsulates a lazily-initializable value. The first call to `.Get` or `.Ptr` 17 | initializes the underlying value by calling its `.Init` method. Initialization 18 | is performed exactly once. Access is synchronized. All methods are 19 | concurrency-safe. Designed to be embeddable. A zero value is ready to use. When 20 | using this as a struct field, you don't need to explicitly initialize the 21 | field. Contains a mutex and must not be copied. 22 | */ 23 | type LazyIniter[Val any, Ptr IniterPtr[Val]] struct { 24 | val Opt[Val] 25 | lock sync.RWMutex 26 | } 27 | 28 | // Returns the underlying value, lazily initializing it on the first call. 29 | func (self *LazyIniter[Val, _]) Get() Val { 30 | out, ok := self.got() 31 | if ok { 32 | return out 33 | } 34 | return self.get() 35 | } 36 | 37 | /* 38 | Clears the underlying value. After this call, the next call to `LazyIniter.Get` 39 | or `LazyIniter.Ptr` will reinitialize by invoking the `.Init` method of the 40 | underlying value. 41 | */ 42 | func (self *LazyIniter[_, _]) Clear() { 43 | defer Lock(&self.lock).Unlock() 44 | self.val.Clear() 45 | } 46 | 47 | /* 48 | Resets the underlying value to the given input. After this call, the underlying 49 | value is considered to be initialized. Further calls to `LazyIniter.Get` or 50 | `LazyIniter.Ptr` will NOT reinitialize until `.Clear` is called. 51 | */ 52 | func (self *LazyIniter[Val, _]) Reset(src Val) { 53 | defer Lock(&self.lock).Unlock() 54 | self.val.Set(src) 55 | } 56 | 57 | func (self *LazyIniter[Val, _]) got() (_ Val, _ bool) { 58 | defer Lock(self.lock.RLocker()).Unlock() 59 | if self.val.IsNotNull() { 60 | return self.val.Val, true 61 | } 62 | return 63 | } 64 | 65 | func (self *LazyIniter[Val, Ptr]) get() Val { 66 | defer Lock(&self.lock).Unlock() 67 | 68 | if self.val.IsNull() { 69 | Ptr(&self.val.Val).Init() 70 | self.val.Ok = true 71 | } 72 | 73 | return self.val.Val 74 | } 75 | -------------------------------------------------------------------------------- /lazy_initer_test.go: -------------------------------------------------------------------------------- 1 | package gg_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mitranim/gg" 7 | "github.com/mitranim/gg/gtest" 8 | ) 9 | 10 | type IniterStr string 11 | 12 | func (self *IniterStr) Init() { 13 | if *self != `` { 14 | panic(`redundant init`) 15 | } 16 | *self = `inited` 17 | } 18 | 19 | // Incomplete: doesn't verify concurrency safety. 20 | func TestLazyIniter(t *testing.T) { 21 | defer gtest.Catch(t) 22 | 23 | var tar gg.LazyIniter[IniterStr, *IniterStr] 24 | 25 | test := func(exp gg.Opt[IniterStr]) { 26 | // Note: `gg.CastUnsafe[A](tar)` would have been simpler, but technically 27 | // involves passing `tar` by value, which is invalid due to inner mutex. 28 | // Wouldn't actually matter. 29 | gtest.Eq(*gg.CastUnsafe[*gg.Opt[IniterStr]](&tar), exp) 30 | } 31 | 32 | test(gg.Zero[gg.Opt[IniterStr]]()) 33 | 34 | gtest.Eq(tar.Get(), `inited`) 35 | gtest.Eq(tar.Get(), tar.Get()) 36 | test(gg.OptVal(IniterStr(`inited`))) 37 | 38 | tar.Clear() 39 | test(gg.Zero[gg.Opt[IniterStr]]()) 40 | 41 | gtest.Eq(tar.Get(), `inited`) 42 | gtest.Eq(tar.Get(), tar.Get()) 43 | test(gg.OptVal(IniterStr(`inited`))) 44 | 45 | tar.Reset(`inited_1`) 46 | gtest.Eq(tar.Get(), `inited_1`) 47 | gtest.Eq(tar.Get(), tar.Get()) 48 | test(gg.OptVal(IniterStr(`inited_1`))) 49 | } 50 | -------------------------------------------------------------------------------- /lazy_test.go: -------------------------------------------------------------------------------- 1 | package gg_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mitranim/gg" 7 | "github.com/mitranim/gg/gtest" 8 | ) 9 | 10 | // TODO: test with concurrency. 11 | func TestLazy(t *testing.T) { 12 | defer gtest.Catch(t) 13 | 14 | var count int 15 | 16 | once := gg.NewLazy(func() int { 17 | count++ 18 | if count > 1 { 19 | panic(gg.Errf(`excessive count %v`, count)) 20 | } 21 | return count 22 | }) 23 | 24 | gtest.Eq(*gg.CastUnsafe[*int](once), 0) 25 | gtest.Eq(once.Get(), 1) 26 | gtest.Eq(once.Get(), once.Get()) 27 | gtest.Eq(*gg.CastUnsafe[*int](once), 1) 28 | } 29 | 30 | func BenchmarkLazy(b *testing.B) { 31 | once := gg.NewLazy(gg.Cwd) 32 | 33 | for ind := 0; ind < b.N; ind++ { 34 | gg.Nop1(once.Get()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | ) 8 | 9 | /* 10 | Shortcut for creating `LogTime` with the current time and with a message 11 | generated from the inputs via `Str`. 12 | */ 13 | func LogTimeNow(msg ...any) LogTime { 14 | return LogTime{Start: time.Now(), Msg: Str(msg...)} 15 | } 16 | 17 | /* 18 | Shortcut for logging execution timing to stderr. Usage examples: 19 | 20 | defer gg.LogTimeNow(`some_activity`).LogStart().LogEnd() 21 | // perform some activity 22 | 23 | defer gg.LogTimeNow(`some_activity`).LogEnd() 24 | // perform some activity 25 | 26 | timer := gg.LogTimeNow(`some_activity`).LogStart() 27 | // perform some activity 28 | timer.LogEnd() 29 | 30 | timer := gg.LogTimeNow(`some_activity`) 31 | // perform some activity 32 | timer.LogEnd() 33 | */ 34 | type LogTime struct { 35 | Start time.Time 36 | Msg string 37 | } 38 | 39 | /* 40 | Logs the beginning of the activity denoted by `.Msg`: 41 | 42 | [some_activity] starting 43 | 44 | Note that when logging start AND end, the time spent in `.LogStart` is 45 | unavoidably included into the difference. If you want precise timing, 46 | avoid logging the start. 47 | */ 48 | func (self LogTime) LogStart() LogTime { 49 | fmt.Fprintf(os.Stderr, "[%v] starting\n", self.Msg) 50 | return self 51 | } 52 | 53 | /* 54 | Prints the end of the activity denoted by `.Msg`, with time elapsed since the 55 | beginning: 56 | 57 | [some_activity] done in 58 | 59 | If deferred, this will detect the current panic, if any, and print the following 60 | instead: 61 | 62 | [some_activity] failed in 63 | */ 64 | func (self LogTime) LogEnd() LogTime { 65 | since := time.Since(self.Start) 66 | err := AnyErrTracedAt(recover(), 1) 67 | 68 | if err != nil { 69 | fmt.Fprintf(os.Stderr, "[%v] failed in %v\n", self.Msg, since) 70 | panic(err) 71 | } 72 | 73 | fmt.Fprintf(os.Stderr, "[%v] done in %v\n", self.Msg, since) 74 | return self 75 | } 76 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package gg_test 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/mitranim/gg" 7 | ) 8 | 9 | func init() { gg.TraceBaseDir = gg.Cwd() } 10 | 11 | var void struct{} 12 | 13 | // Adapted from `reflect.ValueOf`. 14 | func esc(val any) any { 15 | if trap.false { 16 | trap.val = val 17 | } 18 | return val 19 | } 20 | 21 | var trap struct { 22 | false bool 23 | val any 24 | } 25 | 26 | type IntSet = gg.Set[int] 27 | 28 | type IntMap = map[int]int 29 | 30 | type SomeKey int64 31 | 32 | type SomeModel struct { 33 | Id SomeKey `json:"id"` 34 | Name string `json:"name"` 35 | } 36 | 37 | func (self SomeModel) Pk() SomeKey { return self.Id } 38 | 39 | type StructDirect struct { 40 | Public0 int 41 | Public1 string 42 | private *string 43 | } 44 | 45 | //nolint:unused 46 | type StructIndirect struct { 47 | Public0 int 48 | Public1 *string 49 | private *string 50 | } 51 | 52 | type Outer struct { 53 | OuterId int 54 | OuterName string 55 | Embed 56 | Inner *Inner 57 | } 58 | 59 | type Embed struct { 60 | EmbedId int 61 | EmbedName string 62 | } 63 | 64 | type Inner struct { 65 | InnerId *int 66 | InnerName *string 67 | } 68 | 69 | type SomeJsonDbMapper struct { 70 | SomeName string `json:"someName" db:"some_name"` 71 | SomeValue string `json:"someValue" db:"some_value"` 72 | SomeJson string `json:"someJson"` 73 | SomeDb string `db:"some_db"` 74 | } 75 | 76 | type SomeColl = gg.Coll[SomeKey, SomeModel] 77 | 78 | type SomeLazyColl = gg.LazyColl[SomeKey, SomeModel] 79 | 80 | type IsZeroAlwaysTrue string 81 | 82 | func (IsZeroAlwaysTrue) IsZero() bool { return true } 83 | 84 | type IsZeroAlwaysFalse string 85 | 86 | func (IsZeroAlwaysFalse) IsZero() bool { return false } 87 | 88 | type FatStruct struct { 89 | _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, Id int 90 | _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, Name string 91 | } 92 | 93 | type FatStructNonComparable struct { 94 | FatStruct 95 | _ []byte 96 | } 97 | 98 | func ComparerOf[A gg.LesserPrim](val A) Comparer[A] { return Comparer[A]{val} } 99 | 100 | type Comparer[A gg.LesserPrim] [1]A 101 | 102 | func (self Comparer[A]) Less(val Comparer[A]) bool { return self[0] < val[0] } 103 | 104 | func (self Comparer[A]) Get() A { return self[0] } 105 | 106 | func ToPair[A gg.Num](val A) (A, A) { return val - 1, val + 1 } 107 | 108 | func True1[A any](A) bool { return true } 109 | 110 | func False1[A any](A) bool { return false } 111 | 112 | func Id1True[A any](val A) (A, bool) { return val, true } 113 | 114 | func Id1False[A any](val A) (A, bool) { return val, false } 115 | 116 | type ParserStr string 117 | 118 | func (self *ParserStr) Parse(val string) error { 119 | *self = ParserStr(val) 120 | return nil 121 | } 122 | 123 | type UnmarshalerBytes []byte 124 | 125 | func (self *UnmarshalerBytes) UnmarshalText(val []byte) error { 126 | *self = val 127 | return nil 128 | } 129 | 130 | // Implements `error` on the pointer type, not on the value type. 131 | type PtrErrStr string 132 | 133 | func (self *PtrErrStr) Error() string { return gg.PtrGet((*string)(self)) } 134 | 135 | type StrsParser []string 136 | 137 | func (self *StrsParser) Parse(src string) error { 138 | gg.Append(self, src) 139 | return nil 140 | } 141 | 142 | type IntsValue []int 143 | 144 | func (self *IntsValue) Set(src string) error { 145 | return gg.ParseCatch(src, gg.AppendPtrZero(self)) 146 | } 147 | 148 | /* 149 | Defined as a struct to verify that the flag parser supports slices of arbitrary 150 | types implementing `flag.Value`, even if they're not typedefs of text types, 151 | and not something normally compatible with `gg.Parse`. 152 | */ 153 | type IntValue struct{ Val int } 154 | 155 | func (self *IntValue) Set(src string) error { 156 | return gg.ParseCatch(src, &self.Val) 157 | } 158 | 159 | func intStrPair(src int) []string { 160 | return []string{strconv.Itoa(src - 1), strconv.Itoa(src + 1)} 161 | } 162 | 163 | func intPair(src int) []int { 164 | return []int{src - 1, src + 1} 165 | } 166 | 167 | type Cyclic struct { 168 | Id int 169 | Cyclic *Cyclic 170 | } 171 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | MAKEFLAGS := --silent --always-make 2 | MAKE_CONC := $(MAKE) -j 128 CONF=true clear=$(or $(clear),false) 3 | GO_FLAGS := -tags=$(tags) -mod=mod 4 | VERB := $(if $(filter true,$(verb)),-v,) 5 | FAIL := $(if $(filter false,$(fail)),,-failfast) 6 | SHORT := $(if $(filter true,$(short)),-short,) 7 | CLEAR := $(if $(filter false,$(clear)),,-c) 8 | PROF := $(if $(filter true,$(prof)), -cpuprofile=cpu.prof -memprofile=mem.prof,) 9 | TEST_FLAGS := $(GO_FLAGS) -count=1 $(VERB) $(FAIL) $(SHORT) $(PROF) 10 | TEST := test $(TEST_FLAGS) -timeout=1s -run=$(run) 11 | PKG := ./$(or $(pkg),...) 12 | BENCH := test $(TEST_FLAGS) -run=- -bench=$(or $(run),.) -benchmem -benchtime=256ms 13 | GOW_HOTKEYS := -r=$(if $(filter 0,$(MAKELEVEL)),true,false) 14 | GOW := gow $(CLEAR) $(GOW_HOTKEYS) $(VERB) -e=go,mod,pgsql 15 | WATCH := watchexec -r $(CLEAR) -d=0 -n 16 | DOC_HOST := localhost:58214 17 | OK = echo [$@] ok 18 | TAG := $(or $(and $(ver),v0.1.$(ver)),$(tag)) 19 | 20 | default: test_w 21 | 22 | watch: 23 | $(MAKE_CONC) test_w lint_w 24 | 25 | test_w: 26 | $(GOW) $(TEST) $(PKG) 27 | 28 | test: 29 | go $(TEST) $(PKG) 30 | 31 | bench_w: 32 | $(GOW) $(BENCH) $(PKG) 33 | 34 | bench: 35 | go $(BENCH) $(PKG) 36 | 37 | lint_w: 38 | $(WATCH) -- $(MAKE) lint 39 | 40 | lint: 41 | golangci-lint run 42 | $(OK) 43 | 44 | vet_w: 45 | $(WATCH) -- $(MAKE) vet 46 | 47 | vet: 48 | go vet $(GO_FLAGS) $(PKG) 49 | $(OK) 50 | 51 | prof: 52 | $(MAKE_CONC) prof_cpu prof_mem 53 | 54 | prof_cpu: 55 | go tool pprof -web cpu.prof 56 | 57 | prof_mem: 58 | go tool pprof -web mem.prof 59 | 60 | # Requires `pkgsite`: 61 | # go install golang.org/x/pkgsite/cmd/pkgsite@latest 62 | doc: 63 | $(or $(shell which open),echo) http://$(DOC_HOST)/github.com/mitranim/gg 64 | pkgsite $(if $(GOREPO),-gorepo=$(GOREPO)) -http=$(DOC_HOST) 65 | 66 | prep: 67 | $(MAKE_CONC) test lint 68 | 69 | # Examples: 70 | # `make pub ver=1`. 71 | # `make pub tag=v0.0.1`. 72 | pub: prep 73 | ifeq ($(TAG),) 74 | $(error missing tag) 75 | endif 76 | git pull --ff-only 77 | git show-ref --tags --quiet "$(TAG)" || git tag "$(TAG)" 78 | git push origin $$(git symbolic-ref --short HEAD) "$(TAG)" 79 | 80 | # Assumes MacOS and Homebrew. 81 | deps: 82 | go install github.com/mitranim/gow@latest 83 | brew install -q watchexec golangci-lint 84 | -------------------------------------------------------------------------------- /map.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | /* 4 | Non-idempotent version of `MapInit`. If the target pointer is nil, does nothing 5 | and returns nil. If the target pointer is non-nil, allocates the map via 6 | `make`, stores it at the target pointer, and returns the resulting non-nil 7 | map. 8 | */ 9 | func MapMake[Map ~map[Key]Val, Key comparable, Val any](ptr *Map) Map { 10 | if ptr == nil { 11 | return nil 12 | } 13 | val := make(map[Key]Val) 14 | *ptr = val 15 | return val 16 | } 17 | 18 | /* 19 | Shortcut for converting an arbitrary map to `Dict`. Workaround for the 20 | limitations of type inference in Go generics. This is a free cast with no 21 | reallocation. 22 | */ 23 | func ToDict[Src ~map[Key]Val, Key comparable, Val any](val Src) Dict[Key, Val] { 24 | return Dict[Key, Val](val) 25 | } 26 | 27 | /* 28 | Typedef of an arbitrary map with various methods that duplicate global map 29 | functions. Useful as a shortcut for creating bound methods that can be passed 30 | to higher-order functions. 31 | */ 32 | type Dict[Key comparable, Val any] map[Key]Val 33 | 34 | // Same as `len(self)`. 35 | func (self Dict[_, _]) Len() int { return len(self) } 36 | 37 | // Same as `len(self) <= 0`. Inverse of `.IsNotEmpty`. 38 | func (self Dict[_, _]) IsEmpty() bool { return len(self) <= 0 } 39 | 40 | // Same as `len(self) > 0`. Inverse of `.IsEmpty`. 41 | func (self Dict[_, _]) IsNotEmpty() bool { return len(self) > 0 } 42 | 43 | /* 44 | Idempotent map initialization. If the target pointer is nil, does nothing and 45 | returns nil. If the map at the target pointer is non-nil, does nothing and 46 | returns that map. Otherwise allocates the map via `make`, stores it at the 47 | target pointer, and returns the resulting non-nil map. 48 | */ 49 | func MapInit[Map ~map[Key]Val, Key comparable, Val any](ptr *Map) Map { 50 | if ptr == nil { 51 | return nil 52 | } 53 | val := *ptr 54 | if val == nil { 55 | val = make(map[Key]Val) 56 | *ptr = val 57 | } 58 | return val 59 | } 60 | 61 | // Self as global `MapInit`. 62 | func (self *Dict[Key, Val]) Init() Dict[Key, Val] { return MapInit(self) } 63 | 64 | /* 65 | Copies the given map. If the input is nil, the output is nil. Otherwise the 66 | output is a shallow copy. 67 | */ 68 | func MapClone[Map ~map[Key]Val, Key comparable, Val any](src Map) Map { 69 | if src == nil { 70 | return nil 71 | } 72 | 73 | out := make(Map, len(src)) 74 | for key, val := range src { 75 | out[key] = val 76 | } 77 | return out 78 | } 79 | 80 | // Self as global `MapClone`. 81 | func (self Dict[Key, Val]) Clone() Dict[Key, Val] { return MapClone(self) } 82 | 83 | // Returns the maps's keys as a slice. Order is random. 84 | func MapKeys[Key comparable, Val any](src map[Key]Val) []Key { 85 | if src == nil { 86 | return nil 87 | } 88 | 89 | out := make([]Key, 0, len(src)) 90 | for key := range src { 91 | out = append(out, key) 92 | } 93 | return out 94 | } 95 | 96 | // Self as global `MapKeys`. 97 | func (self Dict[Key, _]) Keys() []Key { return MapKeys(self) } 98 | 99 | // Returns the maps's values as a slice. Order is random. 100 | func MapVals[Key comparable, Val any](src map[Key]Val) []Val { 101 | if src == nil { 102 | return nil 103 | } 104 | 105 | out := make([]Val, 0, len(src)) 106 | for _, val := range src { 107 | out = append(out, val) 108 | } 109 | return out 110 | } 111 | 112 | // Self as global `MapVals`. 113 | func (self Dict[_, Val]) Vals() []Val { return MapVals(self) } 114 | 115 | // Same as `_, ok := tar[key]`, expressed as a generic function. 116 | func MapHas[Map ~map[Key]Val, Key comparable, Val any](tar Map, key Key) bool { 117 | _, ok := tar[key] 118 | return ok 119 | } 120 | 121 | // Self as global `MapHas`. 122 | func (self Dict[Key, _]) Has(key Key) bool { return MapHas(self, key) } 123 | 124 | // Same as `val, ok := tar[key]`, expressed as a generic function. 125 | func MapGot[Map ~map[Key]Val, Key comparable, Val any](tar Map, key Key) (Val, bool) { 126 | val, ok := tar[key] 127 | return val, ok 128 | } 129 | 130 | // Self as global `MapGot`. 131 | func (self Dict[Key, Val]) Got(key Key) (Val, bool) { return MapGot(self, key) } 132 | 133 | // Same as `tar[key]`, expressed as a generic function. 134 | func MapGet[Map ~map[Key]Val, Key comparable, Val any](tar Map, key Key) Val { 135 | return tar[key] 136 | } 137 | 138 | // Self as global `MapGet`. 139 | func (self Dict[Key, Val]) Get(key Key) Val { return MapGet(self, key) } 140 | 141 | // Same as `tar[key] = val`, expressed as a generic function. 142 | func MapSet[Map ~map[Key]Val, Key comparable, Val any](tar Map, key Key, val Val) { 143 | tar[key] = val 144 | } 145 | 146 | // Self as global `MapSet`. 147 | func (self Dict[Key, Val]) Set(key Key, val Val) { MapSet(self, key, val) } 148 | 149 | /* 150 | Same as `MapSet`, but key and value should be be non-zero. 151 | If either is zero, this ignores the inputs and does nothing. 152 | */ 153 | func MapSetOpt[Map ~map[Key]Val, Key comparable, Val any](tar Map, key Key, val Val) { 154 | if IsNotZero(key) && IsNotZero(val) { 155 | MapSet(tar, key, val) 156 | } 157 | } 158 | 159 | // Self as global `MapSetOpt`. 160 | func (self Dict[Key, Val]) SetOpt(key Key, val Val) { MapSetOpt(self, key, val) } 161 | 162 | // Same as `delete(tar, key)`, expressed as a generic function. 163 | func MapDel[Map ~map[Key]Val, Key comparable, Val any](tar Map, key Key) { 164 | delete(tar, key) 165 | } 166 | 167 | // Self as global `MapDel`. 168 | func (self Dict[Key, _]) Del(key Key) { delete(self, key) } 169 | 170 | /* 171 | Deletes all entries, returning the resulting map. Passing nil is safe. 172 | Note that this involves iterating the map, which is inefficient in Go. 173 | In many cases, it's more efficient to make a new map. Also note that 174 | Go 1.21 and higher have an equivalent built-in function `clear`. 175 | */ 176 | func MapClear[Map ~map[Key]Val, Key comparable, Val any](tar Map) { 177 | for key := range tar { 178 | delete(tar, key) 179 | } 180 | } 181 | 182 | // Self as global `MapClear`. 183 | func (self Dict[_, _]) Clear() { MapClear(self) } 184 | -------------------------------------------------------------------------------- /map_test.go: -------------------------------------------------------------------------------- 1 | package gg_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mitranim/gg" 7 | "github.com/mitranim/gg/gtest" 8 | ) 9 | 10 | func TestMapInit(t *testing.T) { 11 | defer gtest.Catch(t) 12 | 13 | var tar IntMap 14 | gtest.Equal(gg.MapInit(&tar), tar) 15 | gtest.NotZero(tar) 16 | 17 | tar[10] = 20 18 | gtest.Equal(gg.MapInit(&tar), tar) 19 | gtest.Equal(tar, IntMap{10: 20}) 20 | } 21 | 22 | func TestMapClone(t *testing.T) { 23 | defer gtest.Catch(t) 24 | 25 | gtest.Equal(gg.MapClone(IntMap(nil)), IntMap(nil)) 26 | 27 | src := IntMap{10: 20, 30: 40} 28 | out := gg.MapClone(src) 29 | gtest.Equal(out, src) 30 | 31 | src[10] = 50 32 | gtest.Equal(src, IntMap{10: 50, 30: 40}) 33 | gtest.Equal(out, IntMap{10: 20, 30: 40}) 34 | } 35 | 36 | func TestMapKeys(t *testing.T) { 37 | defer gtest.Catch(t) 38 | 39 | test := func(src IntMap, exp []int) { 40 | gtest.Equal(gg.SortedPrim(gg.MapKeys(src)), exp) 41 | } 42 | 43 | test(IntMap(nil), []int(nil)) 44 | test(IntMap{}, []int{}) 45 | test(IntMap{10: 20}, []int{10}) 46 | test(IntMap{10: 20, 30: 40}, []int{10, 30}) 47 | } 48 | 49 | func TestMapVals(t *testing.T) { 50 | defer gtest.Catch(t) 51 | 52 | test := func(src IntMap, exp []int) { 53 | gtest.Equal(gg.SortedPrim(gg.MapVals(src)), exp) 54 | } 55 | 56 | test(IntMap(nil), []int(nil)) 57 | test(IntMap{}, []int{}) 58 | test(IntMap{10: 20}, []int{20}) 59 | test(IntMap{10: 20, 30: 40}, []int{20, 40}) 60 | } 61 | 62 | func TestMapHas(t *testing.T) { 63 | defer gtest.Catch(t) 64 | 65 | gtest.False(gg.MapHas(IntMap(nil), 10)) 66 | gtest.False(gg.MapHas(IntMap{10: 20}, 20)) 67 | gtest.True(gg.MapHas(IntMap{10: 20}, 10)) 68 | } 69 | 70 | func TestMapGot(t *testing.T) { 71 | defer gtest.Catch(t) 72 | 73 | { 74 | val, ok := gg.MapGot(IntMap(nil), 10) 75 | gtest.Zero(val) 76 | gtest.False(ok) 77 | } 78 | 79 | { 80 | val, ok := gg.MapGot(IntMap{10: 20}, 20) 81 | gtest.Zero(val) 82 | gtest.False(ok) 83 | } 84 | 85 | { 86 | val, ok := gg.MapGot(IntMap{10: 20}, 10) 87 | gtest.Eq(val, 20) 88 | gtest.True(ok) 89 | } 90 | } 91 | 92 | func TestMapGet(t *testing.T) { 93 | defer gtest.Catch(t) 94 | 95 | gtest.Zero(gg.MapGet(IntMap(nil), 10)) 96 | gtest.Zero(gg.MapGet(IntMap{10: 20}, 20)) 97 | gtest.Eq(gg.MapGet(IntMap{10: 20}, 10), 20) 98 | } 99 | 100 | func TestMapSet(t *testing.T) { 101 | defer gtest.Catch(t) 102 | 103 | tar := IntMap{} 104 | 105 | gg.MapSet(tar, 10, 20) 106 | gtest.Equal(tar, IntMap{10: 20}) 107 | 108 | gg.MapSet(tar, 10, 30) 109 | gtest.Equal(tar, IntMap{10: 30}) 110 | } 111 | 112 | func TestMapSetOpt(t *testing.T) { 113 | defer gtest.Catch(t) 114 | 115 | tar := IntMap{} 116 | 117 | gg.MapSetOpt(tar, 0, 20) 118 | gtest.Equal(tar, IntMap{}) 119 | 120 | gg.MapSetOpt(tar, 10, 0) 121 | gtest.Equal(tar, IntMap{}) 122 | 123 | gg.MapSetOpt(tar, 10, 20) 124 | gtest.Equal(tar, IntMap{10: 20}) 125 | 126 | gg.MapSetOpt(tar, 10, 30) 127 | gtest.Equal(tar, IntMap{10: 30}) 128 | } 129 | 130 | func TestMapClear(t *testing.T) { 131 | defer gtest.Catch(t) 132 | 133 | var tar IntMap 134 | gg.MapClear(tar) 135 | gtest.Equal(tar, IntMap(nil)) 136 | 137 | tar = IntMap{10: 20, 30: 40} 138 | gg.MapClear(tar) 139 | gtest.Equal(tar, IntMap{}) 140 | } 141 | 142 | /* 143 | func Benchmark_map_iteration(b *testing.B) { 144 | tar := make(map[string]float64) 145 | for ind := range gg.Iter(1024) { 146 | tar[gg.String(ind)] = float64((ind % 2) * ind) 147 | } 148 | 149 | b.ResetTimer() 150 | 151 | for ind := 0; ind < b.N; ind++ { 152 | for key, val := range tar { 153 | gg.Nop2(key, val) 154 | } 155 | } 156 | } 157 | */ 158 | -------------------------------------------------------------------------------- /math.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | /* 8 | Short for "is finite". Missing feature of the standard "math" package. 9 | True if the input is neither NaN nor infinity. 10 | */ 11 | func IsFin[A Float](val A) bool { 12 | tar := float64(val) 13 | return !math.IsNaN(tar) && !math.IsInf(tar, 0) 14 | } 15 | 16 | // Short for "is natural". True if >= 0. Also see `IsPos`. 17 | func IsNat[A Num](val A) bool { return val >= 0 } 18 | 19 | // Short for "is positive". True if > 0. Also see `IsNat`. 20 | func IsPos[A Num](val A) bool { return val > 0 } 21 | 22 | // Short for "is negative". True if < 0. Also see `IsNat`. 23 | func IsNeg[A Num](val A) bool { return val < 0 } 24 | 25 | /* 26 | True if the remainder of dividing the first argument by the second argument is 27 | zero. If the divisor is zero, does not attempt the division and returns false. 28 | Note that the result is unaffected by the signs of either the dividend or the 29 | divisor. 30 | */ 31 | func IsDivisibleBy[A Int](dividend, divisor A) bool { 32 | return divisor != 0 && dividend%divisor == 0 33 | } 34 | 35 | // True if the input has a fractional component. 36 | func IsFrac[A Float](val A) bool { 37 | _, frac := math.Modf(float64(val)) 38 | return frac != 0 && !math.IsNaN(frac) 39 | } 40 | 41 | // Same as `Add(val, 1)`. Panics on overflow. 42 | func Inc[A Int](val A) A { return Add(val, 1) } 43 | 44 | // Same as `Sub(val, 1)`. Panics on underflow. 45 | func Dec[A Int](val A) A { return Sub(val, 1) } 46 | 47 | /* 48 | Raises a number to a power. Same as `math.Pow` and calls it under the hood, but 49 | accepts arbitrary numeric types and performs checked conversions via `NumConv`. 50 | Panics on overflow or precision loss. Has minor overhead over `math.Pow`. 51 | Compare `PowUncheck` which runs faster but may overflow. 52 | */ 53 | func Pow[Tar, Pow Num](src Tar, pow Pow) Tar { 54 | return NumConv[Tar](math.Pow(NumConv[float64](src), NumConv[float64](pow))) 55 | } 56 | 57 | /* 58 | Raises a number to a power. Same as `math.Pow` and calls it under the hood, but 59 | accepts arbitrary numeric types. Does not check for overflow or precision loss. 60 | Counterpart to `Pow` which panics on overflow. 61 | */ 62 | func PowUncheck[Tar, Pow Num](src Tar, pow Pow) Tar { 63 | return Tar(math.Pow(float64(src), float64(pow))) 64 | } 65 | 66 | /* 67 | Checked factorial. Panics on overflow. Compare `FacUncheck` which runs faster, 68 | but may overflow. 69 | */ 70 | func Fac[A Uint](src A) A { 71 | var tar float64 = 1 72 | mul := NumConv[float64](src) 73 | for mul > 0 { 74 | tar *= mul 75 | mul-- 76 | } 77 | return NumConv[A](tar) 78 | } 79 | 80 | /* 81 | Unchecked factorial. May overflow. Counterpart to `Fac` which panics on 82 | overflow. 83 | */ 84 | func FacUncheck[A Uint](src A) A { 85 | var out A = 1 86 | for src > 0 { 87 | out *= src 88 | src -= 1 89 | } 90 | return out 91 | } 92 | 93 | // Checked addition. Panics on overflow/underflow. Has overhead. 94 | func Add[A Int](one, two A) A { 95 | out := one + two 96 | if (out > one) == (two > 0) { 97 | return out 98 | } 99 | panic(errAdd(one, two, out)) 100 | } 101 | 102 | func errAdd[A Int](one, two, out A) Err { 103 | return Errf( 104 | `addition overflow for %v: %v + %v = %v`, 105 | Type[A](), one, two, out, 106 | ) 107 | } 108 | 109 | /* 110 | Unchecked addition. Same as Go's `+` operator for numbers, expressed as a 111 | generic function. Does not take strings. May overflow. For integers, prefer 112 | `Add` whenever possible, which has overflow checks. 113 | */ 114 | func AddUncheck[A Num](one, two A) A { return one + two } 115 | 116 | // Checked subtraction. Panics on overflow/underflow. Has overhead. 117 | func Sub[A Int](one, two A) A { 118 | out := one - two 119 | if (out < one) == (two > 0) { 120 | return out 121 | } 122 | panic(errSub(one, two, out)) 123 | } 124 | 125 | func errSub[A Int](one, two, out A) Err { 126 | return Errf( 127 | `subtraction overflow for %v: %v - %v = %v`, 128 | Type[A](), one, two, out, 129 | ) 130 | } 131 | 132 | /* 133 | Unchecked subtraction. Same as Go's `-` operator, expressed as a generic 134 | function. May overflow. For integers, prefer `Sub` whenever possible, which has 135 | overflow checks. 136 | */ 137 | func SubUncheck[A Num](one, two A) A { return one - two } 138 | 139 | // Checked multiplication. Panics on overflow/underflow. Has overhead. 140 | func Mul[A Int](one, two A) A { 141 | if one == 0 || two == 0 { 142 | return 0 143 | } 144 | out := one * two 145 | if ((one < 0) == (two < 0)) != (out < 0) && out/two == one { 146 | return out 147 | } 148 | panic(errMul(one, two, out)) 149 | } 150 | 151 | func errMul[A Int](one, two, out A) Err { 152 | return Errf( 153 | `multiplication overflow for %v: %v * %v = %v`, 154 | Type[A](), one, two, out, 155 | ) 156 | } 157 | -------------------------------------------------------------------------------- /math_conv_32_bit.go: -------------------------------------------------------------------------------- 1 | //go:build 386 2 | 3 | package gg 4 | 5 | import "math" 6 | 7 | func isInt32SafeForUint(val int32) bool { 8 | return val >= 0 && val <= math.MaxInt32 9 | } 10 | 11 | func isUint32SafeForInt(val uint32) bool { return val <= math.MaxInt32 } 12 | 13 | func isInt64SafeForInt(val int64) bool { 14 | return val >= math.MinInt32 && val <= math.MaxInt32 15 | } 16 | 17 | func isInt64SafeForUint(val int64) bool { 18 | return val >= 0 && val <= math.MaxUint32 19 | } 20 | 21 | func isUint64SafeForInt(val uint64) bool { 22 | return val <= math.MaxInt32 23 | } 24 | 25 | func isUint64SafeForUint(val uint64) bool { 26 | return val <= math.MaxUint32 27 | } 28 | 29 | func isIntSafeForFloat64(int) bool { return true } 30 | 31 | func isUintSafeForInt64(uint) bool { return true } 32 | 33 | func isUintSafeForFloat64(uint) bool { return true } 34 | 35 | func isFloat64SafeForInt(val float64) bool { 36 | return !IsFrac(val) && val >= math.MinInt32 && val <= math.MaxInt32 37 | } 38 | 39 | func isFloat64SafeForUint(val float64) bool { 40 | return !IsFrac(val) && val >= 0 && val <= math.MaxUint32 41 | } 42 | -------------------------------------------------------------------------------- /math_conv_64_bit.go: -------------------------------------------------------------------------------- 1 | //go:build !386 2 | 3 | package gg 4 | 5 | import "math" 6 | 7 | func isInt32SafeForUint(val int32) bool { return val >= 0 } 8 | 9 | func isUint32SafeForInt(uint32) bool { return true } 10 | 11 | func isInt64SafeForInt(int64) bool { return true } 12 | 13 | func isInt64SafeForUint(val int64) bool { return val >= 0 } 14 | 15 | func isUint64SafeForInt(val uint64) bool { 16 | return val <= math.MaxInt64 17 | } 18 | 19 | func isUint64SafeForUint(uint64) bool { return true } 20 | 21 | func isIntSafeForFloat64(val int) bool { 22 | return val >= MinSafeIntFloat64 && val <= MaxSafeIntFloat64 23 | } 24 | 25 | func isUintSafeForInt64(val uint) bool { 26 | return val <= math.MaxInt 27 | } 28 | 29 | func isUintSafeForFloat64(val uint) bool { return val <= MaxSafeIntFloat64 } 30 | 31 | func isFloat64SafeForInt(val float64) bool { 32 | return !IsFrac(val) && val >= MinSafeIntFloat64 && val <= MaxSafeIntFloat64 33 | } 34 | 35 | func isFloat64SafeForUint(val float64) bool { 36 | return !IsFrac(val) && val >= 0 && val <= MaxSafeIntFloat64 37 | } 38 | -------------------------------------------------------------------------------- /maybe.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import "encoding/json" 4 | 5 | // Shortcut for creating a `Maybe` with the given value. 6 | func MaybeVal[A any](val A) Maybe[A] { return Maybe[A]{val, nil} } 7 | 8 | // Shortcut for creating a `Maybe` with the given error. 9 | func MaybeErr[A any](err error) Maybe[A] { return Maybe[A]{Zero[A](), err} } 10 | 11 | /* 12 | Contains a value or an error. The JSON tags "value" and "error" are chosen due 13 | to their existing popularity in HTTP API. 14 | */ 15 | type Maybe[A any] struct { 16 | Val A `json:"value,omitempty"` 17 | Err error `json:"error,omitempty"` 18 | } 19 | 20 | /* 21 | Asserts that the error is nil, returning the resulting value. If the error is 22 | non-nil, panics via `Try`, idempotently adding a stack trace to the error. 23 | */ 24 | func (self Maybe[A]) Ok() A { 25 | Try(self.Err) 26 | return self.Val 27 | } 28 | 29 | // Implement `Getter`, returning the underlying value as-is. 30 | func (self Maybe[A]) Get() A { return self.Val } 31 | 32 | // Implement `Setter`. Sets the underlying value and clears the error. 33 | func (self *Maybe[A]) Set(val A) { 34 | self.Val = val 35 | self.Err = nil 36 | } 37 | 38 | // Returns the underlying error as-is. 39 | func (self Maybe[_]) GetErr() error { return self.Err } 40 | 41 | // Sets the error. If the error is non-nil, clears the value. 42 | func (self *Maybe[A]) SetErr(err error) { 43 | if err != nil { 44 | self.Val = Zero[A]() 45 | } 46 | self.Err = err 47 | } 48 | 49 | // True if error is non-nil. 50 | func (self Maybe[_]) HasErr() bool { return self.Err != nil } 51 | 52 | /* 53 | Implement `json.Marshaler`. If the underlying error is non-nil, returns that 54 | error. Otherwise uses `json.Marshal` to encode the underlying value. 55 | */ 56 | func (self Maybe[_]) MarshalJSON() ([]byte, error) { 57 | if self.Err != nil { 58 | return nil, self.Err 59 | } 60 | return json.Marshal(self.Val) 61 | } 62 | 63 | // Implement `json.Unmarshaler`, decoding into the underlying value. 64 | func (self *Maybe[_]) UnmarshalJSON(src []byte) error { 65 | self.Err = nil 66 | return json.Unmarshal(src, &self.Val) 67 | } 68 | -------------------------------------------------------------------------------- /mem.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | /* 9 | Tool for deduplicating and caching expensive work. All methods are safe for 10 | concurrent use. The first type parameter is used to determine expiration 11 | duration, and should be a zero-sized stateless type, such as `DurSecond`, 12 | `DurMinute`, `DurHour`, and `DurForever` provided by this package. The given 13 | type `Tar` must implement `Initer` on its pointer type: `(*Tar).Init`. The init 14 | method is used to populate data whenever it's missing or expired. See methods 15 | `Mem.Get` and `Mem.Peek`. A zero value of `Mem` is ready for use. Contains a 16 | synchronization primitive and must not be copied. 17 | 18 | Usage example: 19 | 20 | type Model struct { Id uint64 } 21 | 22 | type DatModels []Model 23 | 24 | // Pretend that this is expensive. 25 | func (self *DatModels) Init() { *self = DatModels{{10}, {20}, {30}} } 26 | 27 | type Dat struct { 28 | Models gg.Mem[gg.DurHour, DatModels, *DatModels] 29 | // ... other caches 30 | } 31 | 32 | var dat Dat 33 | 34 | func init() { fmt.Println(dat.Models.Get()) } 35 | */ 36 | type Mem[Dur Durationer, Tar any, Ptr IniterPtr[Tar]] struct { 37 | val Tar 38 | ok bool 39 | inst time.Time 40 | lock sync.RWMutex 41 | } 42 | 43 | /* 44 | Returns the inner value after ensuring it's initialized and not expired. If the 45 | data is missing or expired, it's initialized by calling `(*Tar).Init`. Otherwise 46 | the data is returned as-is. 47 | 48 | This method avoids redundant concurrent work. When called concurrently by 49 | multiple goroutines, only 1 goroutine performs work, while the others simply 50 | wait for it. 51 | 52 | Method `(*Tar).Init` is always called on a new pointer to a zero value, for 53 | multiple reasons. If `(*Tar).Init` appends data to the target instead of 54 | replacing it, this avoids accumulating redundant data and leaking memory. 55 | Additionally, this avoids accidental concurrent modification between `Mem` and 56 | its callers that could lead to observing an inconsistent state of the data. 57 | 58 | Expiration is determined by consulting the `Durationer` type provided to `Mem` 59 | as its first type parameter, calling `.Duration` on a zero value. As a special 60 | case, 0 duration is considered indefinite, making the `Mem` never expire, and 61 | thus functionally equivalent to `LazyIniter`. Negative durations cause the `Mem` 62 | to expire immediately, making it pointless. 63 | 64 | Compare `Mem.Peek` which does not perform initialization. 65 | */ 66 | func (self *Mem[Dur, Tar, Ptr]) Get() Tar { 67 | defer Lock(&self.lock).Unlock() 68 | 69 | val, ok, inst := self.val, self.ok, self.inst 70 | if ok && !isExpired(inst, Zero[Dur]().Duration()) { 71 | return val 72 | } 73 | 74 | var tar Tar 75 | Ptr(&tar).Init() 76 | self.set(tar) 77 | return tar 78 | } 79 | 80 | /* 81 | Similar to `Mem.Get` but returns the inner value as-is, without checking 82 | expiration. If the value was never initialized, it's zero. 83 | */ 84 | func (self *Mem[_, Tar, _]) Peek() Tar { 85 | defer Lock(self.lock.RLocker()).Unlock() 86 | return self.val 87 | } 88 | 89 | // Clears the inner value and timestamp. 90 | func (self *Mem[_, Tar, _]) Clear() { 91 | defer Lock(&self.lock).Unlock() 92 | self.clear() 93 | } 94 | 95 | /* 96 | Implement `json.Marshaler`. If the value is not initialized, returns a 97 | representation of JSON null. Otherwise uses `json.Marshal` to encode the 98 | underlying value, even if expired. Like other methods, this is safe for 99 | concurrent use. 100 | */ 101 | func (self *Mem[_, _, _]) MarshalJSON() ([]byte, error) { 102 | if self == nil { 103 | return ToBytes(`null`), nil 104 | } 105 | defer Lock(self.lock.RLocker()).Unlock() 106 | if !self.ok { 107 | return ToBytes(`null`), nil 108 | } 109 | return JsonBytesCatch(self.val) 110 | } 111 | 112 | /* 113 | Implement `json.Unmarshaler`. If the input is empty or represents JSON null, 114 | clears the inner value and the timestamp. Otherwise uses `json.Unmarshal` to 115 | decode into the inner value, setting the current timestamp on success. Like 116 | `Mem.Get`, this uses an intermediary zero value, avoiding corruption of the 117 | existing inner value in cases of partially failed decoding. Like other methods, 118 | this is safe for concurrent use. 119 | */ 120 | func (self *Mem[_, Tar, _]) UnmarshalJSON(src []byte) error { 121 | defer Lock(&self.lock).Unlock() 122 | 123 | if IsJsonEmpty(src) { 124 | self.clear() 125 | return nil 126 | } 127 | 128 | var tar Tar 129 | err := JsonDecodeCatch(src, &tar) 130 | if err != nil { 131 | return err 132 | } 133 | 134 | self.set(tar) 135 | return nil 136 | } 137 | 138 | func (self *Mem[_, Tar, _]) clear() { 139 | var val Tar 140 | self.val = val 141 | self.ok = false 142 | self.inst = time.Time{} 143 | } 144 | 145 | func (self *Mem[_, Tar, _]) set(val Tar) { 146 | self.val = val 147 | self.ok = true 148 | self.inst = time.Now() 149 | } 150 | 151 | func isExpired(inst time.Time, dur time.Duration) bool { 152 | return dur != 0 && inst.Add(dur).Before(time.Now()) 153 | } 154 | 155 | /* 156 | Implements `Durationer` by returning `time.Second`. This type is zero-sized, and 157 | can be embedded in other types, like a mixin, at no additional cost. 158 | */ 159 | type DurSecond struct{} 160 | 161 | // Implement `Durationer` by returning `time.Second`. 162 | func (DurSecond) Duration() time.Duration { return time.Second } 163 | 164 | /* 165 | Implements `Durationer` by returning `time.Minute`. This type is zero-sized, and 166 | can be embedded in other types, like a mixin, at no additional cost. 167 | */ 168 | type DurMinute struct{} 169 | 170 | // Implement `Durationer` by returning `time.Minute`. 171 | func (DurMinute) Duration() time.Duration { return time.Minute } 172 | 173 | /* 174 | Implements `Durationer` by returning `time.Hour`. This type is zero-sized, and 175 | can be embedded in other types, like a mixin, at no additional cost. 176 | */ 177 | type DurHour struct{} 178 | 179 | // Implement `Durationer` by returning `time.Hour`. 180 | func (DurHour) Duration() time.Duration { return time.Hour } 181 | 182 | /* 183 | Implements `Durationer` by returning 0, which is understood by `Mem` as 184 | indefinite, making it never expire. This type is zero-sized, and can be 185 | embedded in other types, like a mixin, at no additional cost. 186 | */ 187 | type DurForever struct{} 188 | 189 | // Implement `Durationer` by returning 0. 190 | func (DurForever) Duration() time.Duration { return 0 } 191 | -------------------------------------------------------------------------------- /mem_test.go: -------------------------------------------------------------------------------- 1 | package gg_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mitranim/gg" 7 | "github.com/mitranim/gg/gtest" 8 | ) 9 | 10 | type IniterStrs []string 11 | 12 | func (self *IniterStrs) Init() { 13 | if *self != nil { 14 | panic(`redundant init`) 15 | } 16 | *self = []string{`one`, `two`, `three`} 17 | } 18 | 19 | func TestMem(t *testing.T) { 20 | defer gtest.Catch(t) 21 | 22 | { 23 | // Note: `IniterStr` panics if init is called on a non-zero value. 24 | // This is part of our contract. 25 | var mem gg.Mem[gg.DurSecond, IniterStr, *IniterStr] 26 | 27 | test := func() { 28 | testPeekerZero[IniterStr](&mem) 29 | gtest.Eq(mem.Get(), `inited`) 30 | gtest.Eq(mem.Get(), `inited`) 31 | gtest.Eq(mem.Peek(), `inited`) 32 | gtest.Eq(mem.Peek(), `inited`) 33 | testPeekerNotZero[IniterStr](&mem) 34 | } 35 | 36 | test() 37 | mem.Clear() 38 | test() 39 | } 40 | 41 | { 42 | // Note: `IniterStrs` panics if init is called on a non-zero value. 43 | // This is part of our contract. 44 | var mem gg.Mem[gg.DurSecond, IniterStrs, *IniterStrs] 45 | 46 | test := func() { 47 | testPeekerZero[IniterStrs](&mem) 48 | 49 | prev := mem.Get() 50 | gtest.Equal(prev, []string{`one`, `two`, `three`}) 51 | gtest.SliceIs(mem.Get(), prev) 52 | gtest.SliceIs(mem.Get(), prev) 53 | 54 | testPeekerNotZero[IniterStrs](&mem) 55 | } 56 | 57 | test() 58 | prev := mem.Get() 59 | mem.Clear() 60 | test() 61 | next := mem.Get() 62 | gtest.NotSliceIs(next, prev) 63 | } 64 | } 65 | 66 | /* 67 | Technical note: because `Mem` contains a synchronization primitive, its method 68 | `.MarshalJSON` cannot be safely implemented on the value type, and we cannot 69 | safely pass the value to JSON-encoding functions. This is a known inconsistency 70 | with other types that implement JSON encoding. 71 | */ 72 | func TestMem_MarshalJSON(t *testing.T) { 73 | defer gtest.Catch(t) 74 | 75 | t.Run(`primitive`, func(t *testing.T) { 76 | defer gtest.Catch(t) 77 | 78 | type Type = gg.Mem[gg.DurSecond, IniterStr, *IniterStr] 79 | 80 | gtest.Eq(gg.ToString(gg.Try1((*Type)(nil).MarshalJSON())), `null`) 81 | gtest.Eq(gg.JsonString((*Type)(nil)), `null`) 82 | 83 | var mem Type 84 | gtest.Eq(gg.JsonString(&mem), `null`) 85 | 86 | mem.Get() 87 | gtest.Eq(gg.JsonString(&mem), `"inited"`) 88 | 89 | mem.Clear() 90 | gtest.Eq(gg.JsonString(&mem), `null`) 91 | }) 92 | 93 | t.Run(`slice`, func(t *testing.T) { 94 | defer gtest.Catch(t) 95 | 96 | type Type = gg.Mem[gg.DurSecond, IniterStrs, *IniterStrs] 97 | 98 | gtest.Eq(gg.ToString(gg.Try1((*Type)(nil).MarshalJSON())), `null`) 99 | gtest.Eq(gg.JsonString((*Type)(nil)), `null`) 100 | 101 | var mem Type 102 | gtest.Eq(gg.JsonString(&mem), `null`) 103 | 104 | mem.Get() 105 | gtest.Eq(gg.JsonString(&mem), `["one","two","three"]`) 106 | 107 | mem.Clear() 108 | gtest.Eq(gg.JsonString(&mem), `null`) 109 | }) 110 | } 111 | 112 | func TestMem_UnmarshalJSON(t *testing.T) { 113 | defer gtest.Catch(t) 114 | 115 | t.Run(`primitive`, func(t *testing.T) { 116 | defer gtest.Catch(t) 117 | 118 | var mem gg.Mem[gg.DurSecond, IniterStr, *IniterStr] 119 | gtest.NoErr(mem.UnmarshalJSON([]byte(`null`))) 120 | gtest.Zero(mem.Peek()) 121 | 122 | gtest.NoErr(mem.UnmarshalJSON([]byte(`""`))) 123 | gtest.Eq(mem.Peek(), ``) 124 | 125 | gtest.NoErr(mem.UnmarshalJSON([]byte(`"test"`))) 126 | gtest.Eq(mem.Peek(), `test`) 127 | 128 | gtest.ErrStr(`invalid character 'i'`, mem.UnmarshalJSON([]byte(`invalid`))) 129 | gtest.Eq(mem.Peek(), `test`) 130 | }) 131 | 132 | t.Run(`slice`, func(t *testing.T) { 133 | defer gtest.Catch(t) 134 | 135 | var mem gg.Mem[gg.DurSecond, IniterStrs, *IniterStrs] 136 | gtest.NoErr(mem.UnmarshalJSON([]byte(`null`))) 137 | gtest.Zero(mem.Peek()) 138 | 139 | gtest.NoErr(mem.UnmarshalJSON([]byte(`[]`))) 140 | gtest.Equal(mem.Peek(), IniterStrs{}) 141 | 142 | gtest.NoErr(mem.UnmarshalJSON([]byte(`["four","five","six"]`))) 143 | gtest.Equal(mem.Peek(), IniterStrs{`four`, `five`, `six`}) 144 | 145 | gtest.ErrStr(`invalid character 'i'`, mem.UnmarshalJSON([]byte(`invalid`))) 146 | gtest.Equal(mem.Peek(), IniterStrs{`four`, `five`, `six`}) 147 | }) 148 | } 149 | 150 | func testPeekerZero[Tar any, Src gg.Peeker[Tar]](src Src) { 151 | gtest.Zero(src.Peek()) 152 | gtest.Zero(src.Peek()) 153 | } 154 | 155 | func testPeekerNotZero[Tar any, Src gg.Peeker[Tar]](src Src) { 156 | gtest.NotZero(src.Peek()) 157 | gtest.NotZero(src.Peek()) 158 | } 159 | -------------------------------------------------------------------------------- /opt.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import "database/sql/driver" 4 | 5 | /* 6 | Short for "optional value". Instantiates an optional with the given val. 7 | The result is considered non-null. 8 | */ 9 | func OptVal[A any](val A) Opt[A] { return Opt[A]{val, true} } 10 | 11 | /* 12 | Shortcut for creating an optional from a given value and boolean indicating 13 | validity. If the boolean is false, the output is considered "null" even if the 14 | value is not "zero". 15 | */ 16 | func OptFrom[A any](val A, ok bool) Opt[A] { return Opt[A]{val, ok} } 17 | 18 | /* 19 | Short for "optional". Wraps an arbitrary type. When `.Ok` is false, the value is 20 | considered empty/null in various contexts such as text encoding, JSON encoding, 21 | SQL encoding, even if the value is not "zero". 22 | */ 23 | type Opt[A any] struct { 24 | Val A 25 | Ok bool 26 | } 27 | 28 | // Implement `Nullable`. True if not `.Ok`. 29 | func (self Opt[_]) IsNull() bool { return !self.Ok } 30 | 31 | // Inverse of `.IsNull`. 32 | func (self Opt[_]) IsNotNull() bool { return self.Ok } 33 | 34 | // Implement `Clearer`. Zeroes the receiver. 35 | func (self *Opt[_]) Clear() { PtrClear(self) } 36 | 37 | // Implement `Getter`, returning the underlying value as-is. 38 | func (self Opt[A]) Get() A { return self.Val } 39 | 40 | /* 41 | Implement `Setter`. Modifies the underlying value and sets `.Ok = true`. 42 | The resulting state is considered non-null even if the value is "zero". 43 | */ 44 | func (self *Opt[A]) Set(val A) { 45 | self.Val = val 46 | self.Ok = true 47 | } 48 | 49 | // Implement `Ptrer`, returning a pointer to the underlying value. 50 | func (self *Opt[A]) Ptr() *A { 51 | if self == nil { 52 | return nil 53 | } 54 | return &self.Val 55 | } 56 | 57 | /* 58 | Implement `fmt.Stringer`. If `.IsNull`, returns an empty string. Otherwise uses 59 | the `String` function to encode the inner value. 60 | */ 61 | func (self Opt[A]) String() string { return StringNull[A](self) } 62 | 63 | /* 64 | Implement `Parser`. If the input is empty, clears the receiver via `.Clear`. 65 | Otherwise uses the `ParseCatch` function, decoding into the underlying value. 66 | */ 67 | func (self *Opt[A]) Parse(src string) error { 68 | return self.with(ParseClearCatch[A](src, self)) 69 | } 70 | 71 | // Implement `AppenderTo`, appending the same representation as `.String`. 72 | func (self Opt[A]) AppendTo(buf []byte) []byte { return AppendNull[A](buf, self) } 73 | 74 | // Implement `encoding.TextMarshaler`, returning the same representation as `.String`. 75 | func (self Opt[A]) MarshalText() ([]byte, error) { return MarshalNullCatch[A](self) } 76 | 77 | // Implement `encoding.TextUnmarshaler`, using the same logic as `.Parse`. 78 | func (self *Opt[A]) UnmarshalText(src []byte) error { 79 | if len(src) <= 0 { 80 | self.Clear() 81 | return nil 82 | } 83 | return self.with(ParseClearCatch[A](src, self)) 84 | } 85 | 86 | /* 87 | Implement `json.Marshaler`. If `.IsNull`, returns a representation of JSON null. 88 | Otherwise uses `json.Marshal` to encode the underlying value. 89 | */ 90 | func (self Opt[A]) MarshalJSON() ([]byte, error) { 91 | return JsonBytesNullCatch[A](self) 92 | } 93 | 94 | /* 95 | Implement `json.Unmarshaler`. If the input is empty or represents JSON null, 96 | clears the receiver via `.Clear`. Otherwise uses `json.Unmarshal` to decode 97 | into the underlying value. 98 | */ 99 | func (self *Opt[A]) UnmarshalJSON(src []byte) error { 100 | if IsJsonEmpty(src) { 101 | self.Clear() 102 | return nil 103 | } 104 | return self.with(JsonDecodeCatch(src, &self.Val)) 105 | } 106 | 107 | /* 108 | Implement SQL `driver.Valuer`. If `.IsNull`, returns nil. If the underlying 109 | value implements `driver.Valuer`, delegates to its method. Otherwise returns 110 | the underlying value as-is. 111 | */ 112 | func (self Opt[A]) Value() (driver.Value, error) { return ValueNull[A](self) } 113 | 114 | /* 115 | Implement SQL `Scanner`, decoding an arbitrary input into the underlying value. 116 | If the underlying type implements `Scanner`, delegates to that implementation. 117 | Otherwise input must be nil or text-like (see `Text`). Text decoding uses the 118 | same logic as `.Parse`. 119 | */ 120 | func (self *Opt[A]) Scan(src any) error { 121 | if src == nil { 122 | self.Clear() 123 | return nil 124 | } 125 | 126 | val, ok := src.(A) 127 | if ok { 128 | self.Set(val) 129 | return nil 130 | } 131 | 132 | return self.with(ScanCatch[A](src, self)) 133 | } 134 | 135 | func (self *Opt[_]) with(err error) error { 136 | self.Ok = err == nil 137 | return err 138 | } 139 | 140 | /* 141 | FP-style "mapping". If the original value is considered "null", or if the 142 | function is nil, the output is "zero" and "null". Otherwise the output is the 143 | result of calling the function with the previous value, and is considered 144 | non-"null" even if the value is zero. 145 | */ 146 | func OptMap[A, B any](src Opt[A], fun func(A) B) (out Opt[B]) { 147 | if src.IsNotNull() && fun != nil { 148 | out.Set(fun(src.Val)) 149 | } 150 | return 151 | } 152 | -------------------------------------------------------------------------------- /opt_test.go: -------------------------------------------------------------------------------- 1 | package gg_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mitranim/gg" 7 | "github.com/mitranim/gg/gtest" 8 | ) 9 | 10 | func TestOpt_MarshalJSON(t *testing.T) { 11 | defer gtest.Catch(t) 12 | 13 | type Type = gg.Opt[int] 14 | 15 | gtest.Eq(gg.JsonString(gg.Zero[Type]()), `null`) 16 | gtest.Eq(gg.JsonString(Type{Val: 123}), `null`) 17 | gtest.Eq(gg.JsonString(gg.OptVal(123)), `123`) 18 | } 19 | 20 | func TestOpt_UnmarshalJSON(t *testing.T) { 21 | defer gtest.Catch(t) 22 | 23 | type Type = gg.Opt[int] 24 | 25 | gtest.Zero(gg.JsonDecodeTo[Type](`null`)) 26 | 27 | gtest.Equal( 28 | gg.JsonDecodeTo[Type](`123`), 29 | gg.OptVal(123), 30 | ) 31 | } 32 | 33 | func BenchmarkOpt_String(b *testing.B) { 34 | val := gg.OptVal(`str`) 35 | 36 | for ind := 0; ind < b.N; ind++ { 37 | gg.Nop1(val.String()) 38 | } 39 | } 40 | 41 | func TestOpt_Scan(t *testing.T) { 42 | defer gtest.Catch(t) 43 | 44 | type Type = gg.Opt[float64] 45 | 46 | var tar Type 47 | gtest.NoErr(tar.Scan(float64(9.999999682655225e-18))) 48 | gtest.Eq(tar.Val, 9.999999682655225e-18) 49 | 50 | tar.Clear() 51 | gtest.Zero(tar) 52 | gtest.NoErr(tar.Scan(`9.999999682655225e-18`)) 53 | gtest.Eq(tar.Val, 9.999999682655225e-18) 54 | } 55 | -------------------------------------------------------------------------------- /ord_map.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | /* 4 | Implementation note: this currently lacks its own tests, but is indirectly 5 | tested through `Coll`. TODO dedicated tests. 6 | */ 7 | 8 | /* 9 | Represents an ordered map. Compare `OrdSet` which has only values, not key-value 10 | pairs. Compare `Coll` which is an ordered map where each value determines its 11 | own key. 12 | 13 | This implementation is specialized for easy and efficient appending, iteration, 14 | and membership testing, but as a tradeoff, it does not support deletion. 15 | For "proper" ordered sets that support deletion, see the library 16 | https://github.com/mitranim/gord. 17 | 18 | Known limitation: lack of support for JSON encoding and decoding. Using Go maps 19 | for encoding would be easy but incorrect, because it would randomize element 20 | positions. Compare the types `Coll` and `LazyColl` which do support JSON. 21 | */ 22 | type OrdMap[Key comparable, Val any] struct { 23 | Slice []Val `role:"ref"` 24 | Index map[Key]int 25 | } 26 | 27 | // Same as `len(self.Slice)`. 28 | func (self OrdMap[_, _]) Len() int { return len(self.Slice) } 29 | 30 | // True if `.Len` <= 0. Inverse of `.IsNotEmpty`. 31 | func (self OrdMap[_, _]) IsEmpty() bool { return self.Len() <= 0 } 32 | 33 | // True if `.Len` > 0. Inverse of `.IsEmpty`. 34 | func (self OrdMap[_, _]) IsNotEmpty() bool { return self.Len() > 0 } 35 | 36 | // True if the index has the given key. 37 | func (self OrdMap[Key, _]) Has(key Key) bool { 38 | return MapHas(self.Index, key) 39 | } 40 | 41 | // Returns the value indexed on the given key, or the zero value of that type. 42 | func (self OrdMap[Key, Val]) Get(key Key) Val { 43 | return PtrGet(self.Ptr(key)) 44 | } 45 | 46 | /* 47 | Short for "get required". Returns the value indexed on the given key. Panics if 48 | the value is missing. 49 | */ 50 | func (self OrdMap[Key, Val]) GetReq(key Key) Val { 51 | ptr := self.Ptr(key) 52 | if ptr != nil { 53 | return *ptr 54 | } 55 | panic(errCollMissing[Val](key)) 56 | } 57 | 58 | /* 59 | Returns the value indexed on the given key and a boolean indicating if the value 60 | was actually present. 61 | */ 62 | func (self OrdMap[Key, Val]) Got(key Key) (Val, bool) { 63 | // Note: we must check `ok` because if the entry is missing, `ind` is `0`, 64 | // which is invalid. 65 | ind, ok := self.Index[key] 66 | if ok { 67 | return Got(self.Slice, ind) 68 | } 69 | return Zero[Val](), false 70 | } 71 | 72 | /* 73 | Short for "pointer". Returns a pointer to the value indexed on the given key, or 74 | nil if the value is missing. Because this type does not support deletion, the 75 | correspondence of positions in `.Slice` and indexes in `.Index` does not change 76 | when adding or replacing values. The pointer should remain valid for the 77 | lifetime of the ordered map, unless `.Slice` is directly mutated by external 78 | means. 79 | */ 80 | func (self OrdMap[Key, Val]) Ptr(key Key) *Val { 81 | // Note: we must check `ok` because if the entry is missing, `ind` is `0`, 82 | // which is invalid. 83 | ind, ok := self.Index[key] 84 | if ok { 85 | return GetPtr(self.Slice, ind) 86 | } 87 | return nil 88 | } 89 | 90 | /* 91 | Short for "pointer required". Returns a non-nil pointer to the value indexed 92 | on the given key, or panics if the value is missing. 93 | */ 94 | func (self OrdMap[Key, Val]) PtrReq(key Key) *Val { 95 | ptr := self.Ptr(key) 96 | if ptr != nil { 97 | return ptr 98 | } 99 | panic(errCollMissing[Val](key)) 100 | } 101 | 102 | /* 103 | Idempotently adds or replaces the given value, updating both the inner slice and 104 | the inner index. If the key was already registered in the map, the new value 105 | replaces the old value at the same position in the inner slice. 106 | */ 107 | func (self *OrdMap[Key, Val]) Set(key Key, val Val) *OrdMap[Key, Val] { 108 | index := MapInit(&self.Index) 109 | ind, ok := index[key] 110 | if ok { 111 | self.Slice[ind] = val 112 | return self 113 | } 114 | 115 | index[key] = AppendIndex(&self.Slice, val) 116 | return self 117 | } 118 | 119 | // Same as `OrdMap.Set`, but panics if the key was already present in the index. 120 | func (self *OrdMap[Key, Val]) Add(key Key, val Val) *OrdMap[Key, Val] { 121 | index := MapInit(&self.Index) 122 | 123 | if MapHas(index, key) { 124 | panic(Errf( 125 | `unexpected redundant %v with key %v`, 126 | Type[Val](), key, 127 | )) 128 | } 129 | 130 | index[key] = AppendIndex(&self.Slice, val) 131 | return self 132 | } 133 | 134 | // Nullifies both the slice and the index. Does not preserve their capacity. 135 | func (self *OrdMap[Key, Val]) Clear() *OrdMap[Key, Val] { 136 | if self != nil { 137 | self.Slice = nil 138 | self.Index = nil 139 | } 140 | return self 141 | } 142 | 143 | /* 144 | Returns a newly allocated slice of keys. 145 | Order matches the values in `.Slice`. 146 | */ 147 | func (self OrdMap[Key, _]) Keys() []Key { 148 | var tar []keyIndex[Key] 149 | for key, ind := range self.Index { 150 | tar = append(tar, keyIndex[Key]{key, ind}) 151 | } 152 | Sort(tar) 153 | return Map(tar, keyIndex[Key].getKey) 154 | } 155 | 156 | type keyIndex[A comparable] struct { 157 | key A 158 | ind int 159 | } 160 | 161 | func (self keyIndex[A]) Less(val keyIndex[A]) bool { 162 | return self.ind < val.ind 163 | } 164 | 165 | func (self keyIndex[A]) getKey() A { return self.key } 166 | -------------------------------------------------------------------------------- /ord_set.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import "encoding/json" 4 | 5 | /* 6 | Syntactic shortcut for making an `OrdSet` of the given arguments, with type 7 | inference. 8 | */ 9 | func OrdSetOf[Val comparable](src ...Val) OrdSet[Val] { 10 | var tar OrdSet[Val] 11 | tar.Add(src...) 12 | return tar 13 | } 14 | 15 | /* 16 | Syntactic shortcut for making an `OrdSet` from any number of source slices, with 17 | type inference. 18 | */ 19 | func OrdSetFrom[Slice ~[]Val, Val comparable](src ...Slice) OrdSet[Val] { 20 | var tar OrdSet[Val] 21 | for _, src := range src { 22 | tar.Add(src...) 23 | } 24 | return tar 25 | } 26 | 27 | /* 28 | Represents an ordered set. Compare `OrdMap` which represents an ordered map. 29 | This implementation is specialized for easy and efficient appending, iteration, 30 | and membership testing, but as a tradeoff, it does not support deletion. 31 | For "proper" ordered sets that support deletion, see the library 32 | https://github.com/mitranim/gord. 33 | */ 34 | type OrdSet[Val comparable] struct { 35 | Slice []Val `role:"ref"` 36 | Index Set[Val] 37 | } 38 | 39 | // Same as `len(self.Slice)`. 40 | func (self OrdSet[_]) Len() int { return len(self.Slice) } 41 | 42 | // True if `.Len` <= 0. Inverse of `.IsNotEmpty`. 43 | func (self OrdSet[_]) IsEmpty() bool { return self.Len() <= 0 } 44 | 45 | // True if `.Len` > 0. Inverse of `.IsEmpty`. 46 | func (self OrdSet[_]) IsNotEmpty() bool { return self.Len() > 0 } 47 | 48 | // True if the index has the given value. Ignores the inner slice. 49 | func (self OrdSet[Val]) Has(val Val) bool { return self.Index.Has(val) } 50 | 51 | /* 52 | Idempotently adds each given value to both the inner slice and the inner 53 | index, skipping duplicates. 54 | */ 55 | func (self *OrdSet[Val]) Add(src ...Val) *OrdSet[Val] { 56 | for _, val := range src { 57 | if !self.Has(val) { 58 | Append(&self.Slice, val) 59 | self.Index.Init().Add(val) 60 | } 61 | } 62 | return self 63 | } 64 | 65 | /* 66 | Replaces `.Slice` with the given slice and rebuilds `.Index`. Uses the slice 67 | as-is with no reallocation. Callers must be careful to avoid modifying the 68 | source data, which may invalidate the collection's index. 69 | */ 70 | func (self *OrdSet[Val]) Reset(src ...Val) *OrdSet[Val] { 71 | self.Slice = src 72 | self.Reindex() 73 | return self 74 | } 75 | 76 | // Nullifies both the slice and the index. Does not preserve their capacity. 77 | func (self *OrdSet[Val]) Clear() *OrdSet[Val] { 78 | if self != nil { 79 | self.Slice = nil 80 | self.Index = nil 81 | } 82 | return self 83 | } 84 | 85 | /* 86 | Rebuilds the inner index from the inner slice, without checking the validity of 87 | the existing index. Can be useful for external code that directly modifies the 88 | inner `.Slice`, for example by sorting it. This is NOT used when adding items 89 | via `.Add`, which modifies the index incrementally rather than all-at-once. 90 | */ 91 | func (self *OrdSet[Val]) Reindex() { self.Index = SetOf(self.Slice...) } 92 | 93 | // Implement `json.Marshaler`. Encodes the inner slice, ignoring the index. 94 | func (self OrdSet[_]) MarshalJSON() ([]byte, error) { 95 | return json.Marshal(self.Slice) 96 | } 97 | 98 | // Unmarshals the input into the inner slice and rebuilds the index. 99 | func (self *OrdSet[_]) UnmarshalJSON(src []byte) error { 100 | err := json.Unmarshal(src, &self.Slice) 101 | self.Reindex() 102 | return err 103 | } 104 | -------------------------------------------------------------------------------- /ord_set_test.go: -------------------------------------------------------------------------------- 1 | package gg_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mitranim/gg" 7 | "github.com/mitranim/gg/gtest" 8 | ) 9 | 10 | func TestOrdSetOf(t *testing.T) { 11 | defer gtest.Catch(t) 12 | 13 | gtest.Zero(gg.OrdSetOf[int]()) 14 | 15 | gtest.Equal( 16 | gg.OrdSetOf(10, 30, 20, 20, 10), 17 | gg.OrdSet[int]{ 18 | Slice: []int{10, 30, 20}, 19 | Index: gg.SetOf(10, 20, 30), 20 | }, 21 | ) 22 | } 23 | 24 | func TestOrdSetFrom(t *testing.T) { 25 | defer gtest.Catch(t) 26 | 27 | gtest.Zero(gg.OrdSetFrom[[]int, int]()) 28 | 29 | gtest.Equal( 30 | gg.OrdSetFrom([]int{10, 30}, []int{20, 20, 10}), 31 | gg.OrdSet[int]{ 32 | Slice: []int{10, 30, 20}, 33 | Index: gg.SetOf(10, 20, 30), 34 | }, 35 | ) 36 | } 37 | 38 | func TestOrdSet(t *testing.T) { 39 | defer gtest.Catch(t) 40 | 41 | t.Run(`Has`, func(t *testing.T) { 42 | defer gtest.Catch(t) 43 | 44 | gtest.False(gg.OrdSetOf[int]().Has(0)) 45 | gtest.False(gg.OrdSetOf[int]().Has(10)) 46 | gtest.False(gg.OrdSetOf[int](10).Has(20)) 47 | gtest.True(gg.OrdSetOf[int](10).Has(10)) 48 | gtest.True(gg.OrdSetOf[int](10, 20).Has(10)) 49 | gtest.True(gg.OrdSetOf[int](10, 20).Has(20)) 50 | }) 51 | 52 | t.Run(`Add`, func(t *testing.T) { 53 | defer gtest.Catch(t) 54 | 55 | var tar gg.OrdSet[int] 56 | 57 | tar.Add(10) 58 | gtest.Equal(tar, gg.OrdSetOf(10)) 59 | 60 | tar.Add(20) 61 | gtest.Equal(tar, gg.OrdSetOf(10, 20)) 62 | 63 | tar.Add(10, 10) 64 | gtest.Equal(tar, gg.OrdSetOf(10, 20)) 65 | 66 | tar.Add(20, 20) 67 | gtest.Equal(tar, gg.OrdSetOf(10, 20)) 68 | 69 | tar.Add(40, 30) 70 | gtest.Equal(tar, gg.OrdSetOf(10, 20, 40, 30)) 71 | }) 72 | 73 | t.Run(`Clear`, func(t *testing.T) { 74 | defer gtest.Catch(t) 75 | 76 | tar := gg.OrdSetOf(10, 20, 30) 77 | gtest.NotZero(tar) 78 | 79 | tar.Clear() 80 | gtest.Zero(tar) 81 | }) 82 | 83 | t.Run(`MarshalJSON`, func(t *testing.T) { 84 | defer gtest.Catch(t) 85 | 86 | gtest.Eq(gg.JsonString(gg.OrdSet[int]{}), `null`) 87 | 88 | gtest.Eq( 89 | gg.JsonString(gg.OrdSetOf(20, 10, 30)), 90 | `[20,10,30]`, 91 | ) 92 | }) 93 | 94 | t.Run(`UnmarshalJSON`, func(t *testing.T) { 95 | defer gtest.Catch(t) 96 | 97 | gtest.Equal( 98 | gg.JsonDecodeTo[gg.OrdSet[int]](`[20, 10, 30]`), 99 | gg.OrdSetOf(20, 10, 30), 100 | ) 101 | }) 102 | } 103 | -------------------------------------------------------------------------------- /ptr.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | /* 4 | Takes an arbitrary value and returns a non-nil pointer to a new memory region 5 | containing a shallow copy of that value. 6 | */ 7 | func Ptr[A any](val A) *A { return &val } 8 | 9 | /* 10 | If the pointer is nil, uses `new` to allocate a new value of the given type, 11 | returning the resulting pointer. Otherwise returns the input as-is. 12 | */ 13 | func PtrInited[A any](val *A) *A { 14 | if val != nil { 15 | return val 16 | } 17 | return new(A) 18 | } 19 | 20 | /* 21 | If the outer pointer is nil, returns nil. If the inner pointer is nil, uses 22 | `new` to allocate a new value, sets and returns the resulting new pointer. 23 | Otherwise returns the inner pointer as-is. 24 | */ 25 | func PtrInit[A any](val **A) *A { 26 | if val == nil { 27 | return nil 28 | } 29 | if *val == nil { 30 | *val = new(A) 31 | } 32 | return *val 33 | } 34 | 35 | /* 36 | Zeroes the memory referenced by the given pointer. If the pointer is nil, does 37 | nothing. Also see the interface `Clearer` and method `.Clear` implemented by 38 | various types. 39 | 40 | Note the difference from built-in `clear` (Go 1.21+): when receiving a pointer 41 | to a map or slice, this sets the target to `nil`, erasing the existing 42 | capacity, while built-in `clear` would delete all map elements or zero all 43 | slice elements (preserving length). 44 | */ 45 | func PtrClear[A any](val *A) { 46 | if val != nil { 47 | *val = Zero[A]() 48 | } 49 | } 50 | 51 | // Calls `PtrClear` and returns the same pointer. 52 | func PtrCleared[A any](val *A) *A { 53 | PtrClear(val) 54 | return val 55 | } 56 | 57 | // If the pointer is non-nil, dereferences it. Otherwise returns zero value. 58 | func PtrGet[A any](val *A) A { 59 | if val != nil { 60 | return *val 61 | } 62 | return Zero[A]() 63 | } 64 | 65 | // If the pointer is nil, does nothing. If non-nil, set the given value. 66 | func PtrSet[A any](tar *A, val A) { 67 | if tar != nil { 68 | *tar = val 69 | } 70 | } 71 | 72 | /* 73 | Takes two pointers and copies the value from source to target if both pointers 74 | are non-nil. If either is nil, does nothing. 75 | */ 76 | func PtrSetOpt[A any](tar, src *A) { 77 | if tar != nil && src != nil { 78 | *tar = *src 79 | } 80 | } 81 | 82 | /* 83 | If the pointer is non-nil, returns its value while zeroing the destination. 84 | Otherwise returns zero value. 85 | */ 86 | func PtrPop[A any](src *A) (out A) { 87 | if src != nil { 88 | out, *src = *src, out 89 | } 90 | return 91 | } 92 | -------------------------------------------------------------------------------- /ptr_test.go: -------------------------------------------------------------------------------- 1 | package gg_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mitranim/gg" 7 | "github.com/mitranim/gg/gtest" 8 | ) 9 | 10 | func TestPtrInited(t *testing.T) { 11 | defer gtest.Catch(t) 12 | 13 | gtest.NotZero(gg.PtrInited((*string)(nil))) 14 | 15 | src := new(string) 16 | gtest.Eq(gg.PtrInited(src), src) 17 | } 18 | 19 | func TestPtrInit(t *testing.T) { 20 | defer gtest.Catch(t) 21 | 22 | gtest.Zero(gg.PtrInit((**string)(nil))) 23 | 24 | var tar *string 25 | gtest.Eq(gg.PtrInit(&tar), tar) 26 | gtest.NotZero(tar) 27 | } 28 | 29 | func TestPtrClear(t *testing.T) { 30 | defer gtest.Catch(t) 31 | 32 | gtest.NotPanic(func() { 33 | gg.PtrClear((*string)(nil)) 34 | }) 35 | 36 | val := `str` 37 | gg.PtrClear(&val) 38 | gtest.Equal(val, ``) 39 | } 40 | 41 | func BenchmarkPtrClear(b *testing.B) { 42 | var val string 43 | 44 | for ind := 0; ind < b.N; ind++ { 45 | gg.PtrClear(&val) 46 | val = `str` 47 | } 48 | } 49 | 50 | func TestPtrGet(t *testing.T) { 51 | defer gtest.Catch(t) 52 | 53 | gtest.Eq(gg.PtrGet((*string)(nil)), ``) 54 | gtest.Eq(gg.PtrGet(new(string)), ``) 55 | gtest.Eq(gg.PtrGet(gg.Ptr(`str`)), `str`) 56 | 57 | gtest.Eq(gg.PtrGet((*int)(nil)), 0) 58 | gtest.Eq(gg.PtrGet(new(int)), 0) 59 | gtest.Eq(gg.PtrGet(gg.Ptr(10)), 10) 60 | } 61 | 62 | func BenchmarkPtrGet_miss(b *testing.B) { 63 | for ind := 0; ind < b.N; ind++ { 64 | gg.Nop1(gg.PtrGet((*[]string)(nil))) 65 | } 66 | } 67 | 68 | func BenchmarkPtrGet_hit(b *testing.B) { 69 | ptr := gg.Ptr([]string{`one`, `two`}) 70 | 71 | for ind := 0; ind < b.N; ind++ { 72 | gg.Nop1(gg.PtrGet(ptr)) 73 | } 74 | } 75 | 76 | func TestPtrSet(t *testing.T) { 77 | defer gtest.Catch(t) 78 | 79 | gtest.NotPanic(func() { 80 | gg.PtrSet((*string)(nil), ``) 81 | gg.PtrSet((*string)(nil), `str`) 82 | }) 83 | 84 | var tar string 85 | 86 | gg.PtrSet(&tar, `one`) 87 | gtest.Eq(tar, `one`) 88 | 89 | gg.PtrSet(&tar, `two`) 90 | gtest.Eq(tar, `two`) 91 | } 92 | 93 | func TestPtrSetOpt(t *testing.T) { 94 | defer gtest.Catch(t) 95 | 96 | gtest.NotPanic(func() { 97 | gg.PtrSetOpt((*string)(nil), (*string)(nil)) 98 | gg.PtrSetOpt(new(string), (*string)(nil)) 99 | gg.PtrSetOpt((*string)(nil), new(string)) 100 | }) 101 | 102 | var tar string 103 | gg.PtrSetOpt(&tar, gg.Ptr(`one`)) 104 | gtest.Eq(tar, `one`) 105 | 106 | gg.PtrSetOpt(&tar, gg.Ptr(`two`)) 107 | gtest.Eq(tar, `two`) 108 | } 109 | 110 | func TestPtrPop(t *testing.T) { 111 | defer gtest.Catch(t) 112 | 113 | test := func(src *string, exp string) { 114 | gtest.Eq(gg.PtrPop(src), exp) 115 | } 116 | 117 | test(nil, ``) 118 | test(gg.Ptr(``), ``) 119 | test(gg.Ptr(`val`), `val`) 120 | } 121 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | Essential utilities missing from the Go standard library. 4 | 5 | Docs: https://pkg.go.dev/github.com/mitranim/gg 6 | 7 | Some features: 8 | 9 | * Designed for Go 1.18. Takes massive advantage of generics. 10 | * Errors with stack traces. 11 | * Checked math. 12 | * Various functional programming utilities: map, filter, fold, and more. 13 | * Various shortcuts for reflection. 14 | * Various shortcuts for manipulating slices. 15 | * Various shortcuts for manipulating maps. 16 | * Various shortcuts for manipulating strings. 17 | * Common-sense generic data types: zero optionals, true optionals, sets, indexed collections, and more. 18 | * Various utilities for exception-style error handling, using `panic` and `recover`. 19 | * CLI flag parsing. 20 | * Various utilities for testing. 21 | * Assertion shortcuts with descriptive errors and full stack traces. 22 | * Carefully designed for compatibility with standard interfaces and interfaces commonly supported by 3rd parties. 23 | * No over-modularization. 24 | * No external dependencies. 25 | 26 | Submodules: 27 | 28 | * `gtest`: testing and assertion tools. 29 | * `grepr`: tools for printing Go data structures as Go code. 30 | * `gsql`: SQL tools: 31 | * Support for scanning SQL rows into Go structs. 32 | * Support for SQL arrays. 33 | 34 | Current limitations: 35 | 36 | * Not fully documented. 37 | 38 | ## License 39 | 40 | https://unlicense.org 41 | -------------------------------------------------------------------------------- /reflect_internal.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import r "reflect" 4 | 5 | const expectedStructNesting = 8 6 | 7 | func cloneArray(src r.Value) { 8 | if !(src.Cap() > 0) || !IsIndirect(src.Type().Elem()) { 9 | return 10 | } 11 | 12 | for ind := range Iter(src.Len()) { 13 | ValueClone(src.Index(ind)) 14 | } 15 | } 16 | 17 | func clonedArray(src r.Value) r.Value { 18 | if !(src.Cap() > 0) || !IsIndirect(src.Type().Elem()) { 19 | return src 20 | } 21 | 22 | out := NewElem(src.Type()) 23 | r.Copy(out, src) 24 | cloneArray(out) 25 | return out 26 | } 27 | 28 | /* 29 | Known defect: when cloning, in addition to allocating a new backing array, this 30 | allocates a slice header, which could theoretically be avoided if we could make 31 | just a backing array of the required size and replace the array pointer in the 32 | slice header we already have. 33 | */ 34 | func cloneSlice(src r.Value) { ValueSet(src, clonedSlice(src)) } 35 | 36 | func clonedSlice(src r.Value) r.Value { 37 | if src.IsNil() || !(src.Cap() > 0) { 38 | return src 39 | } 40 | 41 | out := r.MakeSlice(src.Type(), src.Len(), src.Cap()) 42 | r.Copy(out, src) 43 | cloneArray(out) 44 | return out 45 | } 46 | 47 | func cloneInterface(src r.Value) { ValueSet(src, clonedInterface(src)) } 48 | 49 | func clonedInterface(src r.Value) r.Value { 50 | if src.IsNil() { 51 | return src 52 | } 53 | 54 | elem0 := src.Elem() 55 | elem1 := ValueCloned(elem0) 56 | if elem0 == elem1 { 57 | return elem0 58 | } 59 | return elem1.Convert(src.Type()) 60 | } 61 | 62 | func cloneMap(src r.Value) { ValueSet(src, clonedMap(src)) } 63 | 64 | func clonedMap(src r.Value) r.Value { 65 | if src.IsNil() { 66 | return src 67 | } 68 | 69 | out := r.MakeMapWithSize(src.Type(), src.Len()) 70 | iter := src.MapRange() 71 | for iter.Next() { 72 | out.SetMapIndex(ValueCloned(iter.Key()), ValueCloned(iter.Value())) 73 | } 74 | return out 75 | } 76 | 77 | func clonePointer(src r.Value) { ValueSet(src, clonedPointer(src)) } 78 | 79 | func clonedPointer(src r.Value) r.Value { 80 | if src.IsNil() { 81 | return src 82 | } 83 | 84 | out := r.New(src.Type().Elem()) 85 | out.Elem().Set(src.Elem()) 86 | ValueClone(out.Elem()) 87 | return out 88 | } 89 | 90 | func cloneStruct(src r.Value) { 91 | for _, field := range StructPublicFieldCache.Get(src.Type()) { 92 | ValueClone(src.FieldByIndex(field.Index)) 93 | } 94 | } 95 | 96 | func clonedStruct(src r.Value) r.Value { 97 | if !IsIndirect(src.Type()) { 98 | return src 99 | } 100 | 101 | out := NewElem(src.Type()) 102 | out.Set(src) 103 | cloneStruct(out) 104 | return out 105 | } 106 | 107 | func growLenReflect(tar r.Value) { 108 | len, cap := tar.Len(), tar.Cap() 109 | if cap > len { 110 | tar.SetLen(len + 1) 111 | return 112 | } 113 | 114 | buf := r.MakeSlice(tar.Type(), len+1, MaxPrim(len*2, 4)) 115 | r.Copy(buf, tar) 116 | tar.Set(buf) 117 | } 118 | -------------------------------------------------------------------------------- /rnd.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import ( 4 | "crypto/rand" 5 | "io" 6 | "math/big" 7 | ) 8 | 9 | /* 10 | Generates a random integer of the given type, using the given entropy source 11 | (typically `"crypto/rand".Reader`). 12 | */ 13 | func RandomInt[A Int](src io.Reader) (out A) { 14 | Try1(src.Read(AsBytes(&out))) 15 | return out 16 | } 17 | 18 | /* 19 | Generates a random integer in the range `[min,max)`, using the given entropy 20 | source (typically `"crypto/rand".Reader`). All numbers must be below 21 | `math.MaxInt64`. 22 | */ 23 | func RandomIntBetween[A Int](src io.Reader, min, max A) A { 24 | if !(max > min) { 25 | panic(Errf(`invalid range [%v,%v)`, min, max)) 26 | } 27 | 28 | // The following is suboptimal. See implementation notes below. 29 | minInt := NumConv[int64](min) 30 | maxInt := NumConv[int64](max) 31 | maxBig := new(big.Int).SetInt64(maxInt - minInt) 32 | tarBig := Try1(rand.Int(src, maxBig)) 33 | return min + NumConv[A](tarBig.Int64()) 34 | } 35 | 36 | /* 37 | The following implementation doesn't fully pass our test. It performs marginally 38 | better than the wrapper around the "crypto/rand" version. TODO fix. Also TODO 39 | generalize for all int types. 40 | 41 | func RandomUint64Between(src io.Reader, min, max uint64) (out uint64) { 42 | if !(max > min) { 43 | panic(Errf(`invalid range [%v,%v)`, min, max)) 44 | } 45 | 46 | ceil := max - min 47 | bits := bits.Len64(ceil) 48 | buf := AsBytes(&out)[:(bits+7)/8] 49 | 50 | for { 51 | Try1(src.Read(buf)) 52 | buf[0] >>= (8 - (bits % 8)) 53 | 54 | out = 0 55 | for _, byte := range buf { 56 | out = (out << 8) | uint64(byte) 57 | } 58 | if out < ceil { 59 | return out + min 60 | } 61 | } 62 | } 63 | */ 64 | 65 | /* 66 | Picks a random element from the given slice, using the given entropy source 67 | (typically `"crypto/rand".Reader`). Panics if the slice is empty or the reader 68 | is nil. 69 | */ 70 | func RandomElem[A any](reader io.Reader, slice []A) A { 71 | return slice[RandomIntBetween(reader, 0, len(slice))] 72 | } 73 | -------------------------------------------------------------------------------- /rnd_test.go: -------------------------------------------------------------------------------- 1 | package gg_test 2 | 3 | import ( 4 | cr "crypto/rand" 5 | "fmt" 6 | "io" 7 | "math/big" 8 | mr "math/rand" 9 | "testing" 10 | 11 | "github.com/mitranim/gg" 12 | "github.com/mitranim/gg/gtest" 13 | ) 14 | 15 | func rndSrc() io.Reader { return mr.New(mr.NewSource(0)) } 16 | 17 | func TestRandomInt_uint64(t *testing.T) { 18 | defer gtest.Catch(t) 19 | 20 | fun := gg.RandomInt[uint64] 21 | src := rndSrc() 22 | 23 | gtest.Eq(fun(src), 13906042503472976897) 24 | gtest.Eq(fun(src), 14443988502964065089) 25 | gtest.Eq(fun(src), 17196259562495758190) 26 | gtest.Eq(fun(src), 8433884527138253544) 27 | gtest.Eq(fun(src), 16185558041432496379) 28 | gtest.Eq(fun(src), 15280578644808371633) 29 | gtest.Eq(fun(src), 16279533959527364769) 30 | gtest.Eq(fun(src), 5680856659213489233) 31 | gtest.Eq(fun(src), 18012265314398557154) 32 | gtest.Eq(fun(src), 12810001989293853876) 33 | gtest.Eq(fun(src), 8828672906944723960) 34 | gtest.Eq(fun(src), 11259781176380201387) 35 | gtest.Eq(fun(src), 6266933393232556850) 36 | gtest.Eq(fun(src), 8632501143404108278) 37 | gtest.Eq(fun(src), 6856693871787269831) 38 | gtest.Eq(fun(src), 6107792380522581863) 39 | } 40 | 41 | func TestRandomInt_int64(t *testing.T) { 42 | defer gtest.Catch(t) 43 | 44 | fun := gg.RandomInt[int64] 45 | src := rndSrc() 46 | 47 | gtest.Eq(fun(src), -4540701570236574719) 48 | gtest.Eq(fun(src), -4002755570745486527) 49 | gtest.Eq(fun(src), -1250484511213793426) 50 | gtest.Eq(fun(src), 8433884527138253544) 51 | gtest.Eq(fun(src), -2261186032277055237) 52 | gtest.Eq(fun(src), -3166165428901179983) 53 | gtest.Eq(fun(src), -2167210114182186847) 54 | gtest.Eq(fun(src), 5680856659213489233) 55 | gtest.Eq(fun(src), -434478759310994462) 56 | gtest.Eq(fun(src), -5636742084415697740) 57 | gtest.Eq(fun(src), 8828672906944723960) 58 | gtest.Eq(fun(src), -7186962897329350229) 59 | gtest.Eq(fun(src), 6266933393232556850) 60 | gtest.Eq(fun(src), 8632501143404108278) 61 | gtest.Eq(fun(src), 6856693871787269831) 62 | gtest.Eq(fun(src), 6107792380522581863) 63 | } 64 | 65 | func BenchmarkRandomInt_true_random(b *testing.B) { 66 | defer gtest.Catch(b) 67 | 68 | for ind := 0; ind < b.N; ind++ { 69 | gg.RandomInt[uint64](cr.Reader) 70 | } 71 | } 72 | 73 | func BenchmarkRandomInt_pseudo_random(b *testing.B) { 74 | defer gtest.Catch(b) 75 | src := rndSrc() 76 | b.ResetTimer() 77 | 78 | for ind := 0; ind < b.N; ind++ { 79 | gg.RandomInt[uint64](src) 80 | } 81 | } 82 | 83 | func TestRandomIntBetween_uint64(t *testing.T) { 84 | defer gtest.Catch(t) 85 | testRandomUintBetween[uint64]() 86 | } 87 | 88 | func TestRandomIntBetween_int64(t *testing.T) { 89 | defer gtest.Catch(t) 90 | testRandomUintBetween[int64]() 91 | testRandomSintBetween[int64]() 92 | } 93 | 94 | func testRandomUintBetween[A gg.Int]() { 95 | fun := gg.RandomIntBetween[A] 96 | 97 | gtest.PanicStr(`invalid range [0,0)`, func() { 98 | fun(rndSrc(), 0, 0) 99 | }) 100 | 101 | gtest.PanicStr(`invalid range [1,1)`, func() { 102 | fun(rndSrc(), 1, 1) 103 | }) 104 | 105 | gtest.PanicStr(`invalid range [2,1)`, func() { 106 | fun(rndSrc(), 2, 1) 107 | }) 108 | 109 | testRandomIntRange[A](0, 1) 110 | testRandomIntRange[A](0, 2) 111 | testRandomIntRange[A](0, 3) 112 | testRandomIntRange[A](0, 16) 113 | testRandomIntRange[A](32, 48) 114 | 115 | testRandomIntRanges[A](0, 16) 116 | testRandomIntRanges[A](32, 48) 117 | } 118 | 119 | func testRandomSintBetween[A gg.Sint]() { 120 | gtest.PanicStr(`invalid range [1,-1)`, func() { 121 | gg.RandomIntBetween[A](rndSrc(), 1, -1) 122 | }) 123 | 124 | gtest.PanicStr(`invalid range [2,-2)`, func() { 125 | gg.RandomIntBetween[A](rndSrc(), 2, -2) 126 | }) 127 | 128 | testRandomIntRange[A](-10, 10) 129 | testRandomIntRanges[A](-10, 10) 130 | } 131 | 132 | func testRandomIntRange[A gg.Int](min, max A) { 133 | gtest.EqualSet( 134 | randomIntSlice(128, min, max), 135 | gg.Range(min, max), 136 | fmt.Sprintf(`expected range: [%v,%v)`, min, max), 137 | ) 138 | } 139 | 140 | func testRandomIntRanges[A gg.Int](min, max int) { 141 | for _, max := range gg.Range(min+1, max) { 142 | for _, min := range gg.Range(min, max) { 143 | testRandomIntRange(gg.NumConv[A](min), gg.NumConv[A](max)) 144 | } 145 | } 146 | } 147 | 148 | func randomIntSlice[A gg.Int](count int, min, max A) []A { 149 | src := rndSrc() 150 | var set gg.OrdSet[A] 151 | for range gg.Iter(count) { 152 | set.Add(gg.RandomIntBetween(src, min, max)) 153 | } 154 | return set.Slice 155 | } 156 | 157 | func Benchmark_random_int_between_stdlib_true_random(b *testing.B) { 158 | defer gtest.Catch(b) 159 | max := new(big.Int).SetUint64(2 << 16) 160 | b.ResetTimer() 161 | 162 | for ind := 0; ind < b.N; ind++ { 163 | gg.Try1(cr.Int(cr.Reader, max)) 164 | } 165 | } 166 | 167 | func Benchmark_random_int_between_stdlib_pseudo_random(b *testing.B) { 168 | defer gtest.Catch(b) 169 | src := rndSrc() 170 | max := new(big.Int).SetUint64(2 << 16) 171 | b.ResetTimer() 172 | 173 | for ind := 0; ind < b.N; ind++ { 174 | gg.Try1(cr.Int(src, max)) 175 | } 176 | } 177 | 178 | func Benchmark_random_int_between_ours_true_random(b *testing.B) { 179 | defer gtest.Catch(b) 180 | 181 | for ind := 0; ind < b.N; ind++ { 182 | gg.RandomIntBetween(cr.Reader, 0, 2<<16) 183 | } 184 | } 185 | 186 | func Benchmark_random_int_between_ours_pseudo_random(b *testing.B) { 187 | defer gtest.Catch(b) 188 | src := rndSrc() 189 | b.ResetTimer() 190 | 191 | for ind := 0; ind < b.N; ind++ { 192 | gg.RandomIntBetween(src, 0, 2<<16) 193 | } 194 | } 195 | 196 | func TestRandomElem(t *testing.T) { 197 | defer gtest.Catch(t) 198 | 199 | gtest.PanicStr(`invalid range [0,0)`, func() { 200 | gg.RandomElem(rndSrc(), []int{}) 201 | }) 202 | 203 | testRandomElem([]int{10}) 204 | testRandomElem([]int{10, 20}) 205 | testRandomElem([]int{10, 20, 30}) 206 | testRandomElem([]int{10, 20, 30, 40}) 207 | testRandomElem([]int{10, 20, 30, 40, 50}) 208 | testRandomElem([]int{10, 20, 30, 40, 50, 60}) 209 | } 210 | 211 | func testRandomElem[A comparable](slice []A) { 212 | src := rndSrc() 213 | 214 | var set gg.OrdSet[A] 215 | for range gg.Iter(64) { 216 | set.Add(gg.RandomElem(src, slice)) 217 | } 218 | 219 | gtest.EqualSet(set.Slice, slice) 220 | } 221 | -------------------------------------------------------------------------------- /set.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import "encoding/json" 4 | 5 | /* 6 | Syntactic shortcut for making a set from a slice, with element type inference 7 | and capacity preallocation. Always returns non-nil, even if the input is 8 | empty. 9 | */ 10 | func SetOf[A comparable](val ...A) Set[A] { 11 | return make(Set[A], len(val)).Add(val...) 12 | } 13 | 14 | /* 15 | Syntactic shortcut for making a set from multiple slices, with element type 16 | inference and capacity preallocation. Always returns non-nil, even if the input 17 | is empty. 18 | */ 19 | func SetFrom[Slice ~[]Elem, Elem comparable](val ...Slice) Set[Elem] { 20 | buf := make(Set[Elem], Lens(val...)) 21 | for _, val := range val { 22 | buf.Add(val...) 23 | } 24 | return buf 25 | } 26 | 27 | /* 28 | Creates a set by "mapping" the elements of a given slice via the provided 29 | function. Always returns non-nil, even if the input is empty. 30 | */ 31 | func SetMapped[Elem any, Val comparable](src []Elem, fun func(Elem) Val) Set[Val] { 32 | buf := make(Set[Val], len(src)) 33 | if fun != nil { 34 | for _, val := range src { 35 | buf[fun(val)] = struct{}{} 36 | } 37 | } 38 | return buf 39 | } 40 | 41 | // Generic unordered set backed by a map. 42 | type Set[A comparable] map[A]struct{} 43 | 44 | /* 45 | Idempotently inits the map via `make`, making it writable. 46 | The output is always non-nil. 47 | */ 48 | func (self *Set[A]) Init() Set[A] { 49 | if *self == nil { 50 | *self = make(Set[A]) 51 | } 52 | return *self 53 | } 54 | 55 | // Same as `len(set)`. Nil-safe. 56 | func (self Set[_]) Len() int { return len(self) } 57 | 58 | // Same as `len(self) <= 0`. Inverse of `.IsNotEmpty`. 59 | func (self Set[_]) IsEmpty() bool { return len(self) <= 0 } 60 | 61 | // Same as `len(self) > 0`. Inverse of `.IsEmpty`. 62 | func (self Set[_]) IsNotEmpty() bool { return len(self) > 0 } 63 | 64 | // True if the set includes the given value. Nil-safe. 65 | func (self Set[A]) Has(val A) bool { return MapHas(self, val) } 66 | 67 | // Idempotently adds the given values to the receiver, which must be non-nil. 68 | func (self Set[A]) Add(val ...A) Set[A] { 69 | for _, val := range val { 70 | self[val] = struct{}{} 71 | } 72 | return self 73 | } 74 | 75 | /* 76 | Set union. Idempotently adds all values from the given source sets to the 77 | receiver, which must be non-nil. 78 | */ 79 | func (self Set[A]) AddFrom(val ...Set[A]) Set[A] { 80 | for _, val := range val { 81 | for val := range val { 82 | self[val] = struct{}{} 83 | } 84 | } 85 | return self 86 | } 87 | 88 | // Deletes the given values from the receiver, which may be nil. 89 | func (self Set[A]) Del(val ...A) Set[A] { 90 | for _, val := range val { 91 | delete(self, val) 92 | } 93 | return self 94 | } 95 | 96 | /* 97 | Deletes all values present in the given source sets from the receiver, which may 98 | be nil. 99 | */ 100 | func (self Set[A]) DelFrom(val ...Set[A]) Set[A] { 101 | for _, val := range val { 102 | for val := range val { 103 | delete(self, val) 104 | } 105 | } 106 | return self 107 | } 108 | 109 | /* 110 | Clears and returns the receiver, which may be nil. Note that this type is 111 | implemented as a map, and this method involves iterating the map, which is 112 | inefficient in Go. In many cases, it's more efficient to make a new set. 113 | */ 114 | func (self Set[A]) Clear() Set[A] { 115 | for val := range self { 116 | delete(self, val) 117 | } 118 | return self 119 | } 120 | 121 | // Combination of `Set.Clear` and `Set.Add`. 122 | func (self Set[A]) Reset(val ...A) Set[A] { 123 | self.Clear() 124 | self.Add(val...) 125 | return self 126 | } 127 | 128 | // Converts the map to a slice of its values. Order is random. 129 | func (self Set[A]) Slice() []A { return MapKeys(self) } 130 | 131 | /* 132 | Returns the subset of values for which the given function returns true. 133 | Order is random. If function is nil, output is nil. 134 | */ 135 | func (self Set[A]) Filter(fun func(A) bool) []A { 136 | var out []A 137 | if fun != nil { 138 | for val := range self { 139 | if fun(val) { 140 | out = append(out, val) 141 | } 142 | } 143 | } 144 | return out 145 | } 146 | 147 | // JSON-encodes as a list. Order is random. 148 | func (self Set[A]) MarshalJSON() ([]byte, error) { 149 | return json.Marshal(self.Slice()) 150 | } 151 | 152 | /* 153 | JSON-decodes the input, which must either represent JSON "null" or a JSON list 154 | of values compatible with the value type. 155 | */ 156 | func (self *Set[A]) UnmarshalJSON(src []byte) error { 157 | var buf []A 158 | err := json.Unmarshal(src, &buf) 159 | if err != nil { 160 | return err 161 | } 162 | 163 | self.Init().Reset(buf...) 164 | return nil 165 | } 166 | 167 | // Implement `fmt.GoStringer`, returning valid Go code that constructs the set. 168 | func (self Set[A]) GoString() string { 169 | typ := TypeOf(self).String() 170 | 171 | if self == nil { 172 | return typ + `(nil)` 173 | } 174 | 175 | if len(self) <= 0 { 176 | return typ + `{}` 177 | } 178 | 179 | var buf Buf 180 | buf.AppendString(typ) 181 | buf.AppendString(`{}.Add(`) 182 | 183 | var found bool 184 | for val := range self { 185 | if found { 186 | buf.AppendString(`, `) 187 | } 188 | found = true 189 | buf.AppendGoString(val) 190 | } 191 | 192 | buf.AppendString(`)`) 193 | return buf.String() 194 | } 195 | -------------------------------------------------------------------------------- /set_test.go: -------------------------------------------------------------------------------- 1 | package gg_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mitranim/gg" 7 | "github.com/mitranim/gg/gtest" 8 | ) 9 | 10 | func TestSetOf(t *testing.T) { 11 | defer gtest.Catch(t) 12 | 13 | gtest.Equal(gg.SetOf[int](), IntSet{}) 14 | gtest.Equal(gg.SetOf(10), IntSet{10: void}) 15 | gtest.Equal(gg.SetOf(10, 20), IntSet{10: void, 20: void}) 16 | gtest.Equal(gg.SetOf(10, 20, 30), IntSet{10: void, 20: void, 30: void}) 17 | gtest.Equal(gg.SetOf(10, 20, 30, 10, 20), IntSet{10: void, 20: void, 30: void}) 18 | } 19 | 20 | func BenchmarkSetOf_empty(b *testing.B) { 21 | for ind := 0; ind < b.N; ind++ { 22 | gg.Nop1(gg.SetOf[int]()) 23 | } 24 | } 25 | 26 | func BenchmarkSetOf_non_empty(b *testing.B) { 27 | for ind := 0; ind < b.N; ind++ { 28 | gg.Nop1(gg.SetOf(10, 20, 30, 40, 50, 60, 70, 80, 90)) 29 | } 30 | } 31 | 32 | func TestSet(t *testing.T) { 33 | defer gtest.Catch(t) 34 | 35 | t.Run(`SetOf`, func(t *testing.T) { 36 | defer gtest.Catch(t) 37 | 38 | gtest.Equal(gg.SetOf[int](), IntSet{}) 39 | gtest.Equal(gg.SetOf[int](10), IntSet{10: void}) 40 | gtest.Equal(gg.SetOf[int](10, 20), IntSet{10: void, 20: void}) 41 | }) 42 | 43 | t.Run(`Add`, func(t *testing.T) { 44 | defer gtest.Catch(t) 45 | 46 | set := IntSet{} 47 | gtest.Equal(set, IntSet{}) 48 | 49 | set.Add(10) 50 | gtest.Equal(set, IntSet{10: void}) 51 | 52 | set.Add(20, 30) 53 | gtest.Equal(set, IntSet{10: void, 20: void, 30: void}) 54 | }) 55 | 56 | t.Run(`Clear`, func(t *testing.T) { 57 | defer gtest.Catch(t) 58 | 59 | gtest.Equal(IntSet{10: void, 20: void}.Clear(), IntSet{}) 60 | }) 61 | 62 | t.Run(`Reset`, func(t *testing.T) { 63 | defer gtest.Catch(t) 64 | 65 | set := IntSet{} 66 | gtest.Equal(set, IntSet{}) 67 | 68 | set.Add(10) 69 | gtest.Equal(set, IntSet{10: void}) 70 | 71 | set.Reset(20, 30) 72 | gtest.Equal(set, IntSet{20: void, 30: void}) 73 | }) 74 | 75 | // TODO test multiple values (issue: ordering). 76 | t.Run(`Slice`, func(t *testing.T) { 77 | defer gtest.Catch(t) 78 | 79 | gtest.Zero(IntSet(nil).Slice()) 80 | gtest.Equal(IntSet{}.Slice(), []int{}) 81 | gtest.Equal(IntSet{10: void}.Slice(), []int{10}) 82 | }) 83 | 84 | // TODO test multiple values (issue: ordering). 85 | t.Run(`Filter`, func(t *testing.T) { 86 | defer gtest.Catch(t) 87 | 88 | gtest.Zero(IntSet(nil).Filter(gg.IsPos[int])) 89 | gtest.Zero(IntSet{}.Filter(gg.IsPos[int])) 90 | gtest.Zero(IntSet{-10: void}.Filter(gg.IsPos[int])) 91 | gtest.Equal(IntSet{10: void}.Filter(gg.IsPos[int]), []int{10}) 92 | }) 93 | 94 | // TODO test multiple values (issue: ordering). 95 | t.Run(`MarshalJSON`, func(t *testing.T) { 96 | defer gtest.Catch(t) 97 | 98 | test := func(set IntSet, exp string) { 99 | gtest.Equal(gg.JsonString(set), exp) 100 | } 101 | 102 | test(IntSet(nil), `null`) 103 | test(IntSet{}, `[]`) 104 | test(IntSet{}.Add(10), `[10]`) 105 | }) 106 | 107 | // TODO test multiple values (issue: ordering). 108 | t.Run(`UnmarshalJSON`, func(t *testing.T) { 109 | defer gtest.Catch(t) 110 | 111 | test := func(src string, exp IntSet) { 112 | gtest.Equal(gg.JsonDecodeTo[IntSet](src), exp) 113 | } 114 | 115 | test(`[]`, IntSet{}) 116 | test(`[10]`, IntSet{}.Add(10)) 117 | }) 118 | 119 | // TODO test multiple values (issue: ordering). 120 | t.Run(`GoString`, func(t *testing.T) { 121 | defer gtest.Catch(t) 122 | 123 | gtest.Eq(gg.GoString(IntSet(nil)), `gg.Set[int](nil)`) 124 | gtest.Eq(gg.GoString(IntSet{}), `gg.Set[int]{}`) 125 | gtest.Eq(gg.GoString(IntSet{}.Add(10)), `gg.Set[int]{}.Add(10)`) 126 | }) 127 | } 128 | 129 | func Benchmark_Set_GoString(b *testing.B) { 130 | val := gg.SetOf(10, 20, 30) 131 | 132 | for ind := 0; ind < b.N; ind++ { 133 | gg.Nop1(val.GoString()) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /sync_test.go: -------------------------------------------------------------------------------- 1 | package gg_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mitranim/gg" 7 | "github.com/mitranim/gg/gtest" 8 | ) 9 | 10 | // Placeholder, needs a concurrency test. 11 | func TestAtom(t *testing.T) { 12 | defer gtest.Catch(t) 13 | 14 | var ref gg.Atom[string] 15 | gtest.Zero(ref.Load()) 16 | gtest.Eq( 17 | gg.Tuple2(ref.Loaded()), 18 | gg.Tuple2(``, false), 19 | ) 20 | 21 | ref.Store(``) 22 | gtest.Zero(ref.Load()) 23 | gtest.Eq( 24 | gg.Tuple2(ref.Loaded()), 25 | gg.Tuple2(``, true), 26 | ) 27 | 28 | ref.Store(`one`) 29 | gtest.Eq(ref.Load(), `one`) 30 | gtest.Eq( 31 | gg.Tuple2(ref.Loaded()), 32 | gg.Tuple2(`one`, true), 33 | ) 34 | 35 | gtest.False(ref.CompareAndSwap(`three`, `two`)) 36 | gtest.Eq(ref.Load(), `one`) 37 | 38 | gtest.True(ref.CompareAndSwap(`one`, `two`)) 39 | gtest.Eq(ref.Load(), `two`) 40 | } 41 | 42 | func BenchmarkAtom_Store(b *testing.B) { 43 | var ref gg.Atom[string] 44 | 45 | for ind := 0; ind < b.N; ind++ { 46 | ref.Store(`str`) 47 | } 48 | } 49 | 50 | func BenchmarkAtom_Load(b *testing.B) { 51 | var ref gg.Atom[string] 52 | ref.Store(`str`) 53 | 54 | for ind := 0; ind < b.N; ind++ { 55 | gg.Nop1(ref.Load()) 56 | } 57 | } 58 | 59 | func TestChanInit(t *testing.T) { 60 | defer gtest.Catch(t) 61 | 62 | gg.ChanInit((*chan string)(nil)) 63 | 64 | var tar chan string 65 | gtest.Eq(gg.ChanInit(&tar), tar) 66 | 67 | gtest.NotZero(tar) 68 | gtest.Eq(cap(tar), 0) 69 | 70 | prev := tar 71 | gtest.Eq(gg.ChanInit(&tar), prev) 72 | gtest.Eq(tar, prev) 73 | } 74 | 75 | func TestChanInitCap(t *testing.T) { 76 | defer gtest.Catch(t) 77 | 78 | gg.ChanInitCap((*chan string)(nil), 1) 79 | 80 | var tar chan string 81 | gtest.Eq(gg.ChanInitCap(&tar, 3), tar) 82 | 83 | gtest.NotZero(tar) 84 | gtest.Eq(cap(tar), 3) 85 | 86 | prev := tar 87 | gtest.Eq(gg.ChanInitCap(&tar, 5), prev) 88 | gtest.Eq(tar, prev) 89 | gtest.Eq(cap(prev), 3) 90 | gtest.Eq(cap(tar), 3) 91 | } 92 | 93 | func TestSendOpt(t *testing.T) { 94 | defer gtest.Catch(t) 95 | 96 | var tar chan string 97 | gg.SendOpt(tar, `one`) 98 | gg.SendOpt(tar, `two`) 99 | gg.SendOpt(tar, `three`) 100 | 101 | tar = make(chan string, 1) 102 | gg.SendOpt(tar, `one`) 103 | gg.SendOpt(tar, `two`) 104 | gg.SendOpt(tar, `three`) 105 | 106 | gtest.Eq(<-tar, `one`) 107 | } 108 | 109 | func TestSendZeroOpt(t *testing.T) { 110 | defer gtest.Catch(t) 111 | 112 | var tar chan string 113 | gg.SendZeroOpt(tar) 114 | gg.SendZeroOpt(tar) 115 | gg.SendZeroOpt(tar) 116 | 117 | tar = make(chan string, 1) 118 | gg.SendZeroOpt(tar) 119 | gg.SendZeroOpt(tar) 120 | gg.SendZeroOpt(tar) 121 | 122 | val, ok := <-tar 123 | gtest.Zero(val) 124 | gtest.True(ok) 125 | } 126 | -------------------------------------------------------------------------------- /testdata/graph_file_long: -------------------------------------------------------------------------------- 1 | @import aaa7c30c9fe6494db244df541a415b8f 2 | @import ed33e824fe574a2f91712c1a1609df8c 3 | @import ebe9816ee8b14ce9bba478c8e0853581 4 | @import b6728a7d157e4984afb430ed2bf750b7 5 | @import f4f68f8f00dd45fcba1b2a97c1eafc94 6 | @import 5acde9df2bb348d1aeb55dbc8f06565c 7 | @import e6a34f990e2c4bbd85b13f46d96ed708 8 | @import 889b367cd42d42189a1b7d9d3f177e84 9 | @import 00ef58a6eca448c799d744ba5630fc48 10 | @import b737450984cd4daea11170364773e98c 11 | @import fb37e2f97f3f469080eacd08e29e99ad 12 | @import 09c3e5a78bf14e69b61b5c8b10db0bec 13 | @import e9dd168029cd441296ac6d918c8a95b5 14 | @import a83e48bad3eb414c89479bb6666b1e76 15 | @import d3316aeb511a4d9295f4b78a3e330bdc 16 | @import dac680dcf3fd4f0b99d0789cf396f777 17 | @import 42d2a4fb764445818d07e5fee726448d 18 | 19 | a1450d96842e4c9b9cba9534aee6a042 20 | 0c30066f8419420ba2f4d84eb1ec1411 21 | ddfbd4bbea6d43ae977f838de75e39ad 22 | 5925ae5b559141c29ead0876c31cbc01 23 | bc6f61fc0d8148b1ae816566deca46df 24 | 0319992511fa4923a90743af52b2f5d2 25 | 01a06913850e41b4a492776c781c9940 26 | e2021a249dd54af089276e33c4d70c61 27 | 78bbf88923a34bbfa0a6b9393c2a0c3c 28 | a39a67b8555746cb80847685bb9478bd 29 | ac45b8c203484ff0abe4ca7bca532ec9 30 | 920bb86af6f64057a7cf4a1bb16e3470 31 | 29556c6c63a049eba87ac8905052f43a 32 | b26edb4d11b242c5aa3b0fa338bcf811 33 | c4cd3827ae274161818dc6be0744be8f 34 | e912a2e1d6e7455fbe4f34eb3366da86 35 | e6c5b4d120dc4e6ab9dad896e7722cc0 36 | 8fd90ba820ea4da28651c488c5cd91dd 37 | 867d4f3e1a3b4979915a685151e7622f 38 | 672e4b3d930f4c019bc8943efe6dd9b2 39 | f2177b4755ff426aafeda396e4b60479 40 | 8dc3c11a9a9448ddb2eef401f4b8b304 41 | 93f59f15c3af4d6a81805d78a8aaf3e7 42 | f63a1953769a4f11be3b0f69ba1f5785 43 | 7d02b5b00d0045bc8654350103abf182 44 | 62e296a20f8444eca6aa5941048ca2c4 45 | 3ac262e1cc284387b5e373e6c5e6565f 46 | 7f0c1e355ae24e0aabd123d12dca3bb4 47 | 64d39b5cf8fd41a6840070a7baa2b263 48 | 349009b9a04543ad96b77b2423ceaa1d 49 | b1d0aabe4d824aafa3f0df80d141f140 50 | 03bd73d5dd434a84b76e6134ad751b07 51 | 2006e6eee2d6462ab1fa88c2c77bb00d 52 | a96eae99ee564048a23234c7f7551112 53 | e5ddff4ee6f74e29bd3b4785965a1200 54 | 9363d630fa7045c5b95459951d80c3f1 55 | 3dfd76830741433eadf98923d5623c03 56 | 33f9d77cd8b344b6af9ccccca3176ac7 57 | e123963c7b2a479ea2216e38a4903e66 58 | 8b7f592e84e64f4f9b60207c8251c4f0 59 | 0791ed68bf67459aafe85a8679d2c2a4 60 | db726040fcee47bca9f93de73e7acf51 61 | 3d24f1b6c24443b4a8217ea143856f89 62 | 377e367b730244d78f93cdff73f6fffd 63 | 7d5c9f8697e344218afd647e25aa2547 64 | 4374ce099c59414392c628ab74cb6f57 65 | f3f8bb5052964602a38dd59f3e958bc9 66 | 90fb2f5fe42947b1a66d9b2eef0a1132 67 | 9b6b39e01dba4be7814a8045b08f43a8 68 | 15ba85973ff84f29b1a79b5b7fd07843 69 | cecac020e52945f98ea8c0a2b9771c12 70 | ba7c34b037f34a2b92c0aa0c85b273e4 71 | 1f29878731c040c6a2e70e7f01f5971a 72 | f4763cf47d4f43009f881b3291907a7f 73 | 49b6cc06cf2846cb8a8f10daa8837d6c 74 | 1d0f2668aa9d478f93f1f365727efd7e 75 | 03d9cc0b36434f799998aafbf260ca46 76 | 1bce9f6a79e3473cb41ff30d02c5fb91 77 | ffc6147cec3c452c806b362083124ad0 78 | 5ddcc6c09bf442579bc507290466c196 79 | c6a4b283ad2d4c359e55b5ea516304fd 80 | c579c378660f409494285cc2759a9d5b 81 | 314e198824ce4c37a27edf883bed2524 82 | a99771ebeefa42b6a6a9bc29109cbdc3 83 | 07491da03fc742968ff3e9a56f3ba44d 84 | 5ff16a5062004f78966ff8b4fe7c895f 85 | c9838f61a49148988fe2a5504493f8a8 86 | 8c0b1c95c68e43db8d8fd8284beba641 87 | 4542f402815b49b8a2951830724c1d3f 88 | 65ddcd78705a4d52b18da5ae7a867af2 89 | f053cdfcac4b4535a3929cf4192f6085 90 | 3666d0122d84461aa02c08c23b6965f1 91 | 13dfb44ad4a24eefaccd283038bc4e73 92 | e6ba3fb835f64b7cb6ef71138f86e9b7 93 | b3af31e536c040e7899da74ea6abd270 94 | 9089804eb8754c0191ff80e4118c3132 95 | ca8f7176836f4002b130e2fe967a1135 96 | 473eb324ccef4f978715e250a25bb4d7 97 | 42a0e7f012974c769a3298ee7b41ff86 98 | 61a225019dee48e1a4c9ecff18971def 99 | 486dadca81ff494ba1e70c0b8b1a976d 100 | 1b0639840d6340d893cef027e3f72b2e 101 | cb2f575386bb46aab3f04b65afcafb73 102 | 685bef8cf8b8424ba842ef3964cf1f0d 103 | 364fd0dac5d64e3db2dd1a34a7a45a3f 104 | f3486105e0f148b8a9a78dd7bf4fc900 105 | 49cf5465246245c58dc40c8cad8651d1 106 | af3c7b53c1bc428d90b04160cc4370aa 107 | d50049f18cf0486facbe3b31ac17bb25 108 | 50a8cfcaa0ec4b5fa660cd7e87426745 109 | 1f3b3cb796544ddf84e2a9cb5af06358 110 | 630b806d5fb74db9b34149d99d884457 111 | 276551890e8d4ba2b43d466e4e0775ac 112 | d8be4aefb8b142dcb0543ea6b8bc40f5 113 | 49e33cfe7e134bb5a5c87057ef65cc7c 114 | 6af5be416430439cadd21a62caefefa7 115 | 3b79613b871a4c7ea90fa6ac5302b4e5 116 | 09d966e4cd894474b7713c57ae8955d9 117 | 2136e725650c46c8ac061d6ee8d377d1 118 | be3f062861994b429df64037ca230fe7 119 | 183d07b313a9469da49403ee7d0b4bd5 120 | de9d9bf6776e45f6814cbb4970b007c6 121 | fb1d3cfe89a84dbab4322a1b46af20ef 122 | c776ced7aa884bee8faea6c1dc08a67e 123 | 9815de1e399e4b4687298cc7968113a5 124 | 0254fd57dee24670a33baaae34fbfc87 125 | 03cb5c40cb6c4bf7a1eb05703426b016 126 | 114293d9aff84aef943329f6bf53e45c -------------------------------------------------------------------------------- /testdata/graph_invalid_cyclic_direct/one.pgsql: -------------------------------------------------------------------------------- 1 | /* 2 | @import two.pgsql 3 | */ 4 | 5 | create table one (); 6 | -------------------------------------------------------------------------------- /testdata/graph_invalid_cyclic_direct/two.pgsql: -------------------------------------------------------------------------------- 1 | /* 2 | @import one.pgsql 3 | */ 4 | 5 | create table two (); 6 | -------------------------------------------------------------------------------- /testdata/graph_invalid_cyclic_indirect/four.pgsql: -------------------------------------------------------------------------------- 1 | /* 2 | @import main.pgsql 3 | @import one.pgsql 4 | */ 5 | 6 | create table four (); 7 | -------------------------------------------------------------------------------- /testdata/graph_invalid_cyclic_indirect/main.pgsql: -------------------------------------------------------------------------------- 1 | drop schema if exists public cascade; 2 | create schema public; 3 | set search_path to public; 4 | -------------------------------------------------------------------------------- /testdata/graph_invalid_cyclic_indirect/one.pgsql: -------------------------------------------------------------------------------- 1 | /* 2 | @import main.pgsql 3 | @import two.pgsql 4 | */ 5 | 6 | create table one (); 7 | -------------------------------------------------------------------------------- /testdata/graph_invalid_cyclic_indirect/three.pgsql: -------------------------------------------------------------------------------- 1 | /* 2 | @import main.pgsql 3 | @import four.pgsql 4 | */ 5 | 6 | create table three (); 7 | -------------------------------------------------------------------------------- /testdata/graph_invalid_cyclic_indirect/two.pgsql: -------------------------------------------------------------------------------- 1 | /* 2 | @import main.pgsql 3 | @import three.pgsql 4 | */ 5 | 6 | create table two (); 7 | -------------------------------------------------------------------------------- /testdata/graph_invalid_cyclic_self/one.pgsql: -------------------------------------------------------------------------------- 1 | /* 2 | @import one.pgsql 3 | */ 4 | 5 | create table one (); 6 | -------------------------------------------------------------------------------- /testdata/graph_invalid_imports/one.pgsql: -------------------------------------------------------------------------------- 1 | /* 2 | @import dir/base_name.pgsql 3 | */ 4 | 5 | create table one (); 6 | -------------------------------------------------------------------------------- /testdata/graph_invalid_missing_deps/one.pgsql: -------------------------------------------------------------------------------- 1 | /* 2 | @import missing.pgsql 3 | */ 4 | 5 | create table one (); 6 | -------------------------------------------------------------------------------- /testdata/graph_invalid_multiple_entries/one.pgsql: -------------------------------------------------------------------------------- 1 | create table one (); 2 | -------------------------------------------------------------------------------- /testdata/graph_invalid_multiple_entries/two.pgsql: -------------------------------------------------------------------------------- 1 | create table two (); 2 | -------------------------------------------------------------------------------- /testdata/graph_valid_non_empty/four.pgsql: -------------------------------------------------------------------------------- 1 | /* 2 | @import two.pgsql 3 | @import three.pgsql 4 | */ 5 | 6 | create table four (); 7 | -------------------------------------------------------------------------------- /testdata/graph_valid_non_empty/main.pgsql: -------------------------------------------------------------------------------- 1 | drop schema if exists public cascade; 2 | create schema public; 3 | set search_path to public; 4 | -------------------------------------------------------------------------------- /testdata/graph_valid_non_empty/one.pgsql: -------------------------------------------------------------------------------- 1 | /* 2 | @import main.pgsql 3 | */ 4 | 5 | create table one (); 6 | -------------------------------------------------------------------------------- /testdata/graph_valid_non_empty/three.pgsql: -------------------------------------------------------------------------------- 1 | /* 2 | @import main.pgsql 3 | @import one.pgsql 4 | */ 5 | 6 | create table three (); 7 | -------------------------------------------------------------------------------- /testdata/graph_valid_non_empty/two.pgsql: -------------------------------------------------------------------------------- 1 | /* 2 | @import main.pgsql 3 | @import one.pgsql 4 | */ 5 | 6 | create table two (); 7 | -------------------------------------------------------------------------------- /testdata/graph_valid_with_skip/four.pgsql: -------------------------------------------------------------------------------- 1 | /* 2 | @import main.pgsql 3 | @skip 4 | */ 5 | 6 | create table four (); 7 | -------------------------------------------------------------------------------- /testdata/graph_valid_with_skip/main.pgsql: -------------------------------------------------------------------------------- 1 | drop schema if exists public cascade; 2 | create schema public; 3 | set search_path to public; 4 | -------------------------------------------------------------------------------- /testdata/graph_valid_with_skip/one.pgsql: -------------------------------------------------------------------------------- 1 | /* 2 | @import main.pgsql 3 | */ 4 | 5 | create table one (); 6 | -------------------------------------------------------------------------------- /testdata/graph_valid_with_skip/three.pgsql: -------------------------------------------------------------------------------- 1 | /* 2 | @import main.pgsql 3 | @import one.pgsql 4 | */ 5 | 6 | create table three (); 7 | -------------------------------------------------------------------------------- /testdata/graph_valid_with_skip/two.pgsql: -------------------------------------------------------------------------------- 1 | /* 2 | @skip 3 | @import main.pgsql 4 | */ 5 | 6 | create table two (); 7 | -------------------------------------------------------------------------------- /text_decode.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import ( 4 | "encoding" 5 | r "reflect" 6 | "strconv" 7 | ) 8 | 9 | /* 10 | Decodes arbitrary text into a value of the given type, using `ParseCatch`. 11 | Panics on errors. 12 | */ 13 | func ParseTo[Out any, Src Text](src Src) (out Out) { 14 | Try(ParseCatch(src, &out)) 15 | return 16 | } 17 | 18 | /* 19 | Decodes arbitrary text into a value of the given type, using `ParseCatch`. 20 | Panics on errors. 21 | */ 22 | func Parse[Out any, Src Text](src Src, out *Out) { 23 | Try(ParseCatch(src, out)) 24 | } 25 | 26 | /* 27 | Missing feature of the standard library. Decodes arbitrary text into a value of 28 | an arbitrary given type. The output must either implement `Parser`, or 29 | implement `encoding.TextUnmarshaler`, or be a pointer to any of the types 30 | described by the constraint `Textable` defined by this package. If the output 31 | is not decodable, this returns an error. 32 | */ 33 | func ParseCatch[Out any, Src Text](src Src, out *Out) error { 34 | if out == nil { 35 | return nil 36 | } 37 | 38 | parser, _ := AnyNoEscUnsafe(out).(Parser) 39 | if parser != nil { 40 | return parser.Parse(ToString(src)) 41 | } 42 | 43 | unmarshaler, _ := AnyNoEscUnsafe(out).(encoding.TextUnmarshaler) 44 | if unmarshaler != nil { 45 | return unmarshaler.UnmarshalText(ToBytes(src)) 46 | } 47 | 48 | return ParseReflectCatch(src, r.ValueOf(AnyNoEscUnsafe(out)).Elem()) 49 | } 50 | 51 | /* 52 | Reflection-based component of `ParseCatch`. 53 | Mostly for internal use. 54 | */ 55 | func ParseReflectCatch[A Text](src A, out r.Value) error { 56 | typ := out.Type() 57 | kind := typ.Kind() 58 | 59 | switch kind { 60 | case r.Int8, r.Int16, r.Int32, r.Int64, r.Int: 61 | val, err := strconv.ParseInt(ToString(src), 10, typeBitSize(typ)) 62 | out.SetInt(val) 63 | return ErrParse(err, src, typ) 64 | 65 | case r.Uint8, r.Uint16, r.Uint32, r.Uint64, r.Uint: 66 | val, err := strconv.ParseUint(ToString(src), 10, typeBitSize(typ)) 67 | out.SetUint(val) 68 | return ErrParse(err, src, typ) 69 | 70 | case r.Float32, r.Float64: 71 | val, err := strconv.ParseFloat(ToString(src), typeBitSize(typ)) 72 | out.SetFloat(val) 73 | return ErrParse(err, src, typ) 74 | 75 | case r.Bool: 76 | return parseBool(ToString(src), out) 77 | 78 | case r.String: 79 | out.SetString(string(src)) 80 | return nil 81 | 82 | case r.Pointer: 83 | if out.IsNil() { 84 | out.Set(r.New(typ.Elem())) 85 | } 86 | 87 | ptr := out.Interface() 88 | 89 | parser, _ := ptr.(Parser) 90 | if parser != nil { 91 | return parser.Parse(ToString(src)) 92 | } 93 | 94 | unmarshaler, _ := ptr.(encoding.TextUnmarshaler) 95 | if unmarshaler != nil { 96 | return unmarshaler.UnmarshalText(ToBytes(src)) 97 | } 98 | 99 | return ParseReflectCatch[A](src, out.Elem()) 100 | 101 | default: 102 | if IsTypeBytes(typ) { 103 | out.SetBytes([]byte(src)) 104 | return nil 105 | } 106 | return Errf(`unable to convert string to %v: unsupported kind %v`, typ, kind) 107 | } 108 | } 109 | 110 | /* 111 | Shortcut for implementing text decoding of types that wrap other types, such as 112 | `Opt`. Mostly for internal use. 113 | */ 114 | func ParseClearCatch[Out any, Tar ClearerPtrGetter[Out], Src Text](src Src, tar Tar) error { 115 | if len(src) <= 0 { 116 | tar.Clear() 117 | return nil 118 | } 119 | return ParseCatch(src, tar.Ptr()) 120 | } 121 | 122 | /* 123 | Shortcut for implementing `sql.Scanner` on types that wrap other types, such as 124 | `Opt`. Mostly for internal use. 125 | */ 126 | func ScanCatch[Inner any, Outer Ptrer[Inner]](src any, tar Outer) error { 127 | if src == nil { 128 | return nil 129 | } 130 | 131 | ptr := tar.Ptr() 132 | 133 | impl, _ := AnyNoEscUnsafe(ptr).(Scanner) 134 | if impl != nil { 135 | return impl.Scan(src) 136 | } 137 | 138 | str, ok := AnyToText[string](src) 139 | if ok { 140 | return ParseCatch(str, ptr) 141 | } 142 | 143 | return ErrConv(src, Type[Outer]()) 144 | } 145 | -------------------------------------------------------------------------------- /text_decode_test.go: -------------------------------------------------------------------------------- 1 | package gg_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | u "unsafe" 7 | 8 | "github.com/mitranim/gg" 9 | "github.com/mitranim/gg/gtest" 10 | ) 11 | 12 | func TestParseTo(t *testing.T) { 13 | defer gtest.Catch(t) 14 | 15 | gtest.PanicStr(`unsupported kind interface`, func() { gg.ParseTo[any](``) }) 16 | gtest.PanicStr(`unsupported kind struct`, func() { gg.ParseTo[SomeModel](``) }) 17 | gtest.PanicStr(`unsupported kind slice`, func() { gg.ParseTo[[]string](``) }) 18 | gtest.PanicStr(`unsupported kind chan`, func() { gg.ParseTo[chan struct{}](``) }) 19 | gtest.PanicStr(`unsupported kind func`, func() { gg.ParseTo[func()](``) }) 20 | gtest.PanicStr(`unsupported kind uintptr`, func() { gg.ParseTo[uintptr](``) }) 21 | gtest.PanicStr(`unsupported kind unsafe.Pointer`, func() { gg.ParseTo[u.Pointer](``) }) 22 | 23 | gtest.PanicStr(`invalid syntax`, func() { gg.ParseTo[int](``) }) 24 | gtest.PanicStr(`invalid syntax`, func() { gg.ParseTo[*int](``) }) 25 | 26 | gtest.Equal(gg.ParseTo[string](``), ``) 27 | gtest.Equal(gg.ParseTo[string](`str`), `str`) 28 | 29 | gtest.Equal(gg.ParseTo[*string](``), gg.Ptr(``)) 30 | gtest.Equal(gg.ParseTo[*string](`str`), gg.Ptr(`str`)) 31 | 32 | gtest.Equal(gg.ParseTo[int](`0`), 0) 33 | gtest.Equal(gg.ParseTo[int](`123`), 123) 34 | 35 | gtest.Equal(gg.ParseTo[*int](`0`), gg.Ptr(0)) 36 | gtest.Equal(gg.ParseTo[*int](`123`), gg.Ptr(123)) 37 | 38 | gtest.Equal( 39 | gg.ParseTo[time.Time](`1234-05-23T12:34:56Z`), 40 | time.Date(1234, 5, 23, 12, 34, 56, 0, time.UTC), 41 | ) 42 | 43 | gtest.Equal( 44 | gg.ParseTo[*time.Time](`1234-05-23T12:34:56Z`), 45 | gg.Ptr(time.Date(1234, 5, 23, 12, 34, 56, 0, time.UTC)), 46 | ) 47 | } 48 | 49 | func BenchmarkParseTo_int(b *testing.B) { 50 | for ind := 0; ind < b.N; ind++ { 51 | gg.Nop1(gg.ParseTo[int](`123`)) 52 | } 53 | } 54 | 55 | func BenchmarkParseTo_int_ptr(b *testing.B) { 56 | for ind := 0; ind < b.N; ind++ { 57 | gg.Nop1(gg.ParseTo[*int](`123`)) 58 | } 59 | } 60 | 61 | func BenchmarkParseTo_Parser(b *testing.B) { 62 | for ind := 0; ind < b.N; ind++ { 63 | gg.Nop1(gg.ParseTo[ParserStr](`863872f79b1d4cc9a45e8027a6ad66ad`)) 64 | } 65 | } 66 | 67 | func BenchmarkParseTo_Parser_ptr(b *testing.B) { 68 | for ind := 0; ind < b.N; ind++ { 69 | gg.Nop1(gg.ParseTo[*ParserStr](`863872f79b1d4cc9a45e8027a6ad66ad`)) 70 | } 71 | } 72 | 73 | func BenchmarkParseTo_Unmarshaler(b *testing.B) { 74 | src := []byte(`863872f79b1d4cc9a45e8027a6ad66ad`) 75 | 76 | for ind := 0; ind < b.N; ind++ { 77 | gg.Nop1(gg.ParseTo[UnmarshalerBytes](src)) 78 | } 79 | } 80 | 81 | func BenchmarkParseTo_Unmarshaler_ptr(b *testing.B) { 82 | src := []byte(`863872f79b1d4cc9a45e8027a6ad66ad`) 83 | 84 | for ind := 0; ind < b.N; ind++ { 85 | gg.Nop1(gg.ParseTo[*UnmarshalerBytes](src)) 86 | } 87 | } 88 | 89 | func BenchmarkParseTo_time_Time(b *testing.B) { 90 | for ind := 0; ind < b.N; ind++ { 91 | gg.Nop1(gg.ParseTo[time.Time](`1234-05-23T12:34:56Z`)) 92 | } 93 | } 94 | 95 | func BenchmarkParseTo_time_Time_ptr(b *testing.B) { 96 | for ind := 0; ind < b.N; ind++ { 97 | gg.Nop1(gg.ParseTo[*time.Time](`1234-05-23T12:34:56Z`)) 98 | } 99 | } 100 | 101 | func BenchmarkParse(b *testing.B) { 102 | var val int 103 | 104 | for ind := 0; ind < b.N; ind++ { 105 | gg.Parse(`123`, &val) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /time_micro.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | r "reflect" 7 | "strconv" 8 | "time" 9 | ) 10 | 11 | // Calls `time.Now` and converts to `TimeMicro`, truncating precision. 12 | func TimeMicroNow() TimeMicro { return TimeMicro(time.Now().UnixMicro()) } 13 | 14 | // Shortcut for parsing text into `TimeMicro`. Panics on error. 15 | func TimeMicroParse[A Text](src A) TimeMicro { 16 | var out TimeMicro 17 | Try(out.Parse(ToString(src))) 18 | return out 19 | } 20 | 21 | /* 22 | Represents a Unix timestamp in microseconds. In text and JSON, this type 23 | supports parsing numeric timestamps and RFC3339 timestamps, but always encodes 24 | as a number. In SQL, this type is represented in the RFC3339 format. This type 25 | is "zero-optional" or "zero-nullable". The zero value is considered empty in 26 | text and null in JSON/SQL. Conversion to `time.Time` doesn't specify a 27 | timezone, which means it uses `time.Local` by default. If you prefer UTC, 28 | enforce it across the app by updating `time.Local`. 29 | 30 | Caution: corresponding DB columns MUST be restricted to microsecond precision. 31 | Without this restriction, encoding and decoding is not reversible. After losing 32 | precision to an encoding-decoding roundtrip, you might be unable to find a 33 | corresponding value in a database, if timestamp precision is higher than a 34 | microsecond. 35 | 36 | Also see `TimeMilli`, which uses milliseconds. 37 | */ 38 | type TimeMicro int64 39 | 40 | // Implement `Nullable`. True if zero. 41 | func (self TimeMicro) IsNull() bool { return self == 0 } 42 | 43 | // Implement `Clearer`, zeroing the receiver. 44 | func (self *TimeMicro) Clear() { 45 | if self != nil { 46 | *self = 0 47 | } 48 | } 49 | 50 | /* 51 | Convert to `time.Time` by calling `time.UnixMicro`. The resulting timestamp has 52 | the timezone `time.Local`. To enforce UTC, modify `time.Local` at app startup, 53 | or call `.In(time.UTC)`. 54 | */ 55 | func (self TimeMicro) Time() time.Time { return time.UnixMicro(int64(self)) } 56 | 57 | /* 58 | Implement `AnyGetter` for compatibility with some 3rd party libraries. If zero, 59 | returns `nil`, otherwise creates `time.Time` by calling `TimeMicro.Time`. 60 | */ 61 | func (self TimeMicro) Get() any { 62 | if self.IsNull() { 63 | return nil 64 | } 65 | return self.Time() 66 | } 67 | 68 | // Sets the receiver to the given input. 69 | func (self *TimeMicro) SetInt64(val int64) { *self = TimeMicro(val) } 70 | 71 | // Sets the receiver to the result of `time.Time.UnixMicro`. 72 | func (self *TimeMicro) SetTime(val time.Time) { self.SetInt64(val.UnixMicro()) } 73 | 74 | /* 75 | Implement `Parser`. The input must be either an integer in base 10, representing 76 | a Unix millisecond timestamp, or an RFC3339 timestamp. RFC3339 is the default 77 | time encoding/decoding format in Go and some other languages. 78 | */ 79 | func (self *TimeMicro) Parse(src string) error { 80 | if len(src) <= 0 { 81 | self.Clear() 82 | return nil 83 | } 84 | 85 | if isIntString(src) { 86 | num, err := strconv.ParseInt(src, 10, 64) 87 | if err != nil { 88 | return err 89 | } 90 | self.SetInt64(num) 91 | return nil 92 | } 93 | 94 | inst, err := time.Parse(time.RFC3339, src) 95 | if err != nil { 96 | return err 97 | } 98 | self.SetTime(inst) 99 | return nil 100 | } 101 | 102 | /* 103 | Implement `fmt.Stringer`. If zero, returns an empty string. Otherwise returns 104 | the base 10 representation of the underlying number. 105 | */ 106 | func (self TimeMicro) String() string { 107 | if self.IsNull() { 108 | return `` 109 | } 110 | return strconv.FormatInt(int64(self), 10) 111 | } 112 | 113 | // Implement `AppenderTo`, using the same representation as `.String`. 114 | func (self TimeMicro) AppendTo(buf []byte) []byte { 115 | if self.IsNull() { 116 | return buf 117 | } 118 | return strconv.AppendInt(buf, int64(self), 10) 119 | } 120 | 121 | /* 122 | Implement `encoding.TextMarhaler`. If zero, returns nil. Otherwise returns the 123 | same representation as `.String`. 124 | */ 125 | func (self TimeMicro) MarshalText() ([]byte, error) { 126 | if self.IsNull() { 127 | return nil, nil 128 | } 129 | return ToBytes(self.String()), nil 130 | } 131 | 132 | // Implement `encoding.TextUnmarshaler`, using the same algorithm as `.Parse`. 133 | func (self *TimeMicro) UnmarshalText(src []byte) error { 134 | return self.Parse(ToString(src)) 135 | } 136 | 137 | /* 138 | Implement `json.Marshaler`. If zero, returns bytes representing `null`. 139 | Otherwise encodes as a JSON number. 140 | */ 141 | func (self TimeMicro) MarshalJSON() ([]byte, error) { 142 | if self.IsNull() { 143 | return ToBytes(`null`), nil 144 | } 145 | return json.Marshal(int64(self)) 146 | } 147 | 148 | /* 149 | Implement `json.Unmarshaler`. If the input is empty or represents JSON `null`, 150 | zeroes the receiver. If the input is a JSON number, parses it in accordance 151 | with `.Parse`. Otherwise uses the default `json.Unmarshal` behavior for 152 | `*time.Time` and stores the resulting timestamp in milliseconds. 153 | */ 154 | func (self *TimeMicro) UnmarshalJSON(src []byte) error { 155 | if IsJsonEmpty(src) { 156 | self.Clear() 157 | return nil 158 | } 159 | 160 | if isIntString(ToString(src)) { 161 | num, err := strconv.ParseInt(ToString(src), 10, 64) 162 | if err != nil { 163 | return err 164 | } 165 | self.SetInt64(num) 166 | return nil 167 | } 168 | 169 | var inst time.Time 170 | err := json.Unmarshal(src, &inst) 171 | if err != nil { 172 | return err 173 | } 174 | self.SetTime(inst) 175 | return nil 176 | } 177 | 178 | // Implement `driver.Valuer`, using `.Get`. 179 | func (self TimeMicro) Value() (driver.Value, error) { 180 | return self.Get(), nil 181 | } 182 | 183 | /* 184 | Implement `sql.Scanner`, converting an arbitrary input to `TimeMicro` and 185 | modifying the receiver. Acceptable inputs: 186 | 187 | * `nil` -> use `.Clear` 188 | * integer -> assign, assuming milliseconds 189 | * text -> use `.Parse` 190 | * `time.Time` -> use `.SetTime` 191 | * `*time.Time` -> use `.Clear` or `.SetTime` 192 | * `AnyGetter` -> scan underlying value 193 | */ 194 | func (self *TimeMicro) Scan(src any) error { 195 | str, ok := AnyToText[string](src) 196 | if ok { 197 | return self.Parse(str) 198 | } 199 | 200 | switch src := src.(type) { 201 | case nil: 202 | self.Clear() 203 | return nil 204 | 205 | case time.Time: 206 | self.SetTime(src) 207 | return nil 208 | 209 | case *time.Time: 210 | if src == nil { 211 | self.Clear() 212 | } else { 213 | self.SetTime(*src) 214 | } 215 | return nil 216 | 217 | case int64: 218 | self.SetInt64(src) 219 | return nil 220 | 221 | case TimeMicro: 222 | *self = src 223 | return nil 224 | 225 | default: 226 | val := r.ValueOf(src) 227 | if val.CanInt() { 228 | self.SetInt64(val.Int()) 229 | return nil 230 | } 231 | return ErrConv(src, Type[TimeMicro]()) 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /time_milli.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | r "reflect" 7 | "strconv" 8 | "time" 9 | ) 10 | 11 | // Calls `time.Now` and converts to `TimeMilli`, truncating precision. 12 | func TimeMilliNow() TimeMilli { return TimeMilli(time.Now().UnixMilli()) } 13 | 14 | // Shortcut for parsing text into `TimeMilli`. Panics on error. 15 | func TimeMilliParse[A Text](src A) TimeMilli { 16 | var out TimeMilli 17 | Try(out.Parse(ToString(src))) 18 | return out 19 | } 20 | 21 | /* 22 | Represents a Unix timestamp in milliseconds. In text and JSON, this type 23 | supports parsing numeric timestamps and RFC3339 timestamps, but always encodes 24 | as a number. In SQL, this type is represented in the RFC3339 format. This type 25 | is "zero-optional" or "zero-nullable". The zero value is considered empty in 26 | text and null in JSON/SQL. Conversion to `time.Time` doesn't specify a 27 | timezone, which means it uses `time.Local` by default. If you prefer UTC, 28 | enforce it across the app by updating `time.Local`. 29 | 30 | Caution: corresponding DB columns MUST be restricted to millisecond precision. 31 | Without this restriction, encoding and decoding might not be reversible. After 32 | losing precision to an encoding-decoding roundtrip, you might be unable to find 33 | a corresponding value in a database, if timestamp precision is higher than a 34 | millisecond. 35 | 36 | Also see `TimeMicro`, which uses microseconds. 37 | */ 38 | type TimeMilli int64 39 | 40 | // Implement `Nullable`. True if zero. 41 | func (self TimeMilli) IsNull() bool { return self == 0 } 42 | 43 | // Implement `Clearer`, zeroing the receiver. 44 | func (self *TimeMilli) Clear() { 45 | if self != nil { 46 | *self = 0 47 | } 48 | } 49 | 50 | /* 51 | Convert to `time.Time` by calling `time.UnixMilli`. The resulting timestamp has 52 | the timezone `time.Local`. To enforce UTC, modify `time.Local` at app startup, 53 | or call `.In(time.UTC)`. 54 | */ 55 | func (self TimeMilli) Time() time.Time { return time.UnixMilli(int64(self)) } 56 | 57 | /* 58 | Implement `AnyGetter` for compatibility with some 3rd party libraries. If zero, 59 | returns `nil`, otherwise creates `time.Time` by calling `TimeMilli.Time`. 60 | */ 61 | func (self TimeMilli) Get() any { 62 | if self.IsNull() { 63 | return nil 64 | } 65 | return self.Time() 66 | } 67 | 68 | // Sets the receiver to the given input. 69 | func (self *TimeMilli) SetInt64(val int64) { *self = TimeMilli(val) } 70 | 71 | // Sets the receiver to the result of `time.Time.UnixMilli`. 72 | func (self *TimeMilli) SetTime(val time.Time) { self.SetInt64(val.UnixMilli()) } 73 | 74 | /* 75 | Implement `Parser`. The input must be either an integer in base 10, representing 76 | a Unix millisecond timestamp, or an RFC3339 timestamp. RFC3339 is the default 77 | time encoding/decoding format in Go and some other languages. 78 | */ 79 | func (self *TimeMilli) Parse(src string) error { 80 | if len(src) <= 0 { 81 | self.Clear() 82 | return nil 83 | } 84 | 85 | if isIntString(src) { 86 | num, err := strconv.ParseInt(src, 10, 64) 87 | if err != nil { 88 | return err 89 | } 90 | self.SetInt64(num) 91 | return nil 92 | } 93 | 94 | inst, err := time.Parse(time.RFC3339, src) 95 | if err != nil { 96 | return err 97 | } 98 | self.SetTime(inst) 99 | return nil 100 | } 101 | 102 | /* 103 | Implement `fmt.Stringer`. If zero, returns an empty string. Otherwise returns 104 | the base 10 representation of the underlying number. 105 | */ 106 | func (self TimeMilli) String() string { 107 | if self.IsNull() { 108 | return `` 109 | } 110 | return strconv.FormatInt(int64(self), 10) 111 | } 112 | 113 | // Implement `AppenderTo`, using the same representation as `.String`. 114 | func (self TimeMilli) AppendTo(buf []byte) []byte { 115 | if self.IsNull() { 116 | return buf 117 | } 118 | return strconv.AppendInt(buf, int64(self), 10) 119 | } 120 | 121 | /* 122 | Implement `encoding.TextMarhaler`. If zero, returns nil. Otherwise returns the 123 | same representation as `.String`. 124 | */ 125 | func (self TimeMilli) MarshalText() ([]byte, error) { 126 | if self.IsNull() { 127 | return nil, nil 128 | } 129 | return ToBytes(self.String()), nil 130 | } 131 | 132 | // Implement `encoding.TextUnmarshaler`, using the same algorithm as `.Parse`. 133 | func (self *TimeMilli) UnmarshalText(src []byte) error { 134 | return self.Parse(ToString(src)) 135 | } 136 | 137 | /* 138 | Implement `json.Marshaler`. If zero, returns bytes representing `null`. 139 | Otherwise encodes as a JSON number. 140 | */ 141 | func (self TimeMilli) MarshalJSON() ([]byte, error) { 142 | if self.IsNull() { 143 | return ToBytes(`null`), nil 144 | } 145 | return json.Marshal(int64(self)) 146 | } 147 | 148 | /* 149 | Implement `json.Unmarshaler`. If the input is empty or represents JSON `null`, 150 | zeroes the receiver. If the input is a JSON number, parses it in accordance 151 | with `.Parse`. Otherwise uses the default `json.Unmarshal` behavior for 152 | `*time.Time` and stores the resulting timestamp in milliseconds. 153 | */ 154 | func (self *TimeMilli) UnmarshalJSON(src []byte) error { 155 | if IsJsonEmpty(src) { 156 | self.Clear() 157 | return nil 158 | } 159 | 160 | if isIntString(ToString(src)) { 161 | num, err := strconv.ParseInt(ToString(src), 10, 64) 162 | if err != nil { 163 | return err 164 | } 165 | self.SetInt64(num) 166 | return nil 167 | } 168 | 169 | var inst time.Time 170 | err := json.Unmarshal(src, &inst) 171 | if err != nil { 172 | return err 173 | } 174 | self.SetTime(inst) 175 | return nil 176 | } 177 | 178 | // Implement `driver.Valuer`, using `.Get`. 179 | func (self TimeMilli) Value() (driver.Value, error) { 180 | return self.Get(), nil 181 | } 182 | 183 | /* 184 | Implement `sql.Scanner`, converting an arbitrary input to `TimeMilli` and 185 | modifying the receiver. Acceptable inputs: 186 | 187 | * `nil` -> use `.Clear` 188 | * integer -> assign, assuming milliseconds 189 | * text -> use `.Parse` 190 | * `time.Time` -> use `.SetTime` 191 | * `*time.Time` -> use `.Clear` or `.SetTime` 192 | * `AnyGetter` -> scan underlying value 193 | */ 194 | func (self *TimeMilli) Scan(src any) error { 195 | str, ok := AnyToText[string](src) 196 | if ok { 197 | return self.Parse(str) 198 | } 199 | 200 | switch src := src.(type) { 201 | case nil: 202 | self.Clear() 203 | return nil 204 | 205 | case time.Time: 206 | self.SetTime(src) 207 | return nil 208 | 209 | case *time.Time: 210 | if src == nil { 211 | self.Clear() 212 | } else { 213 | self.SetTime(*src) 214 | } 215 | return nil 216 | 217 | case int64: 218 | self.SetInt64(src) 219 | return nil 220 | 221 | case TimeMilli: 222 | *self = src 223 | return nil 224 | 225 | default: 226 | val := r.ValueOf(src) 227 | if val.CanInt() { 228 | self.SetInt64(val.Int()) 229 | return nil 230 | } 231 | return ErrConv(src, Type[TimeMilli]()) 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /trace_test.go: -------------------------------------------------------------------------------- 1 | package gg_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mitranim/gg" 7 | ) 8 | 9 | func BenchmarkCaptureTrace_shallow(b *testing.B) { 10 | for ind := 0; ind < b.N; ind++ { 11 | gg.Nop1(gg.CaptureTrace(0)) 12 | } 13 | } 14 | 15 | func BenchmarkCaptureTrace_deep(b *testing.B) { 16 | for ind := 0; ind < b.N; ind++ { 17 | gg.Nop1(trace0()) 18 | } 19 | } 20 | 21 | func BenchmarkTrace_Frames_shallow(b *testing.B) { 22 | trace := gg.CaptureTrace(0) 23 | b.ResetTimer() 24 | 25 | for ind := 0; ind < b.N; ind++ { 26 | gg.Nop1(trace.Frames()) 27 | } 28 | } 29 | 30 | func BenchmarkTrace_Frames_deep(b *testing.B) { 31 | trace := trace0() 32 | b.ResetTimer() 33 | 34 | for ind := 0; ind < b.N; ind++ { 35 | gg.Nop1(trace.Frames()) 36 | } 37 | } 38 | 39 | func BenchmarkFrames_NameWidth(b *testing.B) { 40 | frames := trace0().Frames() 41 | b.ResetTimer() 42 | 43 | for ind := 0; ind < b.N; ind++ { 44 | gg.Nop1(frames.NameWidth()) 45 | } 46 | } 47 | 48 | func BenchmarkFrames_AppendIndentTableTo(b *testing.B) { 49 | frames := trace0().Frames() 50 | buf := make([]byte, 0, 1<<16) 51 | b.ResetTimer() 52 | 53 | for ind := 0; ind < b.N; ind++ { 54 | gg.Nop1(frames.AppendIndentTableTo(buf, 0)) 55 | } 56 | } 57 | 58 | func BenchmarkFrames_AppendIndentTableTo_rel_path(b *testing.B) { 59 | defer gg.SnapSwap(&gg.TraceSkipLang, true).Done() 60 | defer gg.SnapSwap(&gg.TraceBaseDir, gg.Cwd()).Done() 61 | 62 | frames := trace0().Frames() 63 | buf := make([]byte, 0, 1<<16) 64 | b.ResetTimer() 65 | 66 | for ind := 0; ind < b.N; ind++ { 67 | gg.Nop1(frames.AppendIndentTableTo(buf, 0)) 68 | } 69 | } 70 | 71 | func BenchmarkTrace_capture_append(b *testing.B) { 72 | buf := make([]byte, 0, 1<<16) 73 | b.ResetTimer() 74 | 75 | for ind := 0; ind < b.N; ind++ { 76 | gg.Nop1(trace0().AppendTo(buf)) 77 | } 78 | } 79 | 80 | func trace0() gg.Trace { return trace1() } 81 | func trace1() gg.Trace { return trace2() } 82 | func trace2() gg.Trace { return trace3() } 83 | func trace3() gg.Trace { return trace4() } 84 | func trace4() gg.Trace { return trace5() } 85 | func trace5() gg.Trace { return trace6() } 86 | func trace6() gg.Trace { return trace7() } 87 | func trace7() gg.Trace { return trace8() } 88 | func trace8() gg.Trace { return trace9() } 89 | func trace9() gg.Trace { return gg.CaptureTrace(0) } 90 | -------------------------------------------------------------------------------- /try_test.go: -------------------------------------------------------------------------------- 1 | package gg_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mitranim/gg" 7 | "github.com/mitranim/gg/gtest" 8 | ) 9 | 10 | func TestCatch(t *testing.T) { 11 | defer gtest.Catch(t) 12 | 13 | err := gg.Catch(func() { panic(`string_panic`) }).(gg.Err) 14 | gtest.Equal(err.Msg, `string_panic`) 15 | gtest.True(err.Trace.IsNotEmpty()) 16 | 17 | err = gg.Catch(func() { panic(gg.ErrStr(`string_error`)) }).(gg.Err) 18 | gtest.Zero(err.Msg) 19 | gtest.Equal(err.Cause, error(gg.ErrStr(`string_error`))) 20 | gtest.True(err.Trace.IsNotEmpty()) 21 | } 22 | 23 | func TestDetailf(t *testing.T) { 24 | defer gtest.Catch(t) 25 | 26 | err := gg.Catch(func() { 27 | defer gg.Detailf(`unable to %v`, `do stuff`) 28 | panic(`string_panic`) 29 | }).(gg.Err) 30 | 31 | gtest.Equal(err.Msg, `unable to do stuff`) 32 | gtest.Equal(err.Cause, error(gg.ErrStr(`string_panic`))) 33 | gtest.True(err.Trace.IsNotEmpty()) 34 | } 35 | 36 | /* 37 | func BenchmarkPanicSkip(b *testing.B) { 38 | defer gtest.Catch(b) 39 | 40 | for ind := 0; ind < b.N; ind++ { 41 | benchmarkPanicSkip() 42 | } 43 | } 44 | 45 | func BenchmarkPanicSkipTraced(b *testing.B) { 46 | defer gtest.Catch(b) 47 | 48 | for ind := 0; ind < b.N; ind++ { 49 | benchmarkPanicSkipTraced() 50 | } 51 | } 52 | 53 | func BenchmarkFileExists(b *testing.B) { 54 | defer gtest.Catch(b) 55 | 56 | for ind := 0; ind < b.N; ind++ { 57 | benchmarkFileExists() 58 | } 59 | } 60 | 61 | func benchmarkPanicSkip() { 62 | defer gg.Skip() 63 | panic(`error_message`) 64 | } 65 | 66 | func benchmarkPanicSkipTraced() { 67 | defer gg.Skip() 68 | panic(gg.Err{}.Msgd(`error_message`).TracedAt(1)) 69 | } 70 | 71 | func benchmarkFileExists() { 72 | gtest.True(gg.FileExists(`try_test.go`)) 73 | } 74 | */ 75 | -------------------------------------------------------------------------------- /unlicense: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /unsafe.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import ( 4 | r "reflect" 5 | u "unsafe" 6 | ) 7 | 8 | /* 9 | Amount of bytes in `uintptr`. At the time of writing, in Go 1.20, this is also 10 | the amount of bytes in `int` and `uint`. 11 | */ 12 | const SizeofWord = u.Sizeof(uintptr(0)) 13 | 14 | /* 15 | Amount of bytes in any value of type `~string`. Note that this is the size of 16 | the string header, not of the underlying data. 17 | */ 18 | const SizeofString = u.Sizeof(``) 19 | 20 | /* 21 | Amount of bytes in any slice header, for example of type `[]byte`. Note that the 22 | size of a slice header is constant and does not reflect the size of the 23 | underlying data. 24 | */ 25 | const SizeofSlice = u.Sizeof([]byte(nil)) 26 | 27 | /* 28 | Amount of bytes in our own `SliceHeader`. In the official Go implementation 29 | (version 1.20 at the time of writing), this is equal to `SizeofSlice`. 30 | In case of mismatch, using `SliceHeader` for anything is invalid. 31 | */ 32 | const SizeofSliceHeader = u.Sizeof(SliceHeader{}) 33 | 34 | /* 35 | Returns `unsafe.Sizeof` for the given type. Equivalent to `reflect.Type.Size` 36 | for the same type. Due to Go's limitations, the result is not a constant, thus 37 | you should prefer direct use of `unsafe.Sizeof` which returns a constant. 38 | */ 39 | func Size[A any]() uintptr { return u.Sizeof(Zero[A]()) } 40 | 41 | /* 42 | Memory representation of an arbitrary Go slice. Same as `reflect.SliceHeader` 43 | but with `unsafe.Pointer` instead of `uintptr`. 44 | */ 45 | type SliceHeader struct { 46 | Dat u.Pointer 47 | Len int 48 | Cap int 49 | } 50 | 51 | /* 52 | Takes a regular slice header and converts it to its underlying representation 53 | `SliceHeader`. 54 | */ 55 | func SliceHeaderOf[A any](src []A) SliceHeader { 56 | return CastUnsafe[SliceHeader](src) 57 | } 58 | 59 | /* 60 | Dangerous tool for performance fine-tuning. Converts the given pointer to 61 | `unsafe.Pointer` and tricks the compiler into thinking that the memory 62 | underlying the pointer should not be moved to the heap. Can negate failures of 63 | Go escape analysis, but can also introduce tricky bugs. The caller MUST ensure 64 | that the original is not freed while the resulting pointer is still in use. 65 | */ 66 | func PtrNoEscUnsafe[A any](val *A) u.Pointer { return noescape(u.Pointer(val)) } 67 | 68 | // Dangerous tool for performance fine-tuning. 69 | func NoEscUnsafe[A any](val A) A { return *(*A)(PtrNoEscUnsafe(&val)) } 70 | 71 | // Dangerous tool for performance fine-tuning. 72 | func AnyNoEscUnsafe(src any) any { return NoEscUnsafe(src) } 73 | 74 | /* 75 | Self-explanatory. Slightly cleaner and less error prone than direct use of 76 | unsafe pointers. 77 | */ 78 | func CastUnsafe[Out, Src any](val Src) Out { return *(*Out)(u.Pointer(&val)) } 79 | 80 | /* 81 | Same as `CastUnsafe` but with additional validation: `unsafe.Sizeof` must be the 82 | same for both types, otherwise this panics. 83 | */ 84 | func Cast[Out, Src any](src Src) Out { 85 | out := CastUnsafe[Out](src) 86 | srcSize := u.Sizeof(src) 87 | outSize := u.Sizeof(out) 88 | if srcSize == outSize { 89 | return out 90 | } 91 | panic(errSizeMismatch(Type[Src](), srcSize, Type[Out](), outSize)) 92 | } 93 | 94 | func errSizeMismatch(src r.Type, srcSize uintptr, out r.Type, outSize uintptr) Err { 95 | return Errf( 96 | `size mismatch: %v (size %v) vs %v (size %v)`, 97 | src, srcSize, out, outSize, 98 | ) 99 | } 100 | 101 | /* 102 | Similar to `CastUnsafe` between two slice types but with additional validation: 103 | `unsafe.Sizeof` must be the same for both element types, otherwise this 104 | panics. 105 | */ 106 | func CastSlice[Out, Src any](src []Src) []Out { 107 | srcSize := Size[Src]() 108 | outSize := Size[Out]() 109 | if srcSize == outSize { 110 | return CastUnsafe[[]Out](src) 111 | } 112 | panic(errSizeMismatch(Type[Src](), srcSize, Type[Out](), outSize)) 113 | } 114 | 115 | /* 116 | Reinterprets existing memory as a byte slice. The resulting byte slice is backed 117 | by the given pointer. Mutations of the resulting slice are reflected in the 118 | source memory. Length and capacity are equal to the size of the referenced 119 | memory. If the pointer is nil, the output is nil. 120 | */ 121 | func AsBytes[A any](tar *A) []byte { 122 | if tar == nil { 123 | return nil 124 | } 125 | return u.Slice(CastUnsafe[*byte](tar), Size[A]()) 126 | } 127 | -------------------------------------------------------------------------------- /unsafe_test.go: -------------------------------------------------------------------------------- 1 | package gg_test 2 | 3 | import ( 4 | "math" 5 | r "reflect" 6 | "testing" 7 | u "unsafe" 8 | 9 | "github.com/mitranim/gg" 10 | "github.com/mitranim/gg/gtest" 11 | ) 12 | 13 | func TestAnyNoEscUnsafe(t *testing.T) { 14 | defer gtest.Catch(t) 15 | 16 | testAnyNoEscUnsafe(any(nil)) 17 | testAnyNoEscUnsafe(``) 18 | testAnyNoEscUnsafe(`str`) 19 | testAnyNoEscUnsafe(0) 20 | testAnyNoEscUnsafe(10) 21 | testAnyNoEscUnsafe(SomeModel{}) 22 | testAnyNoEscUnsafe((func())(nil)) 23 | } 24 | 25 | /* 26 | This doesn't verify that the value doesn't escape, because it's tricky to 27 | implement for different types. Instead, various benchmarks serve as indirect 28 | indicators. 29 | */ 30 | func testAnyNoEscUnsafe[A any](src A) { 31 | tar := gg.AnyNoEscUnsafe(src) 32 | gtest.Equal(r.TypeOf(tar), r.TypeOf(src)) 33 | gtest.Equal(tar, any(src)) 34 | } 35 | 36 | func BenchmarkAnyNoEscUnsafe(b *testing.B) { 37 | for ind := 0; ind < b.N; ind++ { 38 | val := []int{ind} 39 | gg.Nop1(esc(gg.AnyNoEscUnsafe(val))) 40 | } 41 | } 42 | 43 | func BenchmarkSize(b *testing.B) { 44 | defer gtest.Catch(b) 45 | 46 | for ind := 0; ind < b.N; ind++ { 47 | gg.Nop1(gg.Size[string]()) 48 | } 49 | } 50 | 51 | func TestAsBytes(t *testing.T) { 52 | defer gtest.Catch(t) 53 | 54 | gtest.Zero(gg.AsBytes[struct{}](nil)) 55 | gtest.Zero(gg.AsBytes[bool](nil)) 56 | gtest.Zero(gg.AsBytes[uint64](nil)) 57 | 58 | { 59 | var src struct{} 60 | tar := gg.AsBytes(&src) 61 | 62 | gtest.Equal(tar, []byte{}) 63 | gtest.Eq(u.Pointer(u.SliceData(tar)), u.Pointer(&src)) 64 | gtest.Len(tar, 0) 65 | gtest.Cap(tar, 0) 66 | } 67 | 68 | { 69 | var src bool 70 | gtest.False(src) 71 | 72 | tar := gg.AsBytes(&src) 73 | 74 | gtest.Equal(tar, []byte{0}) 75 | gtest.Eq(u.Pointer(u.SliceData(tar)), u.Pointer(&src)) 76 | gtest.Len(tar, 1) 77 | gtest.Cap(tar, 1) 78 | 79 | tar[0] = 1 80 | gtest.True(src) 81 | } 82 | 83 | { 84 | var src uint64 85 | gtest.Eq(src, 0) 86 | 87 | tar := gg.AsBytes(&src) 88 | 89 | gtest.Equal(tar, make([]byte, 8)) 90 | gtest.Eq(u.Pointer(u.SliceData(tar)), u.Pointer(&src)) 91 | gtest.Len(tar, 8) 92 | gtest.Cap(tar, 8) 93 | 94 | for ind := range tar { 95 | tar[ind] = 255 96 | } 97 | 98 | gtest.Eq(src, math.MaxUint64) 99 | } 100 | 101 | { 102 | type Tar struct { 103 | One uint64 104 | Two uint64 105 | Three uint64 106 | } 107 | 108 | src := Tar{10, 20, 30} 109 | bytes := gg.AsBytes(&src) 110 | tar := *gg.CastUnsafe[*Tar](bytes) 111 | 112 | gtest.Eq(src, tar) 113 | gtest.Eq(tar, Tar{10, 20, 30}) 114 | 115 | gg.CastUnsafe[*Tar](bytes).Two = 40 116 | tar = *gg.CastUnsafe[*Tar](bytes) 117 | 118 | gtest.Eq(src, tar) 119 | gtest.Eq(src, Tar{10, 40, 30}) 120 | } 121 | } 122 | 123 | func TestCast(t *testing.T) { 124 | defer gtest.Catch(t) 125 | 126 | gtest.PanicStr(`size mismatch: uint8 (size 1) vs int64 (size 8)`, func() { 127 | gg.Cast[int64](byte(0)) 128 | }) 129 | 130 | gtest.PanicStr(`size mismatch: int64 (size 8) vs uint8 (size 1)`, func() { 131 | gg.Cast[byte](int64(0)) 132 | }) 133 | 134 | gtest.PanicStr(`size mismatch: string (size 16) vs []uint8 (size 24)`, func() { 135 | gg.Cast[[]byte](string(``)) 136 | }) 137 | 138 | gtest.PanicStr(`size mismatch: []uint8 (size 24) vs string (size 16)`, func() { 139 | gg.Cast[string]([]byte(nil)) 140 | }) 141 | 142 | gtest.Zero(gg.Cast[struct{}]([0]struct{}{})) 143 | gtest.Eq(gg.Cast[int8](uint8(math.MaxUint8)), -1) 144 | gtest.Eq(gg.Cast[uint8](int8(math.MaxInt8)), 127) 145 | gtest.Eq(gg.Cast[uint8](int8(math.MinInt8)), 128) 146 | 147 | { 148 | type Src [16]byte 149 | type Tar struct{ Src } 150 | 151 | src := Src([]byte(`ef1e7d2249dc45fc`)) 152 | gtest.Eq(string(src[:]), `ef1e7d2249dc45fc`) 153 | 154 | tar := gg.Cast[Tar](src) 155 | gtest.Eq(tar.Src, src) 156 | gtest.Eq(gg.Cast[Src](tar), src) 157 | } 158 | } 159 | 160 | func TestCastSlice(t *testing.T) { 161 | defer gtest.Catch(t) 162 | 163 | gtest.PanicStr(`size mismatch: uint8 (size 1) vs int64 (size 8)`, func() { 164 | gg.CastSlice[int64, byte](nil) 165 | }) 166 | 167 | gtest.PanicStr(`size mismatch: int64 (size 8) vs uint8 (size 1)`, func() { 168 | gg.CastSlice[byte, int64](nil) 169 | }) 170 | 171 | gtest.PanicStr(`size mismatch: string (size 16) vs []uint8 (size 24)`, func() { 172 | gg.CastSlice[[]byte, string](nil) 173 | }) 174 | 175 | gtest.PanicStr(`size mismatch: []uint8 (size 24) vs string (size 16)`, func() { 176 | gg.CastSlice[string, []byte](nil) 177 | }) 178 | 179 | gtest.Zero(gg.CastSlice[uint8, int8](nil)) 180 | gtest.Zero(gg.CastSlice[int8, uint8](nil)) 181 | 182 | { 183 | src := []int8{-128, -127, -1, 0, 1, 127} 184 | tar := gg.CastSlice[uint8](src) 185 | 186 | gtest.Equal(tar, []uint8{128, 129, 255, 0, 1, 127}) 187 | gtest.Eq(u.Pointer(u.SliceData(tar)), u.Pointer(u.SliceData(src))) 188 | gtest.Len(tar, len(src)) 189 | gtest.Cap(tar, cap(src)) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /zop.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | /* 4 | Short for "zero optional value". Syntactic shortcut for creating `Zop` with the 5 | given value. Workaround for the lack of type inference in struct literals. 6 | */ 7 | func ZopVal[A any](val A) Zop[A] { return Zop[A]{val} } 8 | 9 | /* 10 | Short for "zero optional". The zero value is considered empty/null in JSON. 11 | Note that "encoding/json" doesn't support ",omitempty" for structs. This wrapper 12 | allows empty structs to become "null". This type doesn't implement any encoding 13 | or decoding methods other than for JSON, and is intended only for non-scalar 14 | values such as "models" / "data classes". Scalars tend to be compatible with 15 | ",omitempty" in JSON, and don't require such wrappers. 16 | */ 17 | type Zop[A any] struct { 18 | /** 19 | Annotation `role:"ref"` indicates that this field is a reference/pointer to 20 | the inner type/value. Reflection-based code may use this to treat this type 21 | like a pointer. 22 | */ 23 | Val A `role:"ref"` 24 | } 25 | 26 | // Implement `Nullable`. True if zero value of its type. 27 | func (self Zop[_]) IsNull() bool { return IsZero(self.Val) } 28 | 29 | // Inverse of `.IsNull`. 30 | func (self Zop[_]) IsNotNull() bool { return !IsZero(self.Val) } 31 | 32 | // Implement `Clearer`. Zeroes the receiver. 33 | func (self *Zop[_]) Clear() { PtrClear(&self.Val) } 34 | 35 | // Implement `Getter`, returning the underlying value as-is. 36 | func (self Zop[A]) Get() A { return self.Val } 37 | 38 | // Implement `Setter`, modifying the underlying value. 39 | func (self *Zop[A]) Set(val A) { self.Val = val } 40 | 41 | // Implement `Ptrer`, returning a pointer to the underlying value. 42 | func (self *Zop[A]) Ptr() *A { 43 | if self == nil { 44 | return nil 45 | } 46 | return &self.Val 47 | } 48 | 49 | /* 50 | Implement `json.Marshaler`. If `.IsNull`, returns a representation of JSON null. 51 | Otherwise uses `json.Marshal` to encode the underlying value. 52 | */ 53 | func (self Zop[A]) MarshalJSON() ([]byte, error) { 54 | return JsonBytesNullCatch[A](self) 55 | } 56 | 57 | /* 58 | Implement `json.Unmarshaler`. If the input is empty or represents JSON null, 59 | clears the receiver via `.Clear`. Otherwise uses `json.Unmarshal` to decode 60 | into the underlying value. 61 | */ 62 | func (self *Zop[_]) UnmarshalJSON(src []byte) error { 63 | if IsJsonEmpty(src) { 64 | self.Clear() 65 | return nil 66 | } 67 | return JsonDecodeCatch(src, &self.Val) 68 | } 69 | 70 | /* 71 | FP-style "mapping". If the original value is zero, or if the function is nil, 72 | the output is zero. Otherwise the output is the result of calling the function 73 | with the previous value. 74 | */ 75 | func ZopMap[A, B any](src Zop[A], fun func(A) B) (out Zop[B]) { 76 | if src.IsNotNull() && fun != nil { 77 | out.Val = fun(src.Val) 78 | } 79 | return 80 | } 81 | -------------------------------------------------------------------------------- /zop_test.go: -------------------------------------------------------------------------------- 1 | package gg_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mitranim/gg" 7 | "github.com/mitranim/gg/gtest" 8 | ) 9 | 10 | func TestZop_MarshalJSON(t *testing.T) { 11 | defer gtest.Catch(t) 12 | 13 | type Type = gg.Zop[int] 14 | 15 | gtest.Eq(gg.JsonString(gg.Zero[Type]()), `null`) 16 | gtest.Eq(gg.JsonString(Type{123}), `123`) 17 | gtest.Eq(gg.JsonString(gg.ZopVal(123)), `123`) 18 | } 19 | 20 | func TestZop_UnmarshalJSON(t *testing.T) { 21 | defer gtest.Catch(t) 22 | 23 | type Type = gg.Zop[int] 24 | 25 | gtest.Zero(gg.JsonDecodeTo[Type](`null`)) 26 | 27 | gtest.Equal( 28 | gg.JsonDecodeTo[Type](`123`), 29 | gg.ZopVal(123), 30 | ) 31 | } 32 | --------------------------------------------------------------------------------