Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add common parsable types #37

Merged
merged 5 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions xload/type/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package xloadtype contains commonly used types for working with xload.Loader.
package xloadtype
ajatprabha marked this conversation as resolved.
Show resolved Hide resolved
34 changes: 34 additions & 0 deletions xload/type/endpoint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package xloadtype

import (
"fmt"
"net"
"strconv"
)

// Endpoint represents a network endpoint
// It can be used to represent a target host:port pair.
type Endpoint struct {
ajatprabha marked this conversation as resolved.
Show resolved Hide resolved
Host string
Port int
}

func (e *Endpoint) String() string { return fmt.Sprintf("%s:%d", e.Host, e.Port) }

func (e *Endpoint) Decode(v string) error {
host, port, err := net.SplitHostPort(v)
if err != nil {
return err
}

e.Host = host

p, err := strconv.ParseInt(port, 10, 32)
if err != nil {
return err
}

e.Port = int(p)

return nil
}
75 changes: 75 additions & 0 deletions xload/type/endpoint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package xloadtype

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestEndpoint_Decode(t *testing.T) {
tests := []struct {
name string
in string
want *Endpoint
wantErr assert.ErrorAssertionFunc
}{
{
name: "valid",
in: "localhost:8080",
want: &Endpoint{
Host: "localhost",
Port: 8080,
},
wantErr: assert.NoError,
},
{
name: "invalid",
in: "localhost",
want: &Endpoint{},
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
return assert.EqualError(t, err, "address localhost: missing port in address")
},
},
{
name: "invalid port",
in: "localhost:port",
want: &Endpoint{Host: "localhost"},
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
return assert.EqualError(t, err, `strconv.ParseInt: parsing "port": invalid syntax`)
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := new(Endpoint)
tt.wantErr(t, e.Decode(tt.in))
assert.Equal(t, tt.want, e)
})
}
}

func TestEndpoint_String(t *testing.T) {
tests := []struct {
name string
in *Endpoint
want string
}{
{
name: "valid",
in: &Endpoint{Host: "localhost", Port: 8080},
want: "localhost:8080",
},
{
name: "empty",
in: &Endpoint{},
want: ":0",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, tt.in.String())
})
}
}
48 changes: 48 additions & 0 deletions xload/type/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package xloadtype_test

import (
"context"
"fmt"

"github.com/gojekfarm/xtools/xload"
xloadtype "github.com/gojekfarm/xtools/xload/type"
)

var testValues = map[string]string{
"LISTENER": "[::1]:8080",
"ENDPOINT": "example.com:80",
}

var loader = xload.LoaderFunc(func(ctx context.Context, key string) (string, error) {
return testValues[key], nil
})

func ExampleEndpoint() {
type Server struct {
Endpoint xloadtype.Endpoint `env:"ENDPOINT"`
}

var srv Server
if err := xload.Load(context.Background(), &srv, loader); err != nil {
panic(err)
}

fmt.Println(srv.Endpoint.String())

// Output: example.com:80
}

func ExampleListener() {
type Server struct {
Listener xloadtype.Listener `env:"LISTENER"`
}

var srv Server
if err := xload.Load(context.Background(), &srv, loader); err != nil {
panic(err)
}

fmt.Println(srv.Listener.String())

// Output: [::1]:8080
}
45 changes: 45 additions & 0 deletions xload/type/listener.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package xloadtype

import (
"fmt"
"net"
"strconv"
)

// Listener represents a network listener, say, a tcp or http listener.
type Listener struct {
IP net.IP
Port int
}

func (l *Listener) String() string {
if l.IP == nil {
return fmt.Sprintf(":%d", l.Port)
}

return net.JoinHostPort(l.IP.String(), strconv.Itoa(l.Port))
}

func (l *Listener) Decode(v string) error {
host, port, err := net.SplitHostPort(v)
if err != nil {
return err
}

if host != "" {
l.IP = net.ParseIP(host)

if l.IP == nil {
return net.InvalidAddrError("invalid IP address")
}
}

p, err := strconv.ParseInt(port, 10, 32)
if err != nil {
return err
}

l.Port = int(p)

return err
}
100 changes: 100 additions & 0 deletions xload/type/listener_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package xloadtype

import (
"net"
"testing"

"github.com/stretchr/testify/assert"
)

func TestListener_Decode(t *testing.T) {
tests := []struct {
name string
in string
want *Listener
wantErr assert.ErrorAssertionFunc
}{
{
name: "valid ipv4",
in: "127.0.0.1:8080",
want: &Listener{
IP: net.IPv4(127, 0, 0, 1),
Port: 8080,
},
wantErr: assert.NoError,
},
{
name: "valid ipv6",
in: "[::1]:8080",
want: &Listener{
IP: net.IPv6loopback,
Port: 8080,
},
wantErr: assert.NoError,
},
{
name: "missing port",
in: "localhost",
want: &Listener{},
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
return assert.EqualError(t, err, "address localhost: missing port in address")
},
},
{
name: "invalid port",
in: "127.0.0.1:port",
want: &Listener{IP: net.IPv4(127, 0, 0, 1)},
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
return assert.EqualError(t, err, `strconv.ParseInt: parsing "port": invalid syntax`)
},
},
{
name: "invalid ip",
in: "localhost:8080",
want: &Listener{},
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
return assert.EqualError(t, err, "invalid IP address")
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := new(Listener)
tt.wantErr(t, l.Decode(tt.in))
assert.Equal(t, tt.want, l)
})
}
}

func TestListener_String(t *testing.T) {
tests := []struct {
name string
in *Listener
want string
}{
{
name: "valid ipv4",
in: &Listener{IP: net.IPv4(127, 0, 0, 1), Port: 8080},
want: "127.0.0.1:8080",
},
{
name: "valid ipv6",
in: &Listener{IP: net.IPv6loopback, Port: 8080},
want: "[::1]:8080",
},
{
name: "empty ip",
in: &Listener{
Port: 8080,
},
want: ":8080",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, tt.in.String())
})
}
}
35 changes: 35 additions & 0 deletions xload/type/url.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package xloadtype

import "net/url"

// URL represents a URI reference.
//
// URL is a type alias for url.URL.
// The general form represented is: [scheme:][//[userinfo@]host][/]path[?query][#fragment]
// See https://tools.ietf.org/html/rfc3986
type URL url.URL

func (u *URL) String() string { return (*url.URL)(u).String() }

func (u *URL) Decode(v string) error {
parsed, err := url.Parse(v)
if err != nil {
return err
}

*u = URL(*parsed)

return nil
}

// Endpoint returns the endpoint of the URL.
// The URL host must be in the form of `host:port`.
func (u *URL) Endpoint() (*Endpoint, error) {
e := new(Endpoint)

if err := e.Decode(u.Host); err != nil {
return nil, err
}

return e, nil
}
Loading
Loading