diff --git a/v2/address.go b/v2/address.go new file mode 100644 index 0000000..6ff2d8d --- /dev/null +++ b/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 +} diff --git a/v2/argument.go b/v2/argument.go new file mode 100644 index 0000000..96d9742 --- /dev/null +++ b/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 +} diff --git a/v2/client.go b/v2/client.go new file mode 100644 index 0000000..254bcb5 --- /dev/null +++ b/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 +} diff --git a/v2/gqtp.go b/v2/gqtp.go new file mode 100644 index 0000000..3e846f9 --- /dev/null +++ b/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 +} diff --git a/v2/http.go b/v2/http.go new file mode 100644 index 0000000..e342b31 --- /dev/null +++ b/v2/http.go @@ -0,0 +1,39 @@ +package grnci + +import ( + "fmt" + "net/http" + "net/url" +) + +// httpClient is an HTTP client. +type httpClient struct { + url *url.URL + client *http.Client +} + +// newHTTPClient returns a new httpClient. +func newHTTPClient(a *Address, client *http.Client) (*httpClient, error) { + url, err := url.Parse(a.String()) + if err != nil { + return nil, fmt.Errorf("url.Parse failed: %v", err) + } + if client == nil { + client = http.DefaultClient + } + return &httpClient{ + url: url, + client: client, + }, nil +} + +// Close closes a client. +func (c *httpClient) Close() error { + return nil +} + +// Query sends a request and receives a response. +func (c *httpClient) Query(req *Request) (*Response, error) { + // TODO + return nil, nil +} diff --git a/v2/request.go b/v2/request.go new file mode 100644 index 0000000..dcd2fb4 --- /dev/null +++ b/v2/request.go @@ -0,0 +1,43 @@ +package grnci + +import ( + "errors" + "fmt" + "io" +) + +// Request stores a Groonga command with arguments. +type Request struct { + Cmd string + Args []Argument + Body io.Reader +} + +// checkCmd checks if s is valid as a command name. +func checkCmd(s string) error { + if s == "" { + return errors.New("invalid name: s = ") + } + if s[0] == '_' { + return fmt.Errorf("invalid name: s = %s", s) + } + for i := 0; i < len(s); i++ { + if !(s[i] >= 'a' && s[i] <= 'z') && s[i] != '_' { + return fmt.Errorf("invalid name: s = %s", s) + } + } + return nil +} + +// Check checks if req is valid. +func (req *Request) Check() error { + if err := checkCmd(req.Cmd); err != nil { + return fmt.Errorf("CheckCmd failed: %v", err) + } + for _, arg := range req.Args { + if err := arg.Check(); err != nil { + return fmt.Errorf("arg.Check failed: %v", err) + } + } + return nil +} diff --git a/v2/respponse.go b/v2/respponse.go new file mode 100644 index 0000000..437d876 --- /dev/null +++ b/v2/respponse.go @@ -0,0 +1,12 @@ +package grnci + +import ( + "time" +) + +type Response struct { + Bytes []byte + Error error + Time time.Time + Elapsed time.Duration +}