Susumu Yata
null+****@clear*****
Fri Jun 16 10:52:57 JST 2017
Susumu Yata 2017-06-16 10:52:57 +0900 (Fri, 16 Jun 2017) New Revision: e0765d0466e48a525a24bf9e9ee9efbd765582a1 https://github.com/groonga/grnci/commit/e0765d0466e48a525a24bf9e9ee9efbd765582a1 Message: Update v2. Added files: v2/command.go v2/command_test.go v2/info.go v2/type.go Removed files: v2/request.go v2/request_test.go v2/rule.go Modified files: v2/address.go v2/db.go v2/db_test.go v2/error.go v2/error_test.go v2/gqtp.go v2/gqtp_test.go v2/handler.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/response.go Modified: v2/address.go (+11 -11) =================================================================== --- v2/address.go 2017-06-14 10:41:58 +0900 (64275ab) +++ v2/address.go 2017-06-16 10:52:57 +0900 (21648ac) @@ -35,13 +35,13 @@ func (a *Address) fillGQTP() error { a.Scheme = "gqtp" } if a.Username != "" { - return NewError(StatusInvalidAddress, map[string]interface{}{ + return NewError(InvalidAddress, map[string]interface{}{ "username": a.Username, "error": "GQTP does not accept username.", }) } if a.Password != "" { - return NewError(StatusInvalidAddress, map[string]interface{}{ + return NewError(InvalidAddress, map[string]interface{}{ "password": a.Password, "error": "GQTP does not accept password.", }) @@ -53,19 +53,19 @@ func (a *Address) fillGQTP() error { a.Port = GQTPDefaultPort } if a.Path != "" { - return NewError(StatusInvalidAddress, map[string]interface{}{ + return NewError(InvalidAddress, map[string]interface{}{ "path": a.Path, "error": "GQTP does not accept path.", }) } if a.Query != "" { - return NewError(StatusInvalidAddress, map[string]interface{}{ + return NewError(InvalidAddress, map[string]interface{}{ "query": a.Query, "error": "GQTP does not accept query.", }) } if a.Fragment != "" { - return NewError(StatusInvalidAddress, map[string]interface{}{ + return NewError(InvalidAddress, map[string]interface{}{ "fragment": a.Fragment, "error": "GQTP does not accept fragment.", }) @@ -105,7 +105,7 @@ func (a *Address) fill() error { return err } default: - return NewError(StatusInvalidAddress, map[string]interface{}{ + return NewError(InvalidAddress, map[string]interface{}{ "scheme": a.Scheme, "error": "The scheme is not supported.", }) @@ -122,7 +122,7 @@ func (a *Address) parseHostPort(s string) error { if s[0] == '[' { i := strings.IndexByte(s, ']') if i == -1 { - return NewError(StatusInvalidAddress, map[string]interface{}{ + return NewError(InvalidAddress, map[string]interface{}{ "address": s, "error": "IPv6 address must be enclosed in [].", }) @@ -133,7 +133,7 @@ func (a *Address) parseHostPort(s string) error { return nil } if rest[0] != ':' { - return NewError(StatusInvalidAddress, map[string]interface{}{ + return NewError(InvalidAddress, map[string]interface{}{ "address": s, "error": "IPv6 address and port must be separated by ':'.", }) @@ -151,7 +151,7 @@ func (a *Address) parseHostPort(s string) error { if portStr != "" { port, err := net.LookupPort("tcp", portStr) if err != nil { - return NewError(StatusInvalidAddress, map[string]interface{}{ + return NewError(InvalidAddress, map[string]interface{}{ "port": portStr, "error": err.Error(), }) @@ -223,7 +223,7 @@ func ParseGQTPAddress(s string) (*Address, error) { switch strings.ToLower(a.Scheme) { case "", "gqtp": default: - return nil, NewError(StatusInvalidAddress, map[string]interface{}{ + return nil, NewError(InvalidAddress, map[string]interface{}{ "scheme": a.Scheme, "error": "The scheme is not supported.", }) @@ -245,7 +245,7 @@ func ParseHTTPAddress(s string) (*Address, error) { switch strings.ToLower(a.Scheme) { case "", "http", "https": default: - return nil, NewError(StatusInvalidAddress, map[string]interface{}{ + return nil, NewError(InvalidAddress, map[string]interface{}{ "scheme": a.Scheme, "error": "The scheme is not supported.", }) Added: v2/command.go (+899 -0) 100644 =================================================================== --- /dev/null +++ v2/command.go 2017-06-16 10:52:57 +0900 (3b97da3) @@ -0,0 +1,899 @@ +package grnci + +import ( + "io" + "reflect" + "sort" + "strconv" + "strings" +) + +// formatParamValue is a function to format a parameter value. +type formatParamValue func(value interface{}) (string, error) + +// formatParamValueDefault is the default formatParamValue. +var formatParamValueDefault = func(value interface{}) (string, error) { + switch v := value.(type) { + case bool: + return strconv.FormatBool(v), nil + case int: + return strconv.FormatInt(int64(v), 10), nil + case int8: + return strconv.FormatInt(int64(v), 10), nil + case int16: + return strconv.FormatInt(int64(v), 10), nil + case int32: + return strconv.FormatInt(int64(v), 10), nil + case int64: + return strconv.FormatInt(v, 10), nil + case uint: + return strconv.FormatUint(uint64(v), 10), nil + case uint8: + return strconv.FormatUint(uint64(v), 10), nil + case uint16: + return strconv.FormatUint(uint64(v), 10), nil + case uint32: + return strconv.FormatUint(uint64(v), 10), nil + case uint64: + return strconv.FormatUint(v, 10), nil + case float32: + return strconv.FormatFloat(float64(v), 'f', -1, 32), nil + case float64: + return strconv.FormatFloat(v, 'f', -1, 64), nil + case string: + return v, nil + default: + return "", NewError(InvalidCommand, map[string]interface{}{ + "value": value, + "type": reflect.TypeOf(value).Name(), + "error": "The type is not supported.", + }) + } +} + +// yesNo returns yes or no. +func yesNo(value bool) string { + if value { + return "yes" + } + return "no" +} + +// formatParamValueYesNo formats an yes/no value. +func formatParamValueYesNo(value interface{}) (string, error) { + switch v := value.(type) { + case bool: + return yesNo(v), nil + case string: + switch v { + case "yes", "no": + return v, nil + default: + return "", NewError(InvalidCommand, map[string]interface{}{ + "value": v, + "error": "The value must be yes or no.", + }) + } + default: + return "", NewError(InvalidCommand, map[string]interface{}{ + "value": value, + "type": reflect.TypeOf(value).Name(), + "error": "The type is not supported.", + }) + } +} + +// formatParamValueCSV formats comma-separated values. +func formatParamValueCSV(value interface{}) (string, error) { + switch v := value.(type) { + case string: + return v, nil + case []string: + return strings.Join(v, ","), nil + default: + return "", NewError(InvalidCommand, map[string]interface{}{ + "value": value, + "type": reflect.TypeOf(value).Name(), + "error": "The type is not supported.", + }) + } +} + +// formatParamValueFlags formats pipe-separated values. +func formatParamValueFlags(value interface{}) (string, error) { + switch v := value.(type) { + case string: + return v, nil + case []string: + return strings.Join(v, "|"), nil + default: + return "", NewError(InvalidCommand, map[string]interface{}{ + "value": value, + "type": reflect.TypeOf(value).Name(), + "error": "The type is not supported.", + }) + } +} + +// formatParamValueBorder formats an include/exclude value. +func formatParamValueBorder(value interface{}) (string, error) { + switch v := value.(type) { + case bool: + if v { + return "include", nil + } + return "exclude", nil + case string: + switch v { + case "include", "exclude": + return v, nil + default: + return "", NewError(InvalidCommand, map[string]interface{}{ + "value": v, + "error": "The value must be include or exclude.", + }) + } + default: + return "", NewError(InvalidCommand, map[string]interface{}{ + "value": value, + "type": reflect.TypeOf(value).Name(), + "error": "The type is not supported.", + }) + } +} + +type paramFormat struct { + key string // Parameter key + format formatParamValue // Custom function to format a parameter value. + required bool // Whether or not the parameter is required +} + +// newParamFormat returns a new paramFormat. +func newParamFormat(key string, format formatParamValue, required bool) *paramFormat { + return ¶mFormat{ + key: key, + format: format, + required: required, + } +} + +// Format formats a parameter value. +func (pf *paramFormat) Format(value interface{}) (string, error) { + if pf.format != nil { + return pf.format(value) + } + return formatParamValueDefault(value) +} + +// formatParam is a function to format a parameter. +type formatParam func(key string, value interface{}) (string, error) + +// formatParamDefault is the default formatParam. +func formatParamDefault(key string, value interface{}) (string, error) { + if key == "" { + return "", NewError(InvalidCommand, map[string]interface{}{ + "key": key, + "error": "The key must not be empty.", + }) + } + for _, c := range key { + switch { + case c >= 'a' && c <= 'z': + case c == '_': + default: + return "", NewError(InvalidCommand, map[string]interface{}{ + "key": key, + "error": "The key must consist of [a-z_].", + }) + } + } + fv, err := formatParamValueDefault(value) + if err != nil { + return "", EnhanceError(err, map[string]interface{}{ + "key": key, + }) + } + return fv, nil +} + +// formatParamSelect formats a parameter of select. +func formatParamSelect(key string, value interface{}) (string, error) { + if key == "" { + return "", NewError(InvalidCommand, map[string]interface{}{ + "key": key, + "error": "The key must not be empty.", + }) + } + for _, c := range key { + switch { + case c >= '0' && c <= '9': + case c >= 'A' && c <= 'Z': + case c >= 'a' && c <= 'z': + default: + switch c { + case '#', '@', '-', '_', '.', '[', ']': + default: + return "", NewError(InvalidCommand, map[string]interface{}{ + "key": key, + "error": "The key must consist of [0-9A-Za-z#@-_.[]].", + }) + } + } + } + fv, err := formatParamValueDefault(value) + if err != nil { + return "", EnhanceError(err, map[string]interface{}{ + "key": key, + }) + } + return fv, nil +} + +type commandFormat struct { + format formatParam // Custom function to format a parameter + params []*paramFormat // Fixed parameters + paramsByKey map[string]*paramFormat // Index for params + requiredParams []*paramFormat // Required parameters +} + +// newCommandFormat returns a new commandFormat. +func newCommandFormat(format formatParam, params ...*paramFormat) *commandFormat { + paramsByKey := make(map[string]*paramFormat) + var requiredParams []*paramFormat + for _, param := range params { + paramsByKey[param.key] = param + if param.required { + requiredParams = append(requiredParams, param) + } + } + return &commandFormat{ + format: format, + params: params, + paramsByKey: paramsByKey, + requiredParams: requiredParams, + } +} + +// Format formats a parameter. +func (cf *commandFormat) Format(key string, value interface{}) (string, error) { + if pf, ok := cf.paramsByKey[key]; ok { + return pf.Format(value) + } + if cf.format != nil { + return cf.format(key, value) + } + return formatParamDefault(key, value) +} + +// commandFormats defines the available commands. +// The contents are set in initCommandFormats. +var commandFormats = map[string]*commandFormat{ + "cache_limit": newCommandFormat( + nil, + newParamFormat("max", nil, false), + ), + "check": newCommandFormat( + nil, + newParamFormat("obj", nil, true), + ), + "clearlock": newCommandFormat( + nil, + newParamFormat("objname", nil, true), + ), + "column_copy": newCommandFormat( + nil, + newParamFormat("from_table", nil, true), + newParamFormat("from_name", nil, true), + newParamFormat("to_table", nil, true), + newParamFormat("to_name", nil, true), + ), + "column_create": newCommandFormat( + nil, + newParamFormat("table", nil, true), + newParamFormat("name", nil, true), + newParamFormat("flags", formatParamValueFlags, true), + newParamFormat("type", nil, true), + newParamFormat("source", nil, false), + ), + "column_list": newCommandFormat( + nil, + newParamFormat("table", nil, true), + ), + "column_remove": newCommandFormat(nil, + newParamFormat("table", nil, true), + newParamFormat("name", nil, true), + ), + "column_rename": newCommandFormat(nil, + newParamFormat("table", nil, true), + newParamFormat("name", nil, true), + newParamFormat("new_name", nil, true), + ), + "config_delete": newCommandFormat( + nil, + newParamFormat("key", nil, true), + ), + "config_get": newCommandFormat( + nil, + newParamFormat("key", nil, true), + ), + "config_set": newCommandFormat( + nil, + newParamFormat("key", nil, true), + newParamFormat("value", nil, true), + ), + "database_unmap": newCommandFormat(nil), + "define_selector": newCommandFormat( + nil, + newParamFormat("name", nil, true), + newParamFormat("table", nil, true), + newParamFormat("match_columns", formatParamValueCSV, false), + newParamFormat("query", nil, false), + newParamFormat("filter", nil, false), + newParamFormat("scorer", nil, false), + newParamFormat("sortby", formatParamValueCSV, false), + newParamFormat("output_columns", formatParamValueCSV, false), + newParamFormat("offset", nil, false), + newParamFormat("limit", nil, false), + newParamFormat("drilldown", nil, false), + newParamFormat("drilldown_sortby", formatParamValueCSV, false), + newParamFormat("drilldown_output_columns", formatParamValueCSV, false), + newParamFormat("drilldown_offset", nil, false), + newParamFormat("drilldown_limit", nil, false), + ), + "defrag": newCommandFormat( + nil, + newParamFormat("objname", nil, true), + newParamFormat("threshold", nil, true), + ), + "delete": newCommandFormat( + nil, + newParamFormat("table", nil, true), + newParamFormat("key", nil, false), + newParamFormat("id", nil, false), + newParamFormat("filter", nil, false), + ), + "dump": newCommandFormat( + nil, + newParamFormat("tables", formatParamValueCSV, false), + newParamFormat("dump_plugins", formatParamValueYesNo, false), + newParamFormat("dump_schema", formatParamValueYesNo, false), + newParamFormat("dump_records", formatParamValueYesNo, false), + newParamFormat("dump_indexes", formatParamValueYesNo, false), + ), + "io_flush": newCommandFormat( + nil, + newParamFormat("target_name", nil, false), + newParamFormat("recursive", nil, false), + ), + "load": newCommandFormat( + nil, + newParamFormat("values", nil, false), // values may be passed as a body. + newParamFormat("table", nil, true), + newParamFormat("columns", formatParamValueCSV, false), + newParamFormat("ifexists", nil, false), + newParamFormat("input_type", nil, false), + ), + "lock_acquire": newCommandFormat( + nil, + newParamFormat("target_name", nil, false), + ), + "lock_clear": newCommandFormat( + nil, + newParamFormat("target_name", nil, false), + ), + "lock_release": newCommandFormat( + nil, + newParamFormat("target_name", nil, false), + ), + "log_level": newCommandFormat( + nil, + newParamFormat("level", nil, true), + ), + "log_put": newCommandFormat( + nil, + newParamFormat("level", nil, true), + newParamFormat("message", nil, true), + ), + "log_reopen": newCommandFormat(nil), + "logical_count": newCommandFormat( + nil, + newParamFormat("logical_table", nil, true), + newParamFormat("shard_key", nil, true), + newParamFormat("min", nil, false), + newParamFormat("min_border", formatParamValueBorder, false), + newParamFormat("max", nil, false), + newParamFormat("max_border", formatParamValueBorder, false), + newParamFormat("filter", nil, false), + ), + "logical_parameters": newCommandFormat( + nil, + newParamFormat("range_index", nil, false), + ), + "logical_range_filter": newCommandFormat( + nil, + newParamFormat("logical_table", nil, true), + newParamFormat("shard_key", nil, true), + newParamFormat("min", nil, false), + newParamFormat("min_border", formatParamValueBorder, false), + newParamFormat("max", nil, false), + newParamFormat("max_border", formatParamValueBorder, false), + newParamFormat("order", nil, false), + newParamFormat("filter", nil, false), + newParamFormat("offset", nil, false), + newParamFormat("limit", nil, false), + newParamFormat("output_columns", formatParamValueCSV, false), + newParamFormat("use_range_index", nil, false), + ), + "logical_select": newCommandFormat( + nil, + newParamFormat("logical_table", nil, true), + newParamFormat("shard_key", nil, true), + newParamFormat("min", nil, false), + newParamFormat("min_border", formatParamValueBorder, false), + newParamFormat("max", nil, false), + newParamFormat("max_border", formatParamValueBorder, false), + newParamFormat("filter", nil, false), + newParamFormat("sortby", nil, false), + newParamFormat("output_columns", formatParamValueCSV, false), + newParamFormat("offset", nil, false), + newParamFormat("limit", nil, false), + newParamFormat("drilldown", nil, false), + newParamFormat("drilldown_sortby", formatParamValueCSV, false), + newParamFormat("drilldown_output_columns", formatParamValueCSV, false), + newParamFormat("drilldown_offset", nil, false), + newParamFormat("drilldown_limit", nil, false), + newParamFormat("drilldown_calc_types", formatParamValueCSV, false), + newParamFormat("drilldown_calc_target", nil, false), + newParamFormat("sort_keys", nil, false), + newParamFormat("drilldown_sort_keys", formatParamValueCSV, false), + newParamFormat("match_columns", formatParamValueCSV, false), + newParamFormat("query", nil, false), + newParamFormat("drilldown_filter", nil, false), + ), + "logical_shard_list": newCommandFormat( + nil, + newParamFormat("logical_table", nil, true), + ), + "logical_table_remove": newCommandFormat( + nil, + newParamFormat("logical_table", nil, true), + newParamFormat("shard_key", nil, true), + newParamFormat("min", nil, false), + newParamFormat("min_border", formatParamValueBorder, false), + newParamFormat("max", nil, false), + newParamFormat("max_border", formatParamValueBorder, false), + newParamFormat("dependent", formatParamValueYesNo, false), + newParamFormat("force", formatParamValueYesNo, false), + ), + "normalize": newCommandFormat( + nil, + newParamFormat("normalizer", nil, true), + newParamFormat("string", nil, true), + newParamFormat("flags", formatParamValueFlags, false), + ), + "normalizer_list": newCommandFormat(nil), + "object_exist": newCommandFormat( + nil, + newParamFormat("name", nil, true), + ), + "object_inspect": newCommandFormat( + nil, + newParamFormat("name", nil, false), + ), + "object_list": newCommandFormat(nil), + "object_remove": newCommandFormat( + nil, + newParamFormat("name", nil, true), + newParamFormat("force", formatParamValueYesNo, false), + ), + "plugin_register": newCommandFormat( + nil, + newParamFormat("name", nil, true), + ), + "plugin_unregister": newCommandFormat( + nil, + newParamFormat("name", nil, true), + ), + "query_expand": newCommandFormat(nil), // TODO + "quit": newCommandFormat(nil), + "range_filter": newCommandFormat(nil), // TODO + "register": newCommandFormat( + nil, + newParamFormat("path", nil, true), + ), + "reindex": newCommandFormat( + nil, + newParamFormat("target_name", nil, false), + ), + "request_cancel": newCommandFormat( + nil, + newParamFormat("id", nil, true), + ), + "ruby_eval": newCommandFormat( + nil, + newParamFormat("script", nil, true), + ), + "ruby_load": newCommandFormat( + nil, + newParamFormat("path", nil, true), + ), + "schema": newCommandFormat(nil), + "select": newCommandFormat( + formatParamSelect, + newParamFormat("table", nil, true), + newParamFormat("match_columns", formatParamValueCSV, false), + newParamFormat("query", nil, false), + newParamFormat("filter", nil, false), + newParamFormat("scorer", nil, false), + newParamFormat("sortby", nil, false), + newParamFormat("output_columns", formatParamValueCSV, false), + newParamFormat("offset", nil, false), + newParamFormat("limit", nil, false), + newParamFormat("drilldown", nil, false), + newParamFormat("drilldown_sortby", formatParamValueCSV, false), + newParamFormat("drilldown_output_columns", formatParamValueCSV, false), + newParamFormat("drilldown_offset", nil, false), + newParamFormat("drilldown_limit", nil, false), + newParamFormat("cache", formatParamValueYesNo, false), + newParamFormat("match_escalation_threshold", nil, false), + newParamFormat("query_expansion", nil, false), + newParamFormat("query_flags", formatParamValueFlags, false), + newParamFormat("query_expander", nil, false), + newParamFormat("adjuster", nil, false), + newParamFormat("drilldown_calc_types", formatParamValueCSV, false), + newParamFormat("drilldown_calc_target", nil, false), + newParamFormat("drilldown_filter", nil, false), + newParamFormat("sort_keys", formatParamValueCSV, false), + newParamFormat("drilldown_sort_keys", formatParamValueCSV, false), + ), + "shutdown": newCommandFormat( + nil, + newParamFormat("mode", nil, false), + ), + "status": newCommandFormat(nil), + "suggest": newCommandFormat( + nil, + newParamFormat("types", formatParamValueFlags, true), + newParamFormat("table", nil, true), + newParamFormat("column", nil, true), + newParamFormat("query", nil, true), + newParamFormat("sortby", formatParamValueCSV, false), + newParamFormat("output_columns", formatParamValueCSV, false), + newParamFormat("offset", nil, false), + newParamFormat("limit", nil, false), + newParamFormat("frequency_threshold", nil, false), + newParamFormat("conditional_probability_threshold", nil, false), + newParamFormat("prefix_search", nil, false), + ), + "table_copy": newCommandFormat( + nil, + newParamFormat("from_name", nil, true), + newParamFormat("to_name", nil, true), + ), + "table_create": newCommandFormat( + nil, + newParamFormat("name", nil, true), + newParamFormat("flags", formatParamValueFlags, false), + newParamFormat("key_type", nil, false), + newParamFormat("value_type", nil, false), + newParamFormat("default_tokenizer", nil, false), + newParamFormat("normalizer", nil, false), + newParamFormat("token_filters", formatParamValueCSV, false), + ), + "table_list": newCommandFormat(nil), + "table_remove": newCommandFormat( + nil, + newParamFormat("name", nil, true), + newParamFormat("dependent", formatParamValueYesNo, false), + ), + "table_rename": newCommandFormat( + nil, + newParamFormat("name", nil, true), + newParamFormat("new_name", nil, true), + ), + "table_tokenize": newCommandFormat( + nil, + newParamFormat("table", nil, true), + newParamFormat("string", nil, true), + newParamFormat("flags", formatParamValueFlags, false), + newParamFormat("mode", nil, false), + newParamFormat("index_column", nil, false), + ), + "thread_limit": newCommandFormat( + nil, + newParamFormat("max", nil, false), + ), + "tokenize": newCommandFormat( + nil, + newParamFormat("tokenizer", nil, true), + newParamFormat("string", nil, true), + newParamFormat("normalizer", nil, false), + newParamFormat("flags", formatParamValueFlags, false), + newParamFormat("mode", nil, false), + newParamFormat("token_filters", formatParamValueCSV, false), + ), + "tokenizer_list": newCommandFormat(nil), + "truncate": newCommandFormat( + nil, + newParamFormat("target_name", nil, true), + ), +} + +// Command is a command. +type Command struct { + name string // Command name + format *commandFormat // Command format + params map[string]string // Parameters + index int // Number of unnamed parameters + body io.Reader // Command body +} + +// newCommand returns a new Command. +func newCommand(name string) (*Command, error) { + format, ok := commandFormats[name] + if !ok { + return nil, NewError(InvalidCommand, map[string]interface{}{ + "name": name, + "error": "The name is not defined.", + }) + } + return &Command{ + name: name, + format: format, + params: make(map[string]string), + }, nil +} + +// NewCommand formats params and returns a new Command. +func NewCommand(name string, params map[string]interface{}) (*Command, error) { + c, err := newCommand(name) + if err != nil { + return nil, err + } + for k, v := range params { + if err := c.SetParam(k, v); err != nil { + return nil, EnhanceError(err, map[string]interface{}{ + "name": name, + }) + } + } + return c, nil + +} + +// unescapeCommandByte returns an unescaped space character. +func unescapeCommandByte(b byte) byte { + switch b { + case 'b': + return '\b' + case 't': + return '\t' + case 'r': + return '\r' + case 'n': + return '\n' + default: + return b + } +} + +// tokenizeCommand tokenizes a command. +func tokenizeCommand(cmd string) ([]string, error) { + var tokens []string + var token []byte + s := cmd + for { + s = strings.TrimLeft(s, " \t\r\n") + if s == "" { + break + } + switch s[0] { + case '"', '\'': + i := 1 + for ; i < len(s); i++ { + if s[i] == s[0] { + i++ + break + } + if s[i] != '\\' { + token = append(token, s[i]) + continue + } + i++ + if i == len(s) { + return nil, NewError(InvalidCommand, map[string]interface{}{ + "command": cmd, + "error": "The command ends with an unclosed token.", + }) + } + token = append(token, unescapeCommandByte(s[i])) + } + s = s[i:] + default: + i := 0 + Loop: + for ; i < len(s); i++ { + switch s[i] { + case ' ', '\t', '\r', '\n', '"', '\'': + break Loop + case '\\': + i++ + if i == len(s) { + return nil, NewError(InvalidCommand, map[string]interface{}{ + "command": cmd, + "error": "The command ends with an escape character.", + }) + } + token = append(token, unescapeCommandByte(s[i])) + default: + token = append(token, s[i]) + } + } + s = s[i:] + } + tokens = append(tokens, string(token)) + token = token[:0] + } + return tokens, nil +} + +// ParseCommand parses cmd and returns a new Command. +func ParseCommand(cmd string) (*Command, error) { + tokens, err := tokenizeCommand(cmd) + if err != nil { + return nil, err + } + if len(tokens) == 0 { + return nil, NewError(InvalidCommand, map[string]interface{}{ + "command": cmd, + "error": "The command has no tokens.", + }) + } + c, err := newCommand(tokens[0]) + if err != nil { + return nil, EnhanceError(err, map[string]interface{}{ + "command": cmd, + }) + } + for i := 1; i < len(tokens); i++ { + var k, v string + if strings.HasPrefix(tokens[i], "--") { + k = tokens[i][2:] + i++ + if i >= len(tokens) { + return nil, NewError(InvalidCommand, map[string]interface{}{ + "command": cmd, + "key": k, + "error": "The key requires a value.", + }) + } + } + v = tokens[i] + if err := c.SetParam(k, v); err != nil { + return nil, err + } + } + return c, nil +} + +// Name returns the command name. +func (c *Command) Name() string { + return c.name +} + +// Params returns the command parameters. +func (c *Command) Params() map[string]string { + return c.params +} + +// Body returns the command body. +func (c *Command) Body() io.Reader { + return c.body +} + +// NeedsBody returns whether or not the command requires a body. +func (c *Command) NeedsBody() bool { + if c.name == "load" { + if _, ok := c.params["values"]; !ok { + return true + } + } + return false +} + +// Check checks whether or not the command has required parameters. +func (c *Command) Check() error { + for _, pf := range c.format.requiredParams { + if _, ok := c.params[pf.key]; !ok { + return NewError(InvalidCommand, map[string]interface{}{ + "name": c.name, + "key": pf.key, + "error": "The command requires the key.", + }) + } + } + if c.NeedsBody() { + if c.body == nil { + return NewError(InvalidCommand, map[string]interface{}{ + "name": c.name, + "error": "The command requires a body", + }) + } + } + return nil +} + +// SetParam adds or removes a parameter. +// If value == nil, it adds a parameter. +// Otherwise, it removes a parameter. +func (c *Command) SetParam(key string, value interface{}) error { + if value == nil { + if _, ok := c.params[key]; !ok { + return NewError(InvalidCommand, map[string]interface{}{ + "name": c.name, + "key": key, + "error": "The key does not exist.", + }) + } + delete(c.params, key) + return nil + } + if key == "" { + if c.index >= len(c.format.params) { + return NewError(InvalidCommand, map[string]interface{}{ + "name": c.name, + "index": c.index, + "error": "The index is too large.", + }) + } + pf := c.format.params[c.index] + fv, err := pf.Format(value) + if err != nil { + return EnhanceError(err, map[string]interface{}{ + "name": c.name, + "key": key, + }) + } + c.params[pf.key] = fv + c.index++ + return nil + } + fv, err := c.format.Format(key, value) + if err != nil { + return EnhanceError(err, map[string]interface{}{ + "name": c.name, + }) + } + c.params[key] = fv + return nil +} + +// SetBody sets a body. +func (c *Command) SetBody(body io.Reader) { + c.body = body +} + +// String assembles the command name and parameters. +func (c *Command) String() string { + cmd := []byte(c.name) + keys := make([]string, 0, len(c.params)) + for k := range c.params { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + v := c.params[k] + cmd = append(cmd, " --"...) + cmd = append(cmd, k...) + cmd = append(cmd, " '"...) + for i := 0; i < len(v); i++ { + switch v[i] { + case '\'', '\\', '\b', '\t', '\r', '\n': + cmd = append(cmd, '\\') + } + cmd = append(cmd, v[i]) + } + cmd = append(cmd, '\'') + } + return string(cmd) +} Added: v2/command_test.go (+106 -0) 100644 =================================================================== --- /dev/null +++ v2/command_test.go 2017-06-16 10:52:57 +0900 (a8dea64) @@ -0,0 +1,106 @@ +package grnci + +import ( + "testing" +) + +func TestNewCommand(t *testing.T) { + params := map[string]interface{}{ + "table": "Tbl", + "filter": "value < 100", + "sort_keys": "value", + "cache": false, + "offset": 0, + "limit": -1, + } + cmd, err := NewCommand("select", params) + if err != nil { + t.Fatalf("NewCommand failed: %v", err) + } + if cmd.Name() != "select" { + t.Fatalf("NewCommand failed: name = %s, want = %s", cmd.Name(), "select") + } + if key, want := "table", "Tbl"; cmd.Params()[key] != want { + t.Fatalf("NewCommand failed: params[\"%s\"] = %s, want = %v", key, cmd.Params()[key], want) + } + if key, want := "cache", "no"; cmd.Params()[key] != want { + t.Fatalf("NewCommand failed: params[\"%s\"] = %s, want = %v", key, cmd.Params()[key], want) + } + if key, want := "limit", "-1"; cmd.Params()[key] != want { + t.Fatalf("NewCommand failed: params[\"%s\"] = %s, want = %v", key, cmd.Params()[key], want) + } +} + +func TestParseCommand(t *testing.T) { + cmd, err := ParseCommand(`select Tbl --query '"apple juice"' --filter 'price < 100' --cache no`) + if err != nil { + t.Fatalf("ParseCommand failed: %v", err) + } + if want := "select"; cmd.Name() != want { + t.Fatalf("ParseCommand failed: name = %s, want = %s", cmd.Name(), want) + } + if key, want := "table", "Tbl"; cmd.Params()[key] != want { + t.Fatalf("NewCommand failed: params[\"%s\"] = %s, want = %v", key, cmd.Params()[key], want) + } + if key, want := "query", `"apple juice"`; cmd.Params()[key] != want { + t.Fatalf("NewCommand failed: params[\"%s\"] = %s, want = %v", key, cmd.Params()[key], want) + } + if key, want := "cache", "no"; cmd.Params()[key] != want { + t.Fatalf("NewCommand failed: params[\"%s\"] = %s, want = %v", key, cmd.Params()[key], want) + } +} + +func TestCommandSetParam(t *testing.T) { + cmd, err := NewCommand("select", nil) + if err != nil { + t.Fatalf("NewCommand failed: %v", err) + } + if err := cmd.SetParam("", "Tbl"); err != nil { + t.Fatalf("cmd.SetParam failed: %v", err) + } + if err := cmd.SetParam("cache", false); err != nil { + t.Fatalf("cmd.SetParam failed: %v", err) + } + if err := cmd.SetParam("cache", true); err != nil { + t.Fatalf("cmd.SetParam failed: %v", err) + } + if err := cmd.SetParam("cache", nil); err != nil { + t.Fatalf("cmd.SetParam failed: %v", err) + } +} + +func TestCommandString(t *testing.T) { + params := map[string]interface{}{ + "table": "Tbl", + "cache": "no", + "limit": -1, + } + cmd, err := NewCommand("select", params) + if err != nil { + t.Fatalf("NewCommand failed: %v", err) + } + actual := cmd.String() + want := "select --cache 'no' --limit '-1' --table 'Tbl'" + if actual != want { + t.Fatalf("cmd.String failed: actual = %s, want = %s", actual, want) + } +} + +func TestCommandNeedsBody(t *testing.T) { + data := map[string]bool{ + "status": false, + "select Tbl": false, + "load --table Tbl": true, + "load --table Tbl --values []": false, + } + for src, want := range data { + cmd, err := ParseCommand(src) + if err != nil { + t.Fatalf("ParseCommand failed: %v", err) + } + actual := cmd.NeedsBody() + if actual != want { + t.Fatalf("cmd.NeedsBody failed: cmd = %s, needsBody = %v, want = %v", cmd, actual, want) + } + } +} Modified: v2/db.go (+310 -153) =================================================================== --- v2/db.go 2017-06-14 10:41:58 +0900 (4b49949) +++ v2/db.go 2017-06-16 10:52:57 +0900 (418f6c2) @@ -1,10 +1,12 @@ package grnci import ( + "bytes" "encoding/json" "fmt" "io" "io/ioutil" + "reflect" "strings" "time" ) @@ -21,10 +23,10 @@ func NewDB(h Handler) *DB { // ColumnCreate executes column_create. func (db *DB) ColumnCreate(tbl, name, typ, flags string) (bool, Response, error) { - req, err := NewRequest("column_create", map[string]interface{}{ + cmd, err := NewCommand("column_create", map[string]interface{}{ "table": tbl, "name": name, - }, nil) + }) if err != nil { return false, nil, err } @@ -50,18 +52,18 @@ func (db *DB) ColumnCreate(tbl, name, typ, flags string) (bool, Response, error) if withSection { flags += "|WITH_SECTION" } - if err := req.AddParam("flags", flags); err != nil { + if err := cmd.SetParam("flags", flags); err != nil { return false, nil, err } - if err := req.AddParam("type", typ); err != nil { + if err := cmd.SetParam("type", typ); err != nil { return false, nil, err } if src != "" { - if err := req.AddParam("source", src); err != nil { + if err := cmd.SetParam("source", src); err != nil { return false, nil, err } } - resp, err := db.Query(req) + resp, err := db.Query(cmd) if err != nil { return false, nil, err } @@ -72,7 +74,7 @@ func (db *DB) ColumnCreate(tbl, name, typ, flags string) (bool, Response, error) } var result bool if err := json.Unmarshal(jsonData, &result); err != nil { - return false, resp, NewError(StatusInvalidResponse, map[string]interface{}{ + return false, resp, NewError(InvalidResponse, map[string]interface{}{ "method": "json.Unmarshal", "error": err.Error(), }) @@ -82,14 +84,14 @@ func (db *DB) ColumnCreate(tbl, name, typ, flags string) (bool, Response, error) // ColumnRemove executes column_remove. func (db *DB) ColumnRemove(tbl, name string) (bool, Response, error) { - req, err := NewRequest("column_remove", map[string]interface{}{ + cmd, err := NewCommand("column_remove", map[string]interface{}{ "table": tbl, "name": name, - }, nil) + }) if err != nil { return false, nil, err } - resp, err := db.Query(req) + resp, err := db.Query(cmd) if err != nil { return false, nil, err } @@ -100,7 +102,7 @@ func (db *DB) ColumnRemove(tbl, name string) (bool, Response, error) { } var result bool if err := json.Unmarshal(jsonData, &result); err != nil { - return false, resp, NewError(StatusInvalidResponse, map[string]interface{}{ + return false, resp, NewError(InvalidResponse, map[string]interface{}{ "method": "json.Unmarshal", "error": err.Error(), }) @@ -108,30 +110,30 @@ func (db *DB) ColumnRemove(tbl, name string) (bool, Response, error) { return result, resp, nil } -// DumpOptions stores options for DB.Dump. -type DumpOptions struct { +// DBDumpOptions stores options for DB.Dump. +type DBDumpOptions struct { Tables string // --table - DumpPlugins string // --dump_plugins - DumpSchema string // --dump_schema - DumpRecords string // --dump_records - DumpIndexes string // --dump_indexes + DumpPlugins bool // --dump_plugins + DumpSchema bool // --dump_schema + DumpRecords bool // --dump_records + DumpIndexes bool // --dump_indexes } -// NewDumpOptions returns the default DumpOptions. -func NewDumpOptions() *DumpOptions { - return &DumpOptions{ - DumpPlugins: "yes", - DumpSchema: "yes", - DumpRecords: "yes", - DumpIndexes: "yes", +// NewDBDumpOptions returns the default DBDumpOptions. +func NewDBDumpOptions() *DBDumpOptions { + return &DBDumpOptions{ + DumpPlugins: true, + DumpSchema: true, + DumpRecords: true, + DumpIndexes: true, } } // Dump executes dump. // On success, it is the caller's responsibility to close the response. -func (db *DB) Dump(options *DumpOptions) (Response, error) { +func (db *DB) Dump(options *DBDumpOptions) (Response, error) { if options == nil { - options = NewDumpOptions() + options = NewDBDumpOptions() } params := map[string]interface{}{ "dump_plugins": options.DumpPlugins, @@ -142,38 +144,41 @@ func (db *DB) Dump(options *DumpOptions) (Response, error) { if options.Tables != "" { params["tables"] = options.Tables } - req, err := NewRequest("dump", params, nil) - if err != nil { - return nil, err - } - return db.Query(req) + return db.Invoke("dump", params, nil) } -// LoadOptions stores options for DB.Load. +// DBLoadOptions stores options for DB.Load. // http://groonga.org/docs/reference/commands/load.html -type LoadOptions struct { - Columns string // --columns - IfExists string // --ifexists +type DBLoadOptions struct { + Columns []string // --columns + IfExists string // --ifexists +} + +// NewDBLoadOptions returns the default DBLoadOptions. +func NewDBLoadOptions() *DBLoadOptions { + return &DBLoadOptions{} } // Load executes load. -func (db *DB) Load(tbl string, values io.Reader, options *LoadOptions) (int, Response, error) { +func (db *DB) Load(tbl string, values io.Reader, options *DBLoadOptions) (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 - } + if options == nil { + options = NewDBLoadOptions() } - req, err := NewRequest("load", params, values) + if options.Columns != nil { + params["columns"] = options.Columns + } + if options.IfExists != "" { + params["ifexists"] = options.IfExists + } + cmd, err := NewCommand("load", params) if err != nil { return 0, nil, err } - resp, err := db.Query(req) + cmd.SetBody(values) + resp, err := db.Query(cmd) if err != nil { return 0, nil, err } @@ -184,7 +189,7 @@ func (db *DB) Load(tbl string, values io.Reader, options *LoadOptions) (int, Res } var result int if err := json.Unmarshal(jsonData, &result); err != nil { - return 0, resp, NewError(StatusInvalidResponse, map[string]interface{}{ + return 0, resp, NewError(InvalidResponse, map[string]interface{}{ "method": "json.Unmarshal", "error": err.Error(), }) @@ -192,99 +197,236 @@ func (db *DB) Load(tbl string, values io.Reader, options *LoadOptions) (int, Res 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 -// } +// encodeRow encodes a row. +func (db *DB) encodeRow(body []byte, row reflect.Value, fis []*StructFieldInfo) []byte { + // TODO + return body +} + +// encodeRows encodes rows. +func (db *DB) encodeRows(rows reflect.Value, fis []*StructFieldInfo) ([]byte, error) { + body := []byte("[") + for rows.Kind() == reflect.Ptr { + rows = rows.Elem() + } + switch rows.Kind() { + case reflect.Array, reflect.Slice: + for i := 0; i < rows.Len(); i++ { + row := rows.Index(i) + for row.Kind() == reflect.Ptr { + row = row.Elem() + } + body = db.encodeRow(body, row, fis) + } + case reflect.Struct: + } + body = append(body, ']') + return body, nil +} + +// LoadRows executes load. +func (db *DB) LoadRows(tbl string, rows interface{}, options *DBLoadOptions) (int, Response, error) { + if options == nil { + options = NewDBLoadOptions() + } + si, err := GetStructInfo(rows) + if err != nil { + return 0, nil, err + } + var fis []*StructFieldInfo + if options.Columns == nil { + fis = si.Fields + for _, fi := range fis { + options.Columns = append(options.Columns, fi.ColumnName) + } + } else { + for _, col := range options.Columns { + fi, ok := si.FieldsByColumnName[col] + if !ok { + return 0, nil, NewError(InvalidCommand, map[string]interface{}{ + "error": "", + }) + } + fis = append(fis, fi) + } + } + body, err := db.encodeRows(reflect.ValueOf(rows), fis) + if err != nil { + return 0, nil, err + } + return db.Load(tbl, bytes.NewReader(body), options) +} -// 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 -// } +// DBSelectOptionsColumn stores --columns[NAME]. +type DBSelectOptionsColumn struct { + Stage string // --columns[NAME].stage + Type string // --columns[NAME].type + Value string // --columns[NAME].value +} -// 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 -// } +// NewDBSelectOptionsColumn returns the default DBSelectOptionsColumn. +func NewDBSelectOptionsColumn() *DBSelectOptionsColumn { + return &DBSelectOptionsColumn{} +} -// NewSelectOptionsDrilldown returns the default SelectOptionsDrilldown. -// func NewSelectOptionsDrilldown() *SelectOptionsDrilldown { -// return &SelectOptionsDrilldown{ -// Limit: 10, -// } -// } +// DBSelectOptionsDrilldownColumn stores --drilldowns[LABEL].columns[NAME]. +type DBSelectOptionsDrilldownColumn 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 +} -// SelectOptions stores options for DB.Select. +// NewDBSelectOptionsDrilldownColumn returns the default DBSelectOptionsDrilldownColumn. +func NewDBSelectOptionsDrilldownColumn() *DBSelectOptionsDrilldownColumn { + return &DBSelectOptionsDrilldownColumn{} +} + +// DBSelectOptionsDrilldown stores --drilldowns[LABEL]. +type DBSelectOptionsDrilldown 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]*DBSelectOptionsDrilldownColumn +} + +// NewDBSelectOptionsDrilldown returns the default DBSelectOptionsDrilldown. +func NewDBSelectOptionsDrilldown() *DBSelectOptionsDrilldown { + return &DBSelectOptionsDrilldown{ + Limit: 10, + } +} + +// DBSelectOptions 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] +type DBSelectOptions 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]*DBSelectOptionsColumn + Drilldowns map[string]*DBSelectOptionsDrilldown } -// NewSelectOptions returns the default SelectOptions. -func NewSelectOptions() *SelectOptions { - return &SelectOptions{ +// NewDBSelectOptions returns the default DBSelectOptions. +func NewDBSelectOptions() *DBSelectOptions { + return &DBSelectOptions{ Limit: 10, - DrillDownLimit: 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) { +func (db *DB) Select(tbl string, options *DBSelectOptions) (Response, error) { if options == nil { - options = NewSelectOptions() + options = NewDBSelectOptions() } 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 + if options.MatchColumns != nil { + params["match_columns"] = options.MatchColumns + } + if options.Query != "" { + params["query"] = options.Query + } + if options.Filter != "" { + params["filter"] = options.Filter + } + if options.Scorer != "" { + params["scorer"] = options.Scorer + } + if options.SortKeys != nil { + params["sort_keys"] = options.SortKeys + } + if options.OutputColumns != nil { + params["query"] = options.Query + } + if options.Offset != 0 { + params["offset"] = options.Offset + } + if options.Limit != 10 { + params["limit"] = options.Limit + } + if options.Drilldown != "" { + params["drilldown"] = options.Drilldown + } + if options.DrilldownSortKeys != nil { + params["drilldown_sort_keys"] = options.DrilldownSortKeys + } + if options.DrilldownOutputColumns != nil { + params["drilldown_output_columns"] = options.DrilldownOutputColumns + } + if options.DrilldownOffset != 0 { + params["drilldown_offset"] = options.DrilldownOffset } - return db.Query(req) + if options.DrilldownLimit != 10 { + params["drilldown_limit"] = options.DrilldownLimit + } + if !options.Cache { + params["cache"] = options.Cache + } + if options.MatchEscalationThreshold != 0 { + params["match_escalation_threshold"] = options.MatchEscalationThreshold + } + if options.QueryExpansion != "" { + params["query_expansion"] = options.QueryExpansion + } + if options.QueryFlags != nil { + params["query_flags"] = options.QueryFlags + } + if options.QueryExpander != "" { + params["query_expander"] = options.QueryExpander + } + if options.Adjuster != "" { + params["adjuster"] = options.Adjuster + } + if options.DrilldownCalcTypes != nil { + params["drilldown_calc_types"] = options.DrilldownCalcTypes + } + if options.DrilldownCalcTarget != "" { + params["drilldown_calc_target"] = options.DrilldownCalcTarget + } + if options.DrilldownFilter != "" { + params["drilldown_filter"] = options.DrilldownFilter + } + return db.Invoke("select", params, nil) +} + +// SelectRows executes select. +func (db *DB) SelectRows(tbl string, rows interface{}, options *DBSelectOptions) (Response, error) { + // TODO + return nil, nil } -// StatusResult is a response of status. -type StatusResult struct { +// DBStatusResult is a response of status. +type DBStatusResult struct { AllocCount int `json:"alloc_count"` CacheHitRate float64 `json:"cache_hit_rate"` CommandVersion int `json:"command_version"` @@ -297,7 +439,7 @@ type StatusResult struct { } // Status executes status. -func (db *DB) Status() (*StatusResult, Response, error) { +func (db *DB) Status() (*DBStatusResult, Response, error) { resp, err := db.Exec("status", nil) if err != nil { return nil, nil, err @@ -309,12 +451,12 @@ func (db *DB) Status() (*StatusResult, Response, error) { } var data map[string]interface{} if err := json.Unmarshal(jsonData, &data); err != nil { - return nil, resp, NewError(StatusInvalidResponse, map[string]interface{}{ + return nil, resp, NewError(InvalidResponse, map[string]interface{}{ "method": "json.Unmarshal", "error": err.Error(), }) } - var result StatusResult + var result DBStatusResult if v, ok := data["alloc_count"]; ok { if v, ok := v.(float64); ok { result.AllocCount = int(v) @@ -363,48 +505,67 @@ func (db *DB) Status() (*StatusResult, Response, error) { return &result, resp, nil } -// TableCreateOptions stores options for DB.TableCreate. +// DBTableCreateOptions 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 +type DBTableCreateOptions struct { + Flags []string // --flags + KeyType string // --key_type + ValueType string // --value_type + DefaultTokenizer string // --default_tokenizer + Normalizer string // --normalizer + TokenFilters []string // --token_filters +} + +// NewDBTableCreateOptions returns the default DBTableCreateOptions. +func NewDBTableCreateOptions() *DBTableCreateOptions { + return &DBTableCreateOptions{} } // TableCreate executes table_create. -func (db *DB) TableCreate(name string, options *TableCreateOptions) (bool, Response, error) { +func (db *DB) TableCreate(name string, options *DBTableCreateOptions) (bool, Response, error) { if options == nil { - options = &TableCreateOptions{} + options = NewDBTableCreateOptions() } params := map[string]interface{}{ "name": name, } - flags, keyFlag := "", "" - if options.Flags != "" { - for _, flag := range strings.Split(options.Flags, "|") { + flags := options.Flags + var keyFlag string + if options.Flags != nil { + for _, flag := range flags { switch flag { case "TABLE_NO_KEY": if keyFlag != "" { - return false, nil, fmt.Errorf("TABLE_NO_KEY must not be set with %s", keyFlag) + return false, nil, NewError(InvalidCommand, map[string]interface{}{ + "flags": flags, + "error": "The combination of flags is wrong.", + }) } if options.KeyType != "" { - return false, nil, fmt.Errorf("TABLE_NO_KEY disallows KeyType") + return false, nil, NewError(InvalidCommand, map[string]interface{}{ + "flags": flags, + "key_type": options.KeyType, + "error": "TABLE_NO_KEY denies key_type.", + }) } 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) + return false, nil, NewError(InvalidCommand, map[string]interface{}{ + "flags": flags, + "error": "The combination of flags is wrong.", + }) } if options.KeyType == "" { - return false, nil, fmt.Errorf("%s requires KeyType", flag) + return false, nil, NewError(InvalidCommand, map[string]interface{}{ + "flags": flags, + "key_type": options.KeyType, + "error": fmt.Sprintf("%s requires key_type.", flag), + }) } keyFlag = flag } } - flags = options.Flags } if keyFlag == "" { if options.KeyType == "" { @@ -412,15 +573,11 @@ func (db *DB) TableCreate(name string, options *TableCreateOptions) (bool, Respo } else { keyFlag = "TABLE_HASH_KEY" } - if flags == "" { - flags = keyFlag - } else { - flags += "|" + keyFlag + if len(flags) == 0 { + flags = append(flags, keyFlag) } } - if flags != "" { - params["flags"] = flags - } + params["flags"] = flags if options.KeyType != "" { params["key_type"] = options.KeyType } @@ -433,7 +590,7 @@ func (db *DB) TableCreate(name string, options *TableCreateOptions) (bool, Respo if options.Normalizer != "" { params["normalizer"] = options.Normalizer } - if options.TokenFilters != "" { + if options.TokenFilters != nil { params["token_filters"] = options.TokenFilters } resp, err := db.Invoke("table_create", params, nil) @@ -447,7 +604,7 @@ func (db *DB) TableCreate(name string, options *TableCreateOptions) (bool, Respo } var result bool if err := json.Unmarshal(jsonData, &result); err != nil { - return false, resp, NewError(StatusInvalidResponse, map[string]interface{}{ + return false, resp, NewError(InvalidResponse, map[string]interface{}{ "method": "json.Unmarshal", "error": err.Error(), }) @@ -457,14 +614,14 @@ func (db *DB) TableCreate(name string, options *TableCreateOptions) (bool, Respo // TableRemove executes table_remove. func (db *DB) TableRemove(name string, dependent bool) (bool, Response, error) { - req, err := NewRequest("table_remove", map[string]interface{}{ + cmd, err := NewCommand("table_remove", map[string]interface{}{ "name": name, "dependent": dependent, - }, nil) + }) if err != nil { return false, nil, err } - resp, err := db.Query(req) + resp, err := db.Query(cmd) if err != nil { return false, nil, err } @@ -475,7 +632,7 @@ func (db *DB) TableRemove(name string, dependent bool) (bool, Response, error) { } var result bool if err := json.Unmarshal(jsonData, &result); err != nil { - return false, resp, NewError(StatusInvalidResponse, map[string]interface{}{ + return false, resp, NewError(InvalidResponse, map[string]interface{}{ "method": "json.Unmarshal", "error": err.Error(), }) @@ -498,7 +655,7 @@ func (db *DB) Truncate(target string) (bool, Response, error) { } var result bool if err := json.Unmarshal(jsonData, &result); err != nil { - return false, resp, NewError(StatusInvalidResponse, map[string]interface{}{ + return false, resp, NewError(InvalidResponse, map[string]interface{}{ "method": "json.Unmarshal", "error": err.Error(), }) Modified: v2/db_test.go (+67 -0) =================================================================== --- v2/db_test.go 2017-06-14 10:41:58 +0900 (ce22d55) +++ v2/db_test.go 2017-06-16 10:52:57 +0900 (9ac8a08) @@ -1,7 +1,9 @@ package grnci import ( + "io/ioutil" "log" + "strings" "testing" ) @@ -24,6 +26,71 @@ func TestDBColumnRemove(t *testing.T) { } } +func TestDBDump(t *testing.T) { + client, err := NewHTTPClient("", nil) + if err != nil { + t.Skipf("NewHTTPClient failed: %v", err) + } + db := NewDB(client) + defer db.Close() + + resp, err := db.Dump(nil) + if err != nil { + t.Fatalf("db.Dump failed: %v", err) + } + result, err := ioutil.ReadAll(resp) + if err != nil { + t.Fatalf("ioutil.ReadAll failed: %v", err) + } + log.Printf("result = %s", result) + log.Printf("resp = %#v", resp) + if err := resp.Err(); err != nil { + log.Printf("error = %#v", err) + } +} + +func TestDBLoad(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.Load("Tbl", strings.NewReader("[]"), nil) + if err != nil { + t.Fatalf("db.Dump failed: %v", err) + } + log.Printf("result = %d", result) + log.Printf("resp = %#v", resp) + if err := resp.Err(); err != nil { + log.Printf("error = %#v", err) + } +} + +func TestDBSelect(t *testing.T) { + client, err := NewHTTPClient("", nil) + if err != nil { + t.Skipf("NewHTTPClient failed: %v", err) + } + db := NewDB(client) + defer db.Close() + + resp, err := db.Select("Tbl", nil) + if err != nil { + t.Fatalf("db.Select failed: %v", err) + } + result, err := ioutil.ReadAll(resp) + if err != nil { + t.Fatalf("ioutil.ReadAll failed: %v", err) + } + log.Printf("result = %s", 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 { Modified: v2/error.go (+26 -22) =================================================================== --- v2/error.go 2017-06-14 10:41:58 +0900 (2f50085) +++ v2/error.go 2017-06-16 10:52:57 +0900 (fae8b6a) @@ -5,23 +5,22 @@ import ( "net/http" ) -// Error status codes. +// Error codes. const ( - StatusInvalidAddress = 1000 + iota - StatusInvalidCommand - StatusInvalidOperation - StatusInvalidResponse - StatusNetworkError - StatusUnknownError + InvalidAddress = 1000 + iota + InvalidCommand + InvalidOperation + InvalidResponse + InvalidType + NetworkError + UnknownError ) -// StatusText returns a status text. -func StatusText(status int) string { - text := http.StatusText(status) - if text != "" { - return text - } - switch status { +// getCodeText returns a string that briefly describes the specified code. +// getCodeText supports Groonga return codes (C.grn_rc) [,0], +// Grnci error codes [1000,] and HTTP status codes [100,999]. +func getCodeText(code int) string { + switch code { case 0: return "GRN_SUCCESS" case 1: @@ -185,20 +184,25 @@ func StatusText(status int) string { case -79: return "GRN_ZSTD_ERROR" - case StatusInvalidAddress: + case InvalidAddress: return "invalid address" - case StatusInvalidCommand: + case InvalidCommand: return "invalid command" - case StatusInvalidOperation: + case InvalidOperation: return "invalid operation" - case StatusInvalidResponse: + case InvalidResponse: return "invalid response" - case StatusNetworkError: + case InvalidType: + return "invalid type" + case NetworkError: return "network error" - case StatusUnknownError: + case UnknownError: return "unknown error" default: + if text := http.StatusText(code); text != "" { + return text + } return "undefined error" } } @@ -214,7 +218,7 @@ type Error struct { func NewError(code int, data map[string]interface{}) *Error { return &Error{ Code: code, - Text: StatusText(code), + Text: getCodeText(code), Data: data, } } @@ -239,7 +243,7 @@ func EnhanceError(err error, data map[string]interface{}) *Error { } else if _, ok := data["error"]; !ok { data["error"] = err.Error() } - return NewError(StatusUnknownError, data) + return NewError(UnknownError, data) } // Error returns a string which describes the Error. Modified: v2/error_test.go (+32 -27) =================================================================== --- v2/error_test.go 2017-06-14 10:41:58 +0900 (2ab83c5) +++ v2/error_test.go 2017-06-16 10:52:57 +0900 (8cc4292) @@ -3,44 +3,49 @@ package grnci import "testing" func TestNewError(t *testing.T) { - err := NewError(StatusInvalidAddress, map[string]interface{}{ - "key": "value", - }) - if err.Code != StatusInvalidAddress { + data := map[string]interface{}{ + "string": "value", + "int": 100, + } + err := NewError(InvalidAddress, data) + if err.Code != InvalidAddress { t.Fatalf("NewError failed: Code: actual = %d, want = %d", - err.Code, StatusInvalidAddress) + err.Code, InvalidAddress) } - if err.Text != StatusText(StatusInvalidAddress) { + if err.Text != getCodeText(InvalidAddress) { t.Fatalf("NewError failed: Text: actual = %s, want = %s", - err.Text, StatusText(StatusInvalidAddress)) + err.Text, getCodeText(InvalidAddress)) } - if err.Data["key"] != "value" { - t.Fatalf("NewError failed: Data[\"key\"]: actual = %s, want = %s", - err.Data["key"], "value") + for k, v := range data { + if err.Data[k] != v { + t.Fatalf("NewError failed: Data[\"key\"]: actual = %s, want = %s", err.Data[k], v) + } } } func TestEnhanceError(t *testing.T) { - err := NewError(StatusInvalidAddress, map[string]interface{}{ - "key": "value", - }) - err = EnhanceError(err, map[string]interface{}{ - "newKey": "newValue", - }) - if err.Code != StatusInvalidAddress { + data := map[string]interface{}{ + "string": "value", + "int": 100, + } + newData := map[string]interface{}{ + "string": "value2", + "int": 1000, + "float": 1.0, + } + err := NewError(InvalidAddress, data) + err = EnhanceError(err, newData) + if err.Code != InvalidAddress { t.Fatalf("NewError failed: Code: actual = %d, want = %d", - err.Code, StatusInvalidAddress) + err.Code, InvalidAddress) } - if err.Text != StatusText(StatusInvalidAddress) { + if err.Text != getCodeText(InvalidAddress) { t.Fatalf("NewError failed: Text: actual = %s, want = %s", - err.Text, StatusText(StatusInvalidAddress)) - } - if err.Data["key"] != "value" { - t.Fatalf("NewError failed: Data[\"key\"]: actual = %s, want = %s", - err.Data["key"], "value") + err.Text, getCodeText(InvalidAddress)) } - if err.Data["newKey"] != "newValue" { - t.Fatalf("NewError failed: Data[\"newKey\"]: actual = %s, want = %s", - err.Data["newKey"], "newValue") + for k, v := range newData { + if err.Data[k] != v { + t.Fatalf("NewError failed: Data[\"key\"]: actual = %s, want = %s", err.Data[k], v) + } } } Modified: v2/gqtp.go (+59 -56) =================================================================== --- v2/gqtp.go 2017-06-14 10:41:58 +0900 (c83aa83) +++ v2/gqtp.go 2017-06-16 10:52:57 +0900 (8ad8413) @@ -69,29 +69,12 @@ func newGQTPResponse(conn *GQTPConn, head gqtpHeader, start time.Time, name stri left: int(head.Size), } if head.Status > 32767 { - status := int(head.Status) - 65536 - resp.err = NewError(status, nil) - if _, ok := CommandRules[name]; !ok { - data, err := ioutil.ReadAll(resp) - if err != nil { - resp.broken = true - } else { - resp.err = EnhanceError(resp.err, map[string]interface{}{ - "error": string(data), - }) - } - } + code := int(head.Status) - 65536 + resp.err = NewError(code, nil) } return resp } -func (r *gqtpResponse) Status() int { - if err, ok := r.err.(*Error); ok { - return err.Code - } - return 0 -} - func (r *gqtpResponse) Start() time.Time { return r.start } @@ -126,7 +109,7 @@ func (r *gqtpResponse) Read(p []byte) (int, error) { } if err != nil { r.broken = true - return n, NewError(StatusNetworkError, map[string]interface{}{ + return n, NewError(NetworkError, map[string]interface{}{ "method": "net.Conn.Read", "n": n, "error": err.Error(), @@ -142,7 +125,7 @@ func (r *gqtpResponse) Close() error { var err error if _, e := io.CopyBuffer(ioutil.Discard, r, r.conn.getBuffer()); e != nil { r.broken = true - err = NewError(StatusNetworkError, map[string]interface{}{ + err = NewError(NetworkError, map[string]interface{}{ "method": "io.CopyBuffer", "error": err.Error(), }) @@ -190,7 +173,7 @@ func DialGQTP(addr string) (*GQTPConn, error) { } conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", a.Host, a.Port)) if err != nil { - return nil, NewError(StatusNetworkError, map[string]interface{}{ + return nil, NewError(NetworkError, map[string]interface{}{ "host": a.Host, "port": a.Port, "error": err.Error(), @@ -211,7 +194,7 @@ func NewGQTPConn(conn net.Conn) *GQTPConn { // Close closes the connection. func (c *GQTPConn) Close() error { if err := c.conn.Close(); err != nil { - return NewError(StatusNetworkError, map[string]interface{}{ + return NewError(NetworkError, map[string]interface{}{ "method": "net.Conn.Close", "error": err.Error(), }) @@ -243,7 +226,7 @@ func (c *GQTPConn) sendHeader(flags byte, size int) error { Size: uint32(size), } if err := binary.Write(c.conn, binary.BigEndian, head); err != nil { - return NewError(StatusNetworkError, map[string]interface{}{ + return NewError(NetworkError, map[string]interface{}{ "method": "binary.Write", "error": err.Error(), }) @@ -257,7 +240,7 @@ func (c *GQTPConn) sendChunkBytes(data []byte, flags byte) error { return err } if _, err := c.conn.Write(data); err != nil { - return NewError(StatusNetworkError, map[string]interface{}{ + return NewError(NetworkError, map[string]interface{}{ "method": "net.Conn.Write", "error": err.Error(), }) @@ -271,7 +254,7 @@ func (c *GQTPConn) sendChunkString(data string, flags byte) error { return err } if _, err := io.WriteString(c.conn, data); err != nil { - return NewError(StatusNetworkError, map[string]interface{}{ + return NewError(NetworkError, map[string]interface{}{ "method": "io.WriteString", "error": err.Error(), }) @@ -283,7 +266,7 @@ func (c *GQTPConn) sendChunkString(data string, flags byte) error { func (c *GQTPConn) recvHeader() (gqtpHeader, error) { var head gqtpHeader if err := binary.Read(c.conn, binary.BigEndian, &head); err != nil { - return head, NewError(StatusNetworkError, map[string]interface{}{ + return head, NewError(NetworkError, map[string]interface{}{ "method": "binary.Read", "error": err.Error(), }) @@ -291,8 +274,8 @@ func (c *GQTPConn) recvHeader() (gqtpHeader, error) { return head, nil } -// exec sends a command without body and receives a response. -func (c *GQTPConn) exec(cmd string) (Response, error) { +// execNoBody sends a command without body and receives a response. +func (c *GQTPConn) execNoBody(cmd string) (Response, error) { start := time.Now() name := strings.TrimLeft(cmd, " \t\r\n") if idx := strings.IndexAny(name, " \t\r\n"); idx != -1 { @@ -356,44 +339,54 @@ func (c *GQTPConn) execBody(cmd string, body io.Reader) (Response, error) { } } -// Exec sends a request and receives a response. -// It is the caller's responsibility to close the response. -// The GQTPConn should not be used until the response is closed. -func (c *GQTPConn) Exec(cmd string, body io.Reader) (Response, error) { +// exec sends a command without body and receives a response. +func (c *GQTPConn) exec(cmd string, body io.Reader) (Response, error) { if !c.ready { - return nil, NewError(StatusInvalidOperation, map[string]interface{}{ - "error": "The connection is not ready to send a request.", + return nil, NewError(InvalidOperation, map[string]interface{}{ + "error": "The connection is not ready to send a command.", }) } if len(cmd) > gqtpMaxChunkSize { - return nil, NewError(StatusInvalidCommand, map[string]interface{}{ + return nil, NewError(InvalidCommand, map[string]interface{}{ "length": len(cmd), "error": "The command is too long.", }) } c.ready = false if body == nil { - return c.exec(cmd) + return c.execNoBody(cmd) } return c.execBody(cmd, body) } -// 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) +// Exec parses cmd, reassembles it and calls Query. +// The GQTPConn must not be used until the response is closed. +func (c *GQTPConn) Exec(cmd string, body io.Reader) (Response, error) { + command, err := ParseCommand(cmd) if err != nil { return nil, err } - return c.Query(req) + command.SetBody(body) + return c.Query(command) } -// Query calls Exec with req.GQTPRequest and returns the result. -func (c *GQTPConn) Query(req *Request) (Response, error) { - cmd, body, err := req.GQTPRequest() +// Invoke assembles name, params and body into a command and calls Query. +func (c *GQTPConn) Invoke(name string, params map[string]interface{}, body io.Reader) (Response, error) { + cmd, err := NewCommand(name, params) if err != nil { return nil, err } - return c.Exec(cmd, body) + cmd.SetBody(body) + return c.Query(cmd) +} + +// Query sends a command and receives a response. +// It is the caller's responsibility to close the response. +func (c *GQTPConn) Query(cmd *Command) (Response, error) { + if err := cmd.Check(); err != nil { + return nil, err + } + return c.exec(cmd.String(), cmd.Body()) } // GQTPClient is a thread-safe GQTP client. @@ -438,9 +431,8 @@ func (c *GQTPClient) Close() error { } } -// Exec sends a request and receives a response. -// It is the caller's responsibility to close the response. -func (c *GQTPClient) Exec(cmd string, body io.Reader) (Response, error) { +// exec sends a request and receives a response. +func (c *GQTPClient) exec(cmd string, body io.Reader) (Response, error) { var conn *GQTPConn var err error select { @@ -460,20 +452,31 @@ 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) +// Exec parses cmd, reassembles it and calls Query. +func (c *GQTPClient) Exec(cmd string, body io.Reader) (Response, error) { + command, err := ParseCommand(cmd) if err != nil { return nil, err } - return c.Query(req) + command.SetBody(body) + return c.Query(command) } -// Query calls Exec with req.GQTPRequest and returns the result. -func (c *GQTPClient) Query(req *Request) (Response, error) { - cmd, body, err := req.GQTPRequest() +// Invoke assembles name, params and body into a command and calls Query. +func (c *GQTPClient) Invoke(name string, params map[string]interface{}, body io.Reader) (Response, error) { + cmd, err := NewCommand(name, params) if err != nil { return nil, err } - return c.Exec(cmd, body) + cmd.SetBody(body) + return c.Query(cmd) +} + +// Query sends a command and receives a response. +// It is the caller's responsibility to close the response. +func (c *GQTPClient) Query(cmd *Command) (Response, error) { + if err := cmd.Check(); err != nil { + return nil, err + } + return c.exec(cmd.String(), cmd.Body()) } Modified: v2/gqtp_test.go (+8 -4) =================================================================== --- v2/gqtp_test.go 2017-06-14 10:41:58 +0900 (e334f64) +++ v2/gqtp_test.go 2017-06-16 10:52:57 +0900 (a012671) @@ -14,7 +14,7 @@ func TestGQTPConn(t *testing.T) { Body string } pairs := []Pair{ - Pair{"no_such_command", ""}, + // Pair{"no_such_command", ""}, Pair{"status", ""}, Pair{`table_create Tbl TABLE_PAT_KEY ShortText`, ""}, Pair{`column_create Tbl Col COLUMN_SCALAR Int32`, ""}, @@ -46,9 +46,11 @@ func TestGQTPConn(t *testing.T) { if err != nil { t.Fatalf("ioutil.ReadAll failed: %v", err) } - log.Printf("status = %d, err = %v", resp.Status(), resp.Err()) log.Printf("start = %v, elapsed = %v", resp.Start(), resp.Elapsed()) log.Printf("result = %s", result) + if err := resp.Err(); err != nil { + log.Printf("err = %v", err) + } if err := resp.Close(); err != nil { t.Fatalf("resp.Close failed: %v", err) } @@ -61,7 +63,7 @@ func TestGQTPClient(t *testing.T) { Body string } pairs := []Pair{ - Pair{"no_such_command", ""}, + // Pair{"no_such_command", ""}, Pair{"status", ""}, Pair{`table_create Tbl TABLE_PAT_KEY ShortText`, ""}, Pair{`column_create Tbl Col COLUMN_SCALAR Int32`, ""}, @@ -93,9 +95,11 @@ func TestGQTPClient(t *testing.T) { if err != nil { t.Fatalf("ioutil.ReadAll failed: %v", err) } - log.Printf("status = %d, err = %v", resp.Status(), resp.Err()) log.Printf("start = %v, elapsed = %v", resp.Start(), resp.Elapsed()) log.Printf("result = %s", result) + if err := resp.Err(); err != nil { + log.Printf("err = %v", err) + } if err := resp.Close(); err != nil { t.Fatalf("resp.Close failed: %v", err) } Modified: v2/handler.go (+2 -2) =================================================================== --- v2/handler.go 2017-06-14 10:41:58 +0900 (0ecc56d) +++ v2/handler.go 2017-06-16 10:52:57 +0900 (8fcb3b2) @@ -5,7 +5,7 @@ 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) + Invoke(name string, params map[string]interface{}, body io.Reader) (Response, error) + Query(cmd *Command) (Response, error) Close() error } Modified: v2/http.go (+41 -51) =================================================================== --- v2/http.go 2017-06-14 10:41:58 +0900 (fd81667) +++ v2/http.go 2017-06-16 10:52:57 +0900 (39bddd9) @@ -31,7 +31,7 @@ type httpResponse struct { func extractHTTPResponseHeader(data []byte) (head, left []byte, err error) { left = bytes.TrimLeft(data[1:], " \t\r\n") if !bytes.HasPrefix(left, []byte("[")) { - err = NewError(StatusInvalidResponse, map[string]interface{}{ + err = NewError(InvalidResponse, map[string]interface{}{ "error": "The response does not contain a header.", }) return @@ -47,7 +47,7 @@ Loop: stack = append(stack, '}') case ']', '}': if left[i] != stack[len(stack)-1] { - err = NewError(StatusInvalidResponse, map[string]interface{}{ + err = NewError(InvalidResponse, map[string]interface{}{ "error": "The response header is broken.", }) return @@ -69,7 +69,7 @@ Loop: } } if len(stack) != 0 { - err = NewError(StatusInvalidResponse, map[string]interface{}{ + err = NewError(InvalidResponse, map[string]interface{}{ "error": "The response header is too long or broken.", }) return @@ -83,8 +83,8 @@ Loop: } // parseHTTPResponseHeaderError parses the error information in the HTTP resonse header. -func parseHTTPResponseHeaderError(status int, elems []interface{}) error { - err := NewError(status, nil) +func parseHTTPResponseHeaderError(code int, elems []interface{}) error { + err := NewError(code, nil) if len(elems) >= 1 { err = EnhanceError(err, map[string]interface{}{ "message": elems[0], @@ -131,28 +131,28 @@ func parseHTTPResponseHeader(resp *http.Response, data []byte) (*httpResponse, e var elems []interface{} if err := json.Unmarshal(head, &elems); err != nil { - return nil, NewError(StatusInvalidResponse, map[string]interface{}{ + return nil, NewError(InvalidResponse, map[string]interface{}{ "method": "json.Unmarshal", "error": err.Error(), }) } if len(elems) < 3 { - return nil, NewError(StatusInvalidResponse, map[string]interface{}{ + return nil, NewError(InvalidResponse, map[string]interface{}{ "method": "json.Unmarshal", "error": "Too few elements in the response header.", }) } f, ok := elems[0].(float64) if !ok { - return nil, NewError(StatusInvalidResponse, map[string]interface{}{ - "status": elems[0], - "error": "status must be a number.", + return nil, NewError(InvalidResponse, map[string]interface{}{ + "code": elems[0], + "error": "code must be a number.", }) } - status := int(f) + code := int(f) f, ok = elems[1].(float64) if !ok { - return nil, NewError(StatusInvalidResponse, map[string]interface{}{ + return nil, NewError(InvalidResponse, map[string]interface{}{ "start": elems[1], "error": "start must be a number.", }) @@ -161,15 +161,15 @@ func parseHTTPResponseHeader(resp *http.Response, data []byte) (*httpResponse, e start := time.Unix(int64(i), int64(math.Floor(f*1000000+0.5))*1000).Local() f, ok = elems[2].(float64) if !ok { - return nil, NewError(StatusInvalidResponse, map[string]interface{}{ + return nil, NewError(InvalidResponse, map[string]interface{}{ "elapsed": elems[2], "error": "elapsed must be a number.", }) } elapsed := time.Duration(f * float64(time.Second)) - if status != 0 { - err = parseHTTPResponseHeaderError(status, elems[3:]) + if code != 0 { + err = parseHTTPResponseHeaderError(code, elems[3:]) } return &httpResponse{ @@ -192,7 +192,7 @@ func newHTTPResponse(resp *http.Response, start time.Time) (*httpResponse, error break } if err != nil { - return nil, NewError(StatusNetworkError, map[string]interface{}{ + return nil, NewError(NetworkError, map[string]interface{}{ "method": "http.Response.Body.Read", "error": err.Error(), }) @@ -218,13 +218,6 @@ func newHTTPResponse(resp *http.Response, start time.Time) (*httpResponse, error }, nil } -func (r *httpResponse) Status() int { - if err, ok := r.err.(*Error); ok { - return err.Code - } - return 0 -} - func (r *httpResponse) Start() time.Time { return r.start } @@ -250,7 +243,7 @@ func (r *httpResponse) Read(p []byte) (n int, err error) { n-- } if err != io.EOF { - err = NewError(StatusNetworkError, map[string]interface{}{ + err = NewError(NetworkError, map[string]interface{}{ "method": "http.Response.Body.Read", "error": err.Error(), }) @@ -270,7 +263,7 @@ func (r *httpResponse) Read(p []byte) (n int, err error) { n-- } if err != io.EOF { - err = NewError(StatusNetworkError, map[string]interface{}{ + err = NewError(NetworkError, map[string]interface{}{ "method": "http.Response.Body.Read", "error": err.Error(), }) @@ -281,7 +274,7 @@ func (r *httpResponse) Read(p []byte) (n int, err error) { func (r *httpResponse) Close() error { io.Copy(ioutil.Discard, r.resp.Body) if err := r.resp.Body.Close(); err != nil { - return NewError(StatusNetworkError, map[string]interface{}{ + return NewError(NetworkError, map[string]interface{}{ "method": "http.Response.Body.Close", "error": err.Error(), }) @@ -310,7 +303,7 @@ func NewHTTPClient(addr string, client *http.Client) (*HTTPClient, error) { } url, err := url.Parse(a.String()) if err != nil { - return nil, NewError(StatusInvalidAddress, map[string]interface{}{ + return nil, NewError(InvalidAddress, map[string]interface{}{ "url": a.String(), "method": "url.Parse", "error": err.Error(), @@ -330,10 +323,11 @@ func (c *HTTPClient) Close() error { return nil } -// exec sends a request and receives a response. -func (c *HTTPClient) exec(cmd string, params map[string]string, body io.Reader) (*http.Response, error) { +// exec sends a command and receives a response. +func (c *HTTPClient) exec(name string, params map[string]string, body io.Reader) (Response, error) { + start := time.Now() url := *c.url - url.Path = path.Join(url.Path, cmd) + url.Path = path.Join(url.Path, name) if len(params) != 0 { query := url.Query() for k, v := range params { @@ -344,54 +338,50 @@ func (c *HTTPClient) exec(cmd string, params map[string]string, body io.Reader) if body == nil { resp, err := c.client.Get(url.String()) if err != nil { - return nil, NewError(StatusNetworkError, map[string]interface{}{ + return nil, NewError(NetworkError, map[string]interface{}{ "url": url.String(), "method": "http.Client.Get", "error": err.Error(), }) } - return resp, nil + return newHTTPResponse(resp, start) } resp, err := c.client.Post(url.String(), "application/json", body) if err != nil { - return nil, NewError(StatusNetworkError, map[string]interface{}{ + return nil, NewError(NetworkError, map[string]interface{}{ "url": url.String(), "method": "http.Client.Post", "error": err.Error(), }) } - return resp, nil + return newHTTPResponse(resp, start) } -// Exec assembles cmd and body into a Request and calls Query. +// Exec assembles cmd and body into a Command and calls Query. func (c *HTTPClient) Exec(cmd string, body io.Reader) (Response, error) { - req, err := ParseRequest(cmd, body) + command, err := ParseCommand(cmd) if err != nil { return nil, err } - return c.Query(req) + command.SetBody(body) + return c.Query(command) } -// 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) +// Invoke assembles name, params and body into a Command and calls Query. +func (c *HTTPClient) Invoke(name string, params map[string]interface{}, body io.Reader) (Response, error) { + cmd, err := NewCommand(name, params) if err != nil { return nil, err } - return c.Query(req) + cmd.SetBody(body) + return c.Query(cmd) } -// Query sends a request and receives a response. +// Query sends a command 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 { +func (c *HTTPClient) Query(cmd *Command) (Response, error) { + if err := cmd.Check(); err != nil { return nil, err } - resp, err := c.exec(cmd, params, body) - if err != nil { - return nil, err - } - return newHTTPResponse(resp, start) + return c.exec(cmd.Name(), cmd.Params(), cmd.Body()) } Modified: v2/http_test.go (+4 -2) =================================================================== --- v2/http_test.go 2017-06-14 10:41:58 +0900 (dd016d1) +++ v2/http_test.go 2017-06-16 10:52:57 +0900 (0cc8707) @@ -14,7 +14,7 @@ func TestHTTPClient(t *testing.T) { Body string } pairs := []Pair{ - Pair{"no_such_command", ""}, + // Pair{"no_such_command", ""}, Pair{"status", ""}, Pair{`table_create Tbl TABLE_PAT_KEY ShortText`, ""}, Pair{`column_create Tbl Col COLUMN_SCALAR Int32`, ""}, @@ -46,9 +46,11 @@ func TestHTTPClient(t *testing.T) { if err != nil { t.Fatalf("ioutil.ReadAll failed: %v", err) } - log.Printf("status = %d, err = %v", resp.Status(), resp.Err()) log.Printf("start = %v, elapsed = %v", resp.Start(), resp.Elapsed()) log.Printf("result = %s", result) + if err := resp.Err(); err != nil { + log.Printf("err = %v", err) + } if err := resp.Close(); err != nil { t.Fatalf("resp.Close failed: %v", err) } Added: v2/info.go (+156 -0) 100644 =================================================================== --- /dev/null +++ v2/info.go 2017-06-16 10:52:57 +0900 (8b8434f) @@ -0,0 +1,156 @@ +package grnci + +import ( + "fmt" + "reflect" + "strings" + "sync" + "time" +) + +const ( + // tagKey is the tag key for struct fields associated with Groonga columns. + tagKey = "grnci" + + // tagSep is the separator in a struct field tag value. + tagSep = ';' +) + +var ( + structInfos = make(map[reflect.Type]*StructInfo) + structInfosMutex sync.RWMutex +) + +type StructFieldInfo struct { + Index int // Field position + Field *reflect.StructField // Field + Type reflect.Type // Field's underlying type + Tags []string // Field tag semicolon-separated values + ColumnName string // Column name + Dimension int // Vector dimension +} + +// newStructFieldInfo returns a StructFieldInfo. +func newStructFieldInfo(index int, field *reflect.StructField) (*StructFieldInfo, error) { + tagValue := field.Tag.Get(tagKey) + tags := strings.Split(tagValue, ";") + for _, tag := range tags { + tag = strings.TrimSpace(tag) + } + if strings.HasSuffix(tags[0], "*") { + return nil, NewError(InvalidType, map[string]interface{}{ + "tag": tagValue, + "error": "The first tag must not end with '*'.", + }) + } + typ := field.Type + dim := 0 + for { + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + } else if typ.Kind() == reflect.Slice { + typ = typ.Elem() + dim++ + } else { + break + } + } + switch reflect.Zero(typ).Interface().(type) { + case bool: + case int, int8, int16, int32, int64: + case uint, uint8, uint16, uint32, uint64: + case float32, float64: + case string: + case time.Time: + case Geo: + default: + return nil, NewError(InvalidType, map[string]interface{}{ + "type": typ.Name(), + "error": "The type is not supported.", + }) + } + + return &StructFieldInfo{ + Index: index, + Field: field, + Tags: tags, + Type: field.Type, + Dimension: dim, + }, nil +} + +type StructInfo struct { + Type reflect.Type + Fields []*StructFieldInfo + FieldsByName map[string]*StructFieldInfo + FieldsByColumnName map[string]*StructFieldInfo +} + +// getStructInfo returns the StructInfo that represents typ. +func getStructInfo(typ reflect.Type) (*StructInfo, error) { + structInfosMutex.Lock() + defer structInfosMutex.Unlock() + if si, ok := structInfos[typ]; ok { + return si, nil + } + fis := make([]*StructFieldInfo, 0) + fisByName := make(map[string]*StructFieldInfo) + fisByColumnName := make(map[string]*StructFieldInfo) + for i := 0; i < typ.NumField(); i++ { + f := typ.Field(i) + if f.PkgPath != "" { // Skip unexported fields. + continue + } + tag := f.Tag.Get(tagKey) + if tag == "" || tag == "-" { // Skip untagged fields. + continue + } + fi, err := newStructFieldInfo(i, &f) + if err != nil { + return nil, err + } + fis = append(fis, fi) + fisByName[f.Name] = fi + if _, ok := fisByColumnName[fi.ColumnName]; ok { + return nil, NewError(InvalidType, map[string]interface{}{ + "columnName": fi.ColumnName, + "error": "The column name appears more than once.", + }) + } + fisByColumnName[fi.ColumnName] = fi + } + si := &StructInfo{ + Type: typ, + Fields: fis, + FieldsByName: fisByName, + FieldsByColumnName: fisByColumnName, + } + structInfos[typ] = si + return si, nil +} + +// GetStructInfo returns the StructInfo that represents the underlying struct of i. +// If i is nil or the underlying type is not a struct, GetStructInfo returns an error. +func GetStructInfo(v interface{}) (*StructInfo, error) { + if v == nil { + return nil, NewError(InvalidType, map[string]interface{}{ + "value": nil, + "error": "The value must not be nil.", + }) + } + typ := reflect.TypeOf(v) + for { + switch typ.Kind() { + case reflect.Ptr, reflect.Slice, reflect.Array: + typ = typ.Elem() + default: + if kind := typ.Kind(); kind != reflect.Struct { + return nil, NewError(InvalidType, map[string]interface{}{ + "kind": kind.String(), + "error": fmt.Sprintf("The kind must be %s.", reflect.Struct), + }) + } + return getStructInfo(typ) + } + } +} Modified: v2/libgrn/client.go (+21 -11) =================================================================== --- v2/libgrn/client.go 2017-06-14 10:41:58 +0900 (a873c47) +++ v2/libgrn/client.go 2017-06-16 10:52:57 +0900 (d36bd39) @@ -82,9 +82,8 @@ Loop: return err } -// Exec sends a request and receives a response. -// It is the caller's responsibility to close the response. -func (c *Client) Exec(cmd string, body io.Reader) (grnci.Response, error) { +// exec sends a command and receives a response. +func (c *Client) exec(cmd string, body io.Reader) (grnci.Response, error) { var conn *Conn var err error select { @@ -111,20 +110,31 @@ 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) +// Exec parses cmd, reassembles it and calls Query. +func (c *Client) Exec(cmd string, body io.Reader) (grnci.Response, error) { + command, err := grnci.ParseCommand(cmd) if err != nil { return nil, err } - return c.Query(req) + command.SetBody(body) + return c.Query(command) } -// 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() +// Invoke assembles name, params and body into a command and calls Query. +func (c *Client) Invoke(name string, params map[string]interface{}, body io.Reader) (grnci.Response, error) { + cmd, err := grnci.NewCommand(name, params) if err != nil { return nil, err } - return c.Exec(cmd, body) + cmd.SetBody(body) + return c.Query(cmd) +} + +// Query sends a command and receives a response. +// It is the caller's responsibility to close the response. +func (c *Client) Query(cmd *grnci.Command) (grnci.Response, error) { + if err := cmd.Check(); err != nil { + return nil, err + } + return c.exec(cmd.String(), cmd.Body()) } Modified: v2/libgrn/client_test.go (+8 -4) =================================================================== --- v2/libgrn/client_test.go 2017-06-14 10:41:58 +0900 (20f92ef) +++ v2/libgrn/client_test.go 2017-06-16 10:52:57 +0900 (634186f) @@ -16,7 +16,7 @@ func TestClientGQTP(t *testing.T) { Body string } pairs := []Pair{ - Pair{"no_such_command", ""}, + // Pair{"no_such_command", ""}, Pair{"status", ""}, Pair{`table_create Tbl TABLE_PAT_KEY ShortText`, ""}, Pair{`column_create Tbl Col COLUMN_SCALAR Int32`, ""}, @@ -48,9 +48,11 @@ func TestClientGQTP(t *testing.T) { if err != nil { t.Fatalf("ioutil.ReadAll failed: %v", err) } - log.Printf("status = %d, err = %v", resp.Status(), resp.Err()) log.Printf("start = %v, elapsed = %v", resp.Start(), resp.Elapsed()) log.Printf("result = %s", result) + if err := resp.Err(); err != nil { + log.Printf("err = %v", err) + } if err := resp.Close(); err != nil { t.Fatalf("resp.Close failed: %v", err) } @@ -63,7 +65,7 @@ func TestClientDB(t *testing.T) { Body string } pairs := []Pair{ - Pair{"no_such_command", ""}, + // Pair{"no_such_command", ""}, Pair{"status", ""}, Pair{`table_create Tbl TABLE_PAT_KEY ShortText`, ""}, Pair{`column_create Tbl Col COLUMN_SCALAR Int32`, ""}, @@ -95,9 +97,11 @@ func TestClientDB(t *testing.T) { if err != nil { t.Fatalf("ioutil.ReadAll failed: %v", err) } - log.Printf("status = %d, err = %v", resp.Status(), resp.Err()) log.Printf("start = %v, elapsed = %v", resp.Start(), resp.Elapsed()) log.Printf("result = %s", result) + if err := resp.Err(); err != nil { + log.Printf("err = %v", err) + } if err := resp.Close(); err != nil { t.Fatalf("resp.Close failed: %v", err) } Modified: v2/libgrn/conn.go (+35 -25) =================================================================== --- v2/libgrn/conn.go 2017-06-14 10:41:58 +0900 (677e503) +++ v2/libgrn/conn.go 2017-06-16 10:52:57 +0900 (872dd65) @@ -89,7 +89,7 @@ func Create(path string) (*Conn, error) { // Dup duplicates the Conn if it is a DB handle. func (c *Conn) Dup() (*Conn, error) { if c.db == nil { - return nil, grnci.NewError(grnci.StatusInvalidOperation, map[string]interface{}{ + return nil, grnci.NewError(grnci.InvalidOperation, map[string]interface{}{ "error": "GQTP clients do not support Dup.", }) } @@ -132,8 +132,8 @@ func (c *Conn) getBuffer() []byte { return c.buf } -// execGQTP sends a command and receives a response. -func (c *Conn) execGQTP(cmd string) (grnci.Response, error) { +// execNoBodyGQTP sends a command and receives a response. +func (c *Conn) execNoBodyGQTP(cmd string) (grnci.Response, error) { start := time.Now() name := strings.TrimLeft(cmd, " \t\r\n") if idx := strings.IndexAny(name, " \t\r\n"); idx != -1 { @@ -149,8 +149,8 @@ func (c *Conn) execGQTP(cmd string) (grnci.Response, error) { return newGQTPResponse(c, start, name, data, flags, err), nil } -// execDB executes a command and receives a response. -func (c *Conn) execDB(cmd string) (grnci.Response, error) { +// execNoBodyDB executes a command and receives a response. +func (c *Conn) execNoBodyDB(cmd string) (grnci.Response, error) { start := time.Now() if err := c.ctx.Send([]byte(cmd), flagTail); err != nil { data, flags, _ := c.ctx.Recv() @@ -160,12 +160,12 @@ func (c *Conn) execDB(cmd string) (grnci.Response, error) { return newDBResponse(c, start, data, flags, err), nil } -// exec sends a command without body and receives a response. -func (c *Conn) exec(cmd string) (grnci.Response, error) { +// execNoBody sends a command without body and receives a response. +func (c *Conn) execNoBody(cmd string) (grnci.Response, error) { if c.db == nil { - return c.execGQTP(cmd) + return c.execNoBodyGQTP(cmd) } - return c.execDB(cmd) + return c.execNoBodyDB(cmd) } // execBodyGQTP sends a command and receives a response. @@ -262,42 +262,52 @@ func (c *Conn) execBody(cmd string, body io.Reader) (grnci.Response, error) { return c.execBodyDB(cmd, body) } -// Exec sends a request and receives a response. -// It is the caller's responsibility to close the response. -// The Conn should not be used until the response is closed. -func (c *Conn) Exec(cmd string, body io.Reader) (grnci.Response, error) { +// exec sends a command and receives a response. +func (c *Conn) exec(cmd string, body io.Reader) (grnci.Response, error) { if !c.ready { - return nil, grnci.NewError(grnci.StatusInvalidOperation, map[string]interface{}{ - "error": "The connection is not ready to send a request.", + return nil, grnci.NewError(grnci.InvalidOperation, map[string]interface{}{ + "error": "The connection is not ready to send a command.", }) } if len(cmd) > maxChunkSize { - return nil, grnci.NewError(grnci.StatusInvalidCommand, map[string]interface{}{ + return nil, grnci.NewError(grnci.InvalidCommand, map[string]interface{}{ "length": len(cmd), "error": "The command is too long.", }) } c.ready = false if body == nil { - return c.exec(cmd) + return c.execNoBody(cmd) } 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) +// Exec parses cmd, reassembles it and calls Query. +// The Conn must not be used until the response is closed. +func (c *Conn) Exec(cmd string, body io.Reader) (grnci.Response, error) { + command, err := grnci.ParseCommand(cmd) if err != nil { return nil, err } - return c.Query(req) + command.SetBody(body) + return c.Query(command) } -// 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() +// Invoke assembles name, params and body into a command and calls Query. +func (c *Conn) Invoke(name string, params map[string]interface{}, body io.Reader) (grnci.Response, error) { + cmd, err := grnci.NewCommand(name, params) if err != nil { return nil, err } - return c.Exec(cmd, body) + cmd.SetBody(body) + return c.Query(cmd) +} + +// Query sends a command and receives a response. +// It is the caller's responsibility to close the response. +func (c *Conn) Query(cmd *grnci.Command) (grnci.Response, error) { + if err := cmd.Check(); err != nil { + return nil, err + } + return c.exec(cmd.String(), cmd.Body()) } Modified: v2/libgrn/conn_test.go (+8 -4) =================================================================== --- v2/libgrn/conn_test.go 2017-06-14 10:41:58 +0900 (b9a39b9) +++ v2/libgrn/conn_test.go 2017-06-16 10:52:57 +0900 (87a5b25) @@ -16,7 +16,7 @@ func TestConnGQTP(t *testing.T) { Body string } pairs := []Pair{ - Pair{"no_such_command", ""}, + // Pair{"no_such_command", ""}, Pair{"status", ""}, Pair{`table_create Tbl TABLE_PAT_KEY ShortText`, ""}, Pair{`column_create Tbl Col COLUMN_SCALAR Int32`, ""}, @@ -48,9 +48,11 @@ func TestConnGQTP(t *testing.T) { if err != nil { t.Fatalf("ioutil.ReadAll failed: %v", err) } - log.Printf("status = %d, err = %v", resp.Status(), resp.Err()) log.Printf("start = %v, elapsed = %v", resp.Start(), resp.Elapsed()) log.Printf("result = %s", result) + if err := resp.Err(); err != nil { + log.Printf("err = %v", err) + } if err := resp.Close(); err != nil { t.Fatalf("resp.Close failed: %v", err) } @@ -63,7 +65,7 @@ func TestConnDB(t *testing.T) { Body string } pairs := []Pair{ - Pair{"no_such_command", ""}, + // Pair{"no_such_command", ""}, Pair{"status", ""}, Pair{`table_create Tbl TABLE_PAT_KEY ShortText`, ""}, Pair{`column_create Tbl Col COLUMN_SCALAR Int32`, ""}, @@ -95,9 +97,11 @@ func TestConnDB(t *testing.T) { if err != nil { t.Fatalf("ioutil.ReadAll failed: %v", err) } - log.Printf("status = %d, err = %v", resp.Status(), resp.Err()) log.Printf("start = %v, elapsed = %v", resp.Start(), resp.Elapsed()) log.Printf("result = %s", result) + if err := resp.Err(); err != nil { + log.Printf("err = %v", err) + } if err := resp.Close(); err != nil { t.Fatalf("resp.Close failed: %v", err) } Modified: v2/libgrn/libgrn.go (+5 -5) =================================================================== --- v2/libgrn/libgrn.go 2017-06-14 10:41:58 +0900 (07e3edf) +++ v2/libgrn/libgrn.go 2017-06-16 10:52:57 +0900 (030983e) @@ -63,7 +63,7 @@ func Fin() error { libMutex.Lock() defer libMutex.Unlock() if libCount <= 0 { - return grnci.NewError(grnci.StatusInvalidOperation, map[string]interface{}{ + return grnci.NewError(grnci.InvalidOperation, map[string]interface{}{ "libCount": libCount, "error": "libCount must be greater than 0.", }) @@ -100,7 +100,7 @@ func newGrnCtx() (*grnCtx, error) { ctx := C.grn_ctx_open(C.int(0)) if ctx == nil { Fin() - return nil, grnci.NewError(grnci.StatusUnknownError, map[string]interface{}{ + return nil, grnci.NewError(grnci.UnknownError, map[string]interface{}{ "method": "C.grn_ctx_open", }) } @@ -193,7 +193,7 @@ func createGrnDB(ctx *grnCtx, path string) (*grnDB, error) { if err := ctx.Err("C.grn_db_create"); err != nil { return nil, err } - return nil, grnci.NewError(grnci.StatusUnknownError, map[string]interface{}{ + return nil, grnci.NewError(grnci.UnknownError, map[string]interface{}{ "method": "C.grn_db_create", }) } @@ -212,7 +212,7 @@ func openGrnDB(ctx *grnCtx, path string) (*grnDB, error) { if err := ctx.Err("C.grn_db_open"); err != nil { return nil, err } - return nil, grnci.NewError(grnci.StatusUnknownError, map[string]interface{}{ + return nil, grnci.NewError(grnci.UnknownError, map[string]interface{}{ "method": "C.grn_db_open", }) } @@ -227,7 +227,7 @@ func (db *grnDB) Close(ctx *grnCtx) error { db.mutex.Lock() defer db.mutex.Unlock() if db.count <= 0 { - return grnci.NewError(grnci.StatusInvalidOperation, map[string]interface{}{ + return grnci.NewError(grnci.InvalidOperation, map[string]interface{}{ "count": db.count, "error": "count must be greater than 0.", }) Modified: v2/libgrn/response.go (+2 -22) =================================================================== --- v2/libgrn/response.go 2017-06-14 10:41:58 +0900 (f73e353) +++ v2/libgrn/response.go 2017-06-16 10:52:57 +0900 (a3fb52e) @@ -23,7 +23,7 @@ type response struct { // newGQTPResponse returns a new GQTP response. func newGQTPResponse(conn *Conn, start time.Time, name string, data []byte, flags byte, err error) *response { - resp := &response{ + return &response{ conn: conn, start: start, elapsed: time.Now().Sub(start), @@ -31,18 +31,6 @@ func newGQTPResponse(conn *Conn, start time.Time, name string, data []byte, flag flags: flags, err: err, } - if resp.err != nil { - if _, ok := grnci.CommandRules[name]; !ok { - data, err := ioutil.ReadAll(resp) - resp.err = grnci.EnhanceError(resp.err, map[string]interface{}{ - "error": string(data), - }) - if err != nil { - resp.broken = true - } - } - } - return resp } // newDBResponse returns a new DB response. @@ -57,14 +45,6 @@ func newDBResponse(conn *Conn, start time.Time, data []byte, flags byte, err err } } -// Status returns the status code. -func (r *response) Status() int { - if err, ok := r.err.(*grnci.Error); ok { - return err.Code - } - return 0 -} - // Start returns the start time. func (r *response) Start() time.Time { return r.start @@ -107,7 +87,7 @@ func (r *response) Close() error { if !r.broken { if _, err = io.CopyBuffer(ioutil.Discard, r, r.conn.getBuffer()); err != nil { r.broken = true - err = grnci.NewError(grnci.StatusNetworkError, map[string]interface{}{ + err = grnci.NewError(grnci.NetworkError, map[string]interface{}{ "method": "io.CopyBuffer", "error": err.Error(), }) Deleted: v2/request.go (+0 -354) 100644 =================================================================== --- v2/request.go 2017-06-14 10:41:58 +0900 (7488815) +++ /dev/null @@ -1,354 +0,0 @@ -package grnci - -import ( - "fmt" - "io" - "reflect" - "sort" - "strconv" - "strings" -) - -// Request is a request. -type Request struct { - Command string // Command name - CommandRule *CommandRule // Command rule - Params map[string]string // Command parameters - NAnonParams int // Number of unnamed parameters - 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]interface{}, body io.Reader) (*Request, error) { - if err := checkCommand(cmd); err != nil { - return nil, err - } - r := newRequest(cmd, body) - for k, v := range params { - if err := r.AddParam(k, v); err != nil { - return nil, EnhanceError(err, map[string]interface{}{ - "command": cmd, - }) - } - } - return r, nil -} - -// unescapeCommandByte returns an unescaped byte. -func unescapeCommandByte(b byte) byte { - switch b { - case 'b': - return '\b' - case 't': - return '\t' - case 'r': - return '\r' - case 'n': - return '\n' - default: - return b - } -} - -// tokenizeCommand tokenizes s as a command. -func tokenizeCommand(s string) []string { - var tokens []string - var token []byte - for { - s = strings.TrimLeft(s, " \t\r\n") - if len(s) == 0 { - break - } - switch s[0] { - case '"', '\'': - i := 1 - for ; i < len(s); i++ { - if s[i] == s[0] { - i++ - break - } - if s[i] != '\\' { - token = append(token, s[i]) - continue - } - i++ - if i == len(s) { - break - } - token = append(token, unescapeCommandByte(s[i])) - } - s = s[i:] - default: - i := 0 - Loop: - for ; i < len(s); i++ { - switch s[i] { - case ' ', '\t', '\r', '\n', '"', '\'': - break Loop - case '\\': - i++ - if i == len(s) { - break Loop - } - token = append(token, unescapeCommandByte(s[i])) - default: - token = append(token, s[i]) - } - } - s = s[i:] - } - tokens = append(tokens, string(token)) - token = token[:0] - } - return tokens -} - -// ParseRequest parses a request. -func ParseRequest(cmd string, body io.Reader) (*Request, error) { - tokens := tokenizeCommand(cmd) - if len(tokens) == 0 { - return nil, NewError(StatusInvalidCommand, map[string]interface{}{ - "tokens": tokens, - "error": "len(tokens) must not be 0.", - }) - } - if err := checkCommand(tokens[0]); err != nil { - return nil, err - } - r := newRequest(tokens[0], body) - for i := 1; i < len(tokens); i++ { - var k, v string - if strings.HasPrefix(tokens[i], "--") { - k = tokens[i][2:] - i++ - if i < len(tokens) { - v = tokens[i] - } - } else { - v = tokens[i] - } - if err := r.AddParam(k, v); err != nil { - return nil, err - } - } - 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 string, value interface{}) error { - if r.CommandRule == nil { - r.CommandRule = GetCommandRule(r.Command) - } - if key == "" { - if r.NAnonParams >= len(r.CommandRule.ParamRules) { - return NewError(StatusInvalidCommand, map[string]interface{}{ - "command": r.Command, - "error": fmt.Sprintf("The command accepts at most %d unnamed parameters.", - len(r.CommandRule.ParamRules)), - }) - } - pr := r.CommandRule.ParamRules[r.NAnonParams] - if err := pr.CheckValue(value); err != nil { - return EnhanceError(err, map[string]interface{}{ - "command": r.Command, - "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] = v - r.NAnonParams++ - return nil - } - if err := r.CommandRule.CheckParam(key, value); err != nil { - return EnhanceError(err, map[string]interface{}{ - "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] = 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 -} - -// GQTPRequest returns components for a GQTP request. -// If the request is invalid, GQTPRequest returns an error. -// -// GQTPRequest assembles Command and Params into a string. -// Parameters in the string are sorted in key order. -func (r *Request) GQTPRequest() (cmd string, body io.Reader, err error) { - if err = r.Check(); err != nil { - return - } - size := len(r.Command) - for k, v := range r.Params { - size += len(k) + 3 - size += len(v)*2 + 3 - } - buf := make([]byte, 0, size) - buf = append(buf, r.Command...) - keys := make([]string, 0, len(r.Params)) - for k := range r.Params { - keys = append(keys, k) - } - sort.Strings(keys) - for _, k := range keys { - v := r.Params[k] - buf = append(buf, " --"...) - buf = append(buf, k...) - buf = append(buf, " '"...) - for i := 0; i < len(v); i++ { - switch v[i] { - case '\'', '\\', '\b', '\t', '\r', '\n': - buf = append(buf, '\\') - } - buf = append(buf, v[i]) - } - buf = append(buf, '\'') - } - cmd = string(buf) - body = r.Body - return -} - -// HTTPRequest returns components for an HTTP request. -// If the request is invalid, HTTPRequest returns an error. -func (r *Request) HTTPRequest() (cmd string, params map[string]string, body io.Reader, err error) { - if err = r.Check(); err != nil { - return - } - cmd = r.Command - params = r.Params - body = r.Body - return -} - -// NeedBody returns whether or not the request requires a body. -func (r *Request) NeedBody() bool { - switch r.Command { - case "load": - _, ok := r.Params["values"] - return !ok - default: - return false - } -} - -// Check checks whether or not the request is valid. -func (r *Request) Check() error { - if err := checkCommand(r.Command); err != nil { - return err - } - cr := r.CommandRule - if cr == nil { - cr = GetCommandRule(r.Command) - } - for k, v := range r.Params { - if err := cr.CheckParam(k, v); err != nil { - return EnhanceError(err, map[string]interface{}{ - "command": r.Command, - }) - } - } - for _, pr := range cr.ParamRules { - if pr.Required { - if _, ok := r.Params[pr.Key]; !ok { - return NewError(StatusInvalidCommand, map[string]interface{}{ - "command": r.Command, - "key": pr.Key, - "error": "The parameter is required.", - }) - } - } - } - switch r.Command { - case "load": - if _, ok := r.Params["values"]; ok { - if r.Body != nil { - return NewError(StatusInvalidCommand, map[string]interface{}{ - "command": r.Command, - "hasValues": true, - "hasBody": true, - "error": "The command does not accept a body.", - }) - } - } else if r.Body == nil { - return NewError(StatusInvalidCommand, map[string]interface{}{ - "command": r.Command, - "hasValues": false, - "hasBody": false, - "error": "The command requires a body.", - }) - } - default: - if r.Body != nil { - return NewError(StatusInvalidCommand, map[string]interface{}{ - "command": r.Command, - "hasBody": true, - "error": "The command does not accept a body.", - }) - } - } - return nil -} Deleted: v2/request_test.go (+0 -207) 100644 =================================================================== --- v2/request_test.go 2017-06-14 10:41:58 +0900 (9b3f9ee) +++ /dev/null @@ -1,207 +0,0 @@ -package grnci - -import ( - "fmt" - "testing" -) - -func TestNewRequest(t *testing.T) { - 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 { - t.Fatalf("NewRequest failed: %v", err) - } - if req.Command != "select" { - t.Fatalf("ParseRequest failed: cmd = %s, want = %s", - req.Command, "select") - } - for key, value := range params { - if req.Params[key] != fmt.Sprint(value) { - t.Fatalf("ParseRequest failed: params[\"%s\"] = %s, want = %v", - key, req.Params[key], value) - } - } -} - -func TestParseRequest(t *testing.T) { - req, err := ParseRequest(`select Tbl --query "\"apple juice\"" --filter 'price < 100'`, nil) - if err != nil { - t.Fatalf("ParseRequest failed: %v", err) - } - if req.Command != "select" { - t.Fatalf("ParseRequest failed: command: actual = %s, want = %s", - req.Command, "select") - } - if req.Params["table"] != "Tbl" { - t.Fatalf("ParseRequest failed: params[\"table\"] = %s, want = %s", - req.Params["table"], "Tbl") - } - if req.Params["query"] != "\"apple juice\"" { - t.Fatalf("ParseRequest failed: params[\"query\"] = %s, want = %s", - req.Params["query"], "apple juice") - } - if req.Params["filter"] != "price < 100" { - t.Fatalf("ParseRequest failed: params[\"filter\"] = %s, want = %s", - req.Params["filter"], "price < 100") - } -} - -func TestRequestAddParam(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) - } - } - if req.Command != "select" { - t.Fatalf("req.AddParam failed: cmd = %s, want = %s", - req.Command, "select") - } - for key, value := range params { - 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, - "select Tbl": true, - "select --123 xyz": false, - "_select --table Tbl": true, - "load --table Tbl": false, - "load --table Tbl --values []": true, - } - for cmd, want := range data { - req, err := ParseRequest(cmd, nil) - if err != nil { - t.Fatalf("ParseRequest failed: %v", err) - } - err = req.Check() - actual := err == nil - if actual != want { - t.Fatalf("req.Check failed: cmd = %s, actual = %v, want = %v, err = %v", - cmd, actual, want, err) - } - } -} - -func TestRequestGQTPRequest(t *testing.T) { - 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 { - t.Fatalf("NewRequest failed: %v", err) - } - actual, _, err := req.GQTPRequest() - if err != nil { - t.Fatalf("req.GQTPRequest failed: %v", err) - } - 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) - } -} - -func TestRequestHTTPRequest(t *testing.T) { - req, err := ParseRequest(`select Tbl --query "\"apple juice\"" --filter 'price < 100'`, nil) - if err != nil { - t.Fatalf("ParseRequest failed: %v", err) - } - cmd, params, _, err := req.HTTPRequest() - if err != nil { - t.Fatalf("req.HTTPRequest failed: %v", err) - } - if cmd != "select" { - t.Fatalf("req.HTTPRequest failed: cmd = %s, want = %s", cmd, "select") - } - if params["table"] != "Tbl" { - t.Fatalf("ParseRequest failed: params[\"table\"] = %s, want = %s", - params["table"], "Tbl") - } - if params["query"] != "\"apple juice\"" { - t.Fatalf("ParseRequest failed: params[\"query\"] = %s, want = %s", - params["query"], "apple juice") - } - if params["filter"] != "price < 100" { - t.Fatalf("ParseRequest failed: params[\"filter\"] = %s, want = %s", - params["filter"], "price < 100") - } -} - -func TestRequestNeedBody(t *testing.T) { - data := map[string]bool{ - "status": false, - "select Tbl": false, - "load --table Tbl": true, - "load --table Tbl --values []": false, - } - for cmd, want := range data { - req, err := ParseRequest(cmd, nil) - if err != nil { - t.Fatalf("ParseRequest failed: %v", err) - } - actual := req.NeedBody() - if actual != want { - t.Fatalf("req.NeedBody failed: actual = %v, want = %v", actual, want) - } - } -} Modified: v2/response.go (+0 -3) =================================================================== --- v2/response.go 2017-06-14 10:41:58 +0900 (dac839d) +++ v2/response.go 2017-06-16 10:52:57 +0900 (f6cbb2b) @@ -6,9 +6,6 @@ import ( // Response is an interface for responses. type Response interface { - // Status returns the status code. - Status() int - // Start returns the start time. Start() time.Time Deleted: v2/rule.go (+0 -542) 100644 =================================================================== --- v2/rule.go 2017-06-14 10:41:58 +0900 (ccbaf93) +++ /dev/null @@ -1,542 +0,0 @@ -package grnci - -import ( - "reflect" -) - -// TODO: add functions to check parameters. - -// checkParamKeyDefault is the default function to check parameter keys. -func checkParamKeyDefault(k string) error { - if k == "" { - return NewError(StatusInvalidCommand, map[string]interface{}{ - "key": k, - "error": "len(key) must not be 0.", - }) - } - for i := 0; i < len(k); i++ { - switch { - case k[i] >= '0' && k[i] <= '9': - case k[i] >= 'a' && k[i] <= 'z': - case k[i] >= 'A' && k[i] <= 'Z': - default: - switch k[i] { - case '#', '@', '-', '_', '.', '[', ']': - default: - return NewError(StatusInvalidCommand, map[string]interface{}{ - "key": k, - "error": "key must consist of [0-9a-zA-Z#@-_.[]].", - }) - } - } - } - return nil -} - -// checkParamValueDefault is the default function to check parameter values. -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 string, v interface{}) error { - if err := checkParamKeyDefault(k); err != nil { - return EnhanceError(err, map[string]interface{}{ - "value": v, - }) - } - if err := checkParamValueDefault(v); err != nil { - return EnhanceError(err, map[string]interface{}{ - "key": k, - }) - } - return nil -} - -// checkCommand checks whether s is valid as a command. -func checkCommand(s string) error { - if _, ok := CommandRules[s]; ok { - return nil - } - if s == "" { - return NewError(StatusInvalidCommand, map[string]interface{}{ - "command": s, - "error": "len(command) must not be 0.", - }) - } - for i := 0; i < len(s); i++ { - if !(s[i] >= 'a' && s[i] <= 'z') && s[i] != '_' { - return NewError(StatusInvalidCommand, map[string]interface{}{ - "command": s, - "error": "command must consist of [a-z_].", - }) - } - } - return nil -} - -// ParamRule is a parameter rule. -type ParamRule struct { - 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 interface{}) error, required bool) *ParamRule { - return &ParamRule{ - Key: key, - ValueChecker: valueChecker, - Required: required, - } -} - -// CheckValue checks a parameter value. -func (pr *ParamRule) CheckValue(v interface{}) error { - if pr.ValueChecker != nil { - return pr.ValueChecker(v) - } - return checkParamValueDefault(v) -} - -// CommandRule is a command rule. -type CommandRule struct { - 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. -func GetCommandRule(cmd string) *CommandRule { - if cr := CommandRules[cmd]; cr != nil { - return cr - } - return DefaultCommandRule -} - -// NewCommandRule returns a new 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 - } - return &CommandRule{ - ParamChecker: paramChecker, - ParamRules: prs, - ParamRulesMap: prMap, - } -} - -// CheckParam checks a parameter. -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{}{ - "key": k, - }) - } - return nil - } - if cr.ParamChecker != nil { - return cr.ParamChecker(k, v) - } - return checkParamDefault(k, v) -} - -// commandRules is provided to hide CommandRules in doc. -var commandRules = map[string]*CommandRule{ - "cache_limit": NewCommandRule( - nil, - NewParamRule("max", nil, false), - ), - "check": NewCommandRule( - nil, - NewParamRule("obj", nil, true), - ), - "clearlock": NewCommandRule( - nil, - NewParamRule("objname", nil, true), - ), - "column_copy": NewCommandRule( - nil, - NewParamRule("from_table", nil, true), - NewParamRule("from_name", nil, true), - NewParamRule("to_table", nil, true), - NewParamRule("to_name", nil, true), - ), - "column_create": NewCommandRule( - nil, - NewParamRule("table", nil, true), - NewParamRule("name", nil, true), - NewParamRule("flags", nil, true), - NewParamRule("type", nil, true), - NewParamRule("source", nil, false), - ), - "column_list": NewCommandRule( - nil, - NewParamRule("table", nil, true), - ), - "column_remove": NewCommandRule( - nil, - NewParamRule("table", nil, true), - NewParamRule("name", nil, true), - ), - "column_rename": NewCommandRule( - nil, - NewParamRule("table", nil, true), - NewParamRule("name", nil, true), - NewParamRule("new_name", nil, true), - ), - "config_delete": NewCommandRule( - nil, - NewParamRule("key", nil, true), - ), - "config_get": NewCommandRule( - nil, - NewParamRule("key", nil, true), - ), - "config_set": NewCommandRule( - nil, - NewParamRule("key", nil, true), - NewParamRule("value", nil, true), - ), - "database_unmap": NewCommandRule( - nil, - ), - "define_selector": NewCommandRule( - nil, - NewParamRule("name", nil, true), - NewParamRule("table", nil, true), - NewParamRule("match_columns", nil, false), - NewParamRule("query", nil, false), - NewParamRule("filter", nil, false), - NewParamRule("scorer", nil, false), - NewParamRule("sortby", nil, false), - NewParamRule("output_columns", nil, false), - NewParamRule("offset", nil, false), - NewParamRule("limit", nil, false), - NewParamRule("drilldown", nil, false), - NewParamRule("drilldown_sortby", nil, false), - NewParamRule("drilldown_output_columns", nil, false), - NewParamRule("drilldown_offset", nil, false), - NewParamRule("drilldown_limit", nil, false), - ), - "defrag": NewCommandRule( - nil, - NewParamRule("objname", nil, true), - NewParamRule("threshold", nil, true), - ), - "delete": NewCommandRule( - nil, - NewParamRule("table", nil, true), - NewParamRule("key", nil, false), - NewParamRule("id", nil, false), - NewParamRule("filter", nil, false), - ), - "dump": NewCommandRule( - nil, - NewParamRule("tables", nil, false), - NewParamRule("dump_plugins", nil, false), - NewParamRule("dump_schema", nil, false), - NewParamRule("dump_records", nil, false), - NewParamRule("dump_indexes", nil, false), - ), - "io_flush": NewCommandRule( - nil, - NewParamRule("target_name", nil, false), - NewParamRule("recursive", nil, false), - ), - "load": NewCommandRule( - nil, - NewParamRule("values", nil, false), // values may be passes as a body. - NewParamRule("table", nil, true), - NewParamRule("columns", nil, false), - NewParamRule("ifexists", nil, false), - NewParamRule("input_type", nil, false), - ), - "lock_acquire": NewCommandRule( - nil, - NewParamRule("target_name", nil, false), - ), - "lock_clear": NewCommandRule( - nil, - NewParamRule("target_name", nil, false), - ), - "lock_release": NewCommandRule( - nil, - NewParamRule("target_name", nil, false), - ), - "log_level": NewCommandRule( - nil, - NewParamRule("level", nil, true), - ), - "log_put": NewCommandRule( - nil, - NewParamRule("level", nil, true), - NewParamRule("message", nil, true), - ), - "log_reopen": NewCommandRule( - nil, - ), - "logical_count": NewCommandRule( - nil, - NewParamRule("logical_table", nil, true), - NewParamRule("shard_key", nil, true), - NewParamRule("min", nil, false), - NewParamRule("min_border", nil, false), - NewParamRule("max", nil, false), - NewParamRule("max_border", nil, false), - NewParamRule("filter", nil, false), - ), - "logical_parameters": NewCommandRule( - nil, - NewParamRule("range_index", nil, false), - ), - "logical_range_filter": NewCommandRule( - nil, - NewParamRule("logical_table", nil, true), - NewParamRule("shard_key", nil, true), - NewParamRule("min", nil, false), - NewParamRule("min_border", nil, false), - NewParamRule("max", nil, false), - NewParamRule("max_border", nil, false), - NewParamRule("order", nil, false), - NewParamRule("filter", nil, false), - NewParamRule("offset", nil, false), - NewParamRule("limit", nil, false), - NewParamRule("output_columns", nil, false), - NewParamRule("use_range_index", nil, false), - ), - "logical_select": NewCommandRule( - nil, - NewParamRule("logical_table", nil, true), - NewParamRule("shard_key", nil, true), - NewParamRule("min", nil, false), - NewParamRule("min_border", nil, false), - NewParamRule("max", nil, false), - NewParamRule("max_border", nil, false), - NewParamRule("filter", nil, false), - NewParamRule("sortby", nil, false), - NewParamRule("output_columns", nil, false), - NewParamRule("offset", nil, false), - NewParamRule("limit", nil, false), - NewParamRule("drilldown", nil, false), - NewParamRule("drilldown_sortby", nil, false), - NewParamRule("drilldown_output_columns", nil, false), - NewParamRule("drilldown_offset", nil, false), - NewParamRule("drilldown_limit", nil, false), - NewParamRule("drilldown_calc_types", nil, false), - NewParamRule("drilldown_calc_target", nil, false), - NewParamRule("sort_keys", nil, false), - NewParamRule("drilldown_sort_keys", nil, false), - NewParamRule("match_columns", nil, false), - NewParamRule("query", nil, false), - NewParamRule("drilldown_filter", nil, false), - ), - "logical_shard_list": NewCommandRule( - nil, - NewParamRule("logical_table", nil, true), - ), - "logical_table_remove": NewCommandRule( - nil, - NewParamRule("logical_table", nil, true), - NewParamRule("shard_key", nil, true), - NewParamRule("min", nil, false), - NewParamRule("min_border", nil, false), - NewParamRule("max", nil, false), - NewParamRule("max_border", nil, false), - NewParamRule("dependent", nil, false), - NewParamRule("force", nil, false), - ), - "normalize": NewCommandRule( - nil, - NewParamRule("normalizer", nil, true), - NewParamRule("string", nil, true), - NewParamRule("flags", nil, false), - ), - "normalizer_list": NewCommandRule( - nil, - ), - "object_exist": NewCommandRule( - nil, - NewParamRule("name", nil, true), - ), - "object_inspect": NewCommandRule( - nil, - NewParamRule("name", nil, false), - ), - "object_list": NewCommandRule( - nil, - ), - "object_remove": NewCommandRule( - nil, - NewParamRule("name", nil, true), - NewParamRule("force", nil, false), - ), - "plugin_register": NewCommandRule( - nil, - NewParamRule("name", nil, true), - ), - "plugin_unregister": NewCommandRule( - nil, - NewParamRule("name", nil, true), - ), - "query_expand": NewCommandRule( - nil, - ), // TODO - "quit": NewCommandRule( - nil, - ), - "range_filter": NewCommandRule( - nil, - ), // TODO - "register": NewCommandRule( - nil, - NewParamRule("path", nil, true), - ), - "reindex": NewCommandRule( - nil, - NewParamRule("target_name", nil, false), - ), - "request_cancel": NewCommandRule( - nil, - NewParamRule("id", nil, true), - ), - "ruby_eval": NewCommandRule( - nil, - NewParamRule("script", nil, true), - ), - "ruby_load": NewCommandRule( - nil, - NewParamRule("path", nil, true), - ), - "schema": NewCommandRule( - nil, - ), - "select": NewCommandRule( - nil, - NewParamRule("table", nil, true), - NewParamRule("match_columns", nil, false), - NewParamRule("query", nil, false), - NewParamRule("filter", nil, false), - NewParamRule("scorer", nil, false), - NewParamRule("sortby", nil, false), - NewParamRule("output_columns", nil, false), - NewParamRule("offset", nil, false), - NewParamRule("limit", nil, false), - NewParamRule("drilldown", nil, false), - NewParamRule("drilldown_sortby", nil, false), - NewParamRule("drilldown_output_columns", nil, false), - NewParamRule("drilldown_offset", nil, false), - NewParamRule("drilldown_limit", nil, false), - NewParamRule("cache", nil, false), - NewParamRule("match_escalation_threshold", nil, false), - NewParamRule("query_expansion", nil, false), - NewParamRule("query_flags", nil, false), - NewParamRule("query_expander", nil, false), - NewParamRule("adjuster", nil, false), - NewParamRule("drilldown_calc_types", nil, false), - NewParamRule("drilldown_calc_target", nil, false), - NewParamRule("drilldown_filter", nil, false), - NewParamRule("sort_keys", nil, false), - NewParamRule("drilldown_sort_keys", nil, false), - ), - "shutdown": NewCommandRule( - nil, - NewParamRule("mode", nil, false), - ), - "status": NewCommandRule( - nil, - ), - "suggest": NewCommandRule( - nil, - NewParamRule("types", nil, true), - NewParamRule("table", nil, true), - NewParamRule("column", nil, true), - NewParamRule("query", nil, true), - NewParamRule("sortby", nil, false), - NewParamRule("output_columns", nil, false), - NewParamRule("offset", nil, false), - NewParamRule("limit", nil, false), - NewParamRule("frequency_threshold", nil, false), - NewParamRule("conditional_probability_threshold", nil, false), - NewParamRule("prefix_search", nil, false), - ), - "table_copy": NewCommandRule( - nil, - NewParamRule("from_name", nil, true), - NewParamRule("to_name", nil, true), - ), - "table_create": NewCommandRule( - nil, - NewParamRule("name", nil, true), - NewParamRule("flags", nil, false), - NewParamRule("key_type", nil, false), - NewParamRule("value_type", nil, false), - NewParamRule("default_tokenizer", nil, false), - NewParamRule("normalizer", nil, false), - NewParamRule("token_filters", nil, false), - ), - "table_list": NewCommandRule( - nil, - ), - "table_remove": NewCommandRule( - nil, - NewParamRule("name", nil, true), - NewParamRule("dependent", nil, false), - ), - "table_rename": NewCommandRule( - nil, - NewParamRule("name", nil, true), - NewParamRule("new_name", nil, true), - ), - "table_tokenize": NewCommandRule( - nil, - NewParamRule("table", nil, true), - NewParamRule("string", nil, true), - NewParamRule("flags", nil, false), - NewParamRule("mode", nil, false), - NewParamRule("index_column", nil, false), - ), - "thread_limit": NewCommandRule( - nil, - NewParamRule("max", nil, false), - ), - "tokenize": NewCommandRule( - nil, - NewParamRule("tokenizer", nil, true), - NewParamRule("string", nil, true), - NewParamRule("normalizer", nil, false), - NewParamRule("flags", nil, false), - NewParamRule("mode", nil, false), - NewParamRule("token_filters", nil, false), - ), - "tokenizer_list": NewCommandRule( - nil, - ), - "truncate": NewCommandRule( - nil, - NewParamRule("target_name", nil, true), - ), -} - -// CommandRules is a map of command rules. -var CommandRules = commandRules - -// DefaultCommandRule is applied to commands not listed in CommandRules. -var DefaultCommandRule = NewCommandRule(nil) Added: v2/type.go (+7 -0) 100644 =================================================================== --- /dev/null +++ v2/type.go 2017-06-16 10:52:57 +0900 (86ab078) @@ -0,0 +1,7 @@ +package grnci + +// Geo represents TokyoGeoPoint and WGS84GeoPoint. +type Geo struct { + Lat int32 // Latitude in milliseconds. + Long int32 // Longitude in milliseconds. +}