[Groonga-commit] groonga/grnci at a0f451d [master] Update v2.

Back to archive index

Susumu Yata null+****@clear*****
Wed Jun 14 10:37:06 JST 2017


Susumu Yata	2017-06-14 10:37:06 +0900 (Wed, 14 Jun 2017)

  New Revision: a0f451d6e6c3436a2b8c3a3ac4b8f28704839288
  https://github.com/groonga/grnci/commit/a0f451d6e6c3436a2b8c3a3ac4b8f28704839288

  Message:
    Update v2.

  Added files:
    v2/db.go
    v2/db_test.go
    v2/handler.go
  Modified files:
    v2/gqtp.go
    v2/gqtp_test.go
    v2/http.go
    v2/http_test.go
    v2/libgrn/client.go
    v2/libgrn/client_test.go
    v2/libgrn/conn.go
    v2/libgrn/conn_test.go
    v2/libgrn/libgrn.go
    v2/libgrn/response.go
    v2/request.go
    v2/request_test.go
    v2/rule.go

  Added: v2/db.go (+507 -0) 100644
===================================================================
--- /dev/null
+++ v2/db.go    2017-06-14 10:37:06 +0900 (4b49949)
@@ -0,0 +1,507 @@
+package grnci
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"strings"
+	"time"
+)
+
+// DB is a wrapper to provide a high-level command interface.
+type DB struct {
+	Handler
+}
+
+// NewDB returns a new DB that wraps the specified client or handle.
+func NewDB(h Handler) *DB {
+	return &DB{Handler: h}
+}
+
+// ColumnCreate executes column_create.
+func (db *DB) ColumnCreate(tbl, name, typ, flags string) (bool, Response, error) {
+	req, err := NewRequest("column_create", map[string]interface{}{
+		"table": tbl,
+		"name":  name,
+	}, nil)
+	if err != nil {
+		return false, nil, err
+	}
+	typFlag := "COLUMN_SCALAR"
+	withSection := false
+	src := ""
+	if strings.HasPrefix(typ, "[]") {
+		typFlag = "COLUMN_VECTOR"
+		typ = typ[2:]
+	} else if idx := strings.IndexByte(typ, '.'); idx != -1 {
+		typFlag = "COLUMN_INDEX"
+		src = typ[idx+1:]
+		typ = typ[:idx]
+		if idx := strings.IndexByte(src, ','); idx != -1 {
+			withSection = true
+		}
+	}
+	if flags == "" {
+		flags = typFlag
+	} else {
+		flags += "|" + typFlag
+	}
+	if withSection {
+		flags += "|WITH_SECTION"
+	}
+	if err := req.AddParam("flags", flags); err != nil {
+		return false, nil, err
+	}
+	if err := req.AddParam("type", typ); err != nil {
+		return false, nil, err
+	}
+	if src != "" {
+		if err := req.AddParam("source", src); err != nil {
+			return false, nil, err
+		}
+	}
+	resp, err := db.Query(req)
+	if err != nil {
+		return false, nil, err
+	}
+	defer resp.Close()
+	jsonData, err := ioutil.ReadAll(resp)
+	if err != nil {
+		return false, resp, err
+	}
+	var result bool
+	if err := json.Unmarshal(jsonData, &result); err != nil {
+		return false, resp, NewError(StatusInvalidResponse, map[string]interface{}{
+			"method": "json.Unmarshal",
+			"error":  err.Error(),
+		})
+	}
+	return result, resp, nil
+}
+
+// ColumnRemove executes column_remove.
+func (db *DB) ColumnRemove(tbl, name string) (bool, Response, error) {
+	req, err := NewRequest("column_remove", map[string]interface{}{
+		"table": tbl,
+		"name":  name,
+	}, nil)
+	if err != nil {
+		return false, nil, err
+	}
+	resp, err := db.Query(req)
+	if err != nil {
+		return false, nil, err
+	}
+	defer resp.Close()
+	jsonData, err := ioutil.ReadAll(resp)
+	if err != nil {
+		return false, resp, err
+	}
+	var result bool
+	if err := json.Unmarshal(jsonData, &result); err != nil {
+		return false, resp, NewError(StatusInvalidResponse, map[string]interface{}{
+			"method": "json.Unmarshal",
+			"error":  err.Error(),
+		})
+	}
+	return result, resp, nil
+}
+
+// DumpOptions stores options for DB.Dump.
+type DumpOptions struct {
+	Tables      string // --table
+	DumpPlugins string // --dump_plugins
+	DumpSchema  string // --dump_schema
+	DumpRecords string // --dump_records
+	DumpIndexes string // --dump_indexes
+}
+
+// NewDumpOptions returns the default DumpOptions.
+func NewDumpOptions() *DumpOptions {
+	return &DumpOptions{
+		DumpPlugins: "yes",
+		DumpSchema:  "yes",
+		DumpRecords: "yes",
+		DumpIndexes: "yes",
+	}
+}
+
+// Dump executes dump.
+// On success, it is the caller's responsibility to close the response.
+func (db *DB) Dump(options *DumpOptions) (Response, error) {
+	if options == nil {
+		options = NewDumpOptions()
+	}
+	params := map[string]interface{}{
+		"dump_plugins": options.DumpPlugins,
+		"dump_schema":  options.DumpSchema,
+		"dump_records": options.DumpRecords,
+		"dump_indexes": options.DumpIndexes,
+	}
+	if options.Tables != "" {
+		params["tables"] = options.Tables
+	}
+	req, err := NewRequest("dump", params, nil)
+	if err != nil {
+		return nil, err
+	}
+	return db.Query(req)
+}
+
+// LoadOptions stores options for DB.Load.
+// http://groonga.org/docs/reference/commands/load.html
+type LoadOptions struct {
+	Columns  string // --columns
+	IfExists string // --ifexists
+}
+
+// Load executes load.
+func (db *DB) Load(tbl string, values io.Reader, options *LoadOptions) (int, Response, error) {
+	params := map[string]interface{}{
+		"table": tbl,
+	}
+	if options != nil {
+		if options.Columns != "" {
+			params["columns"] = options.Columns
+		}
+		if options.IfExists != "" {
+			params["ifexists"] = options.IfExists
+		}
+	}
+	req, err := NewRequest("load", params, values)
+	if err != nil {
+		return 0, nil, err
+	}
+	resp, err := db.Query(req)
+	if err != nil {
+		return 0, nil, err
+	}
+	defer resp.Close()
+	jsonData, err := ioutil.ReadAll(resp)
+	if err != nil {
+		return 0, resp, err
+	}
+	var result int
+	if err := json.Unmarshal(jsonData, &result); err != nil {
+		return 0, resp, NewError(StatusInvalidResponse, map[string]interface{}{
+			"method": "json.Unmarshal",
+			"error":  err.Error(),
+		})
+	}
+	return result, resp, nil
+}
+
+// For --columns[NAME].stage, type, value.
+// type SelectOptionsColumn struct {
+// 	Stage string // --columns[NAME].stage
+// 	Type  string // --columns[NAME].type
+// 	Value string // --columns[NAME].value
+// }
+
+// For --drilldowns[LABEL].columns[NAME].
+// type SelectOptionsDorilldownColumn struct {
+// 	Stage           string // --drilldowns[LABEL].columns[NAME].stage
+// 	Flags           string // --drilldowns[LABEL].columns[NAME].flags
+// 	Type            string // --drilldowns[LABEL].columns[NAME].type
+// 	Value           string // --drilldowns[LABEL].columns[NAME].value
+// 	WindowSortKeys  string // --drilldowns[LABEL].columns[NAME].window.sort_keys
+// 	WindowGroupKeys string // --drilldowns[LABEL].columns[NAME].window.group_keys
+// }
+
+// For --drilldowns[LABEL].keys, sort_keys, output_columns, offset, limit, calc_types, calc_target, filter, columns[].
+// type SelectOptionsDrilldown struct {
+// 	Keys          string // --drilldowns[LABEL].keys
+// 	SortKeys      string // --drilldowns[LABEL].sort_keys
+// 	OutputColumns string // --drilldowns[LABEL].output_columns
+// 	Offset        int    // --drilldowns[LABEL].offset
+// 	Limit         int    // --drilldowns[LABEL].limit
+// 	CalcTypes     string // --drilldowns[LABEL].calc_types
+// 	CalcTarget    string // --drilldowns[LABEL].calc_target
+// 	Filter        string // --drilldowns[LABEL].filter
+// 	Columns       map[string]*SelectOptionsDorilldownColumn
+// }
+
+// NewSelectOptionsDrilldown returns the default SelectOptionsDrilldown.
+// func NewSelectOptionsDrilldown() *SelectOptionsDrilldown {
+// 	return &SelectOptionsDrilldown{
+// 		Limit: 10,
+// 	}
+// }
+
+// SelectOptions stores options for DB.Select.
+// http://groonga.org/docs/reference/commands/select.html
+type SelectOptions struct {
+	MatchColumns             string // --match_columns
+	Query                    string // --query
+	Filter                   string // --filter
+	Scorer                   string // --scorer
+	SortKeys                 string // --sort_keys
+	OutputColumns            string // --output_columns
+	Offset                   int    // --offset
+	Limit                    int    // --limit
+	Drilldown                string // --drilldown
+	DrilldownSortKeys        string // --drilldown_sort_keys
+	DrilldownOutputColumns   string // --drilldown_output_columns
+	DrillDownOffset          int    // drilldown_offset
+	DrillDownLimit           int    // drilldown_limit
+	Cache                    bool   // --cache
+	MatchEscalationThreshold int    // --match_escalation_threshold
+	QueryExpansion           string // --query_expansion
+	QueryFlags               string // --query_flags
+	QueryExpander            string // --query_expander
+	Adjuster                 string // --adjuster
+	DrilldownCalcTypes       string // --drilldown_calc_types
+	DrilldownCalcTarget      string // --drilldown_calc_target
+	DrilldownFilter          string // --drilldown_filter
+	// Columns    map[string]*SelectOptionsColumn    // --columns[NAME]
+	// Drilldowns map[string]*SelectOptionsDrilldown // --drilldowns[LABEL]
+}
+
+// NewSelectOptions returns the default SelectOptions.
+func NewSelectOptions() *SelectOptions {
+	return &SelectOptions{
+		Limit:          10,
+		DrillDownLimit: 10,
+	}
+}
+
+// Select executes select.
+// On success, it is the caller's responsibility to close the response.
+func (db *DB) Select(tbl string, options *SelectOptions) (Response, error) {
+	if options == nil {
+		options = NewSelectOptions()
+	}
+	params := map[string]interface{}{
+		"table": tbl,
+	}
+	// TODO: copy entries from options to params.
+	req, err := NewRequest("dump", params, nil)
+	if err != nil {
+		return nil, err
+	}
+	return db.Query(req)
+}
+
+// StatusResult is a response of status.
+type StatusResult struct {
+	AllocCount            int           `json:"alloc_count"`
+	CacheHitRate          float64       `json:"cache_hit_rate"`
+	CommandVersion        int           `json:"command_version"`
+	DefaultCommandVersion int           `json:"default_command_version"`
+	MaxCommandVersion     int           `json:"max_command_version"`
+	NQueries              int           `json:"n_queries"`
+	StartTime             time.Time     `json:"start_time"`
+	Uptime                time.Duration `json:"uptime"`
+	Version               string        `json:"version"`
+}
+
+// Status executes status.
+func (db *DB) Status() (*StatusResult, Response, error) {
+	resp, err := db.Exec("status", nil)
+	if err != nil {
+		return nil, nil, err
+	}
+	defer resp.Close()
+	jsonData, err := ioutil.ReadAll(resp)
+	if err != nil {
+		return nil, resp, err
+	}
+	var data map[string]interface{}
+	if err := json.Unmarshal(jsonData, &data); err != nil {
+		return nil, resp, NewError(StatusInvalidResponse, map[string]interface{}{
+			"method": "json.Unmarshal",
+			"error":  err.Error(),
+		})
+	}
+	var result StatusResult
+	if v, ok := data["alloc_count"]; ok {
+		if v, ok := v.(float64); ok {
+			result.AllocCount = int(v)
+		}
+	}
+	if v, ok := data["cache_hit_rate"]; ok {
+		if v, ok := v.(float64); ok {
+			result.CacheHitRate = v
+		}
+	}
+	if v, ok := data["command_version"]; ok {
+		if v, ok := v.(float64); ok {
+			result.CommandVersion = int(v)
+		}
+	}
+	if v, ok := data["default_command_version"]; ok {
+		if v, ok := v.(float64); ok {
+			result.DefaultCommandVersion = int(v)
+		}
+	}
+	if v, ok := data["max_command_version"]; ok {
+		if v, ok := v.(float64); ok {
+			result.MaxCommandVersion = int(v)
+		}
+	}
+	if v, ok := data["n_queries"]; ok {
+		if v, ok := v.(float64); ok {
+			result.NQueries = int(v)
+		}
+	}
+	if v, ok := data["start_time"]; ok {
+		if v, ok := v.(float64); ok {
+			result.StartTime = time.Unix(int64(v), 0)
+		}
+	}
+	if v, ok := data["uptime"]; ok {
+		if v, ok := v.(float64); ok {
+			result.Uptime = time.Duration(time.Duration(v) * time.Second)
+		}
+	}
+	if v, ok := data["version"]; ok {
+		if v, ok := v.(string); ok {
+			result.Version = v
+		}
+	}
+	return &result, resp, nil
+}
+
+// TableCreateOptions stores options for DB.TableCreate.
+// http://groonga.org/docs/reference/commands/table_create.html
+type TableCreateOptions struct {
+	Flags            string // --flags
+	KeyType          string // --key_type
+	ValueType        string // --value_type
+	DefaultTokenizer string // --default_tokenizer
+	Normalizer       string // --normalizer
+	TokenFilters     string // --token_filters
+}
+
+// TableCreate executes table_create.
+func (db *DB) TableCreate(name string, options *TableCreateOptions) (bool, Response, error) {
+	if options == nil {
+		options = &TableCreateOptions{}
+	}
+	params := map[string]interface{}{
+		"name": name,
+	}
+	flags, keyFlag := "", ""
+	if options.Flags != "" {
+		for _, flag := range strings.Split(options.Flags, "|") {
+			switch flag {
+			case "TABLE_NO_KEY":
+				if keyFlag != "" {
+					return false, nil, fmt.Errorf("TABLE_NO_KEY must not be set with %s", keyFlag)
+				}
+				if options.KeyType != "" {
+					return false, nil, fmt.Errorf("TABLE_NO_KEY disallows KeyType")
+				}
+				keyFlag = flag
+			case "TABLE_HASH_KEY", "TABLE_PAT_KEY", "TABLE_DAT_KEY":
+				if keyFlag != "" {
+					return false, nil, fmt.Errorf("%s must not be set with %s", flag, keyFlag)
+				}
+				if options.KeyType == "" {
+					return false, nil, fmt.Errorf("%s requires KeyType", flag)
+				}
+				keyFlag = flag
+			}
+		}
+		flags = options.Flags
+	}
+	if keyFlag == "" {
+		if options.KeyType == "" {
+			keyFlag = "TABLE_NO_KEY"
+		} else {
+			keyFlag = "TABLE_HASH_KEY"
+		}
+		if flags == "" {
+			flags = keyFlag
+		} else {
+			flags += "|" + keyFlag
+		}
+	}
+	if flags != "" {
+		params["flags"] = flags
+	}
+	if options.KeyType != "" {
+		params["key_type"] = options.KeyType
+	}
+	if options.ValueType != "" {
+		params["value_type"] = options.ValueType
+	}
+	if options.DefaultTokenizer != "" {
+		params["default_tokenizer"] = options.DefaultTokenizer
+	}
+	if options.Normalizer != "" {
+		params["normalizer"] = options.Normalizer
+	}
+	if options.TokenFilters != "" {
+		params["token_filters"] = options.TokenFilters
+	}
+	resp, err := db.Invoke("table_create", params, nil)
+	if err != nil {
+		return false, nil, err
+	}
+	defer resp.Close()
+	jsonData, err := ioutil.ReadAll(resp)
+	if err != nil {
+		return false, resp, err
+	}
+	var result bool
+	if err := json.Unmarshal(jsonData, &result); err != nil {
+		return false, resp, NewError(StatusInvalidResponse, map[string]interface{}{
+			"method": "json.Unmarshal",
+			"error":  err.Error(),
+		})
+	}
+	return result, resp, nil
+}
+
+// TableRemove executes table_remove.
+func (db *DB) TableRemove(name string, dependent bool) (bool, Response, error) {
+	req, err := NewRequest("table_remove", map[string]interface{}{
+		"name":      name,
+		"dependent": dependent,
+	}, nil)
+	if err != nil {
+		return false, nil, err
+	}
+	resp, err := db.Query(req)
+	if err != nil {
+		return false, nil, err
+	}
+	defer resp.Close()
+	jsonData, err := ioutil.ReadAll(resp)
+	if err != nil {
+		return false, resp, err
+	}
+	var result bool
+	if err := json.Unmarshal(jsonData, &result); err != nil {
+		return false, resp, NewError(StatusInvalidResponse, map[string]interface{}{
+			"method": "json.Unmarshal",
+			"error":  err.Error(),
+		})
+	}
+	return result, resp, nil
+}
+
+// Truncate executes truncate.
+func (db *DB) Truncate(target string) (bool, Response, error) {
+	resp, err := db.Invoke("truncate", map[string]interface{}{
+		"target_name": target,
+	}, nil)
+	if err != nil {
+		return false, nil, err
+	}
+	defer resp.Close()
+	jsonData, err := ioutil.ReadAll(resp)
+	if err != nil {
+		return false, resp, err
+	}
+	var result bool
+	if err := json.Unmarshal(jsonData, &result); err != nil {
+		return false, resp, NewError(StatusInvalidResponse, map[string]interface{}{
+			"method": "json.Unmarshal",
+			"error":  err.Error(),
+		})
+	}
+	return result, resp, nil
+}

  Added: v2/db_test.go (+82 -0) 100644
===================================================================
--- /dev/null
+++ v2/db_test.go    2017-06-14 10:37:06 +0900 (ce22d55)
@@ -0,0 +1,82 @@
+package grnci
+
+import (
+	"log"
+	"testing"
+)
+
+func TestDBColumnRemove(t *testing.T) {
+	client, err := NewHTTPClient("", nil)
+	if err != nil {
+		t.Skipf("NewHTTPClient failed: %v", err)
+	}
+	db := NewDB(client)
+	defer db.Close()
+
+	result, resp, err := db.ColumnRemove("no_such_table", "no_such_column")
+	if err != nil {
+		t.Fatalf("db.ColumnRemove failed: %v", err)
+	}
+	log.Printf("result = %#v", result)
+	log.Printf("resp = %#v", resp)
+	if err := resp.Err(); err != nil {
+		log.Printf("error = %#v", err)
+	}
+}
+
+func TestDBStatus(t *testing.T) {
+	client, err := NewHTTPClient("", nil)
+	if err != nil {
+		t.Skipf("NewHTTPClient failed: %v", err)
+	}
+	db := NewDB(client)
+	defer db.Close()
+
+	result, resp, err := db.Status()
+	if err != nil {
+		t.Fatalf("db.Status failed: %v", err)
+	}
+	log.Printf("result = %#v", result)
+	log.Printf("resp = %#v", resp)
+	if err := resp.Err(); err != nil {
+		log.Printf("error = %#v", err)
+	}
+}
+
+func TestDBTruncate(t *testing.T) {
+	client, err := NewHTTPClient("", nil)
+	if err != nil {
+		t.Skipf("NewHTTPClient failed: %v", err)
+	}
+	db := NewDB(client)
+	defer db.Close()
+
+	result, resp, err := db.Truncate("no_such_target")
+	if err != nil {
+		t.Fatalf("db.Truncate failed: %v", err)
+	}
+	log.Printf("result = %#v", result)
+	log.Printf("resp = %#v", resp)
+	if err := resp.Err(); err != nil {
+		log.Printf("error = %#v", err)
+	}
+}
+
+func TestDBTableRemove(t *testing.T) {
+	client, err := NewHTTPClient("", nil)
+	if err != nil {
+		t.Skipf("NewHTTPClient failed: %v", err)
+	}
+	db := NewDB(client)
+	defer db.Close()
+
+	result, resp, err := db.TableRemove("no_such_table", false)
+	if err != nil {
+		t.Fatalf("db.TableRemove failed: %v", err)
+	}
+	log.Printf("result = %#v", result)
+	log.Printf("resp = %#v", resp)
+	if err := resp.Err(); err != nil {
+		log.Printf("error = %#v", err)
+	}
+}

  Modified: v2/gqtp.go (+19 -1)
===================================================================
--- v2/gqtp.go    2017-06-09 09:39:36 +0900 (6173c25)
+++ v2/gqtp.go    2017-06-14 10:37:06 +0900 (c83aa83)
@@ -378,7 +378,16 @@ func (c *GQTPConn) Exec(cmd string, body io.Reader) (Response, error) {
 	return c.execBody(cmd, body)
 }
 
-// Query sends a request and receives a response.
+// Invoke assembles cmd, params and body into a Request and calls Query.
+func (c *GQTPConn) Invoke(cmd string, params map[string]interface{}, body io.Reader) (Response, error) {
+	req, err := NewRequest(cmd, params, body)
+	if err != nil {
+		return nil, err
+	}
+	return c.Query(req)
+}
+
+// Query calls Exec with req.GQTPRequest and returns the result.
 func (c *GQTPConn) Query(req *Request) (Response, error) {
 	cmd, body, err := req.GQTPRequest()
 	if err != nil {
@@ -451,6 +460,15 @@ func (c *GQTPClient) Exec(cmd string, body io.Reader) (Response, error) {
 	return resp, nil
 }
 
+// Invoke assembles cmd, params and body into a Request and calls Query.
+func (c *GQTPClient) Invoke(cmd string, params map[string]interface{}, body io.Reader) (Response, error) {
+	req, err := NewRequest(cmd, params, body)
+	if err != nil {
+		return nil, err
+	}
+	return c.Query(req)
+}
+
 // Query calls Exec with req.GQTPRequest and returns the result.
 func (c *GQTPClient) Query(req *Request) (Response, error) {
 	cmd, body, err := req.GQTPRequest()

  Modified: v2/gqtp_test.go (+14 -0)
===================================================================
--- v2/gqtp_test.go    2017-06-09 09:39:36 +0900 (1d9080b)
+++ v2/gqtp_test.go    2017-06-14 10:37:06 +0900 (e334f64)
@@ -101,3 +101,17 @@ func TestGQTPClient(t *testing.T) {
 		}
 	}
 }
+
+func TestGQTPConnHandler(t *testing.T) {
+	var i interface{} = &GQTPConn{}
+	if _, ok := i.(Handler); !ok {
+		t.Fatalf("Failed to cast from *GQTPConn to Handler")
+	}
+}
+
+func TestGQTPClientHandler(t *testing.T) {
+	var i interface{} = &GQTPClient{}
+	if _, ok := i.(Handler); !ok {
+		t.Fatalf("Failed to cast from *GQTPClient to Handler")
+	}
+}

  Added: v2/handler.go (+11 -0) 100644
===================================================================
--- /dev/null
+++ v2/handler.go    2017-06-14 10:37:06 +0900 (0ecc56d)
@@ -0,0 +1,11 @@
+package grnci
+
+import "io"
+
+// Handler defines the required methods of DB clients and handles.
+type Handler interface {
+	Exec(cmd string, body io.Reader) (Response, error)
+	Invoke(cmd string, params map[string]interface{}, body io.Reader) (Response, error)
+	Query(req *Request) (Response, error)
+	Close() error
+}

  Modified: v2/http.go (+21 -8)
===================================================================
--- v2/http.go    2017-06-09 09:39:36 +0900 (9c5e32d)
+++ v2/http.go    2017-06-14 10:37:06 +0900 (fd81667)
@@ -363,22 +363,35 @@ func (c *HTTPClient) exec(cmd string, params map[string]string, body io.Reader)
 	return resp, nil
 }
 
-// Exec sends a command and returns a response.
-// It is the caller's responsibility to close the response.
-func (c *HTTPClient) Exec(cmd string, params map[string]string, body io.Reader) (Response, error) {
-	start := time.Now()
-	resp, err := c.exec(cmd, params, body)
+// Exec assembles cmd and body into a Request and calls Query.
+func (c *HTTPClient) Exec(cmd string, body io.Reader) (Response, error) {
+	req, err := ParseRequest(cmd, body)
 	if err != nil {
 		return nil, err
 	}
-	return newHTTPResponse(resp, start)
+	return c.Query(req)
+}
+
+// Invoke assembles cmd, params and body into a Request and calls Query.
+func (c *HTTPClient) Invoke(cmd string, params map[string]interface{}, body io.Reader) (Response, error) {
+	req, err := NewRequest(cmd, params, body)
+	if err != nil {
+		return nil, err
+	}
+	return c.Query(req)
 }
 
-// Query calls Exec with req.HTTPRequest and returns the result.
+// Query sends a request and receives a response.
+// It is the caller's responsibility to close the response.
 func (c *HTTPClient) Query(req *Request) (Response, error) {
+	start := time.Now()
 	cmd, params, body, err := req.HTTPRequest()
 	if err != nil {
 		return nil, err
 	}
-	return c.Exec(cmd, params, body)
+	resp, err := c.exec(cmd, params, body)
+	if err != nil {
+		return nil, err
+	}
+	return newHTTPResponse(resp, start)
 }

  Modified: v2/http_test.go (+8 -5)
===================================================================
--- v2/http_test.go    2017-06-09 09:39:36 +0900 (eca1bfd)
+++ v2/http_test.go    2017-06-14 10:37:06 +0900 (dd016d1)
@@ -37,12 +37,8 @@ func TestHTTPClient(t *testing.T) {
 		if pair.Body != "" {
 			body = strings.NewReader(pair.Body)
 		}
-		req, err := ParseRequest(pair.Command, body)
-		if err != nil {
-			t.Fatalf("ParseRequest failed: %v", err)
-		}
 		log.Printf("command = %s", pair.Command)
-		resp, err := client.Query(req)
+		resp, err := client.Exec(pair.Command, body)
 		if err != nil {
 			t.Fatalf("conn.Exec failed: %v", err)
 		}
@@ -58,3 +54,10 @@ func TestHTTPClient(t *testing.T) {
 		}
 	}
 }
+
+func TestHTTPClientHandler(t *testing.T) {
+	var i interface{} = &HTTPClient{}
+	if _, ok := i.(Handler); !ok {
+		t.Fatalf("Failed to cast from *HTTPClient to Handler")
+	}
+}

  Modified: v2/libgrn/client.go (+10 -1)
===================================================================
--- v2/libgrn/client.go    2017-06-09 09:39:36 +0900 (eddaa5c)
+++ v2/libgrn/client.go    2017-06-14 10:37:06 +0900 (91b8baf)
@@ -3,7 +3,7 @@ package libgrn
 import (
 	"io"
 
-	"github.com/groonga/grnci/v2"
+	"github.com/s-yata/grnci"
 )
 
 const (
@@ -111,6 +111,15 @@ func (c *Client) Exec(cmd string, body io.Reader) (grnci.Response, error) {
 	return resp, nil
 }
 
+// Invoke assembles cmd, params and body into a grnci.Request and calls Query.
+func (c *Client) Invoke(cmd string, params map[string]interface{}, body io.Reader) (grnci.Response, error) {
+	req, err := grnci.NewRequest(cmd, params, body)
+	if err != nil {
+		return nil, err
+	}
+	return c.Query(req)
+}
+
 // Query calls Exec with req.GQTPRequest and returns the result.
 func (c *Client) Query(req *grnci.Request) (grnci.Response, error) {
 	cmd, body, err := req.GQTPRequest()

  Modified: v2/libgrn/client_test.go (+9 -0)
===================================================================
--- v2/libgrn/client_test.go    2017-06-09 09:39:36 +0900 (74b93a6)
+++ v2/libgrn/client_test.go    2017-06-14 10:37:06 +0900 (bd0cf9d)
@@ -6,6 +6,8 @@ import (
 	"log"
 	"strings"
 	"testing"
+
+	"github.com/s-yata/grnci"
 )
 
 func TestClientGQTP(t *testing.T) {
@@ -101,3 +103,10 @@ func TestClientDB(t *testing.T) {
 		}
 	}
 }
+
+func TestClientHandler(t *testing.T) {
+	var i interface{} = &Client{}
+	if _, ok := i.(grnci.Handler); !ok {
+		t.Fatalf("Failed to cast from *Client to grnci.Handler")
+	}
+}

  Modified: v2/libgrn/conn.go (+10 -1)
===================================================================
--- v2/libgrn/conn.go    2017-06-09 09:39:36 +0900 (f7413de)
+++ v2/libgrn/conn.go    2017-06-14 10:37:06 +0900 (6a7de6d)
@@ -10,7 +10,7 @@ import (
 	"time"
 	"unsafe"
 
-	"github.com/groonga/grnci/v2"
+	"github.com/s-yata/grnci"
 )
 
 const (
@@ -284,6 +284,15 @@ func (c *Conn) Exec(cmd string, body io.Reader) (grnci.Response, error) {
 	return c.execBody(cmd, body)
 }
 
+// Invoke assembles cmd, params and body into a grnci.Request and calls Query.
+func (c *Conn) Invoke(cmd string, params map[string]interface{}, body io.Reader) (grnci.Response, error) {
+	req, err := grnci.NewRequest(cmd, params, body)
+	if err != nil {
+		return nil, err
+	}
+	return c.Query(req)
+}
+
 // Query calls Exec with req.GQTPRequest and returns the result.
 func (c *Conn) Query(req *grnci.Request) (grnci.Response, error) {
 	cmd, body, err := req.GQTPRequest()

  Modified: v2/libgrn/conn_test.go (+9 -0)
===================================================================
--- v2/libgrn/conn_test.go    2017-06-09 09:39:36 +0900 (fd8d110)
+++ v2/libgrn/conn_test.go    2017-06-14 10:37:06 +0900 (f47739d)
@@ -6,6 +6,8 @@ import (
 	"log"
 	"strings"
 	"testing"
+
+	"github.com/s-yata/grnci"
 )
 
 func TestConnGQTP(t *testing.T) {
@@ -101,3 +103,10 @@ func TestConnDB(t *testing.T) {
 		}
 	}
 }
+
+func TestConnHandler(t *testing.T) {
+	var i interface{} = &Conn{}
+	if _, ok := i.(grnci.Handler); !ok {
+		t.Fatalf("Failed to cast from *Conn to grnci.Handler")
+	}
+}

  Modified: v2/libgrn/libgrn.go (+1 -1)
===================================================================
--- v2/libgrn/libgrn.go    2017-06-09 09:39:36 +0900 (07e3edf)
+++ v2/libgrn/libgrn.go    2017-06-14 10:37:06 +0900 (1cbfc9f)
@@ -10,7 +10,7 @@ import (
 	"sync"
 	"unsafe"
 
-	"github.com/groonga/grnci/v2"
+	"github.com/s-yata/grnci"
 )
 
 const (

  Modified: v2/libgrn/response.go (+1 -1)
===================================================================
--- v2/libgrn/response.go    2017-06-09 09:39:36 +0900 (f73e353)
+++ v2/libgrn/response.go    2017-06-14 10:37:06 +0900 (9cceb47)
@@ -5,7 +5,7 @@ import (
 	"io/ioutil"
 	"time"
 
-	"github.com/groonga/grnci/v2"
+	"github.com/s-yata/grnci"
 )
 
 // response is a response.

  Modified: v2/request.go (+69 -21)
===================================================================
--- v2/request.go    2017-06-09 09:39:36 +0900 (c984c0e)
+++ v2/request.go    2017-06-14 10:37:06 +0900 (7488815)
@@ -3,7 +3,9 @@ package grnci
 import (
 	"fmt"
 	"io"
+	"reflect"
 	"sort"
+	"strconv"
 	"strings"
 )
 
@@ -16,27 +18,30 @@ type Request struct {
 	Body        io.Reader         // Body (nil is allowed)
 }
 
+// newRequest returns a new Request with empty Params.
+func newRequest(cmd string, body io.Reader) *Request {
+	return &Request{
+		Command:     cmd,
+		CommandRule: GetCommandRule(cmd),
+		Params:      make(map[string]string),
+		Body:        body,
+	}
+}
+
 // NewRequest returns a new Request.
-func NewRequest(cmd string, params map[string]string, body io.Reader) (*Request, error) {
+func NewRequest(cmd string, params map[string]interface{}, body io.Reader) (*Request, error) {
 	if err := checkCommand(cmd); err != nil {
 		return nil, err
 	}
-	cr := GetCommandRule(cmd)
-	paramsCopy := make(map[string]string)
+	r := newRequest(cmd, body)
 	for k, v := range params {
-		if err := cr.CheckParam(k, v); err != nil {
+		if err := r.AddParam(k, v); err != nil {
 			return nil, EnhanceError(err, map[string]interface{}{
 				"command": cmd,
 			})
 		}
-		paramsCopy[k] = v
 	}
-	return &Request{
-		Command:     cmd,
-		CommandRule: cr,
-		Params:      paramsCopy,
-		Body:        body,
-	}, nil
+	return r, nil
 }
 
 // unescapeCommandByte returns an unescaped byte.
@@ -120,12 +125,7 @@ func ParseRequest(cmd string, body io.Reader) (*Request, error) {
 	if err := checkCommand(tokens[0]); err != nil {
 		return nil, err
 	}
-	cr := GetCommandRule(tokens[0])
-	r := &Request{
-		Command:     tokens[0],
-		CommandRule: cr,
-		Body:        body,
-	}
+	r := newRequest(tokens[0], body)
 	for i := 1; i < len(tokens); i++ {
 		var k, v string
 		if strings.HasPrefix(tokens[i], "--") {
@@ -144,9 +144,33 @@ func ParseRequest(cmd string, body io.Reader) (*Request, error) {
 	return r, nil
 }
 
+// convertParamValue converts a parameter value.
+func (r *Request) convertParamValue(k string, v interface{}) (string, error) {
+	if v == nil {
+		return "null", nil
+	}
+	val := reflect.ValueOf(v)
+	switch val.Kind() {
+	case reflect.Bool:
+		return strconv.FormatBool(val.Bool()), nil
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		return strconv.FormatInt(val.Int(), 10), nil
+	case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+		return strconv.FormatUint(val.Uint(), 10), nil
+	case reflect.String:
+		return val.String(), nil
+	default:
+		return "", NewError(StatusInvalidCommand, map[string]interface{}{
+			"key":   k,
+			"value": v,
+			"error": "The value type is not supported.",
+		})
+	}
+}
+
 // AddParam adds a parameter.
 // AddParam assumes that Command is already set.
-func (r *Request) AddParam(key, value string) error {
+func (r *Request) AddParam(key string, value interface{}) error {
 	if r.CommandRule == nil {
 		r.CommandRule = GetCommandRule(r.Command)
 	}
@@ -165,10 +189,16 @@ func (r *Request) AddParam(key, value string) error {
 				"key":     key,
 			})
 		}
+		v, err := r.convertParamValue(pr.Key, value)
+		if err != nil {
+			return EnhanceError(err, map[string]interface{}{
+				"command": r.Command,
+			})
+		}
 		if r.Params == nil {
 			r.Params = make(map[string]string)
 		}
-		r.Params[pr.Key] = value
+		r.Params[pr.Key] = v
 		r.NAnonParams++
 		return nil
 	}
@@ -177,10 +207,28 @@ func (r *Request) AddParam(key, value string) error {
 			"command": r.Command,
 		})
 	}
+	v, err := r.convertParamValue(key, value)
+	if err != nil {
+		return EnhanceError(err, map[string]interface{}{
+			"command": r.Command,
+		})
+	}
 	if r.Params == nil {
 		r.Params = make(map[string]string)
 	}
-	r.Params[key] = value
+	r.Params[key] = v
+	return nil
+}
+
+// RemoveParam removes a parameter.
+func (r *Request) RemoveParam(key string) error {
+	if _, ok := r.Params[key]; !ok {
+		return NewError(StatusInvalidOperation, map[string]interface{}{
+			"key":   key,
+			"error": "The key does not exist.",
+		})
+	}
+	delete(r.Params, key)
 	return nil
 }
 
@@ -213,7 +261,7 @@ func (r *Request) GQTPRequest() (cmd string, body io.Reader, err error) {
 		for i := 0; i < len(v); i++ {
 			switch v[i] {
 			case '\'', '\\', '\b', '\t', '\r', '\n':
-				buf = append(buf, '\'')
+				buf = append(buf, '\\')
 			}
 			buf = append(buf, v[i])
 		}

  Modified: v2/request_test.go (+50 -9)
===================================================================
--- v2/request_test.go    2017-06-09 09:39:36 +0900 (813843a)
+++ v2/request_test.go    2017-06-14 10:37:06 +0900 (9b3f9ee)
@@ -1,14 +1,17 @@
 package grnci
 
 import (
+	"fmt"
 	"testing"
 )
 
 func TestNewRequest(t *testing.T) {
-	params := map[string]string{
+	params := map[string]interface{}{
 		"table":     "Tbl",
 		"filter":    "value < 100",
 		"sort_keys": "value",
+		"offset":    0,
+		"limit":     -1,
 	}
 	req, err := NewRequest("select", params, nil)
 	if err != nil {
@@ -19,8 +22,8 @@ func TestNewRequest(t *testing.T) {
 			req.Command, "select")
 	}
 	for key, value := range params {
-		if req.Params[key] != value {
-			t.Fatalf("ParseRequest failed: params[\"%s\"] = %s, want = %s",
+		if req.Params[key] != fmt.Sprint(value) {
+			t.Fatalf("ParseRequest failed: params[\"%s\"] = %s, want = %v",
 				key, req.Params[key], value)
 		}
 	}
@@ -50,10 +53,12 @@ func TestParseRequest(t *testing.T) {
 }
 
 func TestRequestAddParam(t *testing.T) {
-	params := map[string]string{
+	params := map[string]interface{}{
 		"table":     "Tbl",
 		"filter":    "value < 100",
 		"sort_keys": "value",
+		"offset":    0,
+		"limit":     -1,
 	}
 	req, err := NewRequest("select", nil, nil)
 	if err != nil {
@@ -65,17 +70,51 @@ func TestRequestAddParam(t *testing.T) {
 		}
 	}
 	if req.Command != "select" {
-		t.Fatalf("ParseRequest failed: cmd = %s, want = %s",
+		t.Fatalf("req.AddParam failed: cmd = %s, want = %s",
 			req.Command, "select")
 	}
 	for key, value := range params {
-		if req.Params[key] != value {
-			t.Fatalf("ParseRequest failed: params[\"%s\"] = %s, want = %s",
+		if req.Params[key] != fmt.Sprint(value) {
+			t.Fatalf("req.AddParam failed: params[\"%s\"] = %s, want = %v",
 				key, req.Params[key], value)
 		}
 	}
 }
 
+func TestRequestRemoveParam(t *testing.T) {
+	params := map[string]interface{}{
+		"table":     "Tbl",
+		"filter":    "value < 100",
+		"sort_keys": "value",
+		"offset":    0,
+		"limit":     -1,
+	}
+	req, err := NewRequest("select", nil, nil)
+	if err != nil {
+		t.Fatalf("NewRequest failed: %v", err)
+	}
+	for key, value := range params {
+		if err := req.AddParam(key, value); err != nil {
+			t.Fatalf("req.AddParam failed: %v", err)
+		}
+	}
+	for key := range params {
+		if err := req.RemoveParam(key); err != nil {
+			t.Fatalf("req.RemoveParam failed: %v", err)
+		}
+	}
+	if req.Command != "select" {
+		t.Fatalf("req.RemoveParam failed: cmd = %s, want = %s",
+			req.Command, "select")
+	}
+	for key := range params {
+		if _, ok := req.Params[key]; ok {
+			t.Fatalf("req.RemoveParam failed: params[\"%s\"] = %s",
+				key, req.Params[key])
+		}
+	}
+}
+
 func TestRequestCheck(t *testing.T) {
 	data := map[string]bool{
 		"status":                       true,
@@ -100,10 +139,12 @@ func TestRequestCheck(t *testing.T) {
 }
 
 func TestRequestGQTPRequest(t *testing.T) {
-	params := map[string]string{
+	params := map[string]interface{}{
 		"table":     "Tbl",
 		"filter":    "value < 100",
 		"sort_keys": "value",
+		"offset":    0,
+		"limit":     -1,
 	}
 	req, err := NewRequest("select", params, nil)
 	if err != nil {
@@ -113,7 +154,7 @@ func TestRequestGQTPRequest(t *testing.T) {
 	if err != nil {
 		t.Fatalf("req.GQTPRequest failed: %v", err)
 	}
-	want := "select --filter 'value < 100' --sort_keys 'value' --table 'Tbl'"
+	want := "select --filter 'value < 100' --limit '-1' --offset '0' --sort_keys 'value' --table 'Tbl'"
 	if actual != want {
 		t.Fatalf("req.GQTPRequest failed: actual = %s, want = %s",
 			actual, want)

  Modified: v2/rule.go (+31 -12)
===================================================================
--- v2/rule.go    2017-06-09 09:39:36 +0900 (3496045)
+++ v2/rule.go    2017-06-14 10:37:06 +0900 (ccbaf93)
@@ -1,5 +1,9 @@
 package grnci
 
+import (
+	"reflect"
+)
+
 // TODO: add functions to check parameters.
 
 // checkParamKeyDefault is the default function to check parameter keys.
@@ -30,12 +34,27 @@ func checkParamKeyDefault(k string) error {
 }
 
 // checkParamValueDefault is the default function to check parameter values.
-func checkParamValueDefault(v string) error {
+func checkParamValueDefault(v interface{}) error {
+	if v == nil {
+		return nil
+	}
+	val := reflect.ValueOf(v)
+	switch val.Kind() {
+	case reflect.Bool:
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+	case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+	case reflect.String:
+	default:
+		return NewError(StatusInvalidCommand, map[string]interface{}{
+			"value": v,
+			"error": "The value type is not supported.",
+		})
+	}
 	return nil
 }
 
 // checkParamDefault is the default function to check parameters.
-func checkParamDefault(k, v string) error {
+func checkParamDefault(k string, v interface{}) error {
 	if err := checkParamKeyDefault(k); err != nil {
 		return EnhanceError(err, map[string]interface{}{
 			"value": v,
@@ -73,13 +92,13 @@ func checkCommand(s string) error {
 
 // ParamRule is a parameter rule.
 type ParamRule struct {
-	Key          string               // Parameter key
-	ValueChecker func(v string) error // Function to check parameter values
-	Required     bool                 // Whether the parameter is required
+	Key          string                    // Parameter key
+	ValueChecker func(v interface{}) error // Function to check parameter values
+	Required     bool                      // Whether the parameter is required
 }
 
 // NewParamRule returns a new ParamRule.
-func NewParamRule(key string, valueChecker func(v string) error, required bool) *ParamRule {
+func NewParamRule(key string, valueChecker func(v interface{}) error, required bool) *ParamRule {
 	return &ParamRule{
 		Key:          key,
 		ValueChecker: valueChecker,
@@ -88,7 +107,7 @@ func NewParamRule(key string, valueChecker func(v string) error, required bool)
 }
 
 // CheckValue checks a parameter value.
-func (pr *ParamRule) CheckValue(v string) error {
+func (pr *ParamRule) CheckValue(v interface{}) error {
 	if pr.ValueChecker != nil {
 		return pr.ValueChecker(v)
 	}
@@ -97,9 +116,9 @@ func (pr *ParamRule) CheckValue(v string) error {
 
 // CommandRule is a command rule.
 type CommandRule struct {
-	ParamChecker  func(k, v string) error // Function to check uncommon parameters
-	ParamRules    []*ParamRule            // Ordered common parameters
-	ParamRulesMap map[string]*ParamRule   // Index for ParamRules
+	ParamChecker  func(k string, v interface{}) error // Function to check uncommon parameters
+	ParamRules    []*ParamRule                        // Ordered common parameters
+	ParamRulesMap map[string]*ParamRule               // Index for ParamRules
 }
 
 // GetCommandRule returns the command rule for the specified command.
@@ -111,7 +130,7 @@ func GetCommandRule(cmd string) *CommandRule {
 }
 
 // NewCommandRule returns a new CommandRule.
-func NewCommandRule(paramChecker func(k, v string) error, prs ...*ParamRule) *CommandRule {
+func NewCommandRule(paramChecker func(k string, v interface{}) error, prs ...*ParamRule) *CommandRule {
 	prMap := make(map[string]*ParamRule)
 	for _, pr := range prs {
 		prMap[pr.Key] = pr
@@ -124,7 +143,7 @@ func NewCommandRule(paramChecker func(k, v string) error, prs ...*ParamRule) *Co
 }
 
 // CheckParam checks a parameter.
-func (cr *CommandRule) CheckParam(k, v string) error {
+func (cr *CommandRule) CheckParam(k string, v interface{}) error {
 	if cr, ok := cr.ParamRulesMap[k]; ok {
 		if err := cr.CheckValue(v); err != nil {
 			return EnhanceError(err, map[string]interface{}{
-------------- next part --------------
HTML����������������������������...
Download 



More information about the Groonga-commit mailing list
Back to archive index