diff --git a/internal/config2/config.go b/internal/config2/config.go new file mode 100644 index 0000000..1b89c78 --- /dev/null +++ b/internal/config2/config.go @@ -0,0 +1,81 @@ +package config2 + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/9seconds/mtg/v2/mtglib" +) + +type Config struct { + Debug TypeBool `json:"debug"` + Secret mtglib.Secret `json:"secret"` + BindTo TypeHostPort `json:"bindTo"` + TCPBuffer TypeBytes `json:"tcpBuffer"` + PreferIP TypePreferIP `json:"preferIp"` + DomainFrontingPort TypePort `json:"domainFrontingPort"` + TolerateTimeSkewness TypeDuration `json:"tolerateTimeSkewness"` + Concurrency TypeConcurrency `json:"concurrency"` + Defense struct { + AntiReplay struct { + Enabled TypeBool `json:"enabled"` + MaxSize TypeBytes `json:"maxSize"` + ErrorRate TypeErrorRate `json:"errorRate"` + } `json:"antiReplay"` + Blocklist struct { + Enabled TypeBool `json:"enabled"` + DownloadConcurrency TypeConcurrency `json:"downloadConcurrency"` + URLs []TypeBlocklistURI `json:"urls"` + UpdateEach TypeDuration `json:"updateEach"` + } `json:"blocklist"` + } `json:"defense"` + Network struct { + Timeout struct { + TCP TypeDuration `json:"tcp"` + HTTP TypeDuration `json:"http"` + Idle TypeDuration `json:"idle"` + } `json:"timeout"` + DOHIP TypeIP `json:"dohIp"` + Proxies []TypeProxyURL `json:"proxies"` + } `json:"network"` + Stats struct { + StatsD struct { + Enabled TypeBool `json:"enabled"` + Address TypeHostPort `json:"address"` + MetricPrefix TypeMetricPrefix `json:"metricPrefix"` + TagFormat TypeStatsdTagFormat `json:"tagFormat"` + } `json:"statsd"` + Prometheus struct { + Enabled TypeBool `json:"enabled"` + BindTo TypeHostPort `json:"bindTo"` + HTTPPath TypeHTTPPath `json:"httpPath"` + MetricPrefix TypeMetricPrefix `json:"metricPrefix"` + } `json:"prometheus"` + } `json:"stats"` +} + +func (c *Config) Validate() error { + if !c.Secret.Valid() { + return fmt.Errorf("invalid secret %s", c.Secret.String()) + } + + if c.BindTo.Get("") == "" { + return fmt.Errorf("incorrect bind-to parameter %s", c.BindTo.String()) + } + + return nil +} + +func (c *Config) String() string { + buf := &bytes.Buffer{} + encoder := json.NewEncoder(buf) + + encoder.SetEscapeHTML(false) + + if err := encoder.Encode(c); err != nil { + panic(err) + } + + return buf.String() +} diff --git a/internal/config2/config_test.go b/internal/config2/config_test.go new file mode 100644 index 0000000..76d32e7 --- /dev/null +++ b/internal/config2/config_test.go @@ -0,0 +1,54 @@ +package config2_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/suite" +) + +type ConfigTestSuite struct { + suite.Suite +} + +func (suite *ConfigTestSuite) ReadConfig(filename string) []byte { + data, err := os.ReadFile(filepath.Join("testdata", filename)) + suite.NoError(err) + + return data +} + +func (suite *ConfigTestSuite) TestParseEmpty() { + _, err := config2.Parse([]byte{}) + suite.Error(err) +} + +func (suite *ConfigTestSuite) TestParseBrokenToml() { + _, err := config2.Parse(suite.ReadConfig("broken.toml")) + suite.Error(err) +} + +func (suite *ConfigTestSuite) TestParseOnlySecret() { + _, err := config2.Parse(suite.ReadConfig("only_secret.toml")) + suite.Error(err) +} + +func (suite *ConfigTestSuite) TestParseMinimalConfig() { + conf, err := config2.Parse(suite.ReadConfig("minimal.toml")) + suite.NoError(err) + suite.Equal("7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t", conf.Secret.Base64()) + suite.Equal("0.0.0.0:3128", conf.BindTo.String()) +} + +func (suite *ConfigTestSuite) TestString() { + conf, err := config2.Parse(suite.ReadConfig("minimal.toml")) + suite.NoError(err) + suite.NotEmpty(conf.String()) +} + +func TestConfig(t *testing.T) { + t.Parallel() + suite.Run(t, &ConfigTestSuite{}) +} diff --git a/internal/config2/parse.go b/internal/config2/parse.go new file mode 100644 index 0000000..c63c762 --- /dev/null +++ b/internal/config2/parse.go @@ -0,0 +1,80 @@ +package config2 + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/pelletier/go-toml" +) + +type tomlConfig struct { + Debug bool `toml:"debug" json:"debug,omitempty"` + Secret string `toml:"secret" json:"secret"` + BindTo string `toml:"bind-to" json:"bindTo"` + TCPBuffer string `toml:"tcp-buffer" json:"tcpBuffer,omitempty"` + PreferIP string `toml:"prefer-ip" json:"preferIp,omitempty"` + DomainFrontingPort uint `toml:"domain-fronting-port" json:"domainFrontingPort,omitempty"` + TolerateTimeSkewness string `toml:"tolerate-time-skewness" json:"tolerateTimeSkewness,omitempty"` + Concurrency uint `toml:"concurrency" json:"concurrency,omitempty"` + Defense struct { + AntiReplay struct { + Enabled bool `toml:"enabled" json:"enabled,omitempty"` + MaxSize string `toml:"max-size" json:"maxSize,omitempty"` + ErrorRate float64 `toml:"error-rate" json:"errorRate,omitempty"` + } `toml:"anti-replay" json:"antiReplay,omitempty"` + Blocklist struct { + Enabled bool `toml:"enabled" json:"enabled,omitempty"` + DownloadConcurrency uint `toml:"download-concurrency" json:"downloadConcurrency,omitempty"` + URLs []string `toml:"urls" json:"urls,omitempty"` + UpdateEach string `toml:"update-each" json:"updateEach,omitempty"` + } `toml:"blocklist" json:"blocklist,omitempty"` + } `toml:"defense" json:"defense,omitempty"` + Network struct { + Timeout struct { + TCP string `toml:"tcp" json:"tcp,omitempty"` + HTTP string `toml:"http" json:"http,omitempty"` + Idle string `toml:"idle" json:"idle,omitempty"` + } `toml:"timeout" json:"timeout,omitempty"` + DOHIP string `toml:"doh-ip" json:"dohIp,omitempty"` + Proxies []string `toml:"proxies" json:"proxies,omitempty"` + } `toml:"network" json:"network,omitempty"` + Stats struct { + StatsD struct { + Enabled bool `toml:"enabled" json:"enabled,omitempty"` + Address string `toml:"address" json:"address,omitempty"` + MetricPrefix string `toml:"metric-prefix" json:"metricPrefix,omitempty"` + TagFormat string `toml:"tag-format" json:"tagFormat,omitempty"` + } `toml:"statsd" json:"statsd,omitempty"` + Prometheus struct { + Enabled bool `toml:"enabled" json:"enabled,omitempty"` + BindTo string `toml:"bind-to" json:"bindTo,omitempty"` + HTTPPath string `toml:"http-path" json:"httpPath,omitempty"` + MetricPrefix string `toml:"metric-prefix" json:"metricPrefix,omitempty"` + } `toml:"prometheus" json:"prometheus,omitempty"` + } `toml:"stats" json:"stats,omitempty"` +} + +func Parse(rawData []byte) (*Config, error) { + tomlConf := &tomlConfig{} + jsonBuf := &bytes.Buffer{} + conf := &Config{} + + jsonEncoder := json.NewEncoder(jsonBuf) + jsonEncoder.SetEscapeHTML(false) + jsonEncoder.SetIndent("", "") + + if err := toml.Unmarshal(rawData, tomlConf); err != nil { + return nil, fmt.Errorf("cannot parse toml config: %w", err) + } + + if err := jsonEncoder.Encode(tomlConf); err != nil { + panic(err) + } + + if err := json.NewDecoder(jsonBuf).Decode(conf); err != nil { + return nil, fmt.Errorf("cannot parse a config: %w", err) + } + + return conf, nil +} diff --git a/internal/config2/testdata/broken.toml b/internal/config2/testdata/broken.toml new file mode 100644 index 0000000..d95f791 --- /dev/null +++ b/internal/config2/testdata/broken.toml @@ -0,0 +1 @@ +s = sdfsdfds diff --git a/internal/config2/testdata/minimal.toml b/internal/config2/testdata/minimal.toml new file mode 100644 index 0000000..9d0961a --- /dev/null +++ b/internal/config2/testdata/minimal.toml @@ -0,0 +1,2 @@ +secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t" +bind-to = "0.0.0.0:3128" diff --git a/internal/config2/testdata/only_secret.toml b/internal/config2/testdata/only_secret.toml new file mode 100644 index 0000000..f6b0bee --- /dev/null +++ b/internal/config2/testdata/only_secret.toml @@ -0,0 +1 @@ +secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t" diff --git a/internal/config2/type_blocklist_uri.go b/internal/config2/type_blocklist_uri.go new file mode 100644 index 0000000..f73ee3c --- /dev/null +++ b/internal/config2/type_blocklist_uri.go @@ -0,0 +1,77 @@ +package config2 + +import ( + "fmt" + "net/url" + "os" + "path/filepath" +) + +type TypeBlocklistURI struct { + Value string +} + +func (t *TypeBlocklistURI) Set(value string) error { + if stat, err := os.Stat(value); err == nil || os.IsExist(err) { + switch { + case stat.IsDir(): + return fmt.Errorf("value is correct filepath but directory") + case stat.Mode().Perm() & 0o400 == 0: + return fmt.Errorf("value is correct filepath but not readable") + } + + value, err = filepath.Abs(value) + if err != nil { + return fmt.Errorf( + "value is correct filepath but cannot resolve absolute (%s): %w", + value, err) + } + + t.Value = value + + return nil + } + + parsedURL, err := url.Parse(value) + if err != nil { + return fmt.Errorf("incorrect url (%s): %w", value, err) + } + + switch parsedURL.Scheme { + case "http", "https": + default: + return fmt.Errorf("unknown schema %s (%s)", parsedURL.Scheme, value) + } + + if parsedURL.Host == "" { + return fmt.Errorf("incorrect url %s", value) + } + + t.Value = parsedURL.String() + + return nil +} + +func (t TypeBlocklistURI) Get(defaultValue string) string { + if t.Value == "" { + return defaultValue + } + + return t.Value +} + +func (t TypeBlocklistURI) IsRemote() bool { + return !filepath.IsAbs(t.Value) +} + +func (t *TypeBlocklistURI) UnmarshalText(data []byte) error { + return t.Set(string(data)) +} + +func (t TypeBlocklistURI) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypeBlocklistURI) String() string { + return t.Value +} diff --git a/internal/config2/type_blocklist_uri_test.go b/internal/config2/type_blocklist_uri_test.go new file mode 100644 index 0000000..cfecc8a --- /dev/null +++ b/internal/config2/type_blocklist_uri_test.go @@ -0,0 +1,113 @@ +package config2_test + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeBlocklistURITestStruct struct { + Value config2.TypeBlocklistURI `json:"value"` +} + +type TypeBlocklistURITestSuite struct { + suite.Suite + + directory string + absDirectory string +} + +func (suite *TypeBlocklistURITestSuite) SetupSuite() { + dir, _ := os.Getwd() + absDir, _ := filepath.Abs(dir) + + suite.directory = dir + suite.absDirectory = absDir +} + +func (suite *TypeBlocklistURITestSuite) TestUnmarshalFail() { + testData := []string{ + "gopher://lalala", + "https:///paths", + "h:/=", + filepath.Join(suite.directory, "___"), + filepath.Join(suite.absDirectory, "___"), + suite.directory, + suite.absDirectory, + } + + for _, v := range testData { + data, err := json.Marshal(map[string]string{ + "value": v, + }) + suite.NoError(err) + + suite.T().Run(v, func(t *testing.T) { + assert.Error(t, json.Unmarshal(data, &typeBlocklistURITestStruct{})) + }) + } +} + +func (suite *TypeBlocklistURITestSuite) TestUnmarshalOk() { + testData := []string{ + "http://lalala", + "https://lalala", + "https://lalala/path", + filepath.Join(suite.directory, "config.go"), + filepath.Join(suite.absDirectory, "config.go"), + } + + for _, v := range testData { + value := v + + data, err := json.Marshal(map[string]string{ + "value": v, + }) + suite.NoError(err) + + suite.T().Run(v, func(t *testing.T) { + testStruct := &typeBlocklistURITestStruct{} + + assert.NoError(t, json.Unmarshal(data, testStruct)) + assert.EqualValues(t, value, testStruct.Value.Get("")) + + if strings.HasPrefix(value, "http") { + assert.True(t, testStruct.Value.IsRemote()) + } else { + assert.False(t, testStruct.Value.IsRemote()) + } + }) + } +} + +func (suite *TypeBlocklistURITestSuite) TestMarshalOk() { + testStruct := &typeBlocklistURITestStruct{ + Value: config2.TypeBlocklistURI{ + Value: "http://some.url/with/path", + }, + } + + data, err := json.Marshal(testStruct) + suite.NoError(err) + suite.JSONEq(`{"value": "http://some.url/with/path"}`, string(data)) +} + +func (suite *TypeBlocklistURITestSuite) TestGet() { + value := config2.TypeBlocklistURI{} + suite.Equal("/path", value.Get("/path")) + + suite.NoError(value.Set("http://lalala.ru")) + suite.Equal("http://lalala.ru", value.Get("/path")) + suite.Equal("http://lalala.ru", value.Get("")) +} + +func TestTypeBlocklistURI(t *testing.T) { + t.Parallel() + suite.Run(t, &TypeBlocklistURITestSuite{}) +} diff --git a/internal/config2/type_bool.go b/internal/config2/type_bool.go new file mode 100644 index 0000000..490d610 --- /dev/null +++ b/internal/config2/type_bool.go @@ -0,0 +1,40 @@ +package config2 + +import ( + "fmt" + "strconv" + "strings" +) + +type TypeBool struct { + Value bool +} + +func (t *TypeBool) Set(data string) error { + switch strings.ToLower(data) { + case "1", "y", "yes", "enabled", "true": + t.Value = true + case "0", "n", "no", "disabled", "false": + t.Value = false + default: + return fmt.Errorf("incorrect bool value %s", data) + } + + return nil +} + +func (t TypeBool) Get(defaultValue bool) bool { + return t.Value || defaultValue +} + +func (t *TypeBool) UnmarshalText(data []byte) error { + return t.Set(string(data)) +} + +func (t TypeBool) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypeBool) String() string { + return strconv.FormatBool(t.Value) +} diff --git a/internal/config2/type_bool_test.go b/internal/config2/type_bool_test.go new file mode 100644 index 0000000..0fa3b09 --- /dev/null +++ b/internal/config2/type_bool_test.go @@ -0,0 +1,113 @@ +package config2_test + +import ( + "encoding/json" + "fmt" + "strconv" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeBoolTestStruct struct { + Value config2.TypeBool `json:"value"` +} + +type TypeBoolTestSuite struct { + suite.Suite +} + +func (suite *TypeBoolTestSuite) TestUnmarshalFail() { + testData := []string{ + "", + "np", + "нет", + } + + for _, v := range testData { + data, err := json.Marshal(map[string]string{ + "value": v, + }) + suite.NoError(err) + + suite.T().Run(v, func(t *testing.T) { + assert.Error(t, json.Unmarshal(data, &typeBoolTestStruct{})) + }) + } +} + +func (suite *TypeBoolTestSuite) TestUnmarshalOk() { + testData := map[string]bool{ + "0": false, + "N": false, + "nO": false, + "no": false, + "dISAbLEd": false, + "False": false, + "false": false, + + "1": true, + "y": true, + "Yes": true, + "yes": true, + "enABLED": true, + "True": true, + "TRUE": true, + "true": true, + } + + for k, v := range testData { + value := v + + data, err := json.Marshal(map[string]string{ + "value": k, + }) + suite.NoError(err) + + suite.T().Run(k, func(t *testing.T) { + testStruct := &typeBoolTestStruct{} + assert.NoError(t, json.Unmarshal(data, testStruct)) + + if value { + assert.True(t, testStruct.Value.Value) + } else { + assert.False(t, testStruct.Value.Value) + } + }) + } +} + +func (suite *TypeBoolTestSuite) TestMarshalOk() { + for _, v := range []bool{true, false} { + name := strconv.FormatBool(v) + + suite.T().Run(name, func(t *testing.T) { + testStruct := typeBoolTestStruct{ + Value: config2.TypeBool{ + Value: v, + }, + } + + data, err := json.Marshal(testStruct) + assert.NoError(t, err) + assert.JSONEq(t, fmt.Sprintf(`{"value": "%s"}`, name), string(data)) + }) + } +} + +func (suite *TypeBoolTestSuite) TestGet() { + value := config2.TypeBool{} + suite.False(value.Get(false)) + suite.True(value.Get(true)) + + value.Value = true + suite.True(value.Get(false)) + suite.True(value.Get(true)) +} + +func TestTypeBool(t *testing.T) { + t.Parallel() + suite.Run(t, &TypeBoolTestSuite{}) +} diff --git a/internal/config2/type_bytes.go b/internal/config2/type_bytes.go new file mode 100644 index 0000000..789ab54 --- /dev/null +++ b/internal/config2/type_bytes.go @@ -0,0 +1,55 @@ +package config2 + +import ( + "fmt" + "strings" + + "github.com/alecthomas/units" +) + +var typeBytesStringCleaner = strings.NewReplacer(" ", "", "\t", "", "IB", "iB") + +type TypeBytes struct { + Value units.Base2Bytes +} + +func (t *TypeBytes) Set(value string) error { + normalizedValue := typeBytesStringCleaner.Replace(strings.ToUpper(value)) + + parsedValue, err := units.ParseBase2Bytes(normalizedValue) + if err != nil { + return fmt.Errorf("incorrect bytes value (%v): %w", value, err) + } + + if parsedValue < 0 { + return fmt.Errorf("bytes should be positive (%s)", value) + } + + t.Value = parsedValue + + return nil +} + +func (t TypeBytes) Get(defaultValue uint) uint { + if t.Value == 0 { + return defaultValue + } + + return uint(t.Value) +} + +func (t *TypeBytes) UnmarshalText(data []byte) error { + return t.Set(string(data)) +} + +func (t TypeBytes) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypeBytes) String() string { + if t.Value == 0 { + return "" + } + + return strings.ToLower(t.Value.String()) +} diff --git a/internal/config2/type_bytes_test.go b/internal/config2/type_bytes_test.go new file mode 100644 index 0000000..c153fb2 --- /dev/null +++ b/internal/config2/type_bytes_test.go @@ -0,0 +1,86 @@ +package config2_test + +import ( + "encoding/json" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeBytesTestStruct struct { + Value config2.TypeBytes `json:"value"` +} + +type TypeBytesTestSuite struct { + suite.Suite +} + +func (suite *TypeBytesTestSuite) TestUnmarshalFail() { + testData := []string{ + "1m", + "1", + "-1kb", + "-1kib", + } + + for _, v := range testData { + data, err := json.Marshal(map[string]string{ + "value": v, + }) + suite.NoError(err) + + suite.T().Run(v, func(t *testing.T) { + assert.Error(t, json.Unmarshal(data, &typeBytesTestStruct{})) + }) + } +} + +func (suite *TypeBytesTestSuite) TestUnmarshalOk() { + testData := map[string]uint{ + "1b": 1, + "1kb": 1024, + "1kib": 1024, + "2mb": 2 * 1024 * 1024, + "2mib": 2 * 1024 * 1024, + } + + for k, v := range testData { + value := v + + data, err := json.Marshal(map[string]string{ + "value": k, + }) + suite.NoError(err) + + suite.T().Run(k, func(t *testing.T) { + testStruct := &typeBytesTestStruct{} + + assert.NoError(t, json.Unmarshal(data, testStruct)) + assert.EqualValues(t, value, testStruct.Value.Get(0)) + }) + } +} + +func (suite *TypeBytesTestSuite) TestMarshalOk() { + value := typeBytesTestStruct{} + suite.NoError(value.Value.Set("1kib")) + + data, err := json.Marshal(value) + suite.NoError(err) + suite.JSONEq(`{"value": "1kib"}`, string(data)) +} + +func (suite *TypeBytesTestSuite) TestGet() { + value := config2.TypeBytes{} + suite.EqualValues(1000, value.Get(1000)) + + suite.NoError(value.Set("1mib")) + suite.EqualValues(1048576, value.Get(1000)) +} + +func TestTypeBytes(t *testing.T) { + t.Parallel() + suite.Run(t, &TypeBytesTestSuite{}) +} diff --git a/internal/config2/type_concurrency.go b/internal/config2/type_concurrency.go new file mode 100644 index 0000000..e54f8ad --- /dev/null +++ b/internal/config2/type_concurrency.go @@ -0,0 +1,45 @@ +package config2 + +import ( + "fmt" + "strconv" +) + +type TypeConcurrency struct { + Value uint +} + +func (t *TypeConcurrency) Set(value string) error { + concurrencyValue, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return fmt.Errorf("Value is not uint (%s): %w", value, err) + } + + if concurrencyValue == 0 { + return fmt.Errorf("Value should be >0 (%s)", value) + } + + t.Value = uint(concurrencyValue) + + return nil +} + +func (t TypeConcurrency) Get(defaultValue uint) uint { + if t.Value == 0 { + return defaultValue + } + + return t.Value +} + +func (t *TypeConcurrency) UnmarshalJSON(data []byte) error { + return t.Set(string(data)) +} + +func (t TypeConcurrency) MarshalJSON() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypeConcurrency) String() string { + return strconv.FormatUint(uint64(t.Value), 10) +} diff --git a/internal/config2/type_concurrency_test.go b/internal/config2/type_concurrency_test.go new file mode 100644 index 0000000..f7227a5 --- /dev/null +++ b/internal/config2/type_concurrency_test.go @@ -0,0 +1,73 @@ +package config2_test + +import ( + "encoding/json" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeConcurrencyTestStruct struct { + Value config2.TypeConcurrency `json:"value"` +} + +type TypeConcurrencyTestSuite struct { + suite.Suite +} + +func (suite *TypeConcurrencyTestSuite) TestUnmarshalFail() { + testData := []string{ + "-1", + "0", + "0.0", + "1.0", + "1.1", + ".", + "some_value", + } + + for _, v := range testData { + data, err := json.Marshal(map[string]string{ + "value": v, + }) + suite.NoError(err) + + suite.T().Run(v, func(t *testing.T) { + assert.Error(t, json.Unmarshal(data, &typeConcurrencyTestStruct{})) + }) + } +} + +func (suite *TypeConcurrencyTestSuite) TestUnmarshalOk() { + testStruct := &typeConcurrencyTestStruct{} + + suite.NoError(json.Unmarshal([]byte(`{"value": 1}`), testStruct)) + suite.EqualValues(1, testStruct.Value.Get(2)) +} + +func (suite *TypeConcurrencyTestSuite) TestMarshalOk() { + testStruct := &typeConcurrencyTestStruct{ + Value: config2.TypeConcurrency{ + Value: 2, + }, + } + + data, err := json.Marshal(testStruct) + suite.NoError(err) + suite.JSONEq(`{"value": 2}`, string(data)) +} + +func (suite *TypeConcurrencyTestSuite) TestGet() { + value := config2.TypeConcurrency{} + suite.EqualValues(1, value.Get(1)) + + value.Value = 3 + suite.EqualValues(3, value.Get(1)) +} + +func TestTypeConcurrency(t *testing.T) { + t.Parallel() + suite.Run(t, &TypeConcurrencyTestSuite{}) +} diff --git a/internal/config2/type_duration.go b/internal/config2/type_duration.go new file mode 100644 index 0000000..85db148 --- /dev/null +++ b/internal/config2/type_duration.go @@ -0,0 +1,53 @@ +package config2 + +import ( + "fmt" + "strings" + "time" +) + +var typeDurationStringCleaner = strings.NewReplacer(" ", "", "\t", "") + +type TypeDuration struct { + Value time.Duration +} + +func (t *TypeDuration) Set(value string) error { + parsedValue, err := time.ParseDuration( + typeDurationStringCleaner.Replace(strings.ToLower(value))) + if err != nil { + return fmt.Errorf("incorrect duration (%s): %w", value, err) + } + + if parsedValue < 0 { + return fmt.Errorf("duration has to be a positive: %s", value) + } + + t.Value = parsedValue + + return nil +} + +func (t TypeDuration) Get(defaultValue time.Duration) time.Duration { + if t.Value == 0 { + return defaultValue + } + + return t.Value +} + +func (t *TypeDuration) UnmarshalText(data []byte) error { + return t.Set(string(data)) +} + +func (t TypeDuration) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypeDuration) String() string { + if t.Value == 0 { + return "" + } + + return t.Value.String() +} diff --git a/internal/config2/type_duration_test.go b/internal/config2/type_duration_test.go new file mode 100644 index 0000000..3f9b21e --- /dev/null +++ b/internal/config2/type_duration_test.go @@ -0,0 +1,110 @@ +package config2_test + +import ( + "encoding/json" + "testing" + "time" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeDurationTestStruct struct { + Value config2.TypeDuration `json:"value"` +} + +type TypeDurationTestSuite struct { + suite.Suite +} + +func (suite *TypeDurationTestSuite) TestUnmarshalFail() { + testData := []string{ + "-1s", + "1 seconds ago", + "1s ago", + "", + } + + for _, v := range testData { + data, err := json.Marshal(map[string]string{ + "value": v, + }) + suite.NoError(err) + + suite.T().Run(v, func(t *testing.T) { + assert.Error(t, json.Unmarshal(data, &typeDurationTestStruct{})) + }) + } +} + +func (suite *TypeDurationTestSuite) TestUnmarshalOk() { + testData := map[string]time.Duration{ + "1s": time.Second, + "0": 0 * time.Second, + "0s": 0 * time.Second, + "1\tM": time.Minute, + "1H": time.Hour, + "1 h": time.Hour, + } + + for k, v := range testData { + value := v + + data, err := json.Marshal(map[string]string{ + "value": k, + }) + suite.NoError(err) + + suite.T().Run(k, func(t *testing.T) { + testStruct := &typeDurationTestStruct{} + + assert.NoError(t, json.Unmarshal(data, testStruct)) + assert.Equal(t, value, testStruct.Value.Value) + }) + } +} + +func (suite *TypeDurationTestSuite) TestMarshalOk() { + testData := map[string]string{ + "1s": "1s", + "0": "", + "0s": "", + "0ms": "", + "1 H": "1h0m0s", + } + + for k, v := range testData { + value := k + expected := v + + suite.T().Run(value, func(t *testing.T) { + testStruct := &typeDurationTestStruct{} + + assert.NoError(t, testStruct.Value.Set(value)) + + data, err := json.Marshal(testStruct) + assert.NoError(t, err) + + expectedJson, err := json.Marshal(map[string]string{ + "value": expected, + }) + assert.NoError(t, err) + + assert.JSONEq(t, string(expectedJson), string(data)) + }) + } +} + +func (suite *TypeDurationTestSuite) TestGet() { + value := config2.TypeDuration{} + suite.Equal(time.Second, value.Get(time.Second)) + + value.Value = 3 * time.Second + suite.Equal(3*time.Second, value.Get(time.Hour)) +} + +func TestTypeDuration(t *testing.T) { + t.Parallel() + suite.Run(t, &TypeDurationTestSuite{}) +} diff --git a/internal/config2/type_error_rate.go b/internal/config2/type_error_rate.go new file mode 100644 index 0000000..1ce8ecc --- /dev/null +++ b/internal/config2/type_error_rate.go @@ -0,0 +1,47 @@ +package config2 + +import ( + "fmt" + "strconv" +) + +const typeErrorRateIgnoreLess = 1e-8 + +type TypeErrorRate struct { + Value float64 +} + +func (t *TypeErrorRate) Set(value string) error { + parsedValue, err := strconv.ParseFloat(value, 64) + if err != nil { + return fmt.Errorf("Value is not a float (%s): %w", value, err) + } + + if parsedValue <= 0.0 || parsedValue >= 100.0 { + return fmt.Errorf("Value should be 0 < x < 100 (%s)", value) + } + + t.Value = parsedValue + + return nil +} + +func (t TypeErrorRate) Get(defaultValue float64) float64 { + if t.Value < typeErrorRateIgnoreLess { + return defaultValue + } + + return t.Value +} + +func (t *TypeErrorRate) UnmarshalText(data []byte) error { + return t.Set(string(data)) +} + +func (t TypeErrorRate) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypeErrorRate) String() string { + return strconv.FormatFloat(t.Value, 'f', -1, 64) +} diff --git a/internal/config2/type_error_rate_test.go b/internal/config2/type_error_rate_test.go new file mode 100644 index 0000000..5d8590a --- /dev/null +++ b/internal/config2/type_error_rate_test.go @@ -0,0 +1,94 @@ +package config2_test + +import ( + "encoding/json" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeErrorRateTestStruct struct { + Value config2.TypeErrorRate `json:"value"` +} + +type TypeErrorRateTestSuite struct { + suite.Suite +} + +func (suite *TypeErrorRateTestSuite) TestUnmarshalFail() { + testData := []string{ + "", + "1s", + "1,", + "1,2", + ".", + "3.4.5", + "3.5.", + ".3.5", + "some word", + "1e2", + "-1.0", + } + + for _, v := range testData { + data, err := json.Marshal(map[string]string{ + "value": v, + }) + suite.NoError(err) + + suite.T().Run(v, func(t *testing.T) { + assert.Error(t, json.Unmarshal(data, &typeErrorRateTestStruct{})) + }) + } +} + +func (suite *TypeErrorRateTestSuite) TestUnmarshalOk() { + testData := map[string]float64{ + "1": 1.0, + "1.0": 1.0, + "0.5": 0.5, + ".5": 0.5, + } + + for k, v := range testData { + value := v + + data, err := json.Marshal(map[string]string{ + "value": k, + }) + suite.NoError(err) + + suite.T().Run(k, func(t *testing.T) { + testStruct := &typeErrorRateTestStruct{} + assert.NoError(t, json.Unmarshal(data, testStruct)) + assert.InEpsilon(t, value, testStruct.Value.Value, 1e-10) + }) + } +} + +func (suite *TypeErrorRateTestSuite) TestMarshalOk() { + testStruct := typeErrorRateTestStruct{ + Value: config2.TypeErrorRate{ + Value: 1.01, + }, + } + + encodedJson, err := json.Marshal(testStruct) + suite.NoError(err) + suite.JSONEq(`{"value": "1.01"}`, string(encodedJson)) +} + +func (suite *TypeErrorRateTestSuite) TestGet() { + value := config2.TypeErrorRate{} + suite.InEpsilon(1.0, value.Get(1.0), 1e-10) + + value.Value = 5.0 + suite.InEpsilon(5.0, value.Get(1.0), 1e-10) +} + +func TestTypeErrorRate(t *testing.T) { + t.Parallel() + suite.Run(t, &TypeErrorRateTestSuite{}) +} diff --git a/internal/config2/type_hostport.go b/internal/config2/type_hostport.go new file mode 100644 index 0000000..73d45c0 --- /dev/null +++ b/internal/config2/type_hostport.go @@ -0,0 +1,59 @@ +package config2 + +import ( + "fmt" + "net" + "strconv" +) + +type TypeHostPort struct { + Value string +} + +func (t *TypeHostPort) Set(value string) error { + host, port, err := net.SplitHostPort(value) + if err != nil { + return fmt.Errorf("incorrect host:port value (%v): %w", value, err) + } + + portValue, err := strconv.ParseUint(port, 10, 16) + if err != nil { + return fmt.Errorf("incorrect port number (%v): %w", value, err) + } + + if portValue == 0 { + return fmt.Errorf("incorrect port number (%s)", value) + } + + if host == "" { + return fmt.Errorf("empty host: %s", value) + } + + if net.ParseIP(host) == nil { + return fmt.Errorf("host is not an IP address: %s", value) + } + + t.Value = net.JoinHostPort(host, port) + + return nil +} + +func (t TypeHostPort) Get(defaultValue string) string { + if t.Value == "" { + return defaultValue + } + + return t.Value +} + +func (t *TypeHostPort) UnmarshalText(data []byte) error { + return t.Set(string(data)) +} + +func (t TypeHostPort) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypeHostPort) String() string { + return t.Value +} diff --git a/internal/config2/type_hostport_test.go b/internal/config2/type_hostport_test.go new file mode 100644 index 0000000..dfaf395 --- /dev/null +++ b/internal/config2/type_hostport_test.go @@ -0,0 +1,88 @@ +package config2_test + +import ( + "encoding/json" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeHostPortTestStruct struct { + Value config2.TypeHostPort `json:"value"` +} + +type TypeHostPortTestSuite struct { + suite.Suite +} + +func (suite *TypeHostPortTestSuite) TestUnmarshalFail() { + testData := []string{ + ":", + ":800", + "127.0.0.1:8000000", + "12...:80", + "", + "localhost", + "google.com:", + } + + for _, v := range testData { + data, err := json.Marshal(map[string]string{ + "value": v, + }) + suite.NoError(err) + + suite.T().Run(v, func(t *testing.T) { + assert.Error(t, json.Unmarshal(data, &typeHostPortTestStruct{})) + }) + } +} + +func (suite *TypeHostPortTestSuite) TestUnmarshalOk() { + testData := []string{ + "127.0.0.1:80", + "10.0.0.10:6553", + } + + for _, v := range testData { + value := v + + data, err := json.Marshal(map[string]string{ + "value": v, + }) + suite.NoError(err) + + suite.T().Run(v, func(t *testing.T) { + testStruct := &typeHostPortTestStruct{} + assert.NoError(t, json.Unmarshal(data, testStruct)) + assert.Equal(t, value, testStruct.Value.Value) + }) + } +} + +func (suite *TypeHostPortTestSuite) TestMarshalOk() { + testStruct := typeHostPortTestStruct{ + Value: config2.TypeHostPort{ + Value: "127.0.0.1:8000", + }, + } + + data, err := json.Marshal(testStruct) + suite.NoError(err) + suite.JSONEq(`{"value": "127.0.0.1:8000"}`, string(data)) +} + +func (suite *TypeHostPortTestSuite) TestGet() { + value := config2.TypeHostPort{} + suite.Equal("127.0.0.1:9000", value.Get("127.0.0.1:9000")) + + value.Value = "127.0.0.1:80" + suite.Equal("127.0.0.1:80", value.Get("127.0.0.1:9000")) +} + +func TestTypeHostPort(t *testing.T) { + t.Parallel() + suite.Run(t, &TypeHostPortTestSuite{}) +} diff --git a/internal/config2/type_http_path.go b/internal/config2/type_http_path.go new file mode 100644 index 0000000..d31ac57 --- /dev/null +++ b/internal/config2/type_http_path.go @@ -0,0 +1,33 @@ +package config2 + +import "strings" + +type TypeHTTPPath struct { + Value string +} + +func (t *TypeHTTPPath) Set(value string) error { + t.Value = "/" + strings.Trim(value, "/") + + return nil +} + +func (t TypeHTTPPath) Get(defaultValue string) string { + if t.Value == "" { + return defaultValue + } + + return t.Value +} + +func (t *TypeHTTPPath) UnmarshalText(data []byte) error { + return t.Set(string(data)) +} + +func (t TypeHTTPPath) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypeHTTPPath) String() string { + return t.Value +} diff --git a/internal/config2/type_http_path_test.go b/internal/config2/type_http_path_test.go new file mode 100644 index 0000000..014907e --- /dev/null +++ b/internal/config2/type_http_path_test.go @@ -0,0 +1,67 @@ +package config2_test + +import ( + "encoding/json" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeHTTPPathTestStruct struct { + Value config2.TypeHTTPPath `json:"value"` +} + +type TypeHTTPPathTestSuite struct { + suite.Suite +} + +func (suite *TypeHTTPPathTestSuite) TestUnmarshalOk() { + testData := map[string]string{ + "": "/", + "/": "/", + "/path": "/path", + "path": "/path", + } + + for k, v := range testData { + value := v + + data, err := json.Marshal(map[string]string{ + "value": k, + }) + suite.NoError(err) + + suite.T().Run(k, func(t *testing.T) { + testStruct := &typeHTTPPathTestStruct{} + assert.NoError(t, json.Unmarshal(data, testStruct)) + assert.Equal(t, value, testStruct.Value.Get("")) + }) + } +} + +func (suite *TypeHTTPPathTestSuite) TestMarshalOk() { + value := typeHTTPPathTestStruct{ + Value: config2.TypeHTTPPath{ + Value: "/path", + }, + } + + data, err := json.Marshal(value) + suite.NoError(err) + suite.JSONEq(`{"value": "/path"}`, string(data)) +} + +func (suite *TypeHTTPPathTestSuite) TestGet() { + value := config2.TypeHTTPPath{} + suite.Equal("/hello", value.Get("/hello")) + + suite.NoError(value.Set("/lalala")) + suite.Equal("/lalala", value.Get("/hello")) +} + +func TestTypeHTTPPath(t *testing.T) { + t.Parallel() + suite.Run(t, &TypeHTTPPathTestSuite{}) +} diff --git a/internal/config2/type_ip.go b/internal/config2/type_ip.go new file mode 100644 index 0000000..207d22e --- /dev/null +++ b/internal/config2/type_ip.go @@ -0,0 +1,45 @@ +package config2 + +import ( + "fmt" + "net" +) + +type TypeIP struct { + Value net.IP +} + +func (t *TypeIP) Set(value string) error { + ip := net.ParseIP(value) + if ip == nil { + return fmt.Errorf("incorret ip %s", value) + } + + t.Value = ip + + return nil +} + +func (t *TypeIP) Get(defaultValue net.IP) net.IP { + if len(t.Value) == 0 { + return defaultValue + } + + return t.Value +} + +func (t *TypeIP) UnmarshalText(data []byte) error { + return t.Set(string(data)) +} + +func (t TypeIP) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypeIP) String() string { + if len(t.Value) == 0 { + return "" + } + + return t.Value.String() +} diff --git a/internal/config2/type_ip_test.go b/internal/config2/type_ip_test.go new file mode 100644 index 0000000..4659cb0 --- /dev/null +++ b/internal/config2/type_ip_test.go @@ -0,0 +1,104 @@ +package config2_test + +import ( + "encoding/json" + "net" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeIPTestStruct struct { + Value config2.TypeIP `json:"value"` +} + +type TypeIPTestSuite struct { + suite.Suite +} + +func (suite *TypeIPTestSuite) TestUnmarshalFail() { + testData := []string{ + "", + "....", + "0...", + "300.200.200.800", + "[]", + } + + for _, v := range testData { + data, err := json.Marshal(map[string]string{ + "value": v, + }) + suite.NoError(err) + + suite.T().Run(v, func(t *testing.T) { + assert.Error(t, json.Unmarshal(data, &typeIPTestStruct{})) + }) + } +} + +func (suite *TypeIPTestSuite) TestUnmarshalOk() { + testData := map[string]string{ + "2001:0db8:85a3:0000:0000:8a2e:0370:7334": "2001:db8:85a3::8a2e:370:7334", + "127.0.0.1": "127.0.0.1", + } + + for k, v := range testData { + expected := v + + data, err := json.Marshal(map[string]string{ + "value": k, + }) + suite.NoError(err) + + suite.T().Run(k, func(t *testing.T) { + testStruct := &typeIPTestStruct{} + assert.NoError(t, json.Unmarshal(data, testStruct)) + assert.Equal(t, expected, testStruct.Value.Get(nil).String()) + }) + } +} + +func (suite *TypeIPTestSuite) TestMarshalOk() { + testData := []string{ + "2001:db8:85a3::8a2e:370:7334", + "127.0.0.1", + } + + for _, v := range testData { + value := v + + suite.T().Run(v, func(t *testing.T) { + testStruct := &typeIPTestStruct{ + Value: config2.TypeIP{ + Value: net.ParseIP(value), + }, + } + + encodedJSON, err := json.Marshal(testStruct) + assert.NoError(t, err) + + expectedJSON, err := json.Marshal(map[string]string{ + "value": value, + }) + assert.NoError(t, err) + + assert.JSONEq(t, string(expectedJSON), string(encodedJSON)) + }) + } +} + +func (suite *TypeIPTestSuite) TestGet() { + value := config2.TypeIP{} + suite.Equal("127.0.0.1", value.Get(net.ParseIP("127.0.0.1")).String()) + + suite.NoError(value.Set("127.0.0.2")) + suite.Equal("127.0.0.2", value.Get(net.ParseIP("127.0.0.1")).String()) +} + +func TestTypeIP(t *testing.T) { + t.Parallel() + suite.Run(t, &TypeIPTestSuite{}) +} diff --git a/internal/config2/type_metric_prefix.go b/internal/config2/type_metric_prefix.go new file mode 100644 index 0000000..d8507de --- /dev/null +++ b/internal/config2/type_metric_prefix.go @@ -0,0 +1,40 @@ +package config2 + +import ( + "fmt" + "regexp" +) + +type TypeMetricPrefix struct { + Value string +} + +func (t *TypeMetricPrefix) Set(value string) error { + if ok, err := regexp.MatchString("^[a-z0-9]+$", value); !ok || err != nil { + return fmt.Errorf("incorrect metric prefix %s: %w", value, err) + } + + t.Value = value + + return nil +} + +func (t TypeMetricPrefix) Get(defaultValue string) string { + if t.Value == "" { + return defaultValue + } + + return t.Value +} + +func (t *TypeMetricPrefix) UnmarshalText(data []byte) error { + return t.Set(string(data)) +} + +func (t TypeMetricPrefix) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypeMetricPrefix) String() string { + return t.Value +} diff --git a/internal/config2/type_metric_prefix_test.go b/internal/config2/type_metric_prefix_test.go new file mode 100644 index 0000000..bed6497 --- /dev/null +++ b/internal/config2/type_metric_prefix_test.go @@ -0,0 +1,70 @@ +package config2_test + +import ( + "encoding/json" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeMetricPrefixTestStruct struct { + Value config2.TypeMetricPrefix `json:"value"` +} + +type TypeMetricPrefixTestSuite struct { + suite.Suite +} + +func (suite *TypeMetricPrefixTestSuite) TestUnmarshalFail() { + testData := []string{ + "", + "-", + "hello/world", + "lala*", + "++sdf++", + } + + for _, v := range testData { + data, err := json.Marshal(map[string]string{ + "value": v, + }) + suite.NoError(err) + + suite.T().Run(v, func(t *testing.T) { + assert.Error(t, json.Unmarshal(data, &typeMetricPrefixTestStruct{})) + }) + } +} + +func (suite *TypeMetricPrefixTestSuite) TestUnmarshalOk() { + testStruct := &typeMetricPrefixTestStruct{} + suite.NoError(json.Unmarshal([]byte(`{"value": "mtg"}`), testStruct)) + suite.Equal("mtg", testStruct.Value.Get("lalala")) +} + +func (suite *TypeMetricPrefixTestSuite) TestMarshalOk() { + testStruct := &typeMetricPrefixTestStruct{ + Value: config2.TypeMetricPrefix{ + Value: "mtg", + }, + } + + data, err := json.Marshal(testStruct) + suite.NoError(err) + suite.JSONEq(`{"value": "mtg"}`, string(data)) +} + +func (suite *TypeMetricPrefixTestSuite) TestGet() { + value := config2.TypeMetricPrefix{} + suite.Equal("lalala", value.Get("lalala")) + + value.Value = "mtg" + suite.Equal("mtg", value.Get("lalala")) +} + +func TestTypeMetricPrefix(t *testing.T) { + t.Parallel() + suite.Run(t, &TypeMetricPrefixTestSuite{}) +} diff --git a/internal/config2/type_port.go b/internal/config2/type_port.go new file mode 100644 index 0000000..3a307d9 --- /dev/null +++ b/internal/config2/type_port.go @@ -0,0 +1,45 @@ +package config2 + +import ( + "fmt" + "strconv" +) + +type TypePort struct { + Value uint16 +} + +func (t *TypePort) Set(value string) error { + portValue, err := strconv.ParseUint(value, 10, 16) + if err != nil { + return fmt.Errorf("incorrect port number (%v): %w", value, err) + } + + if portValue == 0 { + return fmt.Errorf("incorrect port number (%s)", value) + } + + t.Value = uint16(portValue) + + return nil +} + +func (t TypePort) Get(defaultValue uint16) uint16 { + if t.Value == 0 { + return defaultValue + } + + return t.Value +} + +func (t *TypePort) UnmarshalJSON(data []byte) error { + return t.Set(string(data)) +} + +func (t TypePort) MarshalJSON() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypePort) String() string { + return strconv.Itoa(int(t.Value)) +} diff --git a/internal/config2/type_port_test.go b/internal/config2/type_port_test.go new file mode 100644 index 0000000..544a2e9 --- /dev/null +++ b/internal/config2/type_port_test.go @@ -0,0 +1,71 @@ +package config2_test + +import ( + "encoding/json" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typePortTestStruct struct { + Value config2.TypePort `json:"value"` +} + +type TypePortTestSuite struct { + suite.Suite +} + +func (suite *TypePortTestSuite) TestUnmarshalFail() { + testData := []string{ + "", + "port", + "0", + "-1", + "1.5", + "70000", + } + + for _, v := range testData { + data, err := json.Marshal(map[string]string{ + "value": v, + }) + suite.NoError(err) + + suite.T().Run(v, func(t *testing.T) { + assert.Error(t, json.Unmarshal(data, &typePortTestStruct{})) + }) + } +} + +func (suite *TypePortTestSuite) TestUnmarshalOk() { + testStruct := &typePortTestStruct{} + suite.NoError(json.Unmarshal([]byte(`{"value": 5}`), testStruct)) + suite.EqualValues(5, testStruct.Value.Value) +} + +func (suite *TypePortTestSuite) TestMarshalOk() { + testStruct := &typePortTestStruct{ + Value: config2.TypePort{ + Value: 10, + }, + } + + data, err := json.Marshal(testStruct) + suite.NoError(err) + suite.JSONEq(`{"value":10}`, string(data)) +} + +func (suite *TypePortTestSuite) TestGet() { + value := config2.TypePort{} + suite.EqualValues(10, value.Get(10)) + + value.Value = 100 + suite.EqualValues(100, value.Get(10)) +} + +func TestTypePort(t *testing.T) { + t.Parallel() + suite.Run(t, &TypePortTestSuite{}) +} diff --git a/internal/config2/type_prefer_ip.go b/internal/config2/type_prefer_ip.go new file mode 100644 index 0000000..3370a3a --- /dev/null +++ b/internal/config2/type_prefer_ip.go @@ -0,0 +1,62 @@ +package config2 + +import ( + "fmt" + "strings" +) + +const ( + // TypePreferIPPreferIPv4 states that you prefer to use IPv4 addresses + // but IPv6 is also possible. + TypePreferIPPreferIPv4 = "prefer-ipv4" + + // TypePreferIPPreferIPv6 states that you prefer to use IPv6 addresses + // but IPv4 is also possible. + TypePreferIPPreferIPv6 = "prefer-ipv6" + + // TypePreferOnlyIPv4 states that you prefer to use IPv4 addresses + // only. + TypePreferOnlyIPv4 = "only-ipv4" + + // TypePreferOnlyIPv6 states that you prefer to use IPv6 addresses + // only. + TypePreferOnlyIPv6 = "only-ipv6" +) + +type TypePreferIP struct { + Value string +} + +func (t *TypePreferIP) Set(value string) error { + value = strings.ToLower(value) + + switch value { + case TypePreferIPPreferIPv4, TypePreferIPPreferIPv6, + TypePreferOnlyIPv4, TypePreferOnlyIPv6: + t.Value = value + + return nil + default: + return fmt.Errorf("unsupported ip preference: %s", value) + } +} + +func (t *TypePreferIP) Get(defaultValue string) string { + if t.Value == "" { + return defaultValue + } + + return t.Value +} + +func (t *TypePreferIP) UnmarshalText(data []byte) error { + return t.Set(string(data)) +} + +func (t TypePreferIP) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypePreferIP) String() string { + return t.Value +} diff --git a/internal/config2/type_prefer_ip_test.go b/internal/config2/type_prefer_ip_test.go new file mode 100644 index 0000000..420ac13 --- /dev/null +++ b/internal/config2/type_prefer_ip_test.go @@ -0,0 +1,113 @@ +package config2_test + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typePreferIPTestStruct struct { + Value config2.TypePreferIP `json:"value"` +} + +type TypePreferIPTestSuite struct { + suite.Suite +} + +func (suite *TypePreferIPTestSuite) TestUnmarshalFail() { + testData := []string{ + "", + "prefer", + "preferipv4", + config2.TypePreferIPPreferIPv4 + "_", + } + + for _, v := range testData { + data, err := json.Marshal(map[string]string{ + "value": v, + }) + suite.NoError(err) + + suite.T().Run(v, func(t *testing.T) { + assert.Error(t, json.Unmarshal(data, &typePreferIPTestStruct{})) + }) + } +} + +func (suite *TypePreferIPTestSuite) TestUnmarshalOk() { + testData := []string{ + config2.TypePreferIPPreferIPv4, + config2.TypePreferIPPreferIPv6, + config2.TypePreferOnlyIPv4, + config2.TypePreferOnlyIPv6, + strings.ToTitle(config2.TypePreferOnlyIPv4), + strings.ToTitle(config2.TypePreferOnlyIPv6), + strings.ToTitle(config2.TypePreferIPPreferIPv4), + strings.ToTitle(config2.TypePreferIPPreferIPv6), + } + + for _, v := range testData { + value := v + + data, err := json.Marshal(map[string]string{ + "value": v, + }) + suite.NoError(err) + + suite.T().Run(v, func(t *testing.T) { + testStruct := &typePreferIPTestStruct{} + assert.NoError(t, json.Unmarshal(data, testStruct)) + assert.Equal(t, strings.ToLower(value), testStruct.Value.Value) + }) + } +} + +func (suite *TypePreferIPTestSuite) TestMarshalOk() { + testData := []string{ + config2.TypePreferIPPreferIPv4, + config2.TypePreferIPPreferIPv6, + config2.TypePreferOnlyIPv4, + config2.TypePreferOnlyIPv6, + } + + for _, v := range testData { + value := v + + suite.T().Run(v, func(t *testing.T) { + testStruct := &typePreferIPTestStruct{ + Value: config2.TypePreferIP{ + Value: value, + }, + } + + encodedJSON, err := json.Marshal(testStruct) + assert.NoError(t, err) + + expectedJSON, err := json.Marshal(map[string]string{ + "value": value, + }) + assert.NoError(t, err) + + assert.JSONEq(t, string(expectedJSON), string(encodedJSON)) + }) + } +} + +func (suite *TypePreferIPTestSuite) TestGet() { + value := config2.TypePreferIP{} + suite.Equal(config2.TypePreferIPPreferIPv4, + value.Get(config2.TypePreferIPPreferIPv4)) + + suite.NoError(value.Set(config2.TypePreferIPPreferIPv6)) + suite.Equal(config2.TypePreferIPPreferIPv6, + value.Get(config2.TypePreferIPPreferIPv4)) +} + +func TestTypePreferIP(t *testing.T) { + t.Parallel() + suite.Run(t, &TypePreferIPTestSuite{}) +} diff --git a/internal/config2/type_proxy_url.go b/internal/config2/type_proxy_url.go new file mode 100644 index 0000000..7336968 --- /dev/null +++ b/internal/config2/type_proxy_url.go @@ -0,0 +1,61 @@ +package config2 + +import ( + "fmt" + "net" + "net/url" +) + +const typeProxyURLDefaultSOCKS5Port = "1080" + +type TypeProxyURL struct { + Value *url.URL +} + +func (t *TypeProxyURL) Set(value string) error { + parsedURL, err := url.Parse(value) + if err != nil { + return fmt.Errorf("Value is not corect URL (%s): %w", value, err) + } + + if parsedURL.Host == "" { + return fmt.Errorf("url has to have a schema: %s", value) + } + + if parsedURL.Scheme != "socks5" { + return fmt.Errorf("unsupported schema: %s", parsedURL.Scheme) + } + + if _, _, err := net.SplitHostPort(parsedURL.Host); err != nil { + parsedURL.Host = net.JoinHostPort(parsedURL.Host, + typeProxyURLDefaultSOCKS5Port) + } + + t.Value = parsedURL + + return nil +} + +func (t *TypeProxyURL) Get(defaultValue *url.URL) *url.URL { + if t.Value == nil { + return defaultValue + } + + return t.Value +} + +func (t *TypeProxyURL) UnmarshalText(data []byte) error { + return t.Set(string(data)) +} + +func (t TypeProxyURL) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypeProxyURL) String() string { + if t.Value == nil { + return "" + } + + return t.Value.String() +} diff --git a/internal/config2/type_proxy_url_test.go b/internal/config2/type_proxy_url_test.go new file mode 100644 index 0000000..f099231 --- /dev/null +++ b/internal/config2/type_proxy_url_test.go @@ -0,0 +1,95 @@ +package config2_test + +import ( + "encoding/json" + "net/url" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeProxyURLTestStruct struct { + Value config2.TypeProxyURL `json:"value"` +} + +type ProxyURLTestSuite struct { + suite.Suite +} + +func (suite *ProxyURLTestSuite) TestUnmarshalFail() { + testData := []string{ + "", + "socks5://", + "://lala", + "/path", + } + + for _, v := range testData { + data, err := json.Marshal(map[string]string{ + "value": v, + }) + suite.NoError(err) + + suite.T().Run(v, func(t *testing.T) { + assert.Error(t, json.Unmarshal(data, &typeProxyURLTestStruct{})) + }) + } +} + +func (suite *ProxyURLTestSuite) TestUnmarshalOk() { + testData := map[string]string{ + "socks5://127.0.0.1/?open_threshold=1": "socks5://127.0.0.1:1080/?open_threshold=1", + "socks5://127.0.0.1:80": "socks5://127.0.0.1:80", + } + + for k, v := range testData { + value := v + + data, err := json.Marshal(map[string]string{ + "value": k, + }) + suite.NoError(err) + + suite.T().Run(k, func(t *testing.T) { + testStruct := &typeProxyURLTestStruct{} + assert.NoError(t, json.Unmarshal(data, testStruct)) + + parsed, _ := url.Parse(value) + + assert.Equal(t, parsed.Scheme, testStruct.Value.Get(nil).Scheme) + assert.Equal(t, parsed.Host, testStruct.Value.Get(nil).Host) + assert.Equal(t, parsed.RawQuery, testStruct.Value.Get(nil).RawQuery) + assert.Equal(t, parsed.Path, testStruct.Value.Get(nil).Path) + }) + } +} + +func (suite *ProxyURLTestSuite) TestMarshalOk() { + parsed, _ := url.Parse("socks5://127.0.0.1:1080?open_threshold=1") + testStruct := &typeProxyURLTestStruct{ + Value: config2.TypeProxyURL{ + Value: parsed, + }, + } + + encodedJSON, err := json.Marshal(testStruct) + suite.NoError(err) + suite.JSONEq(`{"value": "socks5://127.0.0.1:1080?open_threshold=1"}`, string(encodedJSON)) +} + +func (suite *ProxyURLTestSuite) TestGet() { + emptyURL := &url.URL{} + + value := config2.TypeProxyURL{} + suite.Equal(emptyURL, value.Get(emptyURL)) + + value.Value = &url.URL{} + suite.Equal(value.Value, value.Get(emptyURL)) +} + +func TestTypeProxyURL(t *testing.T) { + t.Parallel() + suite.Run(t, &ProxyURLTestSuite{}) +} diff --git a/internal/config2/type_statsd_tag_format.go b/internal/config2/type_statsd_tag_format.go new file mode 100644 index 0000000..fb4f267 --- /dev/null +++ b/internal/config2/type_statsd_tag_format.go @@ -0,0 +1,58 @@ +package config2 + +import ( + "fmt" + "strings" +) + +const ( + // TypeStatsdTagFormatInfluxdb defines a tag format compatible with + // InfluxDB. + TypeStatsdTagFormatInfluxdb = "influxdb" + + // TypeStatsdTagFormatDatadog defines a tag format compatible with + // DataDog. + TypeStatsdTagFormatDatadog = "datadog" + + // TypeStatsdTagFormatGraphite defines a tag format compatible with + // Graphite. + TypeStatsdTagFormatGraphite = "graphite" +) + +type TypeStatsdTagFormat struct { + Value string +} + +func (t *TypeStatsdTagFormat) Set(value string) error { + lowercasedValue := strings.ToLower(value) + + switch lowercasedValue { + case TypeStatsdTagFormatDatadog, TypeStatsdTagFormatInfluxdb, + TypeStatsdTagFormatGraphite: + t.Value = lowercasedValue + + return nil + default: + return fmt.Errorf("unknown tag format %s", value) + } +} + +func (t TypeStatsdTagFormat) Get(defaultValue string) string { + if t.Value == "" { + return defaultValue + } + + return t.Value +} + +func (t *TypeStatsdTagFormat) UnmarshalText(data []byte) error { + return t.Set(string(data)) +} + +func (t *TypeStatsdTagFormat) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t *TypeStatsdTagFormat) String() string { + return t.Value +} diff --git a/internal/config2/type_statsd_tag_format_test.go b/internal/config2/type_statsd_tag_format_test.go new file mode 100644 index 0000000..fb75b26 --- /dev/null +++ b/internal/config2/type_statsd_tag_format_test.go @@ -0,0 +1,108 @@ +package config2_test + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeStatsdTagFormatTestStruct struct { + Value config2.TypeStatsdTagFormat `json:"value"` +} + +type StatsdTagFormatTestSuite struct { + suite.Suite +} + +func (suite *StatsdTagFormatTestSuite) TestUnmarshalFail() { + testData := []string{ + "", + "dogdog", + } + + for _, v := range testData { + data, err := json.Marshal(map[string]string{ + "value": v, + }) + suite.NoError(err) + + suite.T().Run(v, func(t *testing.T) { + assert.Error(t, json.Unmarshal(data, &typeStatsdTagFormatTestStruct{})) + }) + } +} + +func (suite *StatsdTagFormatTestSuite) TestUnmarshalOk() { + testData := []string{ + config2.TypeStatsdTagFormatInfluxdb, + config2.TypeStatsdTagFormatGraphite, + config2.TypeStatsdTagFormatDatadog, + strings.ToUpper(config2.TypeStatsdTagFormatInfluxdb), + strings.ToUpper(config2.TypeStatsdTagFormatGraphite), + strings.ToUpper(config2.TypeStatsdTagFormatDatadog), + } + + for _, v := range testData { + value := v + + data, err := json.Marshal(map[string]string{ + "value": v, + }) + suite.NoError(err) + + suite.T().Run(v, func(t *testing.T) { + testStruct := &typeStatsdTagFormatTestStruct{} + assert.NoError(t, json.Unmarshal(data, testStruct)) + assert.Equal(t, strings.ToLower(value), testStruct.Value.Value) + }) + } +} + +func (suite *StatsdTagFormatTestSuite) TestMarshalOk() { + testData := []string{ + config2.TypeStatsdTagFormatInfluxdb, + config2.TypeStatsdTagFormatGraphite, + config2.TypeStatsdTagFormatDatadog, + } + + for _, v := range testData { + value := v + + suite.T().Run(v, func(t *testing.T) { + testStruct := &typeStatsdTagFormatTestStruct{ + Value: config2.TypeStatsdTagFormat{ + Value: value, + }, + } + + encodedJSON, err := json.Marshal(testStruct) + assert.NoError(t, err) + + expectedJSON, err := json.Marshal(map[string]string{ + "value": value, + }) + assert.NoError(t, err) + + assert.JSONEq(t, string(expectedJSON), string(encodedJSON)) + }) + } +} + +func (suite *StatsdTagFormatTestSuite) TestGet() { + value := config2.TypeStatsdTagFormat{} + suite.Equal(config2.TypeStatsdTagFormatDatadog, + value.Get(config2.TypeStatsdTagFormatDatadog)) + + suite.NoError(value.Set(config2.TypeStatsdTagFormatInfluxdb)) + suite.Equal(config2.TypeStatsdTagFormatInfluxdb, + value.Get(config2.TypeStatsdTagFormatDatadog)) +} + +func TestTypeStatsdTagFormat(t *testing.T) { + t.Parallel() + suite.Run(t, &StatsdTagFormatTestSuite{}) +}