diff --git a/.gitignore b/.gitignore index f2b2366..37e78f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,21 @@ -# Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib - -# Test binary, build with `go test -c` +*.db +*.db-journal +*.mmdb *.test - -# Output of the go coverage tool, specifically when used with LiteIDE *.out -.idea -.tools +.idea/ +.vscode/ +.tools/ + +coverage.txt +coverage.out + +bin/ +vendor/ +build/ diff --git a/.golangci.yml b/.golangci.yml index 54d9a69..e777aea 100755 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,4 +1,3 @@ - # options for analysis running run: # timeout for analysis, e.g. 30s, 5m, default is 1m @@ -160,10 +159,10 @@ linters-settings: # If 'custom-order' is 'true', it follows the order of 'sections' option. # Default: ["standard", "default"] #sections: - #- standard # Standard section: captures all standard packages. - #- default # Default section: contains all imports that could not be matched to another section type. - #- blank # Blank section: contains all blank imports. This section is not present unless explicitly enabled. - #- dot # Dot section: contains all dot imports. This section is not present unless explicitly enabled. + #- standard # Standard section: captures all standard packages. + #- default # Default section: contains all imports that could not be matched to another section type. + #- blank # Blank section: contains all blank imports. This section is not present unless explicitly enabled. + #- dot # Dot section: contains all dot imports. This section is not present unless explicitly enabled. # Skip generated files. # Default: true skip-generated: true @@ -342,10 +341,16 @@ linters-settings: # Max line length, lines longer will be reported. # '\t' is counted as 1 character by default, and can be changed with the tab-width option. # Default: 120. - line-length: 120 + line-length: 130 # Tab width in spaces. # Default: 1 tab-width: 1 + staticcheck: + # Deprecated: use the global `run.go` instead. + go: "1.15" + # SAxxxx checks in https://staticcheck.io/docs/configuration/options/#checks + # Default: ["*"] + checks: [ "*", "-SA1019" ] linters: disable-all: true @@ -362,14 +367,13 @@ linters: - unused - prealloc - durationcheck -# - nolintlint - staticcheck - makezero - nilerr - errorlint - bodyclose - exportloopref - - gci +# - gci - gosec # - lll fast: false diff --git a/.lic.yaml b/.lic.yaml index 2566215..ee29af9 100755 --- a/.lic.yaml +++ b/.lic.yaml @@ -1,3 +1,3 @@ -author: "Mikhail Knyazhev " +author: "Mikhail Knyazhev " lic_short: "BSD 3-Clause" -lic_file: LICENSE +lic_file: LICENSE \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..861fb6d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,12 @@ +**Use issues for everything** + +- For a small change, just send a PR. +- For bigger changes open an issue for discussion before sending a PR. +- PR should have: + - Test case + - Documentation + - Example (If it makes sense) +- You can also contribute by: + - Reporting issues + - Suggesting new features or enhancements + - Improve/fix documentation diff --git a/LICENSE b/LICENSE index 88542dc..9d3a949 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2019-2024, Mikhail Knyazhev +Copyright (c) 2019-2024, Mikhail Knyazhev Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/Makefile b/Makefile index 918d40d..81f5ee0 100755 --- a/Makefile +++ b/Makefile @@ -3,9 +3,6 @@ install: go install github.com/osspkg/devtool@latest -.PHONY: setup -setup: - devtool setup-lib .PHONY: lint lint: @@ -15,17 +12,17 @@ lint: license: devtool license -.PHONY: build -build: - devtool build --arch=amd64 - .PHONY: tests tests: devtool test -.PHONY: pre-commite -pre-commite: setup lint build tests - .PHONY: ci -ci: install setup lint build tests +ci: install license lint tests + +.PHONY: go_work +go_work: + go work use -r . + go work sync +create_release: + devtool tag \ No newline at end of file diff --git a/README.md b/README.md index bbb1eb7..baf1fcb 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,5 @@ -# Algorithms - -Algorithmic calculation methods - -[![Coverage Status](https://coveralls.io/repos/github/osspkg/go-algorithms/badge.svg?branch=master)](https://coveralls.io/github/osspkg/go-algorithms?branch=master) -[![Release](https://img.shields.io/github/release/osspkg/go-algorithms.svg?style=flat-square)](https://github.com/osspkg/go-algorithms/releases/latest) -[![Go Report Card](https://goreportcard.com/badge/github.com/osspkg/go-algorithms)](https://goreportcard.com/report/github.com/osspkg/go-algorithms) -[![CI](https://github.com/osspkg/go-algorithms/actions/workflows/ci.yml/badge.svg)](https://github.com/osspkg/go-algorithms/actions/workflows/ci.yml) - -## Install - -```shell -go get -u go.osspkg.com/algorithms -``` - -## List - -- Graph - - Topological sorting - - [Kahn's Algorithm](graph/kahn/type.go) -- Information compression - - [Reducing numbers](shorten/shorten.go) -- Sorting algorithm - - [Bubble sort](sorts/bubble.go) - - [Cocktail shaker sort](sorts/cocktail.go) - - [Insertion sort](sorts/insertion.go) - - [Merge sort](sorts/merge.go) - - [Selection sort](sorts/selection.go) - - [Heapsort](sorts/heapsort.go) -- Filtering algorithms - - [Bloom filter](filters/bloom/bloom.go) +# goX ## License -BSD-3-Clause License. See the LICENSE file for details +BSD-3-Clause License. See the LICENSE file for details. \ No newline at end of file diff --git a/algorithms/README.md b/algorithms/README.md new file mode 100644 index 0000000..bddbd45 --- /dev/null +++ b/algorithms/README.md @@ -0,0 +1,13 @@ +# Algorithms + +Algorithmic calculation methods + +## Install + +```shell +go get -u go.osspkg.com/x/algorithms +``` + +## License + +BSD-3-Clause License. See the LICENSE file for details diff --git a/algorithms/encoding/base62/base62.go b/algorithms/encoding/base62/base62.go new file mode 100644 index 0000000..8d60929 --- /dev/null +++ b/algorithms/encoding/base62/base62.go @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package base62 + +import "go.osspkg.com/x/algorithms/sorts" + +const size = 62 + +type Base62 struct { + enc []byte + dec map[byte]uint64 +} + +func New(alphabet string) *Base62 { + if len(alphabet) != size { + panic("encoding alphabet is not 62-bytes long") + } + v := &Base62{ + enc: []byte(alphabet), + dec: make(map[byte]uint64, size), + } + for i, b := range v.enc { + v.dec[b] = uint64(i) + } + return v +} + +func (v *Base62) Encode(id uint64) string { + result := make([]byte, 0, 11) + for id > 0 { + result = append(result, v.enc[id%size]) + id /= size + } + sorts.Reverse(result) + return string(result) +} + +func (v *Base62) Decode(data string) uint64 { + var id uint64 + for _, b := range []byte(data) { + id = id*size + v.dec[b] + } + return id +} diff --git a/algorithms/encoding/base62/base62_test.go b/algorithms/encoding/base62/base62_test.go new file mode 100644 index 0000000..4c44cdc --- /dev/null +++ b/algorithms/encoding/base62/base62_test.go @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package base62 + +import ( + "fmt" + "math" + "testing" + + "go.osspkg.com/x/test" +) + +func TestEncode_EncodeDecode(t *testing.T) { + tests := []struct { + name string + id uint64 + want string + }{ + {name: "Case1", id: 1, want: "p"}, + {name: "Case1", id: 2, want: "L"}, + {name: "Case1", id: 3, want: "K"}, + {name: "Case1", id: 4, want: "G"}, + {name: "Case1", id: 5, want: "R"}, + {name: "Case1", id: 6, want: "S"}, + {name: "Case1", id: 7, want: "u"}, + {name: "Case1", id: 8, want: "D"}, + {name: "Case1", id: 9, want: "v"}, + {name: "Case2", id: 10, want: "o"}, + {name: "Case3", id: 100, want: "pH"}, + {name: "Case4", id: 1000, want: "PD"}, + {name: "Case5", id: 10000, want: "LIn"}, + {name: "Case6", id: 100000, want: "c0k"}, + {name: "Case7", id: 1000000000, want: "pRmUWP"}, + {name: "Case8", id: 999999, want: "Glvp"}, + {name: "Case20", id: math.MaxUint64, want: "XNWjtpSoji4"}, + } + + v := New("0pLKGRSuDvorlO14Pjnd7XgQw9c8YhaIJ5iqtIHy3mWxM6C2TeAbFVBUkZfsNz") + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h := v.Encode(tt.id) + test.Equal(t, tt.want, h) + id := v.Decode(h) + test.Equal(t, tt.id, id) + }) + } +} + +func TestEncode_Encode(t *testing.T) { + tests := []struct { + name string + str string + want uint64 + }{ + {name: "Case1", str: "a", want: 30}, + {name: "Case2", str: "aa", want: 1890}, + {name: "Case3", str: "aaaaaaaa", want: 107380379795850}, + } + + v := New("0pLKGRSuDvorlO14Pjnd7XgQw9c8YhaIJ5iqtIHy3mWxM6C2TeAbFVBUkZfsNz") + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h := v.Decode(tt.str) + fmt.Println(h) + test.Equal(t, tt.want, h) + }) + } +} + +func Benchmark_base62(b *testing.B) { + v := New("0pLKGRSuDvorlO14Pjnd7XgQw9c8YhaIJ5iqtIHy3mWxM6C2TeAbFVBUkZfsNz") + + b.ReportAllocs() + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + v.Decode(v.Encode(math.MaxUint64)) + } + }) +} diff --git a/filters/bloom/bloom.go b/algorithms/filters/bloom/bloom.go similarity index 95% rename from filters/bloom/bloom.go rename to algorithms/filters/bloom/bloom.go index a14382c..409bc2f 100644 --- a/filters/bloom/bloom.go +++ b/algorithms/filters/bloom/bloom.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/filters/bloom/bloom_test.go b/algorithms/filters/bloom/bloom_test.go similarity index 68% rename from filters/bloom/bloom_test.go rename to algorithms/filters/bloom/bloom_test.go index e3a6c2f..acc2332 100644 --- a/filters/bloom/bloom_test.go +++ b/algorithms/filters/bloom/bloom_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ @@ -8,30 +8,30 @@ package bloom import ( "testing" - "github.com/stretchr/testify/require" + "go.osspkg.com/x/test" ) func TestUnit_Bloom(t *testing.T) { bf, err := New(1000, 0.00001) - require.NoError(t, err) + test.NoError(t, err) bf.Add([]byte("hello")) bf.Add([]byte("user")) bf.Add([]byte("home")) - require.False(t, bf.Contain([]byte("users"))) - require.True(t, bf.Contain([]byte("user"))) + test.False(t, bf.Contain([]byte("users"))) + test.True(t, bf.Contain([]byte("user"))) } func TestUnit_Bloom2(t *testing.T) { _, err := New(0, 0.00001) - require.Error(t, err) + test.Error(t, err) _, err = New(1, 1) - require.Error(t, err) + test.Error(t, err) _, err = New(1, 0.0001) - require.NoError(t, err) + test.NoError(t, err) } func Benchmark_Bloom(b *testing.B) { diff --git a/algorithms/go.mod b/algorithms/go.mod new file mode 100644 index 0000000..52ee6e4 --- /dev/null +++ b/algorithms/go.mod @@ -0,0 +1,5 @@ +module go.osspkg.com/x/algorithms + +go 1.20 + +replace go.osspkg.com/x/test => ./../test \ No newline at end of file diff --git a/go.sum b/algorithms/go.sum similarity index 100% rename from go.sum rename to algorithms/go.sum diff --git a/graph/kahn/type.go b/algorithms/graph/kahn/type.go similarity index 96% rename from graph/kahn/type.go rename to algorithms/graph/kahn/type.go index 217bc1b..88b23f0 100644 --- a/graph/kahn/type.go +++ b/algorithms/graph/kahn/type.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/graph/kahn/type_test.go b/algorithms/graph/kahn/type_test.go similarity index 76% rename from graph/kahn/type_test.go rename to algorithms/graph/kahn/type_test.go index 54c0a92..ce71412 100644 --- a/graph/kahn/type_test.go +++ b/algorithms/graph/kahn/type_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ @@ -9,7 +9,7 @@ import ( "strings" "testing" - "github.com/stretchr/testify/require" + "go.osspkg.com/x/test" ) func TestUnit_KahnCoherent(t *testing.T) { @@ -22,10 +22,10 @@ func TestUnit_KahnCoherent(t *testing.T) { graph.Add("c", "d") graph.Add("c", "e") graph.Add("d", "e") - require.NoError(t, graph.Build()) + test.NoError(t, graph.Build()) result := graph.Result() - require.True(t, len(result) == 5) - require.Contains(t, []string{"a,b,c,d,e"}, strings.Join(result, ",")) + test.True(t, len(result) == 5) + test.Contains(t, []string{"a,b,c,d,e"}, strings.Join(result, ",")) } func TestUnit_KahnCoherentBreakPoint(t *testing.T) { @@ -39,10 +39,10 @@ func TestUnit_KahnCoherentBreakPoint(t *testing.T) { graph.Add("c", "e") graph.Add("d", "e") graph.BreakPoint("d") - require.NoError(t, graph.Build()) + test.NoError(t, graph.Build()) result := graph.Result() - require.True(t, len(result) == 4) - require.Contains(t, []string{"a,b,c,d"}, strings.Join(result, ",")) + test.True(t, len(result) == 4) + test.Contains(t, []string{"a,b,c,d"}, strings.Join(result, ",")) } func TestUnit_KahnCoherentBreakPoint2(t *testing.T) { @@ -51,7 +51,7 @@ func TestUnit_KahnCoherentBreakPoint2(t *testing.T) { graph.Add("a", "c") graph.Add("a", "d") graph.BreakPoint("w") - require.Error(t, graph.Build()) + test.Error(t, graph.Build()) } func TestUnit_KahnCyclical(t *testing.T) { @@ -59,7 +59,7 @@ func TestUnit_KahnCyclical(t *testing.T) { graph.Add("1", "2") graph.Add("2", "3") graph.Add("3", "2") - require.Error(t, graph.Build()) + test.Error(t, graph.Build()) } func Benchmark_Kahn1(b *testing.B) { diff --git a/sorts/bubble.go b/algorithms/sorts/bubble.go similarity index 84% rename from sorts/bubble.go rename to algorithms/sorts/bubble.go index c84aa43..20fb01b 100644 --- a/sorts/bubble.go +++ b/algorithms/sorts/bubble.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/sorts/bubble_test.go b/algorithms/sorts/bubble_test.go similarity index 77% rename from sorts/bubble_test.go rename to algorithms/sorts/bubble_test.go index c0bf86b..f853b8e 100644 --- a/sorts/bubble_test.go +++ b/algorithms/sorts/bubble_test.go @@ -1,15 +1,14 @@ /* - * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ -package sorts_test +package sorts import ( "testing" - "github.com/stretchr/testify/require" - "go.osspkg.com/algorithms/sorts" + "go.osspkg.com/x/test" ) func TestUnit_SortBubble(t *testing.T) { @@ -41,10 +40,10 @@ func TestUnit_SortBubble(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - sorts.Bubble(tt.args, func(i, j int) bool { + Bubble(tt.args, func(i, j int) bool { return tt.args[i] < tt.args[j] }) - require.Equal(t, tt.want, tt.args) + test.Equal(t, tt.want, tt.args) }) } } @@ -53,7 +52,7 @@ func Benchmark_SortBubble(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { arr := []int{45, 61, 87, 20, 65, 36, 25, 86, 64, 4, 36, 53, 17, 38, 48, 52, 53, 59, 80, 79, 95, 72, 85, 52, 9, 12, 9, 36, 47, 34} - sorts.Bubble(arr, func(i, j int) bool { + Bubble(arr, func(i, j int) bool { return arr[i] < arr[j] }) } diff --git a/sorts/cocktail.go b/algorithms/sorts/cocktail.go similarity index 88% rename from sorts/cocktail.go rename to algorithms/sorts/cocktail.go index fd05c36..529dd4e 100644 --- a/sorts/cocktail.go +++ b/algorithms/sorts/cocktail.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/sorts/cocktail_test.go b/algorithms/sorts/cocktail_test.go similarity index 78% rename from sorts/cocktail_test.go rename to algorithms/sorts/cocktail_test.go index 58ca058..151556a 100644 --- a/sorts/cocktail_test.go +++ b/algorithms/sorts/cocktail_test.go @@ -1,15 +1,14 @@ /* - * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ -package sorts_test +package sorts import ( "testing" - "github.com/stretchr/testify/require" - "go.osspkg.com/algorithms/sorts" + "go.osspkg.com/x/test" ) func TestUnit_SortCocktail(t *testing.T) { @@ -46,10 +45,10 @@ func TestUnit_SortCocktail(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - sorts.Cocktail(tt.args, func(i, j int) bool { + Cocktail(tt.args, func(i, j int) bool { return tt.args[i] < tt.args[j] }) - require.Equal(t, tt.want, tt.args) + test.Equal(t, tt.want, tt.args) }) } } @@ -58,7 +57,7 @@ func Benchmark_SortCocktail(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { arr := []int{45, 61, 87, 20, 65, 36, 25, 86, 64, 4, 36, 53, 17, 38, 48, 52, 53, 59, 80, 79, 95, 72, 85, 52, 9, 12, 9, 36, 47, 34} - sorts.Cocktail(arr, func(i, j int) bool { + Cocktail(arr, func(i, j int) bool { return arr[i] < arr[j] }) } diff --git a/sorts/common_test.go b/algorithms/sorts/common_test.go similarity index 80% rename from sorts/common_test.go rename to algorithms/sorts/common_test.go index 7b91d51..cc831ec 100644 --- a/sorts/common_test.go +++ b/algorithms/sorts/common_test.go @@ -1,9 +1,9 @@ /* - * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ -package sorts_test +package sorts import ( "sort" diff --git a/sorts/heapsort.go b/algorithms/sorts/heapsort.go similarity index 90% rename from sorts/heapsort.go rename to algorithms/sorts/heapsort.go index 216e22c..5091916 100644 --- a/sorts/heapsort.go +++ b/algorithms/sorts/heapsort.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/sorts/heapsort_test.go b/algorithms/sorts/heapsort_test.go similarity index 78% rename from sorts/heapsort_test.go rename to algorithms/sorts/heapsort_test.go index 3b86137..f1d994f 100644 --- a/sorts/heapsort_test.go +++ b/algorithms/sorts/heapsort_test.go @@ -1,15 +1,14 @@ /* - * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ -package sorts_test +package sorts import ( "testing" - "github.com/stretchr/testify/require" - "go.osspkg.com/algorithms/sorts" + "go.osspkg.com/x/test" ) func TestUnit_SortHeapsort(t *testing.T) { @@ -46,10 +45,10 @@ func TestUnit_SortHeapsort(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - sorts.Heapsort(tt.args, func(i, j int) bool { + Heapsort(tt.args, func(i, j int) bool { return tt.args[i] < tt.args[j] }) - require.Equal(t, tt.want, tt.args) + test.Equal(t, tt.want, tt.args) }) } } @@ -58,7 +57,7 @@ func Benchmark_SortHeapsort(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { arr := []int{45, 61, 87, 20, 65, 36, 25, 86, 64, 4, 36, 53, 17, 38, 48, 52, 53, 59, 80, 79, 95, 72, 85, 52, 9, 12, 9, 36, 47, 34} - sorts.Heapsort(arr, func(i, j int) bool { + Heapsort(arr, func(i, j int) bool { return arr[i] < arr[j] }) } diff --git a/sorts/insertion.go b/algorithms/sorts/insertion.go similarity index 82% rename from sorts/insertion.go rename to algorithms/sorts/insertion.go index 52f1a35..a88e8cc 100644 --- a/sorts/insertion.go +++ b/algorithms/sorts/insertion.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/sorts/insertion_test.go b/algorithms/sorts/insertion_test.go similarity index 78% rename from sorts/insertion_test.go rename to algorithms/sorts/insertion_test.go index a5e4286..efb595b 100644 --- a/sorts/insertion_test.go +++ b/algorithms/sorts/insertion_test.go @@ -1,15 +1,14 @@ /* - * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ -package sorts_test +package sorts import ( "testing" - "github.com/stretchr/testify/require" - "go.osspkg.com/algorithms/sorts" + "go.osspkg.com/x/test" ) func TestUnit_SortInsertion(t *testing.T) { @@ -46,10 +45,10 @@ func TestUnit_SortInsertion(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - sorts.Insertion(tt.args, func(i, j int) bool { + Insertion(tt.args, func(i, j int) bool { return tt.args[i] < tt.args[j] }) - require.Equal(t, tt.want, tt.args) + test.Equal(t, tt.want, tt.args) }) } } @@ -58,7 +57,7 @@ func Benchmark_SortInsertion(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { arr := []int{45, 61, 87, 20, 65, 36, 25, 86, 64, 4, 36, 53, 17, 38, 48, 52, 53, 59, 80, 79, 95, 72, 85, 52, 9, 12, 9, 36, 47, 34} - sorts.Insertion(arr, func(i, j int) bool { + Insertion(arr, func(i, j int) bool { return arr[i] < arr[j] }) } diff --git a/sorts/merge.go b/algorithms/sorts/merge.go similarity index 93% rename from sorts/merge.go rename to algorithms/sorts/merge.go index 0d77cad..5947990 100644 --- a/sorts/merge.go +++ b/algorithms/sorts/merge.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/sorts/merge_test.go b/algorithms/sorts/merge_test.go similarity index 78% rename from sorts/merge_test.go rename to algorithms/sorts/merge_test.go index 57286c7..72dc525 100644 --- a/sorts/merge_test.go +++ b/algorithms/sorts/merge_test.go @@ -1,15 +1,14 @@ /* - * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ -package sorts_test +package sorts import ( "testing" - "github.com/stretchr/testify/require" - "go.osspkg.com/algorithms/sorts" + "go.osspkg.com/x/test" ) func TestUnit_SortMerge(t *testing.T) { @@ -46,10 +45,10 @@ func TestUnit_SortMerge(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - sorts.Merge(tt.args, func(i, j int) bool { + Merge(tt.args, func(i, j int) bool { return tt.args[i] < tt.args[j] }) - require.Equal(t, tt.want, tt.args) + test.Equal(t, tt.want, tt.args) }) } } @@ -58,7 +57,7 @@ func Benchmark_SortMerge(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { arr := []int{45, 61, 87, 20, 65, 36, 25, 86, 64, 4, 36, 53, 17, 38, 48, 52, 53, 59, 80, 79, 95, 72, 85, 52, 9, 12, 9, 36, 47, 34} - sorts.Merge(arr, func(i, j int) bool { + Merge(arr, func(i, j int) bool { return arr[i] < arr[j] }) } diff --git a/algorithms/sorts/reverse.go b/algorithms/sorts/reverse.go new file mode 100644 index 0000000..69ad75b --- /dev/null +++ b/algorithms/sorts/reverse.go @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package sorts + +func Reverse[T any](list []T) { + j := len(list) - 1 + for i := 0; i < len(list)/2; i++ { + list[i], list[j] = list[j], list[i] + j-- + } +} diff --git a/algorithms/sorts/reverse_test.go b/algorithms/sorts/reverse_test.go new file mode 100644 index 0000000..651ac75 --- /dev/null +++ b/algorithms/sorts/reverse_test.go @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package sorts + +import ( + "testing" + + "go.osspkg.com/x/test" +) + +func TestUnit_Reverse(t *testing.T) { + type testCase[T any] struct { + name string + list []T + want []T + } + tests := []testCase[int]{ + { + name: "case1", + list: []int{2, 5, 3, 9, 0, 8}, + want: []int{8, 0, 9, 3, 5, 2}, + }, + { + name: "case2", + list: []int{2, 5, 3, 9, 0, 8, 4}, + want: []int{4, 8, 0, 9, 3, 5, 2}, + }, + { + name: "case3", + list: nil, + want: nil, + }, + { + name: "case4", + list: []int{2}, + want: []int{2}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + Reverse(tt.list) + test.Equal(t, tt.want, tt.list) + }) + } +} + +func Benchmark_SortReverse(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + arr := []int{45, 61, 87, 20, 65, 36, 25, 86, 64, 4, 36, 53, 17, 38, 48, 52, 53, 59, 80, 79, 95, 72, 85, 52, 9, 12, 9, 36, 47, 34} + Reverse(arr) + } +} diff --git a/sorts/selection.go b/algorithms/sorts/selection.go similarity index 83% rename from sorts/selection.go rename to algorithms/sorts/selection.go index 2d62ef5..5b38a9a 100644 --- a/sorts/selection.go +++ b/algorithms/sorts/selection.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/sorts/selection_test.go b/algorithms/sorts/selection_test.go similarity index 78% rename from sorts/selection_test.go rename to algorithms/sorts/selection_test.go index 3f7fa86..4bce7fe 100644 --- a/sorts/selection_test.go +++ b/algorithms/sorts/selection_test.go @@ -1,15 +1,14 @@ /* - * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ -package sorts_test +package sorts import ( "testing" - "github.com/stretchr/testify/require" - "go.osspkg.com/algorithms/sorts" + "go.osspkg.com/x/test" ) func TestUnit_SortSelection(t *testing.T) { @@ -46,10 +45,10 @@ func TestUnit_SortSelection(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - sorts.Selection(tt.args, func(i, j int) bool { + Selection(tt.args, func(i, j int) bool { return tt.args[i] < tt.args[j] }) - require.Equal(t, tt.want, tt.args) + test.Equal(t, tt.want, tt.args) }) } } @@ -58,7 +57,7 @@ func Benchmark_SortSelection(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { arr := []int{45, 61, 87, 20, 65, 36, 25, 86, 64, 4, 36, 53, 17, 38, 48, 52, 53, 59, 80, 79, 95, 72, 85, 52, 9, 12, 9, 36, 47, 34} - sorts.Selection(arr, func(i, j int) bool { + Selection(arr, func(i, j int) bool { return arr[i] < arr[j] }) } diff --git a/config/README.md b/config/README.md new file mode 100644 index 0000000..0edf2c9 --- /dev/null +++ b/config/README.md @@ -0,0 +1,48 @@ +# Config Resolver + +Updating the config through resolver variables. + +## Config example + +update config via ENV + +```text +@env(key#defaut value) +``` + +```yaml +envs: + home: "@env(HOME#/tmp/home)" + path: "@env(PATH#/usr/local/bin)" +``` + +```go +import ( + "go.osspkg.com/x/config" +) + +type ( + ConfigItem struct { + Home string `yaml:"home"` + Path string `yaml:"path"` + } + Config struct { + Envs testConfigItem `yaml:"envs"` + } +) + +func main() { + conf := Config{} + + res := config.NewConfigResolve( + config.EnvResolver(), // env resolver + ) + res.OpenFile("./config.yaml") // open config file + res.Build() // prepare config with resolvers + res.Decode(&conf) // decoding config + + fmt.Println(conf.Envs.Home) + fmt.Println(conf.Envs.Path) +} + +``` \ No newline at end of file diff --git a/config/common.go b/config/common.go new file mode 100644 index 0000000..597b4b4 --- /dev/null +++ b/config/common.go @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package config + +import ( + "bytes" + "fmt" + "os" + "regexp" + + "gopkg.in/yaml.v3" +) + +type ( + Resolver interface { + Name() string + Value(name string) (string, bool) + } + + Config struct { + blob []byte + resolvers []Resolver + } +) + +func New(rs ...Resolver) *Config { + return &Config{ + blob: nil, + resolvers: rs, + } +} + +func (v *Config) OpenFile(filename string) error { + b, err := os.ReadFile(filename) + if err != nil { + return err + } + v.blob = b + return nil +} + +func (v *Config) Decode(cs ...interface{}) error { + for _, c := range cs { + if err := yaml.Unmarshal(v.blob, c); err != nil { + return err + } + } + return nil +} + +var rexName = regexp.MustCompile(`(?m)^[a-z][a-z0-9]+$`) + +func (v *Config) Build() error { + for _, resolver := range v.resolvers { + if !rexName.MatchString(resolver.Name()) { + return fmt.Errorf("resolver '%s' has invalid name, must like regexp [a-z][a-z0-9]+", resolver.Name()) + } + rex := regexp.MustCompile(fmt.Sprintf(`(?mUsi)@%s\((.+)#(.*)\)`, resolver.Name())) + submatchs := rex.FindAllSubmatch(v.blob, -1) + + for _, submatch := range submatchs { + pattern, key, defval := submatch[0], submatch[1], submatch[2] + + if val, ok := resolver.Value(string(key)); ok && len(val) > 0 { + defval = []byte(val) + } + + v.blob = bytes.ReplaceAll(v.blob, pattern, defval) + } + } + + return nil +} diff --git a/config/common_test.go b/config/common_test.go new file mode 100644 index 0000000..f115fd8 --- /dev/null +++ b/config/common_test.go @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package config + +import ( + "os" + "testing" + + "go.osspkg.com/x/test" +) + +type ( + testConfigItem struct { + Home string `yaml:"home"` + Path string `yaml:"path"` + } + testConfig struct { + Envs testConfigItem `yaml:"envs"` + } +) + +func TestUnit_ConfigResolve(t *testing.T) { + filename := "/tmp/TestUnit_ConfigResolve.yaml" + data := ` +envs: + home: "@env(HOME#fail)" + path: "@env(PATH#fail)" +` + err := os.WriteFile(filename, []byte(data), 0755) + test.NoError(t, err) + + res := New(EnvResolver()) + + err = res.OpenFile(filename) + test.NoError(t, err) + err = res.Build() + test.NoError(t, err) + + tc := &testConfig{} + + err = res.Decode(tc) + test.NoError(t, err) + test.NotEqual(t, "fail", tc.Envs.Home) + test.NotEqual(t, "fail", tc.Envs.Path) +} diff --git a/config/go.mod b/config/go.mod new file mode 100644 index 0000000..d0c1f58 --- /dev/null +++ b/config/go.mod @@ -0,0 +1,10 @@ +module go.osspkg.com/x/config + +go 1.20 + +replace go.osspkg.com/x/test => ./../test + +require ( + go.osspkg.com/x/test v0.3.0 + gopkg.in/yaml.v3 v3.0.1 +) diff --git a/config/go.sum b/config/go.sum new file mode 100644 index 0000000..a62c313 --- /dev/null +++ b/config/go.sum @@ -0,0 +1,4 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/config/resolver_env.go b/config/resolver_env.go new file mode 100644 index 0000000..cdf7d78 --- /dev/null +++ b/config/resolver_env.go @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package config + +import "os" + +type envResolver struct{} + +func EnvResolver() Resolver { + return &envResolver{} +} + +func (e envResolver) Name() string { + return "env" +} + +func (e envResolver) Value(name string) (string, bool) { + return os.LookupEnv(name) +} diff --git a/console/README.md b/console/README.md new file mode 100644 index 0000000..96402d3 --- /dev/null +++ b/console/README.md @@ -0,0 +1,114 @@ +# Console application + +## Creating console application + +```go +import "go.osspkg.com/x/console" + +// creating an instance of the application, +// specifying its name and description for flag: --help +root := console.New("tool", "help tool") +// adding root command +root.RootCommand(...) +// adding one or more commands +root.AddCommand(...) +// launching the app +root.Exec() +``` + +## Creating a simple command + +```go +import "go.osspkg.com/x/console" +// creating a new team with settings +console.NewCommand(func(setter console.CommandSetter) { + // passing the command name and description + setter.Setup("simple", "first-level command") + // description of the usage example + setter.Example("simple aa/bb/cc -a=hello -b=123 --cc=123.456 -e") + // description of flags + setter.Flag(func(f console.FlagsSetter) { + // you can specify the flag's name, default value, and information about the flag's value. + f.StringVar("a", "demo", "this is a string argument") + f.IntVar("b", 1, "this is a int64 argument") + f.FloatVar("cc", 1e-5, "this is a float64 argument") + f.Bool("d", "this is a bool argument") + }) + // argument validation: specifies the number of arguments, + // and validation function that should return + // value after validation and validation error + setter.ArgumentFunc(func(s []string) ([]string, error) { + if !strings.Contains(s[0], "/") { + return nil, fmt.Errorf("argument must contain /") + } + return strings.Split(s[0], "/"), nil + }) + // command execution function + // first argument is a slice of arguments from setter.Argument + // all subsequent arguments must be in the same order and types as listed in setter.Flag + setter.ExecFunc(func(args []string, a string, b int64, c float64, d bool) { + fmt.Println(args, a, b, c, d) + }) +}), +``` + +### example of execution results + +**go run main.go --help** + +```text +NAME: + tool - help tool +SYNOPSIS: + tool [arg] +COMMANDS: + one first level + +``` + +**go run main.go simple --help** + +```text +NAME + tool - help tool +SYNOPSIS + tool simple [arg] +DESCRIPTION + first-level command +ARGUMENTS + -a this is a string argument (default: demo) + -b this is a int64 argument (default: 1) + --cc this is a float64 argument (default: 1e-05) + -e this is a bool argument (default: false) + +``` + +## Creating multi-level command tree + +To create a multi-level command tree, +you need to add the child command to the parent via the `AddCommand` method. + +At the same time, in the parent command, it is enough to +specify only the name and description via the `Setup` method. + +```go +root := console.New("tool", "help tool") + +simpleCmd := console.NewCommand(func(setter console.CommandSetter) { + setter.Setup("simple", "third level") + .... +}) + +twoCmd := console.NewCommand(func(setter console.CommandSetter) { + setter.Setup("two", "second level") + setter.AddCommand(simpleCmd) +}) + +oneCmd := console.NewCommand(func(setter console.CommandSetter) { + setter.Setup("one", "first level") + setter.AddCommand(twoCmd) +}) + +root.AddCommand(oneCmd) +root.Exec() +``` diff --git a/console/args.go b/console/args.go new file mode 100644 index 0000000..be6a674 --- /dev/null +++ b/console/args.go @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package console + +import "strings" + +type ( + // ValidFunc validate argument interface + ValidFunc func([]string) ([]string, error) + // Argument model + Argument struct { + ValidFunc ValidFunc + } +) + +// NewArgument constructor +func NewArgument() *Argument { + return &Argument{} +} + +type ( + // Args list model + Args struct { + list []Arg + next []string + } + // Arg model + Arg struct { + Key string + Value string + } + // ArgGetter argument getter interface + ArgGetter interface { + Has(name string) bool + Get(name string) *string + } +) + +// NewArgs constructor +func NewArgs() *Args { + return &Args{ + list: make([]Arg, 0), + next: make([]string, 0), + } +} + +func (a *Args) Has(name string) bool { + for _, v := range a.list { + if v.Key == name { + return true + } + } + return false +} + +func (a *Args) Get(name string) *string { + for _, v := range a.list { + if v.Key == name { + return &v.Value + } + } + return nil +} + +func (a *Args) Next() []string { + return a.next +} + +func (a *Args) Parse(list []string) *Args { + for i := 0; i < len(list); i++ { + // args + if strings.HasPrefix(list[i], "-") { + arg := Arg{} + v := strings.TrimLeft(list[i], "-") + vs := strings.SplitN(v, "=", 2) + switch len(vs) { + case 1: + arg.Key, arg.Value = vs[0], "" + a.list = append(a.list, arg) + continue + case 2: + arg.Key, arg.Value = vs[0], vs[1] + a.list = append(a.list, arg) + continue + } + + if i+1 < len(list) && !strings.HasPrefix(list[i+1], "-") { + arg.Key, arg.Value = vs[0], list[i+1] + a.list = append(a.list, arg) + i++ + continue + } + + arg.Key = vs[0] + a.list = append(a.list, arg) + continue + } + // commands + a.next = append(a.next, list[i]) + } + + return a +} diff --git a/console/command.go b/console/command.go new file mode 100644 index 0000000..f5584eb --- /dev/null +++ b/console/command.go @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package console + +import ( + "fmt" + "reflect" +) + +type Command struct { + root bool + name string + desc string + flags *Flags + args *Argument + execute interface{} + + next []CommandGetter +} + +type CommandGetter interface { + Next(string) CommandGetter + List() []CommandGetter + Validate() error + Is(string) bool + Name() string + Description() string + ArgCall(d []string) ([]string, error) + Flags() FlagsGetter + Call() interface{} + AddCommand(...CommandGetter) + AsRoot() CommandGetter + IsRoot() bool +} + +type CommandSetter interface { + Setup(string, string) + Flag(cb func(FlagsSetter)) + ArgumentFunc(call ValidFunc) + ExecFunc(interface{}) + AddCommand(...CommandGetter) +} + +func NewCommand(cb func(CommandSetter)) CommandGetter { + cmd := &Command{ + next: make([]CommandGetter, 0, 2), + flags: NewFlags(), + args: NewArgument(), + } + cb(cmd) + return cmd +} + +func (c *Command) Setup(name, description string) { + c.name, c.desc = name, description +} + +func (c *Command) AsRoot() CommandGetter { + c.root = true + c.name = "" + return c +} + +func (c *Command) IsRoot() bool { + return c.root +} + +func (c *Command) Name() string { + return c.name +} + +func (c *Command) Description() string { + return c.desc +} + +func (c *Command) Flag(cb func(FlagsSetter)) { + cb(c.flags) +} + +func (c *Command) Flags() FlagsGetter { + return c.flags +} + +func (c *Command) ArgumentFunc(call ValidFunc) { + c.args.ValidFunc = call +} + +func (c *Command) ArgCall(d []string) ([]string, error) { + if c.args.ValidFunc == nil { + return d, nil + } + return c.args.ValidFunc(d) +} + +func (c *Command) ExecFunc(i interface{}) { + c.execute = i +} + +func (c *Command) Next(cmd string) CommandGetter { + for _, getter := range c.next { + if getter.Is(cmd) { + return getter + } + } + return nil +} + +func (c *Command) List() []CommandGetter { + return c.next +} + +func (c *Command) Validate() error { + if len(c.name) == 0 && !c.IsRoot() { + return fmt.Errorf("command name is empty. use Setup(name, description)") + } + if reflect.ValueOf(c.execute).Kind() != reflect.Func { + return nil + // return fmt.Errorf("command [%s] not called. Run with --help to get information", c.name) + } + count := c.flags.Count() + 1 + if reflect.ValueOf(c.execute).Type().NumIn() != count { + return fmt.Errorf("command [%s] Flags: fewer arguments declared than expected in ExecFunc", c.name) + } + return nil +} + +func (c *Command) Call() interface{} { + return c.execute +} + +func (c *Command) Is(s string) bool { + return c.name == s +} + +func (c *Command) AddCommand(getter ...CommandGetter) { + for _, v := range getter { + if err := v.Validate(); err != nil { + Fatalf(err.Error()) + } + c.next = append(c.next, v) + } +} diff --git a/console/console.go b/console/console.go new file mode 100644 index 0000000..65acbb7 --- /dev/null +++ b/console/console.go @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package console + +import ( + "os" + "reflect" +) + +const helpArg = "help" + +type Console struct { + name string + description string + root CommandGetter +} + +func New(name, description string) *Console { + return &Console{ + name: name, + description: description, + root: NewCommand(func(_ CommandSetter) {}).AsRoot(), + } +} + +func (c *Console) recover() { + if d := recover(); d != nil { + Fatalf("%+v", d) + } +} + +func (c *Console) AddCommand(getter ...CommandGetter) { + defer c.recover() + + c.root.AddCommand(getter...) +} + +func (c *Console) RootCommand(getter CommandGetter) { + defer c.recover() + + next := c.root.List() + c.root = getter.AsRoot() + if err := c.root.Validate(); err != nil { + Fatalf(err.Error()) + } + c.root.AddCommand(next...) +} + +func (c *Console) Exec() { + defer c.recover() + + args := NewArgs().Parse(os.Args[1:]) + cmd, cur, h := c.build(args) + if h { + helpView(c.name, c.description, cmd, cur) + return + } + c.run(cmd, args.Next()[len(cur):], args) +} + +func (c *Console) build(args *Args) (CommandGetter, []string, bool) { + var ( + i int + cmd string + + command CommandGetter + cur []string + help bool + ) + for i, cmd = range args.Next() { + if i == 0 { + if nc := c.root.Next(cmd); nc != nil { + command = nc + continue + } + command = c.root + break + } else { + if nc := command.Next(cmd); nc != nil { + command = nc + continue + } + break + } + } + + if len(args.Next()) > 0 { + cur = args.Next()[:i] + } else { + command = c.root + } + + if args.Has(helpArg) { + help = true + } + + return command, cur, help +} + +func (c *Console) run(command CommandGetter, a []string, args *Args) { + rv := make([]reflect.Value, 0) + + if command == nil || command.Call() == nil { + Fatalf("command not found (use --help for information)") + } + + val, err := command.ArgCall(a) + if err != nil { + Fatalf("command \"%s\" validate arguments: %s", command.Name(), err.Error()) + } + rv = append(rv, reflect.ValueOf(val)) + + err = command.Flags().Call(args, func(i interface{}) { + rv = append(rv, reflect.ValueOf(i)) + }) + if err != nil { + Fatalf("command \"%s\" validate flags: %s", command.Name(), err.Error()) + } + + if reflect.ValueOf(command.Call()).Type().NumIn() != len(rv) { + Fatalf("command \"%s\" Flags: fewer arguments declared than expected in ExecFunc", command.Name()) + } + + reflect.ValueOf(command.Call()).Call(rv) +} diff --git a/console/flags.go b/console/flags.go new file mode 100644 index 0000000..53fbf06 --- /dev/null +++ b/console/flags.go @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package console + +import ( + "fmt" + "strconv" +) + +type ( + // Flags model + Flags struct { + d []FlagItem + } + // FlagItem element of flag model + FlagItem struct { + req bool + name string + value interface{} + usage string + call func(getter ArgGetter) (interface{}, error) + } +) + +// FlagsGetter getter interface +type FlagsGetter interface { + Info(cb func(bool, string, interface{}, string)) + Call(g ArgGetter, cb func(interface{})) error +} + +// FlagsSetter setter interface +type FlagsSetter interface { + StringVar(name string, value string, usage string) + String(name string, usage string) + IntVar(name string, value int64, usage string) + Int(name string, usage string) + FloatVar(name string, value float64, usage string) + Float(name string, usage string) + Bool(name string, usage string) +} + +// NewFlags init new flag +func NewFlags() *Flags { + return &Flags{ + d: make([]FlagItem, 0), + } +} + +// Count of flags +func (f *Flags) Count() int { + return len(f.d) +} + +// Info about command +func (f *Flags) Info(cb func(req bool, name string, v interface{}, usage string)) { + for _, item := range f.d { + cb(item.req, item.name, item.value, item.usage) + } +} + +func (f *Flags) Call(g ArgGetter, cb func(interface{})) error { + for _, item := range f.d { + v, err := item.call(g) + if err != nil { + return err + } + cb(v) + } + return nil +} + +// StringVar flag decoder with default value +func (f *Flags) StringVar(name string, value string, usage string) { + f.d = append(f.d, FlagItem{ + req: false, + name: name, + value: value, + usage: usage, + call: func(getter ArgGetter) (interface{}, error) { + if val := getter.Get(name); val != nil { + return *val, nil + } + return value, nil + }, + }) +} + +// String flag decoder +func (f *Flags) String(name string, usage string) { + f.d = append(f.d, FlagItem{ + req: true, + name: name, + usage: usage, + call: func(getter ArgGetter) (interface{}, error) { + if val := getter.Get(name); val != nil && len(*val) > 0 { + return *val, nil + } + return nil, fmt.Errorf("--%s is not found", name) + }, + }) +} + +// IntVar flag decoder with default value +func (f *Flags) IntVar(name string, value int64, usage string) { + f.d = append(f.d, FlagItem{ + req: false, + value: value, + name: name, + usage: usage, + call: func(getter ArgGetter) (interface{}, error) { + if val := getter.Get(name); val != nil && len(*val) > 0 { + return strconv.ParseInt(*val, 10, 64) + } + return value, nil + }, + }) +} + +// Int flag decoder +func (f *Flags) Int(name string, usage string) { + f.d = append(f.d, FlagItem{ + req: true, + value: 0, + name: name, + usage: usage, + call: func(getter ArgGetter) (interface{}, error) { + if val := getter.Get(name); val != nil && len(*val) > 0 { + return strconv.ParseInt(*val, 10, 64) + } + return nil, fmt.Errorf("--%s is not found", name) + }, + }) +} + +// FloatVar flag decoder with default value +func (f *Flags) FloatVar(name string, value float64, usage string) { + f.d = append(f.d, FlagItem{ + req: false, + value: value, + name: name, + usage: usage, + call: func(getter ArgGetter) (interface{}, error) { + if val := getter.Get(name); val != nil && len(*val) > 0 { + return strconv.ParseFloat(*val, 64) + } + return value, nil + }, + }) +} + +// Float flag decoder +func (f *Flags) Float(name string, usage string) { + f.d = append(f.d, FlagItem{ + req: true, + value: 0.0, + name: name, + usage: usage, + call: func(getter ArgGetter) (interface{}, error) { + if val := getter.Get(name); val != nil && len(*val) > 0 { + return strconv.ParseFloat(*val, 64) + } + return nil, fmt.Errorf("--%s is not found", name) + }, + }) +} + +// Bool flag decoder +func (f *Flags) Bool(name string, usage string) { + f.d = append(f.d, FlagItem{ + req: false, + value: false, + name: name, + usage: usage, + call: func(getter ArgGetter) (interface{}, error) { + if getter.Has(name) { + return true, nil + } + return false, nil + }, + }) +} diff --git a/console/go.mod b/console/go.mod new file mode 100644 index 0000000..c0a9cb2 --- /dev/null +++ b/console/go.mod @@ -0,0 +1,7 @@ +module go.osspkg.com/x/console + +go 1.20 + +replace go.osspkg.com/x/errors => ../errors + +require go.osspkg.com/x/errors v0.3.2 diff --git a/console/go.sum b/console/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/console/help.go b/console/help.go new file mode 100644 index 0000000..f9bbcaf --- /dev/null +++ b/console/help.go @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package console + +import ( + "fmt" + "os" + "sort" + "strings" + "text/template" +) + +var helpTemplate = `{{if len .Description | ne 0}}NAME + {{.Name}} - {{.Description}} +{{end}}SYNOPSIS + {{.Name}} {{.Curr}} {{.Args}} +{{if len .CurrDesc | ne 0}}DESCRIPTION + {{.CurrDesc}} +{{end}}{{if len .Flags | ne 0}}ARGUMENTS +{{range $ex := .Flags}} {{$ex}} +{{end}}{{end}}{{if len .Next | ne 0}}COMMANDS +{{range $ex := .Next}} {{$ex}} +{{end}}{{end}} +` + +type helpModel struct { + Name string + Description string + ShowCommand bool + + Args string + Flags []string + + Curr string + CurrDesc string + Next []string +} + +func helpView(tool string, desc string, c CommandGetter, args []string) { + model := &helpModel{ + ShowCommand: c != nil, + Name: tool, + Description: desc, + + Curr: strings.Join(args, " ") + " " + c.Name(), + CurrDesc: func() string { + if c == nil { + return "" + } + return c.Description() + }(), + Next: func() (out []string) { + if c == nil { + return + } + var max int + next := c.List() + for _, v := range next { + if max < len(v.Name()) { + max = len(v.Name()) + } + } + sort.Slice(next, func(i, j int) bool { + return next[i].Name() < next[j].Name() + }) + max += 3 + for _, v := range next { + out = append(out, + v.Name()+ + strings.Repeat(" ", max-len(v.Name()))+ + v.Description()) + } + + return + }(), + } + + if c != nil { + model.Args = "[arg]" + model.Flags = func() (out []string) { + max := 0 + c.Flags().Info(func(_ bool, name string, _ interface{}, _ string) { + length := len(name) + if length > 2 { + length += 2 + } else { + length++ + } + if length > max { + max = length + } + }) + max += 2 + c.Flags().Info(func(req bool, name string, value interface{}, usage string) { + defaultValue, i := "", 1 + if !req { + defaultValue = fmt.Sprintf("(default: %+v)", value) + } + if len(name) > 1 { + i = 2 + } + out = append(out, fmt.Sprintf( + "%s%s%s %s %s", + strings.Repeat("-", i), + name, + strings.Repeat(" ", max-len(name)-i), + usage, + defaultValue, + )) + }) + return out + }() + } + + if err := template.Must(template.New("").Parse(helpTemplate)).Execute(os.Stdout, model); err != nil { + Fatalf(err.Error()) + } +} diff --git a/console/io.go b/console/io.go new file mode 100644 index 0000000..1015f5e --- /dev/null +++ b/console/io.go @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package console + +import ( + "bufio" + "fmt" + "os" + "strings" + "sync/atomic" + + "go.osspkg.com/x/errors" +) + +const ( + cRESET = "\u001B[0m" + cBLACK = "\u001B[30m" + cRED = "\u001B[31m" + cGREEN = "\u001B[32m" + cYELLOW = "\u001B[33m" + cBLUE = "\u001B[34m" + cPURPLE = "\u001B[35m" + cCYAN = "\u001B[36m" + + eof = "\n" +) + +var ( + scan *bufio.Scanner + yesNo = []string{"y", "n"} + debugLevel uint32 = 0 +) + +func init() { + scan = bufio.NewScanner(os.Stdin) +} + +func output(msg string, vars []string, def string) { + if len(def) > 0 { + def = fmt.Sprintf(" [%s]", def) + } + v := "" + if len(vars) > 0 { + v = fmt.Sprintf(" (%s)", strings.Join(vars, "/")) + } + Rawf("%s%s%s: ", msg, v, def) +} + +// Switch console multi input requests +func Switch(msg string, vars [][]string, exit string, call func(s string)) { + fmt.Printf("%s\n", msg) + + list := make(map[string]string, len(vars)*4) + i := 0 + for _, blocks := range vars { + for _, name := range blocks { + i++ + fmt.Printf("(%d) %s, ", i, name) + list[fmt.Sprintf("%d", i)] = name + } + fmt.Printf("\n") + } + fmt.Printf("and (%s) Done: \n", exit) + + for { + if scan.Scan() { + r := scan.Text() + if r == exit { + fmt.Printf("\u001B[1A\u001B[K: Done\n\n") + return + } + if name, ok := list[r]; ok { + call(name) + fmt.Printf("\033[1A\033[K + %s\n", name) + continue + } + fmt.Printf("\u001B[1A\u001B[KBad answer! Try again: ") + } + } +} + +// Input console input request +func Input(msg string, vars []string, def string) string { + output(msg, vars, def) + + for { + if scan.Scan() { + r := scan.Text() + if len(r) == 0 { + return def + } + if len(vars) == 0 { + return r + } + for _, v := range vars { + if v == r { + return r + } + } + output("Bad answer! Try again", vars, def) + } + } +} + +// InputBool console bool input request +func InputBool(msg string, def bool) bool { + v := "n" + if def { + v = "y" + } + v = Input(msg, yesNo, v) + return v == "y" +} + +func writeWithColor(c, msg string, args []interface{}) { + if !strings.HasSuffix(msg, eof) { + msg += eof + } + fmt.Printf(c+msg+cRESET, args...) +} + +// Rawf console message writer without level info +func Rawf(msg string, args ...interface{}) { + writeWithColor(cRESET, msg, args) +} + +// Infof console message writer for info level +func Infof(msg string, args ...interface{}) { + writeWithColor(cRESET, "[INF] "+msg, args) +} + +// Warnf console message writer for warning level +func Warnf(msg string, args ...interface{}) { + writeWithColor(cYELLOW, "[WAR] "+msg, args) +} + +// Errorf console message writer for error level +func Errorf(msg string, args ...interface{}) { + writeWithColor(cRED, "[ERR] "+msg, args) +} + +// ShowDebug init show debug +func ShowDebug(ok bool) { + var v uint32 = 0 + if ok { + v = 1 + } + atomic.StoreUint32(&debugLevel, v) +} + +// Debugf console message writer for debug level +func Debugf(msg string, args ...interface{}) { + if atomic.LoadUint32(&debugLevel) > 0 { + writeWithColor(cBLUE, "[DEB] "+msg, args) + } +} + +// FatalIfErr console message writer if err is not nil +func FatalIfErr(err error, msg string, args ...interface{}) { + if err != nil { + Fatalf(errors.Wrapf(err, msg, args...).Error()) + } +} + +// Fatalf console message writer with exit code 1 +func Fatalf(msg string, args ...interface{}) { + writeWithColor(cRED, "[ERR] "+msg, args) + os.Exit(1) +} diff --git a/domain/domain.go b/domain/domain.go new file mode 100644 index 0000000..1abbee3 --- /dev/null +++ b/domain/domain.go @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package domain + +import ( + "fmt" + "regexp" + "strings" +) + +var dot = byte('.') + +func Level(s string, level int) string { + if level == 0 { + return "." + } + var err error + s, err = Normalize(s) + if err != nil { + return "." + } + maxPos := len(s) - 1 + count, pos := 0, 0 + if s[maxPos] == dot { + maxPos-- + } + + for i := maxPos; i >= 0; i-- { + if s[i] == dot { + count++ + if count == level { + pos = i + 1 + break + } + } + } + return s[pos:] +} + +func CountLevels(s string) int { + ss, err := Normalize(s) + if err != nil { + return 0 + } + return strings.Count(ss, ".") +} + +var domainRegexp = regexp.MustCompile(`^(?i)([a-z0-9-]+\.?)+$`) + +func IsValid(d string) bool { + return domainRegexp.MatchString(d) +} + +func Normalize(domain string) (string, error) { + domain = strings.TrimSpace(domain) + if !domainRegexp.MatchString(domain) { + return "", fmt.Errorf("invalid domain") + } + domain = strings.TrimRight(domain, ".") + domain = strings.ToLower(domain) + return domain + ".", nil +} diff --git a/domain/domain_test.go b/domain/domain_test.go new file mode 100644 index 0000000..4786140 --- /dev/null +++ b/domain/domain_test.go @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package domain + +import ( + "fmt" + "testing" +) + +func TestUnit_Level(t *testing.T) { + type args struct { + s string + level int + } + tests := []struct { + args args + want string + }{ + { + args: args{ + s: "www.domain.ltd", + level: 1, + }, + want: "ltd.", + }, + { + args: args{ + s: "www.domain.ltd", + level: 2, + }, + want: "domain.ltd.", + }, + { + args: args{ + s: "www.domain.ltd", + level: 10, + }, + want: "www.domain.ltd.", + }, + { + args: args{ + s: "www.domain.ltd.", + level: 1, + }, + want: "ltd.", + }, + { + args: args{ + s: "ltd.", + level: 3, + }, + want: "ltd.", + }, + { + args: args{ + s: "www.domain.ltd.", + level: 0, + }, + want: ".", + }, + } + for i, tt := range tests { + t.Run(fmt.Sprintf("Case %d", i), func(t *testing.T) { + if got := Level(tt.args.s, tt.args.level); got != tt.want { + t.Errorf("DomainLevel() = %v, want %v", got, tt.want) + } + }) + } +} + +func BenchmarkDomainLevel(b *testing.B) { + address := "www.domain.ltd." + expected := "domain.ltd." + + b.ReportAllocs() + for i := 0; i < b.N; i++ { + if got := Level(address, 2); got != expected { + b.Errorf("DomainLevel() = %v, want %v", got, expected) + } + } +} + +func TestUnit_Normalize(t *testing.T) { + tests := []struct { + name string + domain string + want string + wantErr bool + }{ + { + name: "Case1", + domain: "1www.a-aa.com", + want: "1www.a-aa.com.", + wantErr: false, + }, + { + name: "Case2", + domain: "1_www.aaa.com", + want: "", + wantErr: true, + }, + { + name: "Case3", + domain: "com", + want: "com.", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Normalize(tt.domain) + if (err != nil) != tt.wantErr { + t.Errorf("Normalize() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("Normalize() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestUnit_CountLevels(t *testing.T) { + tests := []struct { + name string + arg string + want int + }{ + { + name: "Case1", + arg: "", + want: 0, + }, + { + name: "Case2", + arg: "aaa.", + want: 1, + }, + { + name: "Case3", + arg: "aaa.bbb.", + want: 2, + }, + { + name: "Case4", + arg: "aaa.bbb", + want: 2, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := CountLevels(tt.arg); got != tt.want { + t.Errorf("CountLevels() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/domain/go.mod b/domain/go.mod new file mode 100644 index 0000000..41d13cf --- /dev/null +++ b/domain/go.mod @@ -0,0 +1,3 @@ +module go.osspkg.com/x/domain + +go 1.20 diff --git a/domain/go.sum b/domain/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/env/common.go b/env/common.go new file mode 100644 index 0000000..f74d60f --- /dev/null +++ b/env/common.go @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package env + +type ( + // ENV type for environments (prod, dev, stage, etc) + ENV string + + AppName string + AppVersion string + AppDescription string + + AppInfo struct { + AppName AppName + AppVersion AppVersion + AppDescription AppDescription + } +) + +func NewAppInfo() AppInfo { + return AppInfo{ + AppName: "", + AppVersion: "", + AppDescription: "", + } +} diff --git a/env/env.go b/env/env.go new file mode 100644 index 0000000..9cd34e3 --- /dev/null +++ b/env/env.go @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package env + +import "os" + +func Get(key, def string) string { + v, ok := os.LookupEnv(key) + if !ok { + return def + } + return v +} diff --git a/env/go.mod b/env/go.mod new file mode 100644 index 0000000..2f6cd73 --- /dev/null +++ b/env/go.mod @@ -0,0 +1,3 @@ +module go.osspkg.com/x/env + +go 1.20 diff --git a/errors/errors.go b/errors/errors.go new file mode 100644 index 0000000..0c1f9c0 --- /dev/null +++ b/errors/errors.go @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package errors + +import ( + e "errors" + "fmt" +) + +type err struct { + cause error + message string + trace string +} + +func New(message string) error { + return &err{message: message} +} + +func (v *err) Error() string { + switch true { + case len(v.message) > 0 && v.cause != nil: + return v.message + ": " + v.cause.Error() + v.trace + case v.cause != nil: + return v.cause.Error() + v.trace + } + return v.message + v.trace +} + +func (v *err) Cause() error { + return v.cause +} + +func (v *err) Unwrap() error { + return v.cause +} + +func (v *err) WithTrace() { + v.trace = runtimeTrace(10) +} + +func Trace(cause error, message string, args ...interface{}) error { + v := Wrapf(cause, message, args...) + // nolint: errorlint + if vv, ok := v.(*err); ok { + vv.WithTrace() + return vv + } + return v +} + +func Wrapf(cause error, message string, args ...interface{}) error { + if cause == nil { + return nil + } + var err0 *err + if len(args) == 0 { + err0 = &err{ + cause: cause, + message: message, + } + } else { + err0 = &err{ + cause: cause, + message: fmt.Sprintf(message, args...), + } + } + return err0 +} + +func Wrap(msg ...error) error { + if len(msg) == 0 { + return nil + } + var err0 error + for _, v := range msg { + if v == nil { + continue + } + if err0 == nil { + err0 = &err{cause: v} + continue + } + err0 = &err{ + cause: v, + message: err0.Error(), + } + } + return err0 +} + +func Unwrap(err error) error { + // nolint: errorlint + if v, ok := err.(interface { + Unwrap() error + }); ok { + return v.Unwrap() + } + return nil +} + +func Cause(err error) error { + for err != nil { + // nolint: errorlint + v, ok := err.(interface { + Cause() error + }) + if !ok { + return err + } + err = v.Cause() + } + + return nil +} + +func Is(err, target error) bool { + return e.Is(err, target) +} diff --git a/errors/errors_test.go b/errors/errors_test.go new file mode 100644 index 0000000..57b92fd --- /dev/null +++ b/errors/errors_test.go @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package errors + +import ( + e "errors" + "strings" + "testing" +) + +func TestUnit_New(t *testing.T) { + type args struct { + message string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + {name: "Case1", args: args{message: "hello"}, want: "hello", wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := New(tt.args.message) + if (err != nil) != tt.wantErr { + t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err.Error() != tt.want { + t.Errorf("New() error = %v, want %v", err.Error(), tt.want) + return + } + }) + } +} + +func TestUnit_Wrap(t *testing.T) { + type args struct { + msg []error + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "Case1", + args: args{msg: nil}, + want: "", + wantErr: false, + }, + { + name: "Case2", + args: args{msg: []error{New("hello"), e.New("world")}}, + want: "hello: world", + wantErr: true, + }, + { + name: "Case3", + args: args{msg: []error{New("err1"), e.New("err2"), nil, e.New("err3")}}, + want: "err1: err2: err3", + wantErr: true, + }, + { + name: "Case4", + args: args{msg: []error{Wrapf(New("err1"), "err1 message"), + Wrapf(e.New("err2"), "err2 message"), + Wrapf(e.New("err3"), "err3 message")}}, + want: "err1 message: err1: err2 message: err2: err3 message: err3", + wantErr: true, + }, + { + name: "Case5", + args: args{msg: []error{nil, nil, nil}}, + want: "", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := Wrap(tt.args.msg...) + if (err != nil) != tt.wantErr { + t.Errorf("Wrap() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.wantErr && err.Error() != tt.want { + t.Errorf("Wrap() error = %v, want %v", err.Error(), tt.want) + return + } + }) + } +} + +func TestUnit_WrapMessage(t *testing.T) { + type args struct { + cause error + message string + args []interface{} + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "Case1", + args: args{ + cause: nil, + message: "err context", + args: nil, + }, + want: "", + wantErr: false, + }, + { + name: "Case2", + args: args{ + cause: e.New("err1"), + message: "err context", + args: nil, + }, + want: "err context: err1", + wantErr: true, + }, + { + name: "Case3", + args: args{ + cause: e.New("err1"), + message: "bad ip %s", + args: []interface{}{"127.0.0.1"}, + }, + want: "bad ip 127.0.0.1: err1", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := Wrapf(tt.args.cause, tt.args.message, tt.args.args...) + if (err != nil) != tt.wantErr { + t.Errorf("Wrapf() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.wantErr && err.Error() != tt.want { + t.Errorf("Wrapf() error = %v, want %v", err.Error(), tt.want) + return + } + }) + } +} + +func TestUnit_CauseUnwrap(t *testing.T) { + type fields struct { + cause error + message string + } + tests := []struct { + name string + fields fields + want string + wantErr bool + }{ + { + name: "Case1", + fields: fields{ + cause: e.New("err1"), + message: "context", + }, + want: "err1", + wantErr: true, + }, + { + name: "Case2", + fields: fields{ + cause: nil, + message: "context", + }, + want: "err1", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := Wrapf(tt.fields.cause, tt.fields.message) + err := Cause(v) + if (err != nil) != tt.wantErr { + t.Errorf("Cause() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.wantErr && err.Error() != tt.want { + t.Errorf("Cause() error = %v, want %v", err.Error(), tt.want) + return + } + err = Unwrap(v) + if (err != nil) != tt.wantErr { + t.Errorf("Unwrap() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.wantErr && err.Error() != tt.want { + t.Errorf("Unwrap() error = %v, want %v", err.Error(), tt.want) + return + } + }) + } +} + +func TestUnit_Is(t *testing.T) { + err0 := New("test") + type args struct { + err error + target error + } + tests := []struct { + name string + args args + want bool + }{ + {name: "Case1", args: args{err: err0, target: err0}, want: true}, + {name: "Case2", args: args{err: Wrapf(err0, "ttt"), target: err0}, want: true}, + {name: "Case3", args: args{err: New("hello"), target: err0}, want: false}, + {name: "Case4", args: args{err: nil, target: err0}, want: false}, + {name: "Case5", args: args{err: New("hello"), target: nil}, want: false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Is(tt.args.err, tt.args.target); got != tt.want { + t.Errorf("Is() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestUnit_Trace(t *testing.T) { + tests := []struct { + name string + err error + want string + }{ + { + name: "Case1", + err: New("test"), + want: "[trace] go.osspkg.com/goppy/errors_test.TestUnit_Trace.func1", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Trace(tt.err, "msg"); got != nil && !strings.Contains(got.Error(), tt.want) { + t.Errorf("Trace() = %v, want %v", got.Error(), tt.want) + } + }) + } +} diff --git a/errors/go.mod b/errors/go.mod new file mode 100644 index 0000000..acefddc --- /dev/null +++ b/errors/go.mod @@ -0,0 +1,3 @@ +module go.osspkg.com/x/errors + +go 1.20 diff --git a/errors/go.sum b/errors/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/errors/trace.go b/errors/trace.go new file mode 100644 index 0000000..c155886 --- /dev/null +++ b/errors/trace.go @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package errors + +import ( + "fmt" + "runtime" +) + +func runtimeTrace(c int) string { + list := make([]uintptr, c) + + n := runtime.Callers(4, list) + frame := runtime.CallersFrames(list[:n]) + + result := "" + for { + v, ok := frame.Next() + if !ok { + break + } + result += fmt.Sprintf("\n\t[trace] %s:%d", v.Function, v.Line) + } + return result +} diff --git a/go.mod b/go.mod index 07683ac..3fbb7a4 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,3 @@ -module go.osspkg.com/algorithms +module go.osspkg.com/x go 1.20 - -require github.com/stretchr/testify v1.9.0 - -require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/go.work b/go.work new file mode 100644 index 0000000..132e866 --- /dev/null +++ b/go.work @@ -0,0 +1,17 @@ +go 1.20 + +use ( + . + ./algorithms + ./config + ./console + ./domain + ./env + ./errors + ./io + ./random + ./sync + ./syscall + ./test + ./version +) diff --git a/io/ascii.go b/io/ascii.go new file mode 100644 index 0000000..239f97a --- /dev/null +++ b/io/ascii.go @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package io + +import "bytes" + +var asciiEOF = []byte{255, 244, 255, 253, 6} + +func IsAsciiEOF(b []byte) bool { + return bytes.Equal(b, asciiEOF) +} diff --git a/io/encdec.go b/io/encdec.go new file mode 100644 index 0000000..29415ba --- /dev/null +++ b/io/encdec.go @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package io + +import ( + "encoding/json" + "os" + "path/filepath" + + "go.osspkg.com/x/errors" + "go.osspkg.com/x/sync" + "gopkg.in/yaml.v3" +) + +var ( + errBadFileFormat = errors.New("format is not a supported") + + _default = newEncoders(). + Add(".yml", yaml.Marshal, yaml.Unmarshal). + Add(".yaml", yaml.Marshal, yaml.Unmarshal). + Add(".json", json.Marshal, json.Unmarshal) +) + +type encoders struct { + enc map[string]func(v interface{}) ([]byte, error) + dec map[string]func([]byte, interface{}) error + mux sync.Lock +} + +func newEncoders() *encoders { + return &encoders{ + enc: make(map[string]func(v interface{}) ([]byte, error), 10), + dec: make(map[string]func([]byte, interface{}) error, 10), + mux: sync.NewLock(), + } +} + +func AddFileEncoder(ext string, enc func(v interface{}) ([]byte, error), dec func([]byte, interface{}) error) { + _default.Add(ext, enc, dec) +} + +func (v *encoders) Add(ext string, enc func(v interface{}) ([]byte, error), dec func([]byte, interface{}) error) *encoders { + v.mux.Lock(func() { + v.enc[ext] = enc + v.dec[ext] = dec + }) + return v +} + +func (v *encoders) Encoder(ext string) (fn func(v interface{}) ([]byte, error), ok bool) { + v.mux.RLock(func() { + fn, ok = v.enc[ext] + }) + return +} + +func (v *encoders) Decoder(ext string) (fn func([]byte, interface{}) error, ok bool) { + v.mux.RLock(func() { + fn, ok = v.dec[ext] + }) + return +} + +type FileEncoder string + +func (v FileEncoder) Decode(configs ...interface{}) error { + data, err := os.ReadFile(string(v)) + if err != nil { + return err + } + ext := filepath.Ext(string(v)) + c, ok := _default.Decoder(ext) + if !ok { + return errBadFileFormat + } + return v.dec(data, c, configs...) +} + +func (v FileEncoder) Encode(configs ...interface{}) error { + ext := filepath.Ext(string(v)) + c, ok := _default.Encoder(ext) + if !ok { + return errBadFileFormat + } + b, err := v.enc(c, configs...) + if err != nil { + return err + } + return os.WriteFile(string(v), b, 0755) +} + +func (v FileEncoder) dec(data []byte, call func([]byte, interface{}) error, configs ...interface{}) error { + for _, conf := range configs { + if err := call(data, conf); err != nil { + return err + } + } + return nil +} + +func (v FileEncoder) enc(call func(v interface{}) ([]byte, error), configs ...interface{}) ([]byte, error) { + b := make([]byte, 0, 300*len(configs)) + for _, conf := range configs { + bb, err := call(conf) + if err != nil { + return nil, err + } + b = append(b, '\n', '\n') + b = append(b, bb...) + } + return b, nil +} diff --git a/io/encdec_test.go b/io/encdec_test.go new file mode 100644 index 0000000..8ee6fb9 --- /dev/null +++ b/io/encdec_test.go @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package io + +import ( + "os" + "testing" + + "go.osspkg.com/x/test" +) + +func TestFile_EncodeDecode(t *testing.T) { + type TestDataItem1 struct { + AA string `yaml:"aa"` + BB bool `yaml:"bb"` + } + type TestData1 struct { + Data1 TestDataItem1 `yaml:"data-1"` + } + type TestDataItem2 struct { + CC string `yaml:"cc"` + DD int `yaml:"dd"` + } + type TestData2 struct { + Data2 TestDataItem2 `yaml:"data-2"` + } + + os.Remove("/tmp/bdsbdnsabkjlfadlksjfbkljd.yaml") + + model1 := &TestData1{Data1: TestDataItem1{AA: "123", BB: true}} + model2 := &TestData2{Data2: TestDataItem2{CC: "qwer", DD: -100}} + + err := FileEncoder("/tmp/bdsbdnsabkjlfadlksjfbkljd.yaml").Encode(model1, model2) + test.NoError(t, err) + + model11 := &TestData1{} + model22 := &TestData2{} + + err = FileEncoder("/tmp/bdsbdnsabkjlfadlksjfbkljd.yaml").Decode(model11, model22) + test.NoError(t, err) + + test.Equal(t, model1, model11) + test.Equal(t, model2, model22) +} diff --git a/io/files.go b/io/files.go new file mode 100644 index 0000000..48f0c85 --- /dev/null +++ b/io/files.go @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package io + +import ( + "io" + "io/fs" + "os" + "path/filepath" + "strings" +) + +func CurrentDir() string { + dir, err := os.Getwd() + if err != nil { + return "." + } + return dir +} + +func Exist(filename string) bool { + _, err := os.Stat(filename) + return err == nil +} + +func Search(dir, filename string) ([]string, error) { + files := make([]string, 0, 2) + err := filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() || info.Name() != filename { + return nil + } + files = append(files, path) + return nil + }) + return files, err +} + +func SearchByExt(dir, ext string) ([]string, error) { + files := make([]string, 0, 2) + err := filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() || filepath.Ext(info.Name()) != ext { + return nil + } + files = append(files, path) + return nil + }) + return files, err +} + +func Rewrite(filename string, call func([]byte) ([]byte, error)) error { + if !Exist(filename) { + if err := os.WriteFile(filename, []byte(""), 0755); err != nil { + return err + } + } + b, err := os.ReadFile(filename) + if err != nil { + return err + } + if b, err = call(b); err != nil { + return err + } + return os.WriteFile(filename, b, 0755) +} + +func Copy(dst, src string, mode os.FileMode) error { + source, err := os.OpenFile(src, os.O_RDONLY, 0) + if err != nil { + return err + } + defer source.Close() // nolint: errcheck + + if mode == 0 { + fi, err0 := source.Stat() + if err0 != nil { + return err0 + } + mode = fi.Mode() + } + + dist, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode) + if err != nil { + return err + } + defer dist.Close() // nolint: errcheck + + _, err = io.Copy(dist, source) + return err +} + +func Folder(filename string) string { + dir := filepath.Dir(filename) + tree := strings.Split(dir, string(os.PathSeparator)) + return tree[len(tree)-1] +} diff --git a/io/go.mod b/io/go.mod new file mode 100644 index 0000000..b964728 --- /dev/null +++ b/io/go.mod @@ -0,0 +1,16 @@ +module go.osspkg.com/x/io + +go 1.20 + +replace ( + go.osspkg.com/x/errors => ../errors + go.osspkg.com/x/sync => ../sync + go.osspkg.com/x/test => ../test +) + +require ( + go.osspkg.com/x/errors v0.3.1 + go.osspkg.com/x/sync v0.3.0 + go.osspkg.com/x/test v0.3.0 + gopkg.in/yaml.v3 v3.0.1 +) diff --git a/io/go.sum b/io/go.sum new file mode 100644 index 0000000..a62c313 --- /dev/null +++ b/io/go.sum @@ -0,0 +1,4 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/io/hash.go b/io/hash.go new file mode 100644 index 0000000..79dccb5 --- /dev/null +++ b/io/hash.go @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package io + +import ( + "encoding/hex" + "fmt" + "hash" + "io" + "os" + + "go.osspkg.com/x/errors" +) + +func IsValidFileHash(filename string, h hash.Hash, valid string) error { + r, err := os.Open(filename) + if err != nil { + return err + } + defer r.Close() // nolint: errcheck + if _, err = io.Copy(h, r); err != nil { + return errors.Wrapf(err, "calculate file hash") + } + result := hex.EncodeToString(h.Sum(nil)) + h.Reset() + if result != valid { + return fmt.Errorf("invalid hash: expected[%s] actual[%s]", valid, result) + } + return nil +} + +func FileHash(filename string, h hash.Hash) (string, error) { + r, err := os.Open(filename) + if err != nil { + return "", err + } + defer r.Close() // nolint: errcheck + if _, err = io.Copy(h, r); err != nil { + return "", errors.Wrapf(err, "calculate file hash") + } + result := hex.EncodeToString(h.Sum(nil)) + h.Reset() + return result, nil +} diff --git a/io/ioutils.go b/io/ioutils.go new file mode 100644 index 0000000..da0030d --- /dev/null +++ b/io/ioutils.go @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package io + +import ( + "bytes" + "io" + + "go.osspkg.com/x/errors" +) + +func ReadAll(r io.ReadCloser) ([]byte, error) { + b, err := io.ReadAll(r) + err = errors.Wrap(err, r.Close()) + if err != nil { + return nil, err + } + return b, nil +} + +const buffSize = 128 + +var ( + ErrMaximumSize = errors.New("maximum buffer size reached") + ErrInvalidSize = errors.New("invalid size") +) + +func ReadFull(w io.Writer, r io.Reader, maxSize int) error { + if maxSize < 0 { + return ErrInvalidSize + } + + // nolint: ineffassign + var ( + total = 0 + n = 0 + buff = make([]byte, buffSize) + err error + ) + + for { + n, err = r.Read(buff) + if err != nil && !errors.Is(err, io.EOF) { + return err + } + if n < 0 { + return ErrInvalidSize + } + if _, err = w.Write(buff[:n]); err != nil { + return err + } + total += n + if maxSize > 0 && total > maxSize { + return ErrMaximumSize + } + if n < buffSize { + break + } + } + return nil +} + +func ReadBytes(v io.Reader, divide string) ([]byte, error) { + var ( + n int + err error + b = make([]byte, 0, 512) + db = []byte(divide) + dl = len(db) + ) + + for { + if len(b) == cap(b) { + b = append(b, 0)[:len(b)] + } + n, err = v.Read(b[len(b):cap(b)]) + b = b[:len(b)+n] + if err != nil { + if err == io.EOF { + break + } + return nil, err + } + if len(b) < dl { + return b, io.EOF + } + if bytes.Equal(db, b[len(b)-dl:]) { + b = b[:len(b)-dl] + break + } + } + return b, nil +} + +func WriteBytes(v io.Writer, b []byte, divide string) error { + var ( + db = []byte(divide) + dl = len(db) + ) + if len(b) < dl || !bytes.Equal(db, b[len(b)-dl:]) { + b = append(b, db...) + } + if _, err := v.Write(b); err != nil { + return err + } + return nil +} diff --git a/io/ioutils_test.go b/io/ioutils_test.go new file mode 100644 index 0000000..a946faa --- /dev/null +++ b/io/ioutils_test.go @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package io + +import ( + "bytes" + "io" + "reflect" + "testing" + + "go.osspkg.com/x/errors" +) + +type mockReadCloser struct { + Data *bytes.Buffer + ErrRead error + ErrClose error +} + +func (v *mockReadCloser) Read(p []byte) (int, error) { + if v.ErrRead != nil { + return 0, v.ErrRead + } + return v.Data.Read(p) +} + +func (v *mockReadCloser) Close() error { + return v.ErrClose +} + +func TestUnit_ReadAll(t *testing.T) { + type args struct { + r io.ReadCloser + } + tests := []struct { + name string + args args + want []byte + wantErr bool + }{ + { + name: "Case1", + args: args{ + r: &mockReadCloser{ + Data: bytes.NewBuffer([]byte(`hello`)), + ErrRead: nil, + ErrClose: nil, + }, + }, + want: []byte(`hello`), + wantErr: false, + }, + { + name: "Case2", + args: args{ + r: &mockReadCloser{ + Data: nil, + ErrRead: errors.New("read error"), + ErrClose: nil, + }, + }, + want: nil, + wantErr: true, + }, + { + name: "Case3", + args: args{ + r: &mockReadCloser{ + Data: nil, + ErrRead: errors.New("read error"), + ErrClose: errors.New("close error"), + }, + }, + want: nil, + wantErr: true, + }, + { + name: "Case4", + args: args{ + r: &mockReadCloser{ + Data: bytes.NewBuffer([]byte(`hello`)), + ErrRead: nil, + ErrClose: errors.New("close error"), + }, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ReadAll(tt.args.r) + if (err != nil) != tt.wantErr { + t.Errorf("ReadAll() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ReadAll() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/random/go.mod b/random/go.mod new file mode 100644 index 0000000..d578dfd --- /dev/null +++ b/random/go.mod @@ -0,0 +1,3 @@ +module go.osspkg.com/x/random + +go 1.20 diff --git a/random/random.go b/random/random.go new file mode 100644 index 0000000..8999846 --- /dev/null +++ b/random/random.go @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package random + +import ( + "math/rand" + "time" +) + +var rnd = rand.New(rand.NewSource(time.Now().UnixNano())) + +var ( + digest = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-+=~*@#$%&?!<>") +) + +func BytesOf(n int, src []byte) []byte { + tmp := make([]byte, len(src)) + copy(tmp, src) + rnd.Shuffle(len(tmp), func(i, j int) { + tmp[i], tmp[j] = tmp[j], tmp[i] + }) + b := make([]byte, n) + for i := range b { + b[i] = tmp[rnd.Intn(len(tmp))] + } + return b +} + +func StringOf(n int, src string) string { + return string(BytesOf(n, []byte(src))) +} + +func Bytes(n int) []byte { + return BytesOf(n, digest) +} + +func String(n int) string { + return string(Bytes(n)) +} + +func Shuffle(v []string) []string { + rnd.Shuffle(len(v), func(i, j int) { v[i], v[j] = v[j], v[i] }) + return v +} diff --git a/random/random_test.go b/random/random_test.go new file mode 100644 index 0000000..7cb6341 --- /dev/null +++ b/random/random_test.go @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package random + +import ( + "bytes" + "fmt" + "testing" +) + +func TestUnit_Bytes(t *testing.T) { + size := 10 + r1 := Bytes(size) + r2 := Bytes(size) + + fmt.Println(string(r1), string(r2)) + + if len(r1) != size || len(r2) != size { + t.Errorf("invalid len, is not %d", size) + } + if bytes.Equal(r1, r2) { + t.Errorf("result is not random") + } +} + +func TestUnit_BytesOf(t *testing.T) { + size := 10 + src := []byte("1234567890") + r1 := BytesOf(size, src) + r2 := BytesOf(size, src) + + fmt.Println(string(r1), string(r2)) + + if len(r1) != size || len(r2) != size { + t.Errorf("invalid len, is not %d", size) + } + if bytes.Equal(r1, r2) { + t.Errorf("result is not random") + } +} + +func Benchmark_Bytes64(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + Bytes(64) + } +} + +func Benchmark_Bytes256(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + Bytes(256) + } +} diff --git a/shorten/shorten.go b/shorten/shorten.go deleted file mode 100644 index fe8b4e4..0000000 --- a/shorten/shorten.go +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. - * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. - */ - -package shorten - -type Shorten struct { - toStr map[uint64]string - toInt map[string]uint64 - len uint64 -} - -func New(alphabet string) *Shorten { - v := &Shorten{ - toStr: make(map[uint64]string), - toInt: make(map[string]uint64), - len: uint64(len(alphabet)), - } - - var i uint64 - for i = 0; i < v.len; i++ { - v.toInt[alphabet[i:i+1]] = i - v.toStr[i] = alphabet[i : i+1] - } - return v -} - -func (v *Shorten) Encode(id uint64) string { - s := "" - for id > 0 { - s = v.toStr[id%v.len] + s - id /= v.len - } - return s -} - -func (v *Shorten) Decode(data string) uint64 { - var id, i uint64 - for i = 0; i < uint64(len(data)); i++ { - id = id*v.len + v.toInt[data[i:i+1]] - } - return id -} diff --git a/shorten/shorten_test.go b/shorten/shorten_test.go deleted file mode 100644 index b18b882..0000000 --- a/shorten/shorten_test.go +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. - * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. - */ - -package shorten_test - -import ( - "testing" - - "github.com/stretchr/testify/require" - "go.osspkg.com/algorithms/shorten" -) - -func TestEncode_EncodeDecode(t *testing.T) { - tests := []struct { - name string - id uint64 - want string - }{ - {name: "Case1", id: 1, want: "p"}, - {name: "Case1", id: 2, want: "L"}, - {name: "Case1", id: 3, want: "K"}, - {name: "Case1", id: 4, want: "G"}, - {name: "Case1", id: 5, want: "R"}, - {name: "Case1", id: 6, want: "S"}, - {name: "Case1", id: 7, want: "u"}, - {name: "Case1", id: 8, want: "D"}, - {name: "Case1", id: 9, want: "v"}, - {name: "Case2", id: 10, want: "o"}, - {name: "Case3", id: 100, want: "pH"}, - {name: "Case4", id: 1000, want: "PD"}, - {name: "Case5", id: 10000, want: "LIn"}, - {name: "Case6", id: 100000, want: "c0k"}, - {name: "Case7", id: 1000000000, want: "pRmUWP"}, - {name: "Case8", id: 999999, want: "Glvp"}, - } - - v := shorten.New("0pLKGRSuDvorlO14Pjnd7XgQw9c8YhaIJ5iqtIHy3mWxM6C2TeAbFVBUkZfsNz") - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - h := v.Encode(tt.id) - require.Equal(t, tt.want, h) - id := v.Decode(h) - require.Equal(t, tt.id, id) - }) - } -} diff --git a/sync/go.mod b/sync/go.mod new file mode 100644 index 0000000..834fa42 --- /dev/null +++ b/sync/go.mod @@ -0,0 +1,7 @@ +module go.osspkg.com/x/sync + +go 1.20 + +replace go.osspkg.com/x/test => ../test + +require go.osspkg.com/x/test v0.3.0 diff --git a/sync/go.sum b/sync/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/sync/group.go b/sync/group.go new file mode 100644 index 0000000..1a8090e --- /dev/null +++ b/sync/group.go @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package sync + +import s "sync" + +type ( + Group interface { + Wait() + Background(call func()) + Run(call func()) + } + + _group struct { + wg s.WaitGroup + } +) + +func NewGroup() Group { + return &_group{} +} + +func (v *_group) Wait() { + v.wg.Wait() +} + +func (v *_group) Background(call func()) { + v.wg.Add(1) + go func() { + call() + v.wg.Done() + }() +} + +func (v *_group) Run(call func()) { + v.wg.Add(1) + call() + v.wg.Done() +} diff --git a/sync/locker.go b/sync/locker.go new file mode 100644 index 0000000..73f8783 --- /dev/null +++ b/sync/locker.go @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package sync + +import s "sync" + +type ( + Lock interface { + RLock(call func()) + Lock(call func()) + } + _lock struct { + mux s.RWMutex + } +) + +func NewLock() Lock { + return &_lock{} +} + +func (v *_lock) Lock(call func()) { + v.mux.Lock() + call() + v.mux.Unlock() +} +func (v *_lock) RLock(call func()) { + v.mux.RLock() + call() + v.mux.RUnlock() +} diff --git a/sync/switcher.go b/sync/switcher.go new file mode 100644 index 0000000..e3d98a9 --- /dev/null +++ b/sync/switcher.go @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package sync + +import "sync/atomic" + +const ( + on uint64 = 1 + off uint64 = 0 +) + +type ( + Switch interface { + On() bool + Off() bool + IsOn() bool + IsOff() bool + } + + _switch struct { + i uint64 + } +) + +func NewSwitch() Switch { + return &_switch{i: 0} +} + +func (v *_switch) On() bool { + return atomic.CompareAndSwapUint64(&v.i, off, on) +} + +func (v *_switch) Off() bool { + return atomic.CompareAndSwapUint64(&v.i, on, off) +} + +func (v *_switch) IsOn() bool { + return atomic.LoadUint64(&v.i) == on +} + +func (v *_switch) IsOff() bool { + return atomic.LoadUint64(&v.i) == off +} diff --git a/sync/switcher_test.go b/sync/switcher_test.go new file mode 100644 index 0000000..3e8d5fe --- /dev/null +++ b/sync/switcher_test.go @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package sync + +import ( + "testing" + + "go.osspkg.com/x/test" +) + +func TestNewSwitch(t *testing.T) { + sync := NewSwitch() + + test.False(t, sync.IsOn()) + test.True(t, sync.IsOff()) + + test.True(t, sync.On()) + test.False(t, sync.On()) + + test.False(t, sync.IsOff()) + test.True(t, sync.IsOn()) + +} diff --git a/syscall/go.mod b/syscall/go.mod new file mode 100644 index 0000000..13f751b --- /dev/null +++ b/syscall/go.mod @@ -0,0 +1,3 @@ +module go.osspkg.com/x/syscall + +go 1.20 diff --git a/syscall/system.go b/syscall/system.go new file mode 100644 index 0000000..c9ebdde --- /dev/null +++ b/syscall/system.go @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package syscall + +import ( + "os" + "os/signal" + "strconv" + scall "syscall" +) + +// OnStop calling a function if you send a system event stop +func OnStop(callFunc func()) { + quit := make(chan os.Signal, 4) + signal.Notify(quit, os.Interrupt, scall.SIGINT, scall.SIGTERM, scall.SIGKILL) //nolint:staticcheck + <-quit + signal.Stop(quit) + callFunc() +} + +// OnUp calling a function if you send a system event SIGHUP +func OnUp(callFunc func()) { + quit := make(chan os.Signal, 1) + signal.Notify(quit, scall.SIGHUP) + <-quit + signal.Stop(quit) + callFunc() +} + +// OnCustom calling a function if you send a system custom event +func OnCustom(callFunc func(), sig ...os.Signal) { + quit := make(chan os.Signal, 1) + signal.Notify(quit, sig...) + <-quit + signal.Stop(quit) + callFunc() +} + +// Pid write pid file +func Pid(filename string) error { + pid := strconv.Itoa(scall.Getpid()) + return os.WriteFile(filename, []byte(pid), 0755) +} diff --git a/test/common.go b/test/common.go new file mode 100644 index 0000000..ff20243 --- /dev/null +++ b/test/common.go @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package test + +import "fmt" + +type IUnitTest interface { + Errorf(format string, args ...interface{}) + Helper() + FailNow() +} + +func errorMessage(message []interface{}, errMsg string, args ...interface{}) string { + var msg string + switch len(message) { + case 0: + break + case 1: + msg = fmt.Sprintf("%+v", message[0]) + default: + msg = fmt.Sprintf(fmt.Sprintf("%+v", message[0]), message[1:]...) + } + + out := fmt.Sprintf("\n[Error] "+errMsg, args...) + if len(msg) > 0 { + out += "\n[Message] " + msg + } + return out +} diff --git a/test/equal.go b/test/equal.go new file mode 100644 index 0000000..bf4fec4 --- /dev/null +++ b/test/equal.go @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package test + +import ( + "bytes" + "fmt" + "reflect" + "strings" +) + +func Equal(t IUnitTest, expected interface{}, actual interface{}, args ...interface{}) { + et, at := reflect.ValueOf(expected).Kind(), reflect.ValueOf(actual).Kind() + if et != at { + t.Helper() + t.Errorf(errorMessage(args, "Different type\nExpected: %T\nActual: %T", expected, actual)) + t.FailNow() + return + } + ev, av := fmt.Sprintf("%+v", expected), fmt.Sprintf("%+v", actual) + if ev == av { + return + } + t.Helper() + t.Errorf(errorMessage(args, "Value is not identical\nExpected: %+v\nActual: %+v", expected, actual)) + t.FailNow() +} + +func NotEqual(t IUnitTest, expected interface{}, actual interface{}, args ...interface{}) { + et, at := reflect.ValueOf(expected).Kind(), reflect.ValueOf(actual).Kind() + if et != at { + t.Helper() + t.Errorf(errorMessage(args, "Different type\nExpected: %T\nActual: %T", expected, actual)) + t.FailNow() + return + } + ev, av := fmt.Sprintf("%+v", expected), fmt.Sprintf("%+v", actual) + if ev != av { + return + } + t.Helper() + t.Errorf(errorMessage(args, "Value is not identical\nExpected: %+v\nActual: %+v", expected, actual)) + t.FailNow() +} + +func True(t IUnitTest, actual bool, args ...interface{}) { + if actual { + return + } + t.Helper() + t.Errorf(errorMessage(args, "Want , but got: %+v", actual)) + t.FailNow() +} + +func False(t IUnitTest, actual bool, args ...interface{}) { + if !actual { + return + } + t.Helper() + t.Errorf(errorMessage(args, "Want , but got: %+v", actual)) + t.FailNow() +} + +func Contains(t IUnitTest, searchData interface{}, need interface{}, args ...interface{}) { + dt, st := reflect.ValueOf(searchData), reflect.ValueOf(need) + var ( + found bool + ) + + if s1, s2, ok0 := asString(searchData, need); ok0 { + found = strings.Contains(s1, s2) + } else if b1, b2, ok1 := asBytes(searchData, need); ok1 { + found = bytes.Contains(b1, b2) + } else if dt.Kind() == reflect.Map { + for _, value := range dt.MapKeys() { + if value.Kind() != st.Kind() { + continue + } + if reflect.DeepEqual(value.Interface(), st.Interface()) { + found = true + break + } + } + } else { + t.Helper() + t.Errorf(errorMessage(args, "Unsupported types\nSearchData: %T\nNeed: %T", searchData, need)) + t.FailNow() + return + } + + if found { + return + } + t.Helper() + t.Errorf(errorMessage(args, "Not found\nSearchData: %+v\nNeed: %+v", searchData, need)) + t.FailNow() +} + +func NotContains(t IUnitTest, searchData interface{}, need interface{}, args ...interface{}) { + dt, st := reflect.ValueOf(searchData), reflect.ValueOf(need) + var ( + found bool + ) + + if s1, s2, ok0 := asString(searchData, need); ok0 { + found = strings.Contains(s1, s2) + } else if b1, b2, ok1 := asBytes(searchData, need); ok1 { + found = bytes.Contains(b1, b2) + } else if dt.Kind() == reflect.Map { + for _, value := range dt.MapKeys() { + if value.Kind() != st.Kind() { + continue + } + if reflect.DeepEqual(value.Interface(), st.Interface()) { + found = true + break + } + } + } else { + t.Helper() + t.Errorf(errorMessage(args, "Unsupported types\nSearchData: %T\nNeed: %T", searchData, need)) + t.FailNow() + return + } + + if !found { + return + } + t.Helper() + t.Errorf(errorMessage(args, "Found\nSearchData: %+v\nNeed: %+v", searchData, need)) + t.FailNow() +} + +func asString(v0 interface{}, v1 interface{}) (string, string, bool) { + sv0, ok0 := v0.(string) + sv1, ok1 := v1.(string) + return sv0, sv1, ok0 && ok1 +} + +func asBytes(v0 interface{}, v1 interface{}) ([]byte, []byte, bool) { + sv0, ok0 := v0.([]byte) + sv1, ok1 := v1.([]byte) + return sv0, sv1, ok0 && ok1 +} diff --git a/test/errors.go b/test/errors.go new file mode 100644 index 0000000..f5e5eb2 --- /dev/null +++ b/test/errors.go @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package test + +import "strings" + +func NoError(t IUnitTest, err error, args ...interface{}) { + if err == nil { + return + } + t.Helper() + t.Errorf(errorMessage(args, "Want , but got error: %+v", err.Error())) + t.FailNow() +} + +func Error(t IUnitTest, err error, args ...interface{}) { + if err != nil { + return + } + t.Helper() + t.Errorf(errorMessage(args, "Want error, but got ")) + t.FailNow() +} + +func ErrorContains(t IUnitTest, err error, need string, args ...interface{}) { + if err == nil { + t.Helper() + t.Errorf(errorMessage(args, "Want error, but got ")) + t.FailNow() + return + } + + if strings.Contains(err.Error(), need) { + return + } + t.Helper() + t.Errorf(errorMessage(args, "Not found\nSearchData: %+v\nNeed: %+v", err.Error(), need)) + t.FailNow() +} diff --git a/test/go.mod b/test/go.mod new file mode 100644 index 0000000..5b0d7c7 --- /dev/null +++ b/test/go.mod @@ -0,0 +1,3 @@ +module go.osspkg.com/x/test + +go 1.20 diff --git a/test/nil.go b/test/nil.go new file mode 100644 index 0000000..b77a6c9 --- /dev/null +++ b/test/nil.go @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package test + +import ( + "reflect" +) + +func Nil(t IUnitTest, actual interface{}, args ...interface{}) { + if isNil(actual) { + return + } + t.Helper() + t.Errorf(errorMessage(args, "Want , but got %+v", actual)) + t.FailNow() +} + +func NotNil(t IUnitTest, actual interface{}, args ...interface{}) { + if !isNil(actual) { + return + } + t.Helper() + t.Errorf(errorMessage(args, "Want not , but got %+v", actual)) + t.FailNow() +} + +func isNil(value interface{}) bool { + if value == nil { + return true + } + return reflect.ValueOf(value).IsNil() +} diff --git a/version/go.mod b/version/go.mod new file mode 100644 index 0000000..43ae757 --- /dev/null +++ b/version/go.mod @@ -0,0 +1,3 @@ +module go.osspkg.com/x/version + +go 1.20 diff --git a/version/ver.go b/version/ver.go new file mode 100644 index 0000000..648ec01 --- /dev/null +++ b/version/ver.go @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package version + +import ( + "fmt" + "regexp" + "strconv" +) + +var rex = regexp.MustCompile(`v([0-9]+)\.([0-9]+)\.([0-9]+)$`) + +type Version struct { + Major int64 + Minor int64 + Patch int64 +} + +func (v Version) String() string { + return fmt.Sprintf("v%d.%d.%d", v.Major, v.Minor, v.Patch) +} + +func Parse(v string) (*Version, error) { + data := rex.FindStringSubmatch(v) + if len(data) != 4 { + return nil, fmt.Errorf("invalid format: %s", v) + } + result := &Version{ + Major: 0, + Minor: 0, + Patch: 0, + } + var err error + if result.Major, err = strconv.ParseInt(data[1], 10, 64); err != nil { + return nil, err + } + if result.Minor, err = strconv.ParseInt(data[2], 10, 64); err != nil { + return nil, err + } + if result.Patch, err = strconv.ParseInt(data[3], 10, 64); err != nil { + return nil, err + } + return result, nil +} + +func Compare(v1, v2 string) int { + a, e1 := Parse(v1) + b, e2 := Parse(v2) + switch true { + case e1 != nil && e2 != nil: + return 0 + case e1 != nil: + return -1 + case e2 != nil: + return +1 + case a.Major < b.Major: + return -1 + case a.Major > b.Major: + return +1 + case a.Minor < b.Minor: + return -1 + case a.Minor > b.Minor: + return +1 + case a.Patch < b.Patch: + return -1 + case a.Patch > b.Patch: + return +1 + default: + return 0 + } +} + +func Max(versions ...string) *Version { + result := "v0.0.0" + for _, ver := range versions { + if Compare(result, ver) < 0 { + result = ver + } + } + v, err := Parse(result) + if err != nil { + return &Version{ + Major: 0, + Minor: 0, + Patch: 0, + } + } + return v +} diff --git a/version/ver_test.go b/version/ver_test.go new file mode 100644 index 0000000..17910d5 --- /dev/null +++ b/version/ver_test.go @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package version + +import ( + "reflect" + "testing" +) + +func TestUnit_Parse(t *testing.T) { + tests := []struct { + name string + args string + want *Version + wantErr bool + }{ + { + name: "Case1", + args: "v1.1000.1231", + want: &Version{ + Major: 1, + Minor: 1000, + Patch: 1231, + }, + wantErr: false, + }, + { + name: "Case2", + args: "app/v1.1000.1231", + want: &Version{ + Major: 1, + Minor: 1000, + Patch: 1231, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Parse(tt.args) + if (err != nil) != tt.wantErr { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Parse() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestUnit_Max(t *testing.T) { + tests := []struct { + name string + vers []string + wantOut string + }{ + { + name: "Case1", + vers: []string{"v0.0.1", "v0.0.119991"}, + wantOut: "v0.0.119991", + }, + { + name: "Case2", + vers: []string{}, + wantOut: "v0.0.0", + }, + { + name: "Case3", + vers: []string{" "}, + wantOut: "v0.0.0", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotOut := Max(tt.vers...); gotOut.String() != tt.wantOut { + t.Errorf("Max() = %v, want %v", gotOut, tt.wantOut) + } + }) + } +} diff --git a/x.go b/x.go new file mode 100644 index 0000000..970a914 --- /dev/null +++ b/x.go @@ -0,0 +1,6 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package x