Skip to content

Commit

Permalink
feat: add common parsable types (#37)
Browse files Browse the repository at this point in the history
  • Loading branch information
ajatprabha authored Feb 26, 2024
1 parent 72aee65 commit dbe8597
Show file tree
Hide file tree
Showing 8 changed files with 455 additions and 0 deletions.
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
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 {
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

0 comments on commit dbe8597

Please sign in to comment.