Skip to content

Commit

Permalink
Initial commit for v2.
Browse files Browse the repository at this point in the history
  • Loading branch information
s-yata committed May 11, 2017
1 parent adcff2a commit 09787f8
Show file tree
Hide file tree
Showing 7 changed files with 472 additions and 0 deletions.
208 changes: 208 additions & 0 deletions v2/address.go
@@ -0,0 +1,208 @@
package grnci

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

// Address represents a parsed address.
// The expected address format is
// [scheme://][username[:password]@]host[:port][path][?query][#fragment].
type Address struct {
Raw string
Scheme string
Username string
Password string
Host string
Port int
Path string
Query string
Fragment string
}

// String reassembles the address fields except Raw into an address string.
func (a *Address) String() string {
var url string
if a.Scheme != "" {
url += a.Scheme + "://"
}
if a.Password != "" {
url += a.Username + ":" + a.Password + "@"
} else if a.Username != "" {
url += a.Username + "@"
}
url += a.Host
if a.Port != 0 {
url += ":" + strconv.Itoa(a.Port)
}
url += a.Path
if a.Query != "" {
url += "?" + a.Query
}
if a.Fragment != "" {
url += "#" + a.Fragment
}
return url
}

const (
gqtpScheme = "gqtp"
gqtpDefaultHost = "localhost"
gqtpDefaultPort = 10043
)

// fillGQTP checks fields and fills missing fields in a GQTP address.
func (a *Address) fillGQTP() error {
if a.Username != "" {
return fmt.Errorf("invalid username: raw = %s", a.Raw)
}
if a.Password != "" {
return fmt.Errorf("invalid password: raw = %s", a.Raw)
}
if a.Host == "" {
a.Host = gqtpDefaultHost
}
if a.Port == 0 {
a.Port = gqtpDefaultPort
}
if a.Path != "" {
return fmt.Errorf("invalid path: raw = %s", a.Raw)
}
if a.Query != "" {
return fmt.Errorf("invalid query: raw = %s", a.Raw)
}
if a.Fragment != "" {
return fmt.Errorf("invalid fragment: raw = %s", a.Raw)
}
return nil
}

const (
httpScheme = "http"
httpsScheme = "https"
httpDefaultHost = "localhost"
httpDefaultPort = 10041
httpDefaultPath = "/d/"
)

// fillHTTP checks fields and fills missing fields in an HTTP address.
func (a *Address) fillHTTP() error {
if a.Host == "" {
a.Host = httpDefaultHost
}
if a.Port == 0 {
a.Port = httpDefaultPort
}
if a.Path == "" {
a.Path = httpDefaultPath
}
if a.Query != "" {
return fmt.Errorf("invalid query: raw = %s", a.Raw)
}
if a.Fragment != "" {
return fmt.Errorf("invalid fragment: raw = %s", a.Raw)
}
return nil
}

const (
defaultScheme = gqtpScheme
)

// fill checks fields and fills missing fields.
func (a *Address) fill() error {
if a.Scheme == "" {
a.Scheme = defaultScheme
} else {
a.Scheme = strings.ToLower(a.Scheme)
}
switch a.Scheme {
case gqtpScheme:
if err := a.fillGQTP(); err != nil {
return err
}
case httpScheme, httpsScheme:
if err := a.fillHTTP(); err != nil {
return err
}
default:
return fmt.Errorf("invalid scheme: raw = %s", a.Raw)
}
return nil
}

// ParseAddress parses an address.
// The expected address format is
// [scheme://][username[:password]@]host[:port][path][?query][#fragment].
func ParseAddress(s string) (*Address, error) {
a := &Address{Raw: s}
if i := strings.IndexByte(s, '#'); i != -1 {
a.Fragment = s[i+1:]
s = s[:i]
}
if i := strings.IndexByte(s, '?'); i != -1 {
a.Query = s[i+1:]
s = s[:i]
}
if i := strings.Index(s, "://"); i != -1 {
a.Scheme = s[:i]
s = s[i+len("://"):]
}
if i := strings.IndexByte(s, '/'); i != -1 {
a.Path = s[i:]
s = s[:i]
}
if i := strings.IndexByte(s, '@'); i != -1 {
auth := s[:i]
if j := strings.IndexByte(auth, ':'); j != -1 {
a.Username = auth[:j]
a.Password = auth[j+1:]
} else {
a.Username = auth
a.Password = ""
}
s = s[i+1:]
}
if s == "" {
return a, nil
}

portStr := ""
if s[0] == '[' {
i := strings.IndexByte(s, ']')
if i == -1 {
return nil, fmt.Errorf("missing ']': s = %s", s)
}
a.Host = s[:i+1]
rest := s[i+1:]
if rest == "" {
return a, nil
}
if rest[0] != ':' {
return nil, fmt.Errorf("missing ':' after ']': s = %s", s)
}
portStr = rest[1:]
} else {
i := strings.LastIndexByte(s, ':')
if i == -1 {
a.Host = s
return a, nil
}
a.Host = s[:i]
portStr = s[i+1:]
}
if portStr != "" {
port, err := net.LookupPort("tcp", portStr)
if err != nil {
return nil, fmt.Errorf("net.LookupPort failed: %v", err)
}
a.Port = port
}

if err := a.fill(); err != nil {
return nil, err
}
return a, nil
}
53 changes: 53 additions & 0 deletions v2/argument.go
@@ -0,0 +1,53 @@
package grnci

import (
"errors"
"fmt"
)

// Argument represents a named argument of a request.
type Argument struct {
Key string
Value string
}

// isDigit checks if c is a digit.
func isDigit(c byte) bool {
return c >= '0' && c <= '9'
}

// isAlpha checks if c is an alphabet.
func isAlpha(c byte) bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}

// isAlnum checks if c is a digit or alphabet.
func isAlnum(c byte) bool {
return isDigit(c) || isAlpha(c)
}

// checkArgumentKey checks if s is valid as an argument key.
func checkArgumentKey(s string) error {
if s == "" {
return errors.New("invalid format: s = ")
}
for i := 0; i < len(s); i++ {
if isAlnum(s[i]) {
continue
}
switch s[i] {
case '#', '@', '-', '_':
default:
return fmt.Errorf("invalid format: s = %s", s)
}
}
return nil
}

// Check checks if arg is valid.
func (arg *Argument) Check() error {
if err := checkArgumentKey(arg.Key); err != nil {
return fmt.Errorf("checkArgumentKey failed: %v", err)
}
return nil
}
78 changes: 78 additions & 0 deletions v2/client.go
@@ -0,0 +1,78 @@
package grnci

import (
"fmt"
"net"
"net/http"
)

// iClient is a Groonga client interface.
type iClient interface {
Close() error
Query(*Request) (*Response, error)
}

// Client is a Groonga client.
type Client struct {
iClient
}

// NewClient returns a new Client using an existing client.
func NewClient(c iClient) *Client {
return &Client{iClient: c}
}

// NewClientByAddress returns a new Client to access a Groonga server.
func NewClientByAddress(addr string) (*Client, error) {
a, err := ParseAddress(addr)
if err != nil {
return nil, err
}
switch a.Scheme {
case gqtpScheme:
c, err := dialGQTP(a)
if err != nil {
return nil, err
}
return NewClient(c), nil
case httpScheme, httpsScheme:
c, err := newHTTPClient(a, nil)
if err != nil {
return nil, err
}
return NewClient(c), nil
default:
return nil, fmt.Errorf("invalid scheme: raw = %s", a.Raw)
}
}

// NewGQTPClient returns a new Client using an existing connection.
func NewGQTPClient(conn net.Conn) (*Client, error) {
c, err := newGQTPClient(conn)
if err != nil {
return nil, err
}
return NewClient(c), nil
}

// NewHTTPClient returns a new Client using an existing HTTP client.
// If client is nil, NewHTTPClient uses http.DefaultClient.
func NewHTTPClient(addr string, client *http.Client) (*Client, error) {
a, err := ParseAddress(addr)
if err != nil {
return nil, err
}
if client == nil {
client = http.DefaultClient
}
switch a.Scheme {
case httpScheme, httpsScheme:
default:
return nil, fmt.Errorf("invalid scheme: raw = %s", a.Raw)
}
c, err := newHTTPClient(a, client)
if err != nil {
return nil, err
}
return NewClient(c), nil
}
39 changes: 39 additions & 0 deletions v2/gqtp.go
@@ -0,0 +1,39 @@
package grnci

import (
"fmt"
"net"
)

// gqtpClient is a GQTP client.
type gqtpClient struct {
conn net.Conn
}

// dialGQTP returns a new gqtpClient connected to a GQTP server.
func dialGQTP(a *Address) (*gqtpClient, error) {
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", a.Host, a.Port))
if err != nil {
return nil, err
}
return newGQTPClient(conn)
}

// newGQTPClient returns a new gqtpClient using an existing connection.
func newGQTPClient(conn net.Conn) (*gqtpClient, error) {
return &gqtpClient{conn: conn}, nil
}

// Close closes the connection.
func (c *gqtpClient) Close() error {
if err := c.conn.Close(); err != nil {
return err
}
return nil
}

// Query sends a request and receives a response.
func (c *gqtpClient) Query(req *Request) (*Response, error) {
// TODO
return nil, nil
}

0 comments on commit 09787f8

Please sign in to comment.