v0.5.0-rc4: initial support for ovh, provider generator implementation update, replaced all interface{} to any

This commit is contained in:
yusing 2024-09-17 12:06:58 +08:00
parent 82f06374f7
commit 21fcceb391
21 changed files with 286 additions and 64 deletions

View file

@ -114,6 +114,8 @@ See [providers.example.yml](providers.example.yml) for examples
- Cert "renewal" is actually obtaining a new cert instead of renewing the existing one
- `autocert` config is not hot-reloadable
[🔼Back to top](#table-of-content)
## Build it yourself

View file

@ -1,11 +1,13 @@
# Supported DNS Providers
<!-- TOC -->
- [Cloudflare](#cloudflare)
- [CloudDNS](#clouddns)
- [DuckDNS](#duckdns)
- [Implement other DNS providers](#implement-other-dns-providers)
<!-- /TOC -->
- [Supported DNS Providers](#supported-dns-providers)
- [Cloudflare](#cloudflare)
- [CloudDNS](#clouddns)
- [DuckDNS](#duckdns)
- [OVHCloud](#ovhcloud)
- [Implement other DNS providers](#implement-other-dns-providers)
## Cloudflare
@ -23,10 +25,29 @@ Follow [this guide](https://cloudkul.com/blog/automcatic-renew-and-generate-ssl-
## DuckDNS
`token`: DuckDNS Token
- `token`: DuckDNS Token
Tested by [earvingad](https://github.com/earvingad)
## OVHCloud
_Note, `application_key` and `oauth2_config` **CANNOT** be used together_
- `api_endpoint`: Endpoint URL, or one of
- `ovh-eu`,
- `ovh-ca`,
- `ovh-us`,
- `kimsufi-eu`,
- `kimsufi-ca`,
- `soyoustart-eu`,
- `soyoustart-ca`
- `application_secret`
- `application_key`
- `consumer_key`
- `oauth2_config`: Client ID and Client Secret
- `client_id`
- `client_secret`
## Implement other DNS providers
See [add_dns_provider.md](docs/add_dns_provider.md)

View file

@ -1,4 +1,4 @@
go 1.22
go 1.22.0
toolchain go1.23.1

View file

@ -37,7 +37,7 @@
"title": "DNS Challenge Provider",
"default": "local",
"type": "string",
"enum": ["local", "cloudflare", "clouddns", "duckdns"]
"enum": ["local", "cloudflare", "clouddns", "duckdns", "ovh"]
},
"options": {
"title": "Provider specific options",
@ -135,6 +135,82 @@
}
}
}
},
{
"if": {
"properties": {
"provider": {
"const": "ovh"
}
}
},
"then": {
"properties": {
"options": {
"required": ["application_secret", "consumer_key"],
"additionalProperties": false,
"oneOf": [
{
"required": ["application_key"]
},
{
"required": ["oauth2_config"]
}
],
"properties": {
"api_endpoint": {
"description": "OVH API endpoint",
"default": "ovh-eu",
"anyOf": [
{
"enum": [
"ovh-eu",
"ovh-ca",
"ovh-us",
"kimsufi-eu",
"kimsufi-ca",
"soyoustart-eu",
"soyoustart-ca"
]
},
{
"type": "string",
"format": "uri"
}
]
},
"application_secret": {
"description": "OVH Application Secret",
"type": "string"
},
"consumer_key": {
"description": "OVH Consumer Key",
"type": "string"
},
"application_key": {
"description": "OVH Application Key",
"type": "string"
},
"oauth2_config": {
"description": "OVH OAuth2 config",
"type": "object",
"additionalProperties": false,
"properties": {
"client_id": {
"description": "OVH Client ID",
"type": "string"
},
"client_secret": {
"description": "OVH Client Secret",
"type": "string"
}
},
"required": ["client_id", "client_secret"]
}
}
}
}
}
}
]
},

View file

@ -10,7 +10,7 @@ import (
)
func Stats(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
stats := map[string]interface{}{
stats := map[string]any{
"proxies": cfg.Statistics(),
"uptime": utils.FormatDuration(server.GetProxyServer().Uptime()),
}

View file

@ -4,6 +4,7 @@ import (
"github.com/go-acme/lego/v4/providers/dns/clouddns"
"github.com/go-acme/lego/v4/providers/dns/cloudflare"
"github.com/go-acme/lego/v4/providers/dns/duckdns"
"github.com/go-acme/lego/v4/providers/dns/ovh"
"github.com/sirupsen/logrus"
)
@ -19,6 +20,7 @@ const (
ProviderCloudflare = "cloudflare"
ProviderClouddns = "clouddns"
ProviderDuckdns = "duckdns"
ProviderOVH = "ovh"
)
var providersGenMap = map[string]ProviderGenerator{
@ -26,6 +28,7 @@ var providersGenMap = map[string]ProviderGenerator{
ProviderCloudflare: providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig),
ProviderClouddns: providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig),
ProviderDuckdns: providerGenerator(duckdns.NewDefaultConfig, duckdns.NewDNSProviderConfig),
ProviderOVH: providerGenerator(ovh.NewDefaultConfig, ovh.NewDNSProviderConfig),
}
var logger = logrus.WithField("module", "autocert")

View file

@ -272,23 +272,13 @@ func getCertExpiries(cert *tls.Certificate) (CertExpiries, E.NestedError) {
return r, E.Nil()
}
func setOptions[T interface{}](cfg *T, opt M.AutocertProviderOpt) E.NestedError {
for k, v := range opt {
err := U.SetFieldFromSnake(cfg, k, v)
if err.HasError() {
return E.Failure("set autocert option").Subject(k).With(err)
}
}
return E.Nil()
}
func providerGenerator[CT any, PT challenge.Provider](
defaultCfg func() *CT,
newProvider func(*CT) (PT, error),
) ProviderGenerator {
return func(opt M.AutocertProviderOpt) (challenge.Provider, E.NestedError) {
cfg := defaultCfg()
err := setOptions(cfg, opt)
err := U.Deserialize(opt, cfg)
if err.HasError() {
return nil, err
}

View file

@ -0,0 +1,49 @@
package provider_test
import (
"testing"
"github.com/go-acme/lego/v4/providers/dns/ovh"
. "github.com/yusing/go-proxy/utils"
"gopkg.in/yaml.v3"
)
// type Config struct {
// APIEndpoint string
// ApplicationKey string
// ApplicationSecret string
// ConsumerKey string
// OAuth2Config *OAuth2Config
// PropagationTimeout time.Duration
// PollingInterval time.Duration
// TTL int
// HTTPClient *http.Client
// }
func TestOVH(t *testing.T) {
cfg := &ovh.Config{}
testYaml := `
api_endpoint: https://eu.api.ovh.com
application_key: <application_key>
application_secret: <application_secret>
consumer_key: <consumer_key>
oauth2_config:
client_id: <client_id>
client_secret: <client_secret>
`
cfgExpected := &ovh.Config{
APIEndpoint: "https://eu.api.ovh.com",
ApplicationKey: "<application_key>",
ApplicationSecret: "<application_secret>",
ConsumerKey: "<consumer_key>",
OAuth2Config: &ovh.OAuth2Config{ClientID: "<client_id>", ClientSecret: "<client_secret>"},
}
testYaml = testYaml[1:] // remove first \n
opt := make(map[string]any)
ExpectNoError(t, yaml.Unmarshal([]byte(testYaml), opt))
ExpectNoError(t, Deserialize(opt, cfg))
ExpectEqual(t, cfg, cfgExpected)
}

View file

@ -95,7 +95,7 @@ func (cfg *Config) RoutesByAlias() map[string]U.SerializedObject {
prName := p.GetName()
p.GetCurrentRoutes().EachKV(func(a string, r R.Route) {
obj, err := U.Serialize(r)
if err != nil {
if err.HasError() {
cfg.l.Error(err)
return
}
@ -114,13 +114,13 @@ func (cfg *Config) RoutesByAlias() map[string]U.SerializedObject {
return routes
}
func (cfg *Config) Statistics() map[string]interface{} {
func (cfg *Config) Statistics() map[string]any {
nTotalStreams := 0
nTotalRPs := 0
providerStats := make(map[string]interface{})
providerStats := make(map[string]any)
cfg.proxyProviders.Each(func(p *PR.Provider) {
stats := make(map[string]interface{})
stats := make(map[string]any)
nStreams := 0
nRPs := 0
p.GetCurrentRoutes().EachKV(func(a string, r R.Route) {
@ -141,7 +141,7 @@ func (cfg *Config) Statistics() map[string]interface{} {
providerStats[p.GetName()] = stats
})
return map[string]interface{}{
return map[string]any{
"num_total_streams": nTotalStreams,
"num_total_reverse_proxies": nTotalRPs,
"providers": providerStats,

View file

@ -9,7 +9,7 @@ type (
Icon string
Category string
Description string
WidgetConfig map[string]interface{}
WidgetConfig map[string]any
}
)

View file

@ -19,7 +19,7 @@ func TestHomePageLabel(t *testing.T) {
field := "ip"
v := "bar"
pl, err := ParseLabel(makeLabel(NSHomePage, alias, field), v)
ExpectErrNil(t, err)
ExpectNoError(t, err)
if pl.Target != alias {
t.Errorf("Expected alias=%s, got %s", alias, pl.Target)
}
@ -34,7 +34,7 @@ func TestHomePageLabel(t *testing.T) {
func TestStringProxyLabel(t *testing.T) {
v := "bar"
pl, err := ParseLabel(makeLabel(NSProxy, "foo", "ip"), v)
ExpectErrNil(t, err)
ExpectNoError(t, err)
ExpectEqual(t, pl.Value, v)
}
@ -52,7 +52,7 @@ func TestBoolProxyLabelValid(t *testing.T) {
for k, v := range tests {
pl, err := ParseLabel(makeLabel(NSProxy, "foo", "no_tls_verify"), k)
ExpectErrNil(t, err)
ExpectNoError(t, err)
ExpectEqual(t, pl.Value, v)
}
}
@ -78,7 +78,7 @@ X-Custom-Header2: boo`
}
pl, err := ParseLabel(makeLabel(NSProxy, "foo", "set_headers"), v)
ExpectErrNil(t, err)
ExpectNoError(t, err)
hGot := ExpectType[map[string]string](t, pl.Value)
if hGot != nil && !reflect.DeepEqual(h, hGot) {
t.Errorf("Expected %v, got %v", h, hGot)
@ -109,7 +109,7 @@ func TestHideHeadersProxyLabel(t *testing.T) {
`
v = strings.TrimPrefix(v, "\n")
pl, err := ParseLabel(makeLabel(NSProxy, "foo", "hide_headers"), v)
ExpectErrNil(t, err)
ExpectNoError(t, err)
sGot := ExpectType[[]string](t, pl.Value)
sWant := []string{"X-Custom-Header1", "X-Custom-Header2", "X-Custom-Header3"}
if sGot != nil {
@ -120,7 +120,7 @@ func TestHideHeadersProxyLabel(t *testing.T) {
func TestCommaSepProxyLabelSingle(t *testing.T) {
v := "a"
pl, err := ParseLabel("proxy.aliases", v)
ExpectErrNil(t, err)
ExpectNoError(t, err)
sGot := ExpectType[[]string](t, pl.Value)
sWant := []string{"a"}
if sGot != nil {
@ -132,7 +132,7 @@ func TestCommaSepProxyLabelSingle(t *testing.T) {
func TestCommaSepProxyLabelMulti(t *testing.T) {
v := "X-Custom-Header1, X-Custom-Header2,X-Custom-Header3"
pl, err := ParseLabel("proxy.aliases", v)
ExpectErrNil(t, err)
ExpectNoError(t, err)
sGot := ExpectType[[]string](t, pl.Value)
sWant := []string{"X-Custom-Header1", "X-Custom-Header2", "X-Custom-Header3"}
if sGot != nil {

View file

@ -1,8 +1,6 @@
module github.com/yusing/go-proxy
go 1.22
toolchain go1.23.1
go 1.22.0
require (
github.com/docker/cli v27.2.1+incompatible
@ -36,6 +34,7 @@ require (
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/ovh/go-ovh v1.6.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect
go.opentelemetry.io/otel v1.30.0 // indirect
@ -45,10 +44,12 @@ require (
go.opentelemetry.io/otel/trace v1.30.0 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/oauth2 v0.23.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/text v0.18.0 // indirect
golang.org/x/time v0.6.0 // indirect
golang.org/x/tools v0.25.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gotest.tools/v3 v3.5.1 // indirect
)

View file

@ -45,12 +45,16 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g=
github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
@ -63,6 +67,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/ovh/go-ovh v1.6.0 h1:ixLOwxQdzYDx296sXcgS35TOPEahJkpjMGtzPadCjQI=
github.com/ovh/go-ovh v1.6.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -110,6 +116,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -149,6 +157,8 @@ google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHh
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -9,5 +9,5 @@ type (
Provider string `json:"provider"`
Options AutocertProviderOpt `yaml:",flow" json:"options"`
}
AutocertProviderOpt map[string]string
AutocertProviderOpt map[string]any
)

View file

@ -37,7 +37,6 @@ func NewStreamRoute(entry *P.StreamEntry) (*StreamRoute, E.NestedError) {
base := &StreamRoute{
StreamEntry: entry,
wg: sync.WaitGroup{},
stopCh: make(chan struct{}, 1),
connCh: make(chan any),
}
if entry.Scheme.ListeningScheme.IsTCP() {
@ -57,6 +56,7 @@ func (r *StreamRoute) Start() E.NestedError {
if r.started.Load() {
return E.Invalid("state", "already started")
}
r.stopCh = make(chan struct{}, 1)
r.wg.Wait()
if err := r.Setup(); err != nil {
return E.Failure("setup").With(err)

View file

@ -38,11 +38,11 @@ func (route *TCPRoute) Setup() error {
return nil
}
func (route *TCPRoute) Accept() (interface{}, error) {
func (route *TCPRoute) Accept() (any, error) {
return route.listener.Accept()
}
func (route *TCPRoute) Handle(c interface{}) error {
func (route *TCPRoute) Handle(c any) error {
clientConn := c.(net.Conn)
defer clientConn.Close()

View file

@ -55,7 +55,7 @@ func (route *UDPRoute) Setup() error {
return nil
}
func (route *UDPRoute) Accept() (interface{}, error) {
func (route *UDPRoute) Accept() (any, error) {
in := route.listeningConn
buffer := make([]byte, udpBufferSize)
@ -103,7 +103,7 @@ func (route *UDPRoute) Accept() (interface{}, error) {
return conn, err
}
func (route *UDPRoute) Handle(c interface{}) error {
func (route *UDPRoute) Handle(c any) error {
return c.(*UDPConn).Start()
}

View file

@ -2,25 +2,25 @@ package functional
import "sync"
func ForEachKey[K comparable, V interface{}](obj map[K]V, do func(K)) {
func ForEachKey[K comparable, V any](obj map[K]V, do func(K)) {
for k := range obj {
do(k)
}
}
func ForEachValue[K comparable, V interface{}](obj map[K]V, do func(V)) {
func ForEachValue[K comparable, V any](obj map[K]V, do func(V)) {
for _, v := range obj {
do(v)
}
}
func ForEachKV[K comparable, V interface{}](obj map[K]V, do func(K, V)) {
func ForEachKV[K comparable, V any](obj map[K]V, do func(K, V)) {
for k, v := range obj {
do(k, v)
}
}
func ParallelForEach[T interface{}](obj []T, do func(T)) {
func ParallelForEach[T any](obj []T, do func(T)) {
var wg sync.WaitGroup
wg.Add(len(obj))
for _, v := range obj {
@ -32,7 +32,7 @@ func ParallelForEach[T interface{}](obj []T, do func(T)) {
wg.Wait()
}
func ParallelForEachKey[K comparable, V interface{}](obj map[K]V, do func(K)) {
func ParallelForEachKey[K comparable, V any](obj map[K]V, do func(K)) {
var wg sync.WaitGroup
wg.Add(len(obj))
for k := range obj {
@ -44,7 +44,7 @@ func ParallelForEachKey[K comparable, V interface{}](obj map[K]V, do func(K)) {
wg.Wait()
}
func ParallelForEachValue[K comparable, V interface{}](obj map[K]V, do func(V)) {
func ParallelForEachValue[K comparable, V any](obj map[K]V, do func(V)) {
var wg sync.WaitGroup
wg.Add(len(obj))
for _, v := range obj {
@ -56,7 +56,7 @@ func ParallelForEachValue[K comparable, V interface{}](obj map[K]V, do func(V))
wg.Wait()
}
func ParallelForEachKV[K comparable, V interface{}](obj map[K]V, do func(K, V)) {
func ParallelForEachKV[K comparable, V any](obj map[K]V, do func(K, V)) {
var wg sync.WaitGroup
wg.Add(len(obj))
for k, v := range obj {

View file

@ -9,7 +9,7 @@ import (
E "github.com/yusing/go-proxy/error"
)
type Map[KT comparable, VT interface{}] struct {
type Map[KT comparable, VT any] struct {
m map[KT]VT
defVals map[KT]VT
sync.RWMutex
@ -22,7 +22,7 @@ type Map[KT comparable, VT interface{}] struct {
//
// Return:
// - *Map[KT, VT]: a pointer to the newly created Map.
func NewMap[KT comparable, VT interface{}](dv ...map[KT]VT) *Map[KT, VT] {
func NewMap[KT comparable, VT any](dv ...map[KT]VT) *Map[KT, VT] {
return NewMapFrom(make(map[KT]VT), dv...)
}
@ -36,7 +36,7 @@ func NewMap[KT comparable, VT interface{}](dv ...map[KT]VT) *Map[KT, VT] {
//
// Return:
// - *Map[KT, VT]: a pointer to the newly created Map.
func NewMapOf[M Map[KT, VT], KT comparable, VT interface{}](dv ...map[KT]VT) *Map[KT, VT] {
func NewMapOf[M Map[KT, VT], KT comparable, VT any](dv ...map[KT]VT) *Map[KT, VT] {
return NewMapFrom(make(map[KT]VT), dv...)
}
@ -48,7 +48,7 @@ func NewMapOf[M Map[KT, VT], KT comparable, VT interface{}](dv ...map[KT]VT) *Ma
//
// Return:
// - *Map[KT, VT]: a pointer to the newly created Map.
func NewMapFrom[KT comparable, VT interface{}](from map[KT]VT, dv ...map[KT]VT) *Map[KT, VT] {
func NewMapFrom[KT comparable, VT any](from map[KT]VT, dv ...map[KT]VT) *Map[KT, VT] {
if len(dv) > 0 {
return &Map[KT, VT]{m: from, defVals: dv[0]}
}

View file

@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"reflect"
"strings"
"github.com/santhosh-tekuri/jsonschema"
E "github.com/yusing/go-proxy/error"
@ -12,7 +13,7 @@ import (
)
func ValidateYaml(schema *jsonschema.Schema, data []byte) E.NestedError {
var i interface{}
var i any
err := yaml.Unmarshal(data, &i)
if err != nil {
@ -55,7 +56,7 @@ func TryJsonStringify(o any) string {
return string(b)
}
// Serialize converts the given data into a map[string]interface{} representation.
// Serialize converts the given data into a map[string]any representation.
//
// It uses reflection to inspect the data type and handle different kinds of data.
// For a struct, it extracts the fields using the json tag if present, or the field name if not.
@ -66,9 +67,9 @@ func TryJsonStringify(o any) string {
// - data: The data to be converted into a map.
//
// Returns:
// - result: The resulting map[string]interface{} representation of the data.
// - result: The resulting map[string]any representation of the data.
// - error: An error if the data type is unsupported or if there is an error during conversion.
func Serialize(data interface{}) (SerializedObject, error) {
func Serialize(data any) (SerializedObject, E.NestedError) {
result := make(map[string]any)
// Use reflection to inspect the data type
@ -76,7 +77,7 @@ func Serialize(data interface{}) (SerializedObject, error) {
// Check if the value is valid
if !value.IsValid() {
return nil, fmt.Errorf("invalid data")
return nil, E.Invalid("data", fmt.Sprintf("type: %T", data))
}
// Dereference pointers if necessary
@ -107,7 +108,7 @@ func Serialize(data interface{}) (SerializedObject, error) {
} else if field.Anonymous {
// If the field is an embedded struct, add its fields to the result
fieldMap, err := Serialize(value.Field(i).Interface())
if err != nil {
if err.HasError() {
return nil, err
}
for k, v := range fieldMap {
@ -118,10 +119,72 @@ func Serialize(data interface{}) (SerializedObject, error) {
}
}
default:
return nil, fmt.Errorf("unsupported type: %s", value.Kind())
// return nil, fmt.Errorf("unsupported type: %s", value.Kind())
return nil, E.Unsupported("type", value.Kind())
}
return result, nil
return result, E.Nil()
}
type SerializedObject map[string]any
func Deserialize(src map[string]any, target any) E.NestedError {
// convert data fields to lower no-snake
// convert target fields to lower
// then check if the field of data is in the target
mapping := make(map[string]string)
t := reflect.TypeOf(target).Elem()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
snakeCaseField := strings.ToLower(field.Name)
mapping[snakeCaseField] = field.Name
}
for k, v := range src {
kCleaned := toLowerNoSnake(k)
if fieldName, ok := mapping[kCleaned]; ok {
prop := reflect.ValueOf(target).Elem().FieldByName(fieldName)
propType := prop.Type()
isPtr := prop.Kind() == reflect.Ptr
if prop.CanSet() {
val := reflect.ValueOf(v)
vType := val.Type()
switch {
case isPtr && vType.ConvertibleTo(propType.Elem()):
ptr := reflect.New(propType.Elem())
ptr.Elem().Set(val.Convert(propType.Elem()))
prop.Set(ptr)
case vType.ConvertibleTo(propType):
prop.Set(val.Convert(propType))
case isPtr:
var vSerialized SerializedObject
vSerialized, ok = v.(SerializedObject)
if !ok {
if vType.ConvertibleTo(reflect.TypeFor[SerializedObject]()) {
vSerialized = val.Convert(reflect.TypeFor[SerializedObject]()).Interface().(SerializedObject)
} else {
return E.Failure(fmt.Sprintf("convert %s (%T) to %s", k, v, reflect.TypeFor[SerializedObject]()))
}
}
propNew := reflect.New(propType.Elem())
err := Deserialize(vSerialized, propNew.Interface())
if err.HasError() {
return E.Failure("set field").With(k).With(err)
}
prop.Set(propNew)
default:
return E.Unsupported("field", k).Extraf("type=%s", propType)
}
} else {
return E.Unsupported("field", k).Extraf("type=%s", propType)
}
} else {
return E.Failure("unknown field").With(k)
}
}
return E.Nil()
}
func toLowerNoSnake(s string) string {
return strings.ToLower(strings.ReplaceAll(s, "_", ""))
}
type SerializedObject = map[string]any

View file

@ -7,9 +7,16 @@ import (
E "github.com/yusing/go-proxy/error"
)
func ExpectErrNil(t *testing.T, err E.NestedError) {
func ExpectNoError(t *testing.T, err error) {
t.Helper()
if err.HasError() {
var noError bool
switch t := err.(type) {
case E.NestedError:
noError = t.NoError()
default:
noError = err == nil
}
if !noError {
t.Errorf("expected err=nil, got %s", err.Error())
}
}