Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
472 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
Oops, something went wrong.