ls 5 years ago
parent
commit
e4faa3b31a
39 changed files with 3447 additions and 0 deletions
  1. 3 0
      .vscode/settings.json
  2. 21 0
      binding/LICENSE
  3. 155 0
      binding/binding.go
  4. 52 0
      binding/default_validator.go
  5. 58 0
      binding/form.go
  6. 223 0
      binding/form_mapping.go
  7. 47 0
      binding/json.go
  8. 25 0
      binding/msgpack.go
  9. 35 0
      binding/protobuf.go
  10. 23 0
      binding/query.go
  11. 18 0
      binding/uri.go
  12. 33 0
      binding/xml.go
  13. 35 0
      binding/yaml.go
  14. 221 0
      cache/redis.go
  15. 15 0
      cookie.go
  16. 83 0
      crypto.go
  17. 83 0
      db/config.go
  18. 261 0
      db/db.go
  19. 11 0
      error.go
  20. 13 0
      go.mod
  21. 26 0
      go.sum
  22. 29 0
      handler.go
  23. 41 0
      hash.go
  24. 123 0
      path.go
  25. 119 0
      reply.go
  26. 125 0
      request.go
  27. 38 0
      response.go
  28. 398 0
      router.go
  29. 58 0
      serve.go
  30. 657 0
      tree.go
  31. 30 0
      types/bool.go
  32. 68 0
      types/datetime.go
  33. 50 0
      types/gzip.go
  34. 103 0
      types/json.go
  35. 59 0
      types/numeric.go
  36. 32 0
      types/string.go
  37. 8 0
      util/string.go
  38. 61 0
      vars.go
  39. 7 0
      version.go

+ 3 - 0
.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+  "go.inferGopath": false
+}

+ 21 - 0
binding/LICENSE

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Manuel Martínez-Almeida
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 155 - 0
binding/binding.go

@@ -0,0 +1,155 @@
+// Copyright 2014 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package binding
+
+import (
+	"net/http"
+	//validator "gopkg.in/go-playground/validator.v8"
+	//validator "gopkg.in/go-playground/validator.v8"
+)
+
+// Content-Type MIME of the most common data formats.
+const (
+	MIMEJSON              = "application/json"
+	MIMEHTML              = "text/html"
+	MIMEXML               = "application/xml"
+	MIMEXML2              = "text/xml"
+	MIMEPlain             = "text/plain"
+	MIMEPOSTForm          = "application/x-www-form-urlencoded"
+	MIMEMultipartPOSTForm = "multipart/form-data"
+	MIMEPROTOBUF          = "application/x-protobuf"
+	MIMEMSGPACK           = "application/x-msgpack"
+	MIMEMSGPACK2          = "application/msgpack"
+	MIMEYAML              = "application/x-yaml"
+)
+
+// Binding describes the interface which needs to be implemented for binding the
+// data present in the request such as JSON request body, query parameters or
+// the form POST.
+type Binding interface {
+	Name() string
+	Bind(*http.Request, interface{}) error
+}
+
+// BindingBody adds BindBody method to Binding. BindBody is similar with Bind,
+// but it reads the body from supplied bytes instead of req.Body.
+type BindingBody interface {
+	Binding
+	BindBody([]byte, interface{}) error
+}
+
+// BindingURI adds BindUri method to Binding. BindUri is similar with Bind,
+// but it read the Params.
+type BindingURI interface {
+	Name() string
+	BindURI(map[string][]string, interface{}) error
+}
+
+/*
+// StructValidator is the minimal interface which needs to be implemented in
+// order for it to be used as the validator engine for ensuring the correctness
+// of the request. Gin provides a default implementation for this using
+// https://github.com/go-playground/validator/tree/v8.18.2.
+type StructValidator interface {
+	// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
+	// If the received type is not a struct, any validation should be skipped and nil must be returned.
+	// If the received type is a struct or pointer to a struct, the validation should be performed.
+	// If the struct is not valid or the validation itself fails, a descriptive error should be returned.
+	// Otherwise nil must be returned.
+	ValidateStruct(interface{}) error
+
+	// Engine returns the underlying validator engine which powers the
+	// StructValidator implementation.
+	Engine() interface{}
+}
+
+// Validator is the default validator which implements the StructValidator
+// interface. It uses https://github.com/go-playground/validator/tree/v8.18.2
+// under the hood.
+var Validator StructValidator = &defaultValidator{}
+// */
+
+// These implement the Binding interface and can be used to bind the data
+// present in the request to struct instances.
+var (
+	JSON          = jsonBinding{}
+	XML           = xmlBinding{}
+	Form          = formBinding{}
+	Query         = queryBinding{}
+	FormPost      = formPostBinding{}
+	FormMultipart = formMultipartBinding{}
+	ProtoBuf      = protobufBinding{}
+	MsgPack       = msgpackBinding{}
+	YAML          = yamlBinding{}
+	URI           = uriBinding{}
+)
+
+// Default returns the appropriate Binding instance based on the HTTP method
+// and the content type.
+func Default(method, contentType string) Binding {
+	if method == "GET" {
+		return Form
+	}
+
+	switch contentType {
+	case MIMEJSON:
+		return JSON
+	case MIMEXML, MIMEXML2:
+		return XML
+	case MIMEPROTOBUF:
+		return ProtoBuf
+	case MIMEMSGPACK, MIMEMSGPACK2:
+		return MsgPack
+	case MIMEYAML:
+		return YAML
+	default: //case MIMEPOSTForm, MIMEMultipartPOSTForm:
+		return Form
+	}
+}
+
+func validate(obj interface{}) error {
+	return nil
+	/*
+		if Validator == nil {
+			return nil
+		}
+
+		return Validator.ValidateStruct(obj)
+		// */
+}
+
+// Bind checks the Content-Type to select a binding engine automatically,
+// Depending the "Content-Type" header different bindings are used:
+//     "application/json" --> JSON binding
+//     "application/xml"  --> XML binding
+// otherwise --> returns an error.
+// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input.
+// It decodes the json payload into the struct specified as a pointer.
+// It writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid.
+func Bind(req *http.Request, obj interface{}) error {
+	b := Default(req.Method, ContentType(req))
+	return MustBindWith(req, obj, b)
+}
+
+// MustBindWith binds the passed struct pointer using the specified binding engine.
+// It will abort the request with HTTP 400 if any error occurs.
+// See the binding package.
+func MustBindWith(req *http.Request, obj interface{}, b Binding) (err error) {
+	return b.Bind(req, obj)
+}
+
+// ContentType returns the Content-Type header of the request.
+func ContentType(req *http.Request) string {
+	return filterFlags(req.Header.Get("Content-Type"))
+}
+
+func filterFlags(content string) string {
+	for i, char := range content {
+		if char == ' ' || char == ';' {
+			return content[:i]
+		}
+	}
+	return content
+}

+ 52 - 0
binding/default_validator.go

@@ -0,0 +1,52 @@
+// Copyright 2017 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package binding
+
+//"gopkg.in/go-playground/validator.v8"
+
+/*
+type defaultValidator struct {
+	once     sync.Once
+	validate *validator.Validate
+}
+
+var _ StructValidator = &defaultValidator{}
+
+func (v *defaultValidator) ValidateStruct(obj interface{}) error {
+	if kindOfData(obj) == reflect.Struct {
+		v.lazyinit()
+		if err := v.validate.Struct(obj); err != nil {
+			buf := bytes.NewBufferString("")
+			for _, v := range err.(validator.ValidationErrors) {
+				buf.WriteString(fmt.Sprintf("%s %s %s", v.Name, v.Tag, v.Param))
+				buf.WriteString(";")
+			}
+			return errors.New(buf.String()) //error(err)
+		}
+	}
+	return nil
+}
+
+func (v *defaultValidator) RegisterValidation(key string, fn validator.Func) error {
+	v.lazyinit()
+	return v.validate.RegisterValidation(key, fn)
+}
+
+func (v *defaultValidator) lazyinit() {
+	v.once.Do(func() {
+		config := &validator.Config{TagName: "validate"}
+		v.validate = validator.New(config)
+	})
+}
+
+func kindOfData(data interface{}) reflect.Kind {
+	value := reflect.ValueOf(data)
+	valueType := value.Kind()
+	if valueType == reflect.Ptr {
+		valueType = value.Elem().Kind()
+	}
+	return valueType
+}
+//*/

+ 58 - 0
binding/form.go

@@ -0,0 +1,58 @@
+// Copyright 2014 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package binding
+
+import (
+	"net/http"
+)
+
+const defaultMemory = 32 * 1024 * 1024
+
+type formBinding struct{}
+type formPostBinding struct{}
+type formMultipartBinding struct{}
+
+func (formBinding) Name() string {
+	return "form"
+}
+
+func (formBinding) Bind(req *http.Request, obj interface{}) error {
+	if err := req.ParseForm(); err != nil {
+		return err
+	}
+	req.ParseMultipartForm(defaultMemory)
+	if err := mapForm(obj, req.Form); err != nil {
+		return err
+	}
+	return validate(obj)
+}
+
+func (formPostBinding) Name() string {
+	return "form-urlencoded"
+}
+
+func (formPostBinding) Bind(req *http.Request, obj interface{}) error {
+	if err := req.ParseForm(); err != nil {
+		return err
+	}
+	if err := mapForm(obj, req.PostForm); err != nil {
+		return err
+	}
+	return validate(obj)
+}
+
+func (formMultipartBinding) Name() string {
+	return "multipart/form-data"
+}
+
+func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error {
+	if err := req.ParseMultipartForm(defaultMemory); err != nil {
+		return err
+	}
+	if err := mapForm(obj, req.MultipartForm.Value); err != nil {
+		return err
+	}
+	return validate(obj)
+}

+ 223 - 0
binding/form_mapping.go

@@ -0,0 +1,223 @@
+// Copyright 2014 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package binding
+
+import (
+	"errors"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// MapForm form values map to struct
+func MapForm(ptr interface{}, values url.Values) error {
+	return mapFormByTag(ptr, values, "form")
+}
+
+func mapURI(ptr interface{}, m map[string][]string) error {
+	return mapFormByTag(ptr, m, "uri")
+}
+
+func mapForm(ptr interface{}, form map[string][]string) error {
+	return mapFormByTag(ptr, form, "form")
+}
+
+func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error {
+	typ := reflect.TypeOf(ptr).Elem()
+	val := reflect.ValueOf(ptr).Elem()
+	for i := 0; i < typ.NumField(); i++ {
+		typeField := typ.Field(i)
+		structField := val.Field(i)
+		if !structField.CanSet() {
+			continue
+		}
+
+		structFieldKind := structField.Kind()
+		inputFieldName := typeField.Tag.Get(tag)
+		inputFieldNameList := strings.Split(inputFieldName, ",")
+		inputFieldName = inputFieldNameList[0]
+		var defaultValue string
+		if len(inputFieldNameList) > 1 {
+			defaultList := strings.SplitN(inputFieldNameList[1], "=", 2)
+			if defaultList[0] == "default" {
+				defaultValue = defaultList[1]
+			}
+		}
+		if inputFieldName == "" {
+			inputFieldName = typeField.Name
+
+			// if "form" tag is nil, we inspect if the field is a struct or struct pointer.
+			// this would not make sense for JSON parsing but it does for a form
+			// since data is flatten
+			if structFieldKind == reflect.Ptr {
+				if !structField.Elem().IsValid() {
+					structField.Set(reflect.New(structField.Type().Elem()))
+				}
+				structField = structField.Elem()
+				structFieldKind = structField.Kind()
+			}
+			if structFieldKind == reflect.Struct {
+				err := mapFormByTag(structField.Addr().Interface(), form, tag)
+				if err != nil {
+					return err
+				}
+				continue
+			}
+		}
+		inputValue, exists := form[inputFieldName]
+
+		if !exists {
+			if defaultValue == "" {
+				continue
+			}
+			inputValue = make([]string, 1)
+			inputValue[0] = defaultValue
+		}
+
+		numElems := len(inputValue)
+		if structFieldKind == reflect.Slice && numElems > 0 {
+			sliceOf := structField.Type().Elem().Kind()
+			slice := reflect.MakeSlice(structField.Type(), numElems, numElems)
+			for i := 0; i < numElems; i++ {
+				if err := setWithProperType(sliceOf, inputValue[i], slice.Index(i)); err != nil {
+					return err
+				}
+			}
+			val.Field(i).Set(slice)
+			continue
+		}
+		if _, isTime := structField.Interface().(time.Time); isTime {
+			if err := setTimeField(inputValue[0], typeField, structField); err != nil {
+				return err
+			}
+			continue
+		}
+		if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error {
+	switch valueKind {
+	case reflect.Int:
+		return setIntField(val, 0, structField)
+	case reflect.Int8:
+		return setIntField(val, 8, structField)
+	case reflect.Int16:
+		return setIntField(val, 16, structField)
+	case reflect.Int32:
+		return setIntField(val, 32, structField)
+	case reflect.Int64:
+		return setIntField(val, 64, structField)
+	case reflect.Uint:
+		return setUintField(val, 0, structField)
+	case reflect.Uint8:
+		return setUintField(val, 8, structField)
+	case reflect.Uint16:
+		return setUintField(val, 16, structField)
+	case reflect.Uint32:
+		return setUintField(val, 32, structField)
+	case reflect.Uint64:
+		return setUintField(val, 64, structField)
+	case reflect.Bool:
+		return setBoolField(val, structField)
+	case reflect.Float32:
+		return setFloatField(val, 32, structField)
+	case reflect.Float64:
+		return setFloatField(val, 64, structField)
+	case reflect.String:
+		structField.SetString(val)
+	case reflect.Ptr:
+		if !structField.Elem().IsValid() {
+			structField.Set(reflect.New(structField.Type().Elem()))
+		}
+		structFieldElem := structField.Elem()
+		return setWithProperType(structFieldElem.Kind(), val, structFieldElem)
+	default:
+		return errors.New("Unknown type")
+	}
+	return nil
+}
+
+func setIntField(val string, bitSize int, field reflect.Value) error {
+	if val == "" {
+		val = "0"
+	}
+	intVal, err := strconv.ParseInt(val, 10, bitSize)
+	if err == nil {
+		field.SetInt(intVal)
+	}
+	return err
+}
+
+func setUintField(val string, bitSize int, field reflect.Value) error {
+	if val == "" {
+		val = "0"
+	}
+	uintVal, err := strconv.ParseUint(val, 10, bitSize)
+	if err == nil {
+		field.SetUint(uintVal)
+	}
+	return err
+}
+
+func setBoolField(val string, field reflect.Value) error {
+	if val == "" {
+		val = "false"
+	}
+	boolVal, err := strconv.ParseBool(val)
+	if err == nil {
+		field.SetBool(boolVal)
+	}
+	return err
+}
+
+func setFloatField(val string, bitSize int, field reflect.Value) error {
+	if val == "" {
+		val = "0.0"
+	}
+	floatVal, err := strconv.ParseFloat(val, bitSize)
+	if err == nil {
+		field.SetFloat(floatVal)
+	}
+	return err
+}
+
+func setTimeField(val string, structField reflect.StructField, value reflect.Value) error {
+	timeFormat := structField.Tag.Get("time_format")
+	if timeFormat == "" {
+		timeFormat = time.RFC3339
+	}
+
+	if val == "" {
+		value.Set(reflect.ValueOf(time.Time{}))
+		return nil
+	}
+
+	l := time.Local
+	if isUTC, _ := strconv.ParseBool(structField.Tag.Get("time_utc")); isUTC {
+		l = time.UTC
+	}
+
+	if locTag := structField.Tag.Get("time_location"); locTag != "" {
+		loc, err := time.LoadLocation(locTag)
+		if err != nil {
+			return err
+		}
+		l = loc
+	}
+
+	t, err := time.ParseInLocation(timeFormat, val, l)
+	if err != nil {
+		return err
+	}
+
+	value.Set(reflect.ValueOf(t))
+	return nil
+}

+ 47 - 0
binding/json.go

@@ -0,0 +1,47 @@
+// Copyright 2014 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package binding
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"net/http"
+
+	"encoding/json"
+)
+
+// EnableDecoderUseNumber is used to call the UseNumber method on the JSON
+// Decoder instance. UseNumber causes the Decoder to unmarshal a number into an
+// interface{} as a Number instead of as a float64.
+var EnableDecoderUseNumber = false
+
+type jsonBinding struct{}
+
+func (jsonBinding) Name() string {
+	return "json"
+}
+
+func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
+	if req == nil || req.Body == nil {
+		return fmt.Errorf("invalid request")
+	}
+	return decodeJSON(req.Body, obj)
+}
+
+func (jsonBinding) BindBody(body []byte, obj interface{}) error {
+	return decodeJSON(bytes.NewReader(body), obj)
+}
+
+func decodeJSON(r io.Reader, obj interface{}) error {
+	decoder := json.NewDecoder(r)
+	if EnableDecoderUseNumber {
+		decoder.UseNumber()
+	}
+	if err := decoder.Decode(obj); err != nil {
+		return err
+	}
+	return validate(obj)
+}

+ 25 - 0
binding/msgpack.go

@@ -0,0 +1,25 @@
+// Copyright 2017 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package binding
+
+import (
+	"net/http"
+	//"github.com/ugorji/go/codec"
+)
+
+type msgpackBinding struct{}
+
+func (msgpackBinding) Name() string {
+	return "msgpack"
+}
+
+func (msgpackBinding) Bind(req *http.Request, obj interface{}) error {
+	/*
+		if err := codec.NewDecoder(req.Body, new(codec.MsgpackHandle)).Decode(&obj); err != nil {
+			return err
+		}
+		// */
+	return validate(obj)
+}

+ 35 - 0
binding/protobuf.go

@@ -0,0 +1,35 @@
+// Copyright 2014 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package binding
+
+import (
+	"net/http"
+	//"github.com/golang/protobuf/proto"
+)
+
+type protobufBinding struct{}
+
+func (protobufBinding) Name() string {
+	return "protobuf"
+}
+
+func (protobufBinding) Bind(req *http.Request, obj interface{}) error {
+	/*
+		buf, err := ioutil.ReadAll(req.Body)
+		if err != nil {
+			return err
+		}
+
+		if err = proto.Unmarshal(buf, obj.(proto.Message)); err != nil {
+			return err
+		}
+
+		//Here it's same to return validate(obj), but util now we cann't add `binding:""` to the struct
+		//which automatically generate by gen-proto
+		//return nil
+		return validate(obj)
+		// */
+	return validate(obj)
+}

+ 23 - 0
binding/query.go

@@ -0,0 +1,23 @@
+// Copyright 2017 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package binding
+
+import (
+	"net/http"
+)
+
+type queryBinding struct{}
+
+func (queryBinding) Name() string {
+	return "query"
+}
+
+func (queryBinding) Bind(req *http.Request, obj interface{}) error {
+	values := req.URL.Query()
+	if err := mapForm(obj, values); err != nil {
+		return err
+	}
+	return validate(obj)
+}

+ 18 - 0
binding/uri.go

@@ -0,0 +1,18 @@
+// Copyright 2018 Gin Core Team.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package binding
+
+type uriBinding struct{}
+
+func (uriBinding) Name() string {
+	return "uri"
+}
+
+func (uriBinding) BindURI(m map[string][]string, obj interface{}) error {
+	if err := mapURI(obj, m); err != nil {
+		return err
+	}
+	return validate(obj)
+}

+ 33 - 0
binding/xml.go

@@ -0,0 +1,33 @@
+// Copyright 2014 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package binding
+
+import (
+	"bytes"
+	"encoding/xml"
+	"io"
+	"net/http"
+)
+
+type xmlBinding struct{}
+
+func (xmlBinding) Name() string {
+	return "xml"
+}
+
+func (xmlBinding) Bind(req *http.Request, obj interface{}) error {
+	return decodeXML(req.Body, obj)
+}
+
+func (xmlBinding) BindBody(body []byte, obj interface{}) error {
+	return decodeXML(bytes.NewReader(body), obj)
+}
+func decodeXML(r io.Reader, obj interface{}) error {
+	decoder := xml.NewDecoder(r)
+	if err := decoder.Decode(obj); err != nil {
+		return err
+	}
+	return validate(obj)
+}

+ 35 - 0
binding/yaml.go

@@ -0,0 +1,35 @@
+// Copyright 2018 Gin Core Team.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package binding
+
+import (
+	"bytes"
+	"io"
+	"net/http"
+
+	"gopkg.in/yaml.v2"
+)
+
+type yamlBinding struct{}
+
+func (yamlBinding) Name() string {
+	return "yaml"
+}
+
+func (yamlBinding) Bind(req *http.Request, obj interface{}) error {
+	return decodeYAML(req.Body, obj)
+}
+
+func (yamlBinding) BindBody(body []byte, obj interface{}) error {
+	return decodeYAML(bytes.NewReader(body), obj)
+}
+
+func decodeYAML(r io.Reader, obj interface{}) error {
+	decoder := yaml.NewDecoder(r)
+	if err := decoder.Decode(obj); err != nil {
+		return err
+	}
+	return validate(obj)
+}

+ 221 - 0
cache/redis.go

@@ -0,0 +1,221 @@
+package cache
+
+import (
+	"sync"
+	"time"
+
+	"github.com/go-redis/redis"
+)
+
+// RedisConfig config
+type RedisConfig struct {
+	Addr         string
+	Password     string
+	DialTimeout  time.Duration
+	ReadTimeout  time.Duration
+	WriteTimeout time.Duration
+	PoolTimeout  time.Duration
+	PoolSize     int
+}
+
+// RedisClusterConfig redis cluster configure
+type RedisClusterConfig struct {
+	// A seed list of host:port addresses of cluster nodes.
+	Addrs []string
+
+	// The maximum number of retries before giving up. Command is retried
+	// on network errors and MOVED/ASK redirects.
+	// Default is 16.
+	MaxRedirects int
+
+	// Enables read-only commands on slave nodes.
+	ReadOnly bool
+	// Allows routing read-only commands to the closest master or slave node.
+	RouteByLatency bool
+
+	//OnConnect func(*Conn) error
+
+	MaxRetries      int
+	MinRetryBackoff time.Duration
+	MaxRetryBackoff time.Duration
+	Password        string
+
+	DialTimeout  time.Duration
+	ReadTimeout  time.Duration
+	WriteTimeout time.Duration
+
+	// PoolSize applies per cluster node and not for the whole cluster.
+	PoolSize           int
+	PoolTimeout        time.Duration
+	IdleTimeout        time.Duration
+	IdleCheckFrequency time.Duration
+}
+
+var (
+	redisConfig        RedisConfig
+	redisClusterConfig RedisClusterConfig
+	once               sync.Once
+	cache              *RedisCache
+	isCluster          bool
+)
+
+// RedisCache define
+type RedisCache struct {
+	c  *redis.Client
+	cc *redis.ClusterClient
+}
+
+// SetRedisConfig set
+func SetRedisConfig(cfg RedisConfig) {
+	redisConfig = cfg
+}
+
+// SetRedisClusterConfig set
+func SetRedisClusterConfig(cfg RedisClusterConfig) {
+	redisClusterConfig = cfg
+}
+
+// NewRedisCache new RedisCache object
+func NewRedisCache() *RedisCache {
+	client := redis.NewClient(&redis.Options{
+		Addr:         redisConfig.Addr,
+		Password:     redisConfig.Password,
+		DialTimeout:  redisConfig.DialTimeout,
+		ReadTimeout:  redisConfig.ReadTimeout,
+		WriteTimeout: redisConfig.WriteTimeout,
+		PoolSize:     redisConfig.PoolSize,
+		PoolTimeout:  redisConfig.PoolTimeout,
+	})
+
+	client.Ping().Result()
+	return &RedisCache{c: client}
+}
+
+// NewRedisClusterCache new RedisCluster object
+func NewRedisClusterCache() *RedisCache {
+	var config redis.ClusterOptions
+
+	config.Addrs = redisClusterConfig.Addrs
+	config.Password = redisClusterConfig.Password
+
+	config.DialTimeout = redisClusterConfig.DialTimeout
+	config.ReadTimeout = redisClusterConfig.ReadTimeout
+	config.WriteTimeout = redisClusterConfig.WriteTimeout
+
+	config.PoolSize = redisClusterConfig.PoolSize
+	config.PoolTimeout = redisClusterConfig.PoolTimeout
+	config.IdleTimeout = redisClusterConfig.IdleTimeout
+	config.IdleCheckFrequency = redisClusterConfig.IdleCheckFrequency
+
+	client := redis.NewClusterClient(&config)
+
+	client.Ping()
+	return &RedisCache{cc: client}
+}
+
+// Get get value from cache
+func (c RedisCache) Get(key string) (string, error) {
+	if c.cc != nil {
+		return c.cc.Get(key).Result()
+	}
+	return c.c.Get(key).Result()
+}
+
+// Set set key-value to cache
+func (c RedisCache) Set(key, value string, expiration time.Duration) error {
+	if c.cc != nil {
+		return c.cc.Set(key, value, expiration).Err()
+	}
+	return c.c.Set(key, value, expiration).Err()
+}
+
+// Del Del value from cache
+func (c RedisCache) Del(key string) (int64, error) {
+	if c.cc != nil {
+		return c.cc.Del(key).Result()
+	}
+	return c.c.Del(key).Result()
+}
+
+// Subscribe subscribe message
+func (c RedisCache) Subscribe(channels string, cb func(channel string, message string, err error)) {
+	pubsub := &redis.PubSub{}
+	if c.cc != nil {
+		pubsub = c.cc.Subscribe(channels)
+	} else {
+		pubsub = c.c.Subscribe(channels)
+	}
+	defer pubsub.Close()
+
+	var (
+		msg *redis.Message
+		err error
+	)
+	msg, err = pubsub.ReceiveMessage()
+	for err == nil {
+		cb(msg.Channel, msg.Payload, nil)
+		msg, err = pubsub.ReceiveMessage()
+	}
+
+	cb("", "", err)
+
+	return
+}
+
+// Publish publish message
+func (c RedisCache) Publish(channel, message string) error {
+	return c.c.Publish(channel, message).Err()
+}
+
+// PublishCluster publish message
+func (c RedisCache) PublishCluster(channel, message string) error {
+	return c.cc.Publish(channel, message).Err()
+}
+
+func connect() (*RedisCache, error) {
+	if cache != nil {
+		return cache, nil
+	}
+	//*
+	var err error
+	once.Do(func() {
+		isCluster = false
+		if len(redisConfig.Addr) > 0 {
+			cache = NewRedisCache()
+		}
+
+		if len(redisClusterConfig.Addrs) > 0 {
+			cache = NewRedisClusterCache()
+			isCluster = true
+		}
+	})
+	//*/
+	return cache, err
+}
+
+// Get key from cache
+func Get(key string) (string, error) {
+	c, err := connect()
+	if err != nil {
+		return "", err
+	}
+	return c.Get(key)
+}
+
+// Set key-value to cache
+func Set(key, value string, expiration time.Duration) error {
+	c, err := connect()
+	if err != nil {
+		return err
+	}
+	return c.Set(key, value, expiration)
+}
+
+// Del delete key from cache
+func Del(key string) (int64, error) {
+	c, err := connect()
+	if err != nil {
+		return 0, err
+	}
+	return c.Del(key)
+}

+ 15 - 0
cookie.go

@@ -0,0 +1,15 @@
+package myth
+
+import "net/http"
+
+// SetCookie set http cookie
+func SetCookie(w http.ResponseWriter, name, value, path string, maxAge int) {
+	cookie := &http.Cookie{
+		Name:     name,
+		Value:    value,
+		Path:     path,
+		HttpOnly: false,
+		MaxAge:   maxAge}
+
+	http.SetCookie(w, cookie)
+}

+ 83 - 0
crypto.go

@@ -0,0 +1,83 @@
+package myth
+
+import (
+	"bytes"
+	"crypto/aes"
+	"crypto/cipher"
+)
+
+var (
+	aesKey string
+)
+
+// AesCrypto define
+type AesCrypto struct {
+	Key []byte
+}
+
+// SetAesCryptoKey set key,
+// key length:16, 24, 32 bytes to AES-128, AES-192, AES-256
+func SetAesCryptoKey(password string) {
+	aesKey = password
+}
+
+// GetAesCryptoKey get current key
+func GetAesCryptoKey() string {
+	return aesKey
+}
+
+// NewAesCrypto new AesCrypto
+func NewAesCrypto() *AesCrypto {
+	return &AesCrypto{[]byte(aesKey)}
+}
+
+// SetKey set key
+func (a *AesCrypto) SetKey(key string) {
+	a.Key = []byte(key)
+}
+
+// Encrypt encrypt data
+func (a *AesCrypto) Encrypt(data []byte) ([]byte, error) {
+	block, err := aes.NewCipher(a.Key)
+	if err != nil {
+		return nil, err
+	}
+	blockSize := block.BlockSize()
+	data = pkcs5Padding(data, blockSize)
+
+	blockMode := cipher.NewCBCEncrypter(block, a.Key[:blockSize])
+	crypted := make([]byte, len(data))
+
+	blockMode.CryptBlocks(crypted, data)
+	return crypted, nil
+}
+
+// Decrypt decrypt data
+func (a *AesCrypto) Decrypt(crypted []byte) ([]byte, error) {
+	block, err := aes.NewCipher(a.Key)
+	if err != nil {
+		return nil, err
+	}
+	blockSize := block.BlockSize()
+	blockMode := cipher.NewCBCDecrypter(block, a.Key[:blockSize])
+	origData := make([]byte, len(crypted))
+	blockMode.CryptBlocks(origData, crypted)
+	origData = pkcs5UnPadding(origData)
+
+	return origData, nil
+}
+
+func pkcs5Padding(cipherText []byte, blockSize int) []byte {
+	padding := blockSize - len(cipherText)%blockSize
+	padtext := bytes.Repeat([]byte{byte(padding)}, padding)
+	return append(cipherText, padtext...)
+}
+
+func pkcs5UnPadding(data []byte) []byte {
+	length := len(data)
+	unpadding := int(data[length-1])
+	if unpadding >= length {
+		return []byte(``)
+	}
+	return data[:(length - unpadding)]
+}

+ 83 - 0
db/config.go

@@ -0,0 +1,83 @@
+package db
+
+import (
+	"time"
+
+	"git.chuangxin1.com/cx/myth"
+)
+
+// DB error code
+const (
+	ErrOK         = 0 // ErrOK ok
+	ErrException  = 1 // ErrException exception
+	ErrExists     = 2 // ErrExists exists
+	ErrNotFound   = 3 // ErrNotFound not found
+	ErrAuthorized = 4 // ErrAuthorized authorized
+	ErrNotConnect = 5 // ErrNotConnect connect error
+)
+
+// Config config
+type Config struct {
+	Driver       string
+	DNS          string
+	MaxOpenConns int
+	MaxIdle      int
+	MaxLifetime  time.Duration
+}
+
+// Reply db exec return insert/update/delete
+type Reply struct {
+	OK           bool
+	Err          error
+	LastErr      error
+	ErrCode      int
+	LastID       int64
+	RowsAffected int64
+}
+
+// ReplyOk exec ok
+func ReplyOk(rowsAffected, lastID int64) Reply {
+	var reply Reply
+
+	reply.OK = true
+	reply.ErrCode = 0
+	reply.LastID = lastID
+	reply.RowsAffected = rowsAffected
+
+	return reply
+}
+
+// ReplyFaild exec faild
+func ReplyFaild(errCode int, err, errText error) (reply Reply) {
+	reply.OK = false
+	reply.ErrCode = errCode
+	reply.LastID = -1
+	reply.RowsAffected = -1
+
+	reply.Err = err
+	reply.LastErr = errText
+
+	return
+}
+
+// ReplyToReplyData db reply to response
+func ReplyToReplyData(reply Reply) *myth.ReplyData {
+	status := myth.ErrOk
+	if !reply.OK {
+		switch reply.ErrCode {
+		case ErrException:
+			status = myth.ErrException
+		case ErrExists:
+			status = myth.ErrDataExists
+		case ErrNotFound:
+			status = myth.ErrDataNotFound
+		case ErrAuthorized:
+			status = myth.ErrUnAuthorized
+		case ErrNotConnect:
+			status = myth.ErrNotFound
+		}
+		return myth.ReplyErr(status, reply.LastErr.Error())
+	}
+
+	return myth.ReplyOk()
+}

+ 261 - 0
db/db.go

@@ -0,0 +1,261 @@
+package db
+
+import (
+	"database/sql"
+	"errors"
+	"fmt"
+	"sync"
+	"time"
+
+	// mysql
+	_ "github.com/go-sql-driver/mysql"
+	"github.com/jmoiron/sqlx"
+)
+
+var (
+	config Config
+	db     *sqlx.DB
+	err    error
+	once   sync.Once
+)
+
+// DB define
+type DB struct {
+	conn *sqlx.DB
+	tx   *sqlx.Tx
+}
+
+// SetConfig set
+func SetConfig(cfg Config) {
+	config.Driver = cfg.Driver
+	config.DNS = cfg.DNS
+	config.MaxOpenConns = cfg.MaxOpenConns
+	config.MaxIdle = cfg.MaxIdle
+	config.MaxLifetime = cfg.MaxLifetime * time.Second
+}
+
+// New new DB object
+func New() *DB {
+	return &DB{}
+}
+
+// Release free db connect
+func Release() {
+	if db != nil {
+		db.Close()
+	}
+}
+
+// NewConfig new DB dynamic object
+func NewConfig(config Config) (dbx *DB, err error) {
+	dbx = &DB{}
+
+	dbx.conn, err = sqlx.Connect(config.Driver, config.DNS)
+	if err == nil {
+		dbx.conn.SetMaxOpenConns(config.MaxOpenConns)
+		dbx.conn.SetMaxIdleConns(config.MaxIdle)
+		dbx.conn.SetConnMaxLifetime(config.MaxLifetime)
+		dbx.conn.Ping()
+	}
+
+	return
+}
+
+// ReleaseConfig free db connect
+func ReleaseConfig(dbx *DB) {
+	if dbx.conn != nil {
+		dbx.conn.Close()
+	}
+}
+
+func connect() (dbx *sqlx.DB, err error) {
+	once.Do(func() {
+		db, err = sqlx.Connect(config.Driver, config.DNS)
+		if err == nil {
+			db.DB.SetMaxOpenConns(config.MaxOpenConns)
+			db.DB.SetMaxIdleConns(config.MaxIdle)
+			db.DB.SetConnMaxLifetime(config.MaxLifetime)
+			db.Ping()
+		}
+	})
+	dbx = db
+	return
+}
+
+// Connect connect to database
+func (d *DB) Connect() (err error) {
+	if d.conn != nil {
+		return
+	}
+
+	d.conn, err = connect()
+
+	return
+}
+
+// Close close database connect
+func (d *DB) Close() {
+	//d.conn.Close()
+}
+
+// BeginTrans begin trans
+func (d *DB) BeginTrans() (err error) {
+	d.conn, err = connect()
+	if err != nil {
+		return
+	}
+	d.tx = d.conn.MustBegin()
+	return
+}
+
+// Commit commit
+func (d *DB) Commit() error {
+	return d.tx.Commit()
+}
+
+// Rollback rollback
+func (d *DB) Rollback() error {
+	return d.tx.Rollback()
+}
+
+// TransExec trans execute
+func (d *DB) TransExec(query string, args interface{}) (LastInsertId, RowsAffected int64, err error) {
+	if rs, err := d.tx.NamedExec(query, args); err == nil {
+		RowsAffected, _ = rs.RowsAffected()
+		LastInsertId, _ = rs.LastInsertId()
+	}
+	return
+}
+
+// TransUpdate trans update
+func (d *DB) TransUpdate(query string, args interface{}) (reply Reply) {
+	var (
+		err error
+		rs  sql.Result
+	)
+
+	if rs, err = d.tx.NamedExec(query, args); err == nil {
+		a, _ := rs.RowsAffected()
+		reply = ReplyOk(a, 0)
+	} else {
+		reply = ReplyFaild(ErrException, err, errors.New(`数据执行错误`))
+	}
+	return
+}
+
+// Rows get rows
+func (d *DB) Rows(dest interface{}, query string, args interface{}) error {
+	err := d.Connect()
+	if err != nil {
+		return err
+	}
+	defer d.Close()
+
+	nstmt, err := d.conn.PrepareNamed(query)
+	if err != nil {
+		return err
+	}
+	defer nstmt.Close()
+
+	err = nstmt.Select(dest, args)
+
+	return err
+}
+
+// Row get row
+func (d *DB) Row(dest interface{}, query string, args interface{}) error {
+	err := d.Connect()
+	if err != nil {
+		return err
+	}
+	defer d.Close()
+
+	nstmt, err := d.conn.PrepareNamed(query)
+	if err != nil {
+		return err
+	}
+	defer nstmt.Close()
+
+	err = nstmt.Get(dest, args)
+
+	return err
+}
+
+// InsertReply insert and return DbReply
+func (d *DB) InsertReply(query string, args interface{}) (reply Reply) {
+	var (
+		err error
+		rs  sql.Result
+	)
+	err = d.Connect()
+	if err != nil {
+		reply = ReplyFaild(ErrNotConnect, err, errors.New(`数据库连接错误`))
+		return
+	}
+	defer d.Close()
+
+	if rs, err = d.conn.NamedExec(query, args); err == nil {
+		a, _ := rs.RowsAffected()
+		n, _ := rs.LastInsertId()
+		reply = ReplyOk(a, n)
+	} else {
+		reply = ReplyFaild(ErrException, err, errors.New(`数据执行错误`))
+	}
+	return
+}
+
+// UpdateReply update/delete and return DbReply
+func (d *DB) UpdateReply(query string, args interface{}) (reply Reply) {
+	var (
+		err error
+		rs  sql.Result
+	)
+	err = d.Connect()
+	if err != nil {
+		reply = ReplyFaild(ErrNotConnect, err, errors.New(`数据库连接错误`))
+		return
+	}
+	defer d.Close()
+
+	if rs, err = d.conn.NamedExec(query, args); err == nil {
+		a, _ := rs.RowsAffected()
+		reply = ReplyOk(a, 0)
+	} else {
+		reply = ReplyFaild(ErrException, err, errors.New(`数据执行错误`))
+	}
+	return
+}
+
+// Insert insert into
+func (d *DB) Insert(query string, args interface{}) (LastInsertId, RowsAffected int64, err error) {
+	err = d.Connect()
+	if err != nil {
+		return
+	}
+	defer d.Close()
+
+	if rs, err := d.conn.NamedExec(query, args); err == nil {
+		LastInsertId, _ = rs.LastInsertId()
+		RowsAffected, _ = rs.RowsAffected()
+	}
+	return
+}
+
+// Update update/delete
+func (d *DB) Update(query string, args interface{}) (RowsAffected int64, err error) {
+	err = d.Connect()
+	if err != nil {
+		return
+	}
+	defer d.Close()
+
+	if rs, err := d.conn.NamedExec(query, args); err == nil {
+		RowsAffected, _ = rs.RowsAffected()
+	}
+	return
+}
+
+// Limit MySQL limit
+func (d *DB) Limit(page, pagesize int) string {
+	return fmt.Sprintf(" limit %d, %d", (page-1)*pagesize, pagesize)
+}

+ 11 - 0
error.go

@@ -0,0 +1,11 @@
+package myth
+
+import "database/sql"
+
+// ErrSQLNoRows check norows error
+func ErrSQLNoRows(err error) bool {
+	if err == sql.ErrNoRows {
+		return true
+	}
+	return false
+}

+ 13 - 0
go.mod

@@ -0,0 +1,13 @@
+module git.chuangxin1.com/cx/myth
+
+go 1.12
+
+require (
+	github.com/go-redis/redis v6.15.2+incompatible
+	github.com/go-sql-driver/mysql v1.4.1
+	github.com/golang/protobuf v1.3.1
+	github.com/jmoiron/sqlx v1.2.0
+	github.com/stretchr/testify v1.3.0
+	github.com/ugorji/go v1.1.4
+	gopkg.in/yaml.v2 v2.2.2
+)

+ 26 - 0
go.sum

@@ -0,0 +1,26 @@
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs=
+github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
+github.com/go-redis/redis v6.15.2+incompatible h1:9SpNVG76gr6InJGxoZ6IuuxaCOQwDAhzyXg+Bs+0Sb4=
+github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
+github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk=
+github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
+github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
+github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
+github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw=
+github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

+ 29 - 0
handler.go

@@ -0,0 +1,29 @@
+package myth
+
+import (
+	"net/http"
+	"runtime"
+)
+
+// PanicHandler panic router
+func PanicHandler(w http.ResponseWriter, r *http.Request, err interface{}) {
+	var text string
+	e, ok := err.(runtime.Error)
+	if ok {
+		text = e.Error()
+	} else {
+		text = err.(string)
+	}
+
+	WriteJSON(w, ReplyErr(ErrException, text))
+}
+
+// NotFoundHandler 404
+func NotFoundHandler(w http.ResponseWriter, req *http.Request) {
+	WriteJSON(w, ReplyErr(ErrNotFound, `NotFound`))
+}
+
+// NotAllowedHandler 405
+func NotAllowedHandler(w http.ResponseWriter, req *http.Request) {
+	WriteJSON(w, ReplyErr(ErrNotAllowed, `Method Not Allowed`))
+}

+ 41 - 0
hash.go

@@ -0,0 +1,41 @@
+package myth
+
+import (
+	"crypto/md5"
+	"crypto/sha1"
+	"crypto/sha256"
+	"crypto/sha512"
+	"encoding/hex"
+)
+
+// SHA512 hash
+func SHA512(data []byte) ([]byte, error) {
+	hash := sha512.New()
+	_, err := hash.Write(data)
+	if err != nil {
+		return nil, err
+	}
+
+	return hash.Sum(nil), nil
+}
+
+// SHA256 hash string
+func SHA256(data string) string {
+	hash := sha256.New()
+	hash.Write([]byte(data))
+	return hex.EncodeToString(hash.Sum(nil))
+}
+
+// MD5 hash string
+func MD5(data string) string {
+	hash := md5.New()
+	hash.Write([]byte(data))
+	return hex.EncodeToString(hash.Sum(nil))
+}
+
+// SHA1 hash string
+func SHA1(data string) string {
+	hash := sha1.New()
+	hash.Write([]byte(data))
+	return hex.EncodeToString(hash.Sum(nil))
+}

+ 123 - 0
path.go

@@ -0,0 +1,123 @@
+// Copyright 2013 Julien Schmidt. All rights reserved.
+// Based on the path package, Copyright 2009 The Go Authors.
+// Use of this source code is governed by a BSD-style license that can be found
+// in the LICENSE file.
+
+package myth
+
+// CleanPath is the URL version of path.Clean, it returns a canonical URL path
+// for p, eliminating . and .. elements.
+//
+// The following rules are applied iteratively until no further processing can
+// be done:
+//	1. Replace multiple slashes with a single slash.
+//	2. Eliminate each . path name element (the current directory).
+//	3. Eliminate each inner .. path name element (the parent directory)
+//	   along with the non-.. element that precedes it.
+//	4. Eliminate .. elements that begin a rooted path:
+//	   that is, replace "/.." by "/" at the beginning of a path.
+//
+// If the result of this process is an empty string, "/" is returned
+func CleanPath(p string) string {
+	// Turn empty string into "/"
+	if p == "" {
+		return "/"
+	}
+
+	n := len(p)
+	var buf []byte
+
+	// Invariants:
+	//      reading from path; r is index of next byte to process.
+	//      writing to buf; w is index of next byte to write.
+
+	// path must start with '/'
+	r := 1
+	w := 1
+
+	if p[0] != '/' {
+		r = 0
+		buf = make([]byte, n+1)
+		buf[0] = '/'
+	}
+
+	trailing := n > 1 && p[n-1] == '/'
+
+	// A bit more clunky without a 'lazybuf' like the path package, but the loop
+	// gets completely inlined (bufApp). So in contrast to the path package this
+	// loop has no expensive function calls (except 1x make)
+
+	for r < n {
+		switch {
+		case p[r] == '/':
+			// empty path element, trailing slash is added after the end
+			r++
+
+		case p[r] == '.' && r+1 == n:
+			trailing = true
+			r++
+
+		case p[r] == '.' && p[r+1] == '/':
+			// . element
+			r += 2
+
+		case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
+			// .. element: remove to last /
+			r += 3
+
+			if w > 1 {
+				// can backtrack
+				w--
+
+				if buf == nil {
+					for w > 1 && p[w] != '/' {
+						w--
+					}
+				} else {
+					for w > 1 && buf[w] != '/' {
+						w--
+					}
+				}
+			}
+
+		default:
+			// real path element.
+			// add slash if needed
+			if w > 1 {
+				bufApp(&buf, p, w, '/')
+				w++
+			}
+
+			// copy element
+			for r < n && p[r] != '/' {
+				bufApp(&buf, p, w, p[r])
+				w++
+				r++
+			}
+		}
+	}
+
+	// re-append trailing slash
+	if trailing && w > 1 {
+		bufApp(&buf, p, w, '/')
+		w++
+	}
+
+	if buf == nil {
+		return p[:w]
+	}
+	return string(buf[:w])
+}
+
+// internal helper to lazily create a buffer if necessary
+func bufApp(buf *[]byte, s string, w int, c byte) {
+	if *buf == nil {
+		if s[w] == c {
+			return
+		}
+
+		*buf = make([]byte, len(s))
+		copy(*buf, s[:w])
+	}
+	(*buf)[w] = c
+}

+ 119 - 0
reply.go

@@ -0,0 +1,119 @@
+package myth
+
+// Error code & constant
+const (
+	ErrOk               = 0
+	ErrNotFound         = 1001 // ErrNotFound 404 route not found
+	ErrException        = 1002 // ErrException 500
+	ErrBadRequest       = 1003 // ErrBadRequest 400 route params error
+	ErrMethodNotAllowed = 1004 // ErrMethodNotAllowed 405
+	ErrParamsError      = 1005 // ErrParamsError 415
+	ErrUnAuthorized     = 1006 // ErrUnAuthorized 401
+	ErrDataNotFound     = 1007 // ErrDataNotFound 404
+	ErrNotAllowed       = 1008 // ErrNotAllowed 405
+	ErrDataExists       = 1009 // ErrDataExists 400
+	ErrDataValidate     = 1010 // ErrDataValidate 403
+
+	VarUserAuthorization    = `access_token`  // oauth token
+	HTTPHeaderAuthorization = `Authorization` // HTTP header Authorization
+	HTTPHeaderToken         = `X-Token`       // HTTP header Authorization X-Token
+)
+
+var (
+	statusMessage map[int]string
+)
+
+// ReplyData define API output data
+type ReplyData struct {
+	Status    int               `json:"status" xml:"status"`                     // Status code
+	Message   string            `json:"message" xml:"message"`                   // Message description
+	Errs      map[string]string `json:"errors,omitempty" xml:"errors,omitempty"` // Errs errors
+	PageCount int               `json:"pageCount,omitempty"`
+	Total     int               `json:"total,omitempty" xml:"total,omitempty"` // Total data total
+	List      interface{}       `json:"rows,omitempty" xml:"rows,omitempty"`   // List data list
+	Data      interface{}       `json:"data,omitempty" xml:"data,omitempty"`   // Data data attribute
+}
+
+func init() {
+	statusMessage = make(map[int]string)
+	statusMessage[ErrOk] = `ok`
+	statusMessage[ErrNotFound] = `Not found`
+	statusMessage[ErrException] = `Exception`
+	statusMessage[ErrBadRequest] = `Routing parameter error`
+	statusMessage[ErrMethodNotAllowed] = `Method not allowed`
+	statusMessage[ErrParamsError] = `Parameter or format error`
+	statusMessage[ErrUnAuthorized] = `Not sign in or session has expired`
+	statusMessage[ErrDataNotFound] = `Data not found`
+	statusMessage[ErrNotAllowed] = `No access`
+	statusMessage[ErrDataExists] = `Data exists`
+	statusMessage[ErrDataValidate] = `Data verification failed`
+}
+
+// NewReplyData creates and return ReplyData with status and message
+func NewReplyData(status int) *ReplyData {
+	var (
+		text   string
+		exists bool
+	)
+	if text, exists = statusMessage[status]; !exists {
+		text = `incorrect data type`
+	}
+	return &ReplyData{
+		Status:  status,
+		Message: text,
+	}
+}
+
+// ReplyOk creates and return ReplyData with ok
+func ReplyOk() *ReplyData {
+	message, _ := statusMessage[ErrOk]
+	return &ReplyData{
+		Status:  ErrOk,
+		Message: message,
+	}
+}
+
+// ReplyErr creates and return ReplyData with error and message
+func ReplyErr(status int, message string) *ReplyData {
+	text, _ := statusMessage[status]
+	errs := map[string]string{
+		"message": message,
+	}
+	return &ReplyData{
+		Status:  status,
+		Message: text,
+		Errs:    errs,
+	}
+}
+
+// ReplyErrors creates and return ReplyData with errors
+func ReplyErrors(status int, errors map[string]string) *ReplyData {
+	message, _ := statusMessage[status]
+	return &ReplyData{
+		Status:  status,
+		Message: message,
+		Errs:    errors,
+	}
+}
+
+// ReplyRows creates and return ReplyData with total and list
+func ReplyRows(total, pageCount int, rows interface{}) *ReplyData {
+	message, _ := statusMessage[ErrOk]
+	return &ReplyData{
+		Status:    ErrOk,
+		Message:   message,
+		List:      rows,
+		Total:     total,
+		PageCount: pageCount,
+	}
+}
+
+// ReplyRow creates and return ReplyData with attr row
+func ReplyRow(row interface{}) *ReplyData {
+	message, _ := statusMessage[ErrOk]
+	return &ReplyData{
+		Status:  ErrOk,
+		Message: message,
+		Data:    row,
+	}
+}

+ 125 - 0
request.go

@@ -0,0 +1,125 @@
+package myth
+
+import (
+	"compress/gzip"
+	"crypto/tls"
+	"crypto/x509"
+	"io"
+	"io/ioutil"
+	"net"
+	"net/http"
+	"time"
+)
+
+const (
+	// RequestTimeOut http request timeout (second)
+	RequestTimeOut = 30
+)
+
+// GetRealIP get real IP from Request
+func GetRealIP(req *http.Request) (ip string) {
+	if ips := req.Header["X-Real-Ip"]; ips != nil {
+		ip = ips[0]
+	}
+	return
+}
+
+// HTTPMessage HTTP response
+type HTTPMessage struct {
+	StatusCode int
+	Body       []byte
+	Header     http.Header
+}
+
+func newRequest(method, uri, certPath, keyPath string, header map[string]string, body io.Reader) (res *http.Response, err error) {
+	t := &http.Transport{
+		Dial: func(netw, addr string) (net.Conn, error) {
+			conn, err := net.DialTimeout(netw, addr, time.Second*RequestTimeOut)
+			if err != nil {
+				return nil, err
+			}
+			conn.SetDeadline(time.Now().Add(time.Second * RequestTimeOut))
+			return conn, nil
+		},
+		ResponseHeaderTimeout: time.Second * RequestTimeOut,
+	}
+
+	if certPath != "" {
+		cert, e := tls.LoadX509KeyPair(certPath, keyPath)
+		if e != nil {
+			t.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+		} else {
+			pool := x509.NewCertPool()
+			t.TLSClientConfig = &tls.Config{InsecureSkipVerify: true, Certificates: []tls.Certificate{cert}, RootCAs: pool}
+		}
+	}
+	client := &http.Client{Transport: t}
+	var (
+		req *http.Request
+	)
+
+	if body != nil {
+		req, err = http.NewRequest(method, uri, body)
+	} else {
+		req, err = http.NewRequest(method, uri, nil)
+	}
+
+	if err != nil {
+		return
+	}
+
+	for k := range header {
+		req.Header.Add(k, header[k])
+	}
+
+	res, err = client.Do(req)
+	return
+}
+
+func readBody(res *http.Response) (msg HTTPMessage, err error) {
+	var (
+		body   []byte
+		reader io.Reader
+	)
+	encoding := res.Header.Get("Content-Encoding")
+	switch encoding {
+	case "gzip":
+		reader, err = gzip.NewReader(res.Body)
+		if err == nil {
+			body, err = ioutil.ReadAll(reader)
+		}
+	default:
+		body, err = ioutil.ReadAll(res.Body)
+	}
+	if err != nil {
+		return
+	}
+
+	msg.StatusCode = res.StatusCode
+	msg.Header = res.Header
+	msg.Body = body
+	return
+}
+
+// Post HTTP request POST
+func Post(uri, certPath, keyPath string, header map[string]string, data io.Reader) (msg HTTPMessage, err error) {
+	var res *http.Response
+	if res, err = newRequest("POST", uri, certPath, keyPath, header, data); err != nil {
+		return
+	}
+	defer res.Body.Close()
+	msg, err = readBody(res)
+	return
+}
+
+// Get HTTP request GET
+func Get(uri, certPath, keyPath string, header map[string]string) (msg HTTPMessage, err error) {
+	var res *http.Response
+	if res, err = newRequest("GET", uri, certPath, keyPath, header, nil); err != nil {
+		return
+	}
+
+	defer res.Body.Close()
+	msg, err = readBody(res)
+	return
+}

+ 38 - 0
response.go

@@ -0,0 +1,38 @@
+package myth
+
+import (
+	"encoding/json"
+	"encoding/xml"
+	"net/http"
+)
+
+func header(w http.ResponseWriter, contentType string) {
+	w.Header().Set(`Content-Type`, contentType)
+	w.Header().Set(`X-Powered-By`, LibName+`/`+LibVersion)
+	w.WriteHeader(http.StatusOK)
+}
+
+// Redirect redirect
+func Redirect(w http.ResponseWriter, url string) {
+	w.Header().Set(`Location`, url)
+	w.WriteHeader(http.StatusFound)
+}
+
+// WriteJSON response JSON data.
+func WriteJSON(w http.ResponseWriter, response interface{}) error {
+	header(w, `application/json; charset=utf-8`)
+	return json.NewEncoder(w).Encode(response)
+}
+
+// WriteXML response XML data.
+func WriteXML(w http.ResponseWriter, response interface{}) error {
+	header(w, `application/xml; charset=utf-8`)
+	return xml.NewEncoder(w).Encode(response)
+}
+
+// WriteBytes response bytes
+func WriteBytes(w http.ResponseWriter, response interface{}) error {
+	header(w, `text/html; charset=utf-8`)
+	_, err := w.Write(response.([]byte))
+	return err
+}

+ 398 - 0
router.go

@@ -0,0 +1,398 @@
+// Copyright 2013 Julien Schmidt. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be found
+// in the LICENSE file.
+
+package myth
+
+// Package httprouter is a trie based high performance HTTP request router.
+//
+// A trivial example is:
+//
+//  package main
+//
+//  import (
+//      "fmt"
+//      "github.com/julienschmidt/httprouter"
+//      "net/http"
+//      "log"
+//  )
+//
+//  func Index(w http.ResponseWriter, r *http.Request) {
+//      fmt.Fprint(w, "Welcome!\n")
+//  }
+//
+//  func Hello(w http.ResponseWriter, r *http.Request) {
+//			var name string
+//			if vars := httprouter.Vars(); vars != nil {
+//				name, _ = vars["name"]
+//			}
+//      fmt.Fprintf(w, "hello, %s!\n", name)
+//  }
+//
+//  func main() {
+//      router := httprouter.New()
+//      router.GET("/", Index)
+//      router.GET("/hello/:name", Hello)
+//
+//      log.Fatal(http.ListenAndServe(":8080", router))
+//  }
+//
+// The router matches incoming requests by the request method and the path.
+// If a handle is registered for this path and method, the router delegates the
+// request to that function.
+// For the methods GET, POST, PUT, PATCH and DELETE shortcut functions exist to
+// register handles, for all other methods router.Handle can be used.
+//
+// The registered path, against which the router matches incoming requests, can
+// contain two types of parameters:
+//  Syntax    Type
+//  :name     named parameter
+//  *name     catch-all parameter
+//
+// Named parameters are dynamic path segments. They match anything until the
+// next '/' or the path end:
+//  Path: /blog/:category/:post
+//
+//  Requests:
+//   /blog/go/request-routers            match: category="go", post="request-routers"
+//   /blog/go/request-routers/           no match, but the router would redirect
+//   /blog/go/                           no match
+//   /blog/go/request-routers/comments   no match
+//
+// Catch-all parameters match anything until the path end, including the
+// directory index (the '/' before the catch-all). Since they match anything
+// until the end, catch-all parameters must always be the final path element.
+//  Path: /files/*filepath
+//
+//  Requests:
+//   /files/                             match: filepath="/"
+//   /files/LICENSE                      match: filepath="/LICENSE"
+//   /files/templates/article.html       match: filepath="/templates/article.html"
+//   /files                              no match, but the router would redirect
+//
+// The value of parameters is saved as a slice of the Param struct, consisting
+// each of a key and a value. The slice is passed to the Handle func as a third
+// parameter.
+// There are two ways to retrieve the value of a parameter:
+//  // by the name of the parameter
+//  user := ps.ByName("user") // defined by :user or *user
+//
+//  // by the index of the parameter. This way you can also get the name (key)
+//  thirdKey   := ps[2].Key   // the name of the 3rd parameter
+//  thirdValue := ps[2].Value // the value of the 3rd parameter
+
+import (
+	"net/http"
+)
+
+// Handle is a function that can be registered to a route to handle HTTP
+// requests. Like http.HandlerFunc, but has a third parameter for the values of
+// wildcards (variables).
+type Handle func(http.ResponseWriter, *http.Request)
+
+// Router is a http.Handler which can be used to dispatch requests to different
+// handler functions via configurable routes
+type Router struct {
+	trees map[string]*node
+
+	// Enables automatic redirection if the current route can't be matched but a
+	// handler for the path with (without) the trailing slash exists.
+	// For example if /foo/ is requested but a route only exists for /foo, the
+	// client is redirected to /foo with http status code 301 for GET requests
+	// and 307 for all other request methods.
+	RedirectTrailingSlash bool
+
+	// If enabled, the router tries to fix the current request path, if no
+	// handle is registered for it.
+	// First superfluous path elements like ../ or // are removed.
+	// Afterwards the router does a case-insensitive lookup of the cleaned path.
+	// If a handle can be found for this route, the router makes a redirection
+	// to the corrected path with status code 301 for GET requests and 307 for
+	// all other request methods.
+	// For example /FOO and /..//Foo could be redirected to /foo.
+	// RedirectTrailingSlash is independent of this option.
+	RedirectFixedPath bool
+
+	// If enabled, the router checks if another method is allowed for the
+	// current route, if the current request can not be routed.
+	// If this is the case, the request is answered with 'Method Not Allowed'
+	// and HTTP status code 405.
+	// If no other Method is allowed, the request is delegated to the NotFound
+	// handler.
+	HandleMethodNotAllowed bool
+
+	// If enabled, the router automatically replies to OPTIONS requests.
+	// Custom OPTIONS handlers take priority over automatic replies.
+	HandleOPTIONS bool
+
+	// Configurable http.Handler which is called when no matching route is
+	// found. If it is not set, http.NotFound is used.
+	NotFound http.Handler
+
+	// Configurable http.Handler which is called when a request
+	// cannot be routed and HandleMethodNotAllowed is true.
+	// If it is not set, http.Error with http.StatusMethodNotAllowed is used.
+	// The "Allow" header with allowed request methods is set before the handler
+	// is called.
+	MethodNotAllowed http.Handler
+
+	// Function to handle panics recovered from http handlers.
+	// It should be used to generate a error page and return the http error code
+	// 500 (Internal Server Error).
+	// The handler can be used to keep your server from crashing because of
+	// unrecovered panics.
+	PanicHandler func(http.ResponseWriter, *http.Request, interface{})
+}
+
+// Make sure the Router conforms with the http.Handler interface
+var _ http.Handler = NewRouter()
+
+// NewRouter returns a new initialized Router.
+// Path auto-correction, including trailing slashes, is enabled by default.
+func NewRouter() *Router {
+	return &Router{
+		RedirectTrailingSlash:  true,
+		RedirectFixedPath:      true,
+		HandleMethodNotAllowed: true,
+		HandleOPTIONS:          true,
+	}
+}
+
+// GET is a shortcut for router.Handle("GET", path, handle)
+func (r *Router) GET(path string, handle Handle) {
+	r.Handle("GET", path, handle)
+}
+
+// HEAD is a shortcut for router.Handle("HEAD", path, handle)
+func (r *Router) HEAD(path string, handle Handle) {
+	r.Handle("HEAD", path, handle)
+}
+
+// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle)
+func (r *Router) OPTIONS(path string, handle Handle) {
+	r.Handle("OPTIONS", path, handle)
+}
+
+// POST is a shortcut for router.Handle("POST", path, handle)
+func (r *Router) POST(path string, handle Handle) {
+	r.Handle("POST", path, handle)
+}
+
+// PUT is a shortcut for router.Handle("PUT", path, handle)
+func (r *Router) PUT(path string, handle Handle) {
+	r.Handle("PUT", path, handle)
+}
+
+// PATCH is a shortcut for router.Handle("PATCH", path, handle)
+func (r *Router) PATCH(path string, handle Handle) {
+	r.Handle("PATCH", path, handle)
+}
+
+// DELETE is a shortcut for router.Handle("DELETE", path, handle)
+func (r *Router) DELETE(path string, handle Handle) {
+	r.Handle("DELETE", path, handle)
+}
+
+// Handle registers a new request handle with the given path and method.
+//
+// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut
+// functions can be used.
+//
+// This function is intended for bulk loading and to allow the usage of less
+// frequently used, non-standardized or custom methods (e.g. for internal
+// communication with a proxy).
+func (r *Router) Handle(method, path string, handle Handle) {
+	if path[0] != '/' {
+		panic("path must begin with '/' in path '" + path + "'")
+	}
+
+	if r.trees == nil {
+		r.trees = make(map[string]*node)
+	}
+
+	root := r.trees[method]
+	if root == nil {
+		root = new(node)
+		r.trees[method] = root
+	}
+
+	root.addRoute(path, handle)
+}
+
+// Handler is an adapter which allows the usage of an http.Handler as a
+// request handle. With go 1.7+, the Params will be available in the
+// request context under ParamsKey.
+func (r *Router) Handler(method, path string, handler http.Handler) {
+	r.Handle(method, path,
+		func(w http.ResponseWriter, req *http.Request) {
+			handler.ServeHTTP(w, req)
+		},
+	)
+}
+
+// HandlerFunc is an adapter which allows the usage of an http.HandlerFunc as a
+// request handle.
+func (r *Router) HandlerFunc(method, path string, handler http.HandlerFunc) {
+	r.Handler(method, path, handler)
+}
+
+// ServeFiles serves files from the given file system root.
+// The path must end with "/*filepath", files are then served from the local
+// path /defined/root/dir/*filepath.
+// For example if root is "/etc" and *filepath is "passwd", the local file
+// "/etc/passwd" would be served.
+// Internally a http.FileServer is used, therefore http.NotFound is used instead
+// of the Router's NotFound handler.
+// To use the operating system's file system implementation,
+// use http.Dir:
+//     router.ServeFiles("/src/*filepath", http.Dir("/var/www"))
+func (r *Router) ServeFiles(path string, root http.FileSystem) {
+	if len(path) < 10 || path[len(path)-10:] != "/*filepath" {
+		panic("path must end with /*filepath in path '" + path + "'")
+	}
+
+	fileServer := http.FileServer(root)
+
+	r.GET(path, func(w http.ResponseWriter, req *http.Request) {
+		if vars := Vars(req); vars != nil {
+			path, _ := vars["filepath"]
+			req.URL.Path = path
+		}
+		fileServer.ServeHTTP(w, req)
+	})
+}
+
+func (r *Router) recv(w http.ResponseWriter, req *http.Request) {
+	if rcv := recover(); rcv != nil {
+		r.PanicHandler(w, req, rcv)
+	}
+}
+
+// Lookup allows the manual lookup of a method + path combo.
+// This is e.g. useful to build a framework around this router.
+// If the path was found, it returns the handle function and the path parameter
+// values. Otherwise the third return value indicates whether a redirection to
+// the same path with an extra / without the trailing slash should be performed.
+func (r *Router) Lookup(method, path string) (Handle, map[string]string, bool) {
+	if root := r.trees[method]; root != nil {
+		return root.getValue(path)
+	}
+	return nil, nil, false
+}
+
+func (r *Router) allowed(path, reqMethod string) (allow string) {
+	if path == "*" { // server-wide
+		for method := range r.trees {
+			if method == "OPTIONS" {
+				continue
+			}
+
+			// add request method to list of allowed methods
+			if len(allow) == 0 {
+				allow = method
+			} else {
+				allow += ", " + method
+			}
+		}
+	} else { // specific path
+		for method := range r.trees {
+			// Skip the requested method - we already tried this one
+			if method == reqMethod || method == "OPTIONS" {
+				continue
+			}
+
+			handle, _, _ := r.trees[method].getValue(path)
+			if handle != nil {
+				// add request method to list of allowed methods
+				if len(allow) == 0 {
+					allow = method
+				} else {
+					allow += ", " + method
+				}
+			}
+		}
+	}
+	if len(allow) > 0 {
+		allow += ", OPTIONS"
+	}
+	return
+}
+
+// ServeHTTP makes the router implement the http.Handler interface.
+func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	if r.PanicHandler != nil {
+		defer r.recv(w, req)
+	}
+
+	path := req.URL.Path
+
+	if root := r.trees[req.Method]; root != nil {
+		if handle, ps, tsr := root.getValue(path); handle != nil {
+			req = setVars(req, ps)
+			req = setCurrentPath(req, path)
+			handle(w, req)
+			return
+		} else if req.Method != "CONNECT" && path != "/" {
+			code := 301 // Permanent redirect, request with GET method
+			if req.Method != "GET" {
+				// Temporary redirect, request with same method
+				// As of Go 1.3, Go does not support status code 308.
+				code = 307
+			}
+
+			if tsr && r.RedirectTrailingSlash {
+				if len(path) > 1 && path[len(path)-1] == '/' {
+					req.URL.Path = path[:len(path)-1]
+				} else {
+					req.URL.Path = path + "/"
+				}
+				http.Redirect(w, req, req.URL.String(), code)
+				return
+			}
+
+			// Try to fix the request path
+			if r.RedirectFixedPath {
+				fixedPath, found := root.findCaseInsensitivePath(
+					CleanPath(path),
+					r.RedirectTrailingSlash,
+				)
+				if found {
+					req.URL.Path = string(fixedPath)
+					http.Redirect(w, req, req.URL.String(), code)
+					return
+				}
+			}
+		}
+	}
+
+	if req.Method == "OPTIONS" && r.HandleOPTIONS {
+		// Handle OPTIONS requests
+		if allow := r.allowed(path, req.Method); len(allow) > 0 {
+			w.Header().Set("Allow", allow)
+			return
+		}
+	} else {
+		// Handle 405
+		if r.HandleMethodNotAllowed {
+			if allow := r.allowed(path, req.Method); len(allow) > 0 {
+				w.Header().Set("Allow", allow)
+				if r.MethodNotAllowed != nil {
+					r.MethodNotAllowed.ServeHTTP(w, req)
+				} else {
+					http.Error(w,
+						http.StatusText(http.StatusMethodNotAllowed),
+						http.StatusMethodNotAllowed,
+					)
+				}
+				return
+			}
+		}
+	}
+
+	// Handle 404
+	if r.NotFound != nil {
+		r.NotFound.ServeHTTP(w, req)
+	} else {
+		http.NotFound(w, req)
+	}
+}

+ 58 - 0
serve.go

@@ -0,0 +1,58 @@
+package myth
+
+import (
+	"fmt"
+	"log"
+	"net/http"
+	"os"
+	"os/signal"
+	"syscall"
+	"time"
+)
+
+func newServe(addr string, router http.Handler) *http.Server {
+	return &http.Server{
+		Addr:           addr,
+		Handler:        router,
+		ReadTimeout:    30 * time.Second,
+		WriteTimeout:   30 * time.Second,
+		MaxHeaderBytes: 1 << 20,
+	}
+}
+
+// ListenAndServe new server and start
+func ListenAndServe(addr string, router http.Handler) {
+	s := newServe(addr, router)
+	errc := make(chan error)
+	go func() {
+		c := make(chan os.Signal)
+		signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
+		errc <- fmt.Errorf("%s", <-c)
+	}()
+
+	go func() {
+		log.Println("HTTP Server listen on", addr)
+		errc <- s.ListenAndServe()
+		log.Println("Exit HTTP server", "Quit")
+	}()
+	log.Println("Exit", <-errc)
+}
+
+// ListenAndServeTLS new server and start
+func ListenAndServeTLS(addr, certFile, keyFile string, router http.Handler) {
+	s := newServe(addr, router)
+
+	errc := make(chan error)
+	go func() {
+		c := make(chan os.Signal)
+		signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
+		errc <- fmt.Errorf("%s", <-c)
+	}()
+
+	go func() {
+		log.Println("HTTP Server listen on", addr)
+		errc <- s.ListenAndServeTLS(certFile, keyFile)
+		log.Println("Exit HTTP server", "Quit")
+	}()
+	log.Println("Exit", <-errc)
+}

+ 657 - 0
tree.go

@@ -0,0 +1,657 @@
+// Copyright 2013 Julien Schmidt. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be found
+// in the LICENSE file.
+
+package myth
+
+import (
+	"strings"
+	"unicode"
+	"unicode/utf8"
+)
+
+func min(a, b int) int {
+	if a <= b {
+		return a
+	}
+	return b
+}
+
+func countParams(path string) uint8 {
+	var n uint
+	for i := 0; i < len(path); i++ {
+		if path[i] != ':' && path[i] != '*' {
+			continue
+		}
+		n++
+	}
+	if n >= 255 {
+		return 255
+	}
+	return uint8(n)
+}
+
+type nodeType uint8
+
+const (
+	static nodeType = iota // default
+	root
+	param
+	catchAll
+)
+
+type node struct {
+	path      string
+	wildChild bool
+	nType     nodeType
+	maxParams uint8
+	indices   string
+	children  []*node
+	handle    Handle
+	priority  uint32
+}
+
+// increments priority of the given child and reorders if necessary
+func (n *node) incrementChildPrio(pos int) int {
+	n.children[pos].priority++
+	prio := n.children[pos].priority
+
+	// adjust position (move to front)
+	newPos := pos
+	for newPos > 0 && n.children[newPos-1].priority < prio {
+		// swap node positions
+		n.children[newPos-1], n.children[newPos] = n.children[newPos], n.children[newPos-1]
+
+		newPos--
+	}
+
+	// build new index char string
+	if newPos != pos {
+		n.indices = n.indices[:newPos] + // unchanged prefix, might be empty
+			n.indices[pos:pos+1] + // the index char we move
+			n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos'
+	}
+
+	return newPos
+}
+
+// addRoute adds a node with the given handle to the path.
+// Not concurrency-safe!
+func (n *node) addRoute(path string, handle Handle) {
+	fullPath := path
+	n.priority++
+	numParams := countParams(path)
+
+	// non-empty tree
+	if len(n.path) > 0 || len(n.children) > 0 {
+	walk:
+		for {
+			// Update maxParams of the current node
+			if numParams > n.maxParams {
+				n.maxParams = numParams
+			}
+
+			// Find the longest common prefix.
+			// This also implies that the common prefix contains no ':' or '*'
+			// since the existing key can't contain those chars.
+			i := 0
+			max := min(len(path), len(n.path))
+			for i < max && path[i] == n.path[i] {
+				i++
+			}
+
+			// Split edge
+			if i < len(n.path) {
+				child := node{
+					path:      n.path[i:],
+					wildChild: n.wildChild,
+					nType:     static,
+					indices:   n.indices,
+					children:  n.children,
+					handle:    n.handle,
+					priority:  n.priority - 1,
+				}
+
+				// Update maxParams (max of all children)
+				for i := range child.children {
+					if child.children[i].maxParams > child.maxParams {
+						child.maxParams = child.children[i].maxParams
+					}
+				}
+
+				n.children = []*node{&child}
+				// []byte for proper unicode char conversion, see #65
+				n.indices = string([]byte{n.path[i]})
+				n.path = path[:i]
+				n.handle = nil
+				n.wildChild = false
+			}
+
+			// Make new node a child of this node
+			if i < len(path) {
+				path = path[i:]
+
+				if n.wildChild {
+					n = n.children[0]
+					n.priority++
+
+					// Update maxParams of the child node
+					if numParams > n.maxParams {
+						n.maxParams = numParams
+					}
+					numParams--
+
+					// Check if the wildcard matches
+					if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
+						// Check for longer wildcard, e.g. :name and :names
+						(len(n.path) >= len(path) || path[len(n.path)] == '/') {
+						continue walk
+					} else {
+						// Wildcard conflict
+						var pathSeg string
+						if n.nType == catchAll {
+							pathSeg = path
+						} else {
+							pathSeg = strings.SplitN(path, "/", 2)[0]
+						}
+						prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
+						panic("'" + pathSeg +
+							"' in new path '" + fullPath +
+							"' conflicts with existing wildcard '" + n.path +
+							"' in existing prefix '" + prefix +
+							"'")
+					}
+				}
+
+				c := path[0]
+
+				// slash after param
+				if n.nType == param && c == '/' && len(n.children) == 1 {
+					n = n.children[0]
+					n.priority++
+					continue walk
+				}
+
+				// Check if a child with the next path byte exists
+				for i := 0; i < len(n.indices); i++ {
+					if c == n.indices[i] {
+						i = n.incrementChildPrio(i)
+						n = n.children[i]
+						continue walk
+					}
+				}
+
+				// Otherwise insert it
+				if c != ':' && c != '*' {
+					// []byte for proper unicode char conversion, see #65
+					n.indices += string([]byte{c})
+					child := &node{
+						maxParams: numParams,
+					}
+					n.children = append(n.children, child)
+					n.incrementChildPrio(len(n.indices) - 1)
+					n = child
+				}
+				n.insertChild(numParams, path, fullPath, handle)
+				return
+
+			} else if i == len(path) { // Make node a (in-path) leaf
+				if n.handle != nil {
+					panic("a handle is already registered for path '" + fullPath + "'")
+				}
+				n.handle = handle
+			}
+			return
+		}
+	} else { // Empty tree
+		n.insertChild(numParams, path, fullPath, handle)
+		n.nType = root
+	}
+}
+
+func (n *node) insertChild(numParams uint8, path, fullPath string, handle Handle) {
+	var offset int // already handled bytes of the path
+
+	// find prefix until first wildcard (beginning with ':'' or '*'')
+	for i, max := 0, len(path); numParams > 0; i++ {
+		c := path[i]
+		if c != ':' && c != '*' {
+			continue
+		}
+
+		// find wildcard end (either '/' or path end)
+		end := i + 1
+		for end < max && path[end] != '/' {
+			switch path[end] {
+			// the wildcard name must not contain ':' and '*'
+			case ':', '*':
+				panic("only one wildcard per path segment is allowed, has: '" +
+					path[i:] + "' in path '" + fullPath + "'")
+			default:
+				end++
+			}
+		}
+
+		// check if this Node existing children which would be
+		// unreachable if we insert the wildcard here
+		if len(n.children) > 0 {
+			panic("wildcard route '" + path[i:end] +
+				"' conflicts with existing children in path '" + fullPath + "'")
+		}
+
+		// check if the wildcard has a name
+		if end-i < 2 {
+			panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
+		}
+
+		if c == ':' { // param
+			// split path at the beginning of the wildcard
+			if i > 0 {
+				n.path = path[offset:i]
+				offset = i
+			}
+
+			child := &node{
+				nType:     param,
+				maxParams: numParams,
+			}
+			n.children = []*node{child}
+			n.wildChild = true
+			n = child
+			n.priority++
+			numParams--
+
+			// if the path doesn't end with the wildcard, then there
+			// will be another non-wildcard subpath starting with '/'
+			if end < max {
+				n.path = path[offset:end]
+				offset = end
+
+				child := &node{
+					maxParams: numParams,
+					priority:  1,
+				}
+				n.children = []*node{child}
+				n = child
+			}
+
+		} else { // catchAll
+			if end != max || numParams > 1 {
+				panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
+			}
+
+			if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
+				panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
+			}
+
+			// currently fixed width 1 for '/'
+			i--
+			if path[i] != '/' {
+				panic("no / before catch-all in path '" + fullPath + "'")
+			}
+
+			n.path = path[offset:i]
+
+			// first node: catchAll node with empty path
+			child := &node{
+				wildChild: true,
+				nType:     catchAll,
+				maxParams: 1,
+			}
+			n.children = []*node{child}
+			n.indices = string(path[i])
+			n = child
+			n.priority++
+
+			// second node: node holding the variable
+			child = &node{
+				path:      path[i:],
+				nType:     catchAll,
+				maxParams: 1,
+				handle:    handle,
+				priority:  1,
+			}
+			n.children = []*node{child}
+
+			return
+		}
+	}
+
+	// insert remaining path part and handle to the leaf
+	n.path = path[offset:]
+	n.handle = handle
+}
+
+// Returns the handle registered with the given path (key). The values of
+// wildcards are saved to a map.
+// If no handle can be found, a TSR (trailing slash redirect) recommendation is
+// made if a handle exists with an extra (without the) trailing slash for the
+// given path.
+func (n *node) getValue(path string) (handle Handle, p map[string]string, tsr bool) {
+walk: // outer loop for walking the tree
+	for {
+		if len(path) > len(n.path) {
+			if path[:len(n.path)] == n.path {
+				path = path[len(n.path):]
+				// If this node does not have a wildcard (param or catchAll)
+				// child,  we can just look up the next child node and continue
+				// to walk down the tree
+				if !n.wildChild {
+					c := path[0]
+					for i := 0; i < len(n.indices); i++ {
+						if c == n.indices[i] {
+							n = n.children[i]
+							continue walk
+						}
+					}
+
+					// Nothing found.
+					// We can recommend to redirect to the same URL without a
+					// trailing slash if a leaf exists for that path.
+					tsr = (path == "/" && n.handle != nil)
+					return
+
+				}
+
+				// handle wildcard child
+				n = n.children[0]
+				switch n.nType {
+				case param:
+					// find param end (either '/' or path end)
+					end := 0
+					for end < len(path) && path[end] != '/' {
+						end++
+					}
+
+					// save param value
+					if p == nil {
+						// lazy allocation
+						p = make(map[string]string) // make(Params, 0, n.maxParams)
+					}
+					//i := len(p)
+					//p = p[:i+1] // expand slice within preallocated capacity
+					//p[i].Key = n.path[1:]
+					//p[i].Value = path[:end]
+					p[n.path[1:]] = path[:end]
+
+					// we need to go deeper!
+					if end < len(path) {
+						if len(n.children) > 0 {
+							path = path[end:]
+							n = n.children[0]
+							continue walk
+						}
+
+						// ... but we can't
+						tsr = (len(path) == end+1)
+						return
+					}
+
+					if handle = n.handle; handle != nil {
+						return
+					} else if len(n.children) == 1 {
+						// No handle found. Check if a handle for this path + a
+						// trailing slash exists for TSR recommendation
+						n = n.children[0]
+						tsr = (n.path == "/" && n.handle != nil)
+					}
+
+					return
+
+				case catchAll:
+					// save param value
+					if p == nil {
+						// lazy allocation
+						p = make(map[string]string) // make(Params, 0, n.maxParams)
+					}
+					//i := len(p)
+					//p = p[:i+1] // expand slice within preallocated capacity
+					//p[i].Key = n.path[2:]
+					//p[i].Value = path
+					p[n.path[2:]] = path
+					handle = n.handle
+					return
+
+				default:
+					panic("invalid node type")
+				}
+			}
+		} else if path == n.path {
+			// We should have reached the node containing the handle.
+			// Check if this node has a handle registered.
+			if handle = n.handle; handle != nil {
+				return
+			}
+
+			if path == "/" && n.wildChild && n.nType != root {
+				tsr = true
+				return
+			}
+
+			// No handle found. Check if a handle for this path + a
+			// trailing slash exists for trailing slash recommendation
+			for i := 0; i < len(n.indices); i++ {
+				if n.indices[i] == '/' {
+					n = n.children[i]
+					tsr = (len(n.path) == 1 && n.handle != nil) ||
+						(n.nType == catchAll && n.children[0].handle != nil)
+					return
+				}
+			}
+
+			return
+		}
+
+		// Nothing found. We can recommend to redirect to the same URL with an
+		// extra trailing slash if a leaf exists for that path
+		tsr = (path == "/") ||
+			(len(n.path) == len(path)+1 && n.path[len(path)] == '/' &&
+				path == n.path[:len(n.path)-1] && n.handle != nil)
+		return
+	}
+}
+
+// Makes a case-insensitive lookup of the given path and tries to find a handler.
+// It can optionally also fix trailing slashes.
+// It returns the case-corrected path and a bool indicating whether the lookup
+// was successful.
+func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPath []byte, found bool) {
+	return n.findCaseInsensitivePathRec(
+		path,
+		strings.ToLower(path),
+		make([]byte, 0, len(path)+1), // preallocate enough memory for new path
+		[4]byte{},                    // empty rune buffer
+		fixTrailingSlash,
+	)
+}
+
+// shift bytes in array by n bytes left
+func shiftNRuneBytes(rb [4]byte, n int) [4]byte {
+	switch n {
+	case 0:
+		return rb
+	case 1:
+		return [4]byte{rb[1], rb[2], rb[3], 0}
+	case 2:
+		return [4]byte{rb[2], rb[3]}
+	case 3:
+		return [4]byte{rb[3]}
+	default:
+		return [4]byte{}
+	}
+}
+
+// recursive case-insensitive lookup function used by n.findCaseInsensitivePath
+func (n *node) findCaseInsensitivePathRec(path, loPath string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) ([]byte, bool) {
+	loNPath := strings.ToLower(n.path)
+
+walk: // outer loop for walking the tree
+	for len(loPath) >= len(loNPath) && (len(loNPath) == 0 || loPath[1:len(loNPath)] == loNPath[1:]) {
+		// add common path to result
+		ciPath = append(ciPath, n.path...)
+
+		if path = path[len(n.path):]; len(path) > 0 {
+			loOld := loPath
+			loPath = loPath[len(loNPath):]
+
+			// If this node does not have a wildcard (param or catchAll) child,
+			// we can just look up the next child node and continue to walk down
+			// the tree
+			if !n.wildChild {
+				// skip rune bytes already processed
+				rb = shiftNRuneBytes(rb, len(loNPath))
+
+				if rb[0] != 0 {
+					// old rune not finished
+					for i := 0; i < len(n.indices); i++ {
+						if n.indices[i] == rb[0] {
+							// continue with child node
+							n = n.children[i]
+							loNPath = strings.ToLower(n.path)
+							continue walk
+						}
+					}
+				} else {
+					// process a new rune
+					var rv rune
+
+					// find rune start
+					// runes are up to 4 byte long,
+					// -4 would definitely be another rune
+					var off int
+					for max := min(len(loNPath), 3); off < max; off++ {
+						if i := len(loNPath) - off; utf8.RuneStart(loOld[i]) {
+							// read rune from cached lowercase path
+							rv, _ = utf8.DecodeRuneInString(loOld[i:])
+							break
+						}
+					}
+
+					// calculate lowercase bytes of current rune
+					utf8.EncodeRune(rb[:], rv)
+					// skipp already processed bytes
+					rb = shiftNRuneBytes(rb, off)
+
+					for i := 0; i < len(n.indices); i++ {
+						// lowercase matches
+						if n.indices[i] == rb[0] {
+							// must use a recursive approach since both the
+							// uppercase byte and the lowercase byte might exist
+							// as an index
+							if out, found := n.children[i].findCaseInsensitivePathRec(
+								path, loPath, ciPath, rb, fixTrailingSlash,
+							); found {
+								return out, true
+							}
+							break
+						}
+					}
+
+					// same for uppercase rune, if it differs
+					if up := unicode.ToUpper(rv); up != rv {
+						utf8.EncodeRune(rb[:], up)
+						rb = shiftNRuneBytes(rb, off)
+
+						for i := 0; i < len(n.indices); i++ {
+							// uppercase matches
+							if n.indices[i] == rb[0] {
+								// continue with child node
+								n = n.children[i]
+								loNPath = strings.ToLower(n.path)
+								continue walk
+							}
+						}
+					}
+				}
+
+				// Nothing found. We can recommend to redirect to the same URL
+				// without a trailing slash if a leaf exists for that path
+				return ciPath, (fixTrailingSlash && path == "/" && n.handle != nil)
+			}
+
+			n = n.children[0]
+			switch n.nType {
+			case param:
+				// find param end (either '/' or path end)
+				k := 0
+				for k < len(path) && path[k] != '/' {
+					k++
+				}
+
+				// add param value to case insensitive path
+				ciPath = append(ciPath, path[:k]...)
+
+				// we need to go deeper!
+				if k < len(path) {
+					if len(n.children) > 0 {
+						// continue with child node
+						n = n.children[0]
+						loNPath = strings.ToLower(n.path)
+						loPath = loPath[k:]
+						path = path[k:]
+						continue
+					}
+
+					// ... but we can't
+					if fixTrailingSlash && len(path) == k+1 {
+						return ciPath, true
+					}
+					return ciPath, false
+				}
+
+				if n.handle != nil {
+					return ciPath, true
+				} else if fixTrailingSlash && len(n.children) == 1 {
+					// No handle found. Check if a handle for this path + a
+					// trailing slash exists
+					n = n.children[0]
+					if n.path == "/" && n.handle != nil {
+						return append(ciPath, '/'), true
+					}
+				}
+				return ciPath, false
+
+			case catchAll:
+				return append(ciPath, path...), true
+
+			default:
+				panic("invalid node type")
+			}
+		} else {
+			// We should have reached the node containing the handle.
+			// Check if this node has a handle registered.
+			if n.handle != nil {
+				return ciPath, true
+			}
+
+			// No handle found.
+			// Try to fix the path by adding a trailing slash
+			if fixTrailingSlash {
+				for i := 0; i < len(n.indices); i++ {
+					if n.indices[i] == '/' {
+						n = n.children[i]
+						if (len(n.path) == 1 && n.handle != nil) ||
+							(n.nType == catchAll && n.children[0].handle != nil) {
+							return append(ciPath, '/'), true
+						}
+						return ciPath, false
+					}
+				}
+			}
+			return ciPath, false
+		}
+	}
+
+	// Nothing found.
+	// Try to fix the path by adding / removing a trailing slash
+	if fixTrailingSlash {
+		if path == "/" {
+			return ciPath, true
+		}
+		if len(loPath)+1 == len(loNPath) && loNPath[len(loPath)] == '/' &&
+			loPath[1:] == loNPath[1:len(loPath)] && n.handle != nil {
+			return append(ciPath, n.path...), true
+		}
+	}
+	return ciPath, false
+}

+ 30 - 0
types/bool.go

@@ -0,0 +1,30 @@
+package types
+
+import (
+	"database/sql/driver"
+	"errors"
+)
+
+// BitBool is an implementation of a bool for the MySQL type BIT(1).
+// This type allows you to avoid wasting an entire byte for MySQL's boolean type TINYINT.
+type BitBool bool
+
+// Value implements the driver.Valuer interface,
+// and turns the BitBool into a bitfield (BIT(1)) for MySQL storage.
+func (b BitBool) Value() (driver.Value, error) {
+	if b {
+		return []byte{1}, nil
+	}
+	return []byte{0}, nil
+}
+
+// Scan implements the sql.Scanner interface,
+// and turns the bitfield incoming from MySQL into a BitBool
+func (b *BitBool) Scan(src interface{}) error {
+	v, ok := src.([]byte)
+	if !ok {
+		return errors.New("bad []byte type assertion")
+	}
+	*b = v[0] == 1
+	return nil
+}

+ 68 - 0
types/datetime.go

@@ -0,0 +1,68 @@
+package types
+
+import (
+	"database/sql/driver"
+	"errors"
+	"time"
+)
+
+// DateText is an implementation of a string for the MySQL type date.
+type DateText string
+
+// TimeText is an implementation of a string for the MySQL type datetime.
+type TimeText string
+
+const (
+	formtDate = `2006-01-02`
+	formtTime = `2006-01-02 15:04:05`
+)
+
+// Value implements the driver.Valuer interface,
+// and turns the date into a DateText (date) for MySQL storage.
+func (d DateText) Value() (driver.Value, error) {
+	t, err := time.Parse(formtDate, string(d))
+	if err != nil {
+		return nil, err
+	}
+	return DateText(t.Format(formtDate)), nil
+}
+
+// Scan implements the sql.Scanner interface,
+// and turns the bitfield incoming from MySQL into a Date
+func (d *DateText) Scan(src interface{}) error {
+	v, ok := src.([]byte)
+	if !ok {
+		return errors.New("bad []byte type assertion")
+	}
+	t, err := time.Parse(formtDate, string(v))
+	if err != nil {
+		return err
+	}
+	*d = DateText(t.Format(formtDate))
+	return nil
+}
+
+// Value implements the driver.Valuer interface,
+// and turns the date into a DateText (date) for MySQL storage.
+func (d TimeText) Value() (driver.Value, error) {
+	t, err := time.Parse(formtDate, string(d))
+	if err != nil {
+		return nil, err
+	}
+	return TimeText(t.Format(formtTime)), nil
+}
+
+// Scan implements the sql.Scanner interface,
+// and turns the bitfield incoming from MySQL into a Date
+func (d *TimeText) Scan(src interface{}) error {
+	v, ok := src.([]byte)
+	if !ok {
+		return errors.New("bad []byte type assertion")
+	}
+	t, err := time.Parse(formtDate, string(v))
+	if err != nil {
+		return err
+	}
+	*d = TimeText(t.Format(formtTime))
+	return nil
+}

+ 50 - 0
types/gzip.go

@@ -0,0 +1,50 @@
+package types
+
+import (
+	"bytes"
+	"compress/gzip"
+	"database/sql/driver"
+	"errors"
+	"io/ioutil"
+)
+
+// GzippedText is a []byte which transparently gzips data being submitted to
+// a database and ungzips data being Scanned from a database.
+type GzippedText []byte
+
+// Value implements the driver.Valuer interface, gzipping the raw value of
+// this GzippedText.
+func (g GzippedText) Value() (driver.Value, error) {
+	b := make([]byte, 0, len(g))
+	buf := bytes.NewBuffer(b)
+	w := gzip.NewWriter(buf)
+	w.Write(g)
+	w.Close()
+	return buf.Bytes(), nil
+
+}
+
+// Scan implements the sql.Scanner interface, ungzipping the value coming off
+// the wire and storing the raw result in the GzippedText.
+func (g *GzippedText) Scan(src interface{}) error {
+	var source []byte
+	switch src.(type) {
+	case string:
+		source = []byte(src.(string))
+	case []byte:
+		source = src.([]byte)
+	default:
+		return errors.New("Incompatible type for GzippedText")
+	}
+	reader, err := gzip.NewReader(bytes.NewReader(source))
+	if err != nil {
+		return err
+	}
+	defer reader.Close()
+	b, err := ioutil.ReadAll(reader)
+	if err != nil {
+		return err
+	}
+	*g = GzippedText(b)
+	return nil
+}

+ 103 - 0
types/json.go

@@ -0,0 +1,103 @@
+package types
+
+import (
+	"database/sql/driver"
+	"encoding/json"
+	"errors"
+)
+
+// JSONText is a json.RawMessage, which is a []byte underneath.
+// Value() validates the json format in the source, and returns an error if
+// the json is not valid.  Scan does no validation.  JSONText additionally
+// implements `Unmarshal`, which unmarshals the json within to an interface{}
+type JSONText json.RawMessage
+
+var emptyJSON = JSONText("{}")
+
+// MarshalJSON returns the *j as the JSON encoding of j.
+func (j JSONText) MarshalJSON() ([]byte, error) {
+	if len(j) == 0 {
+		return emptyJSON, nil
+	}
+	return j, nil
+}
+
+// UnmarshalJSON sets *j to a copy of data
+func (j *JSONText) UnmarshalJSON(data []byte) error {
+	if j == nil {
+		return errors.New("JSONText: UnmarshalJSON on nil pointer")
+	}
+	*j = append((*j)[0:0], data...)
+	return nil
+}
+
+// Value returns j as a value.  This does a validating unmarshal into another
+// RawMessage.  If j is invalid json, it returns an error.
+func (j JSONText) Value() (driver.Value, error) {
+	var m json.RawMessage
+	var err = j.Unmarshal(&m)
+	if err != nil {
+		return []byte{}, err
+	}
+	return []byte(j), nil
+}
+
+// Scan stores the src in *j.  No validation is done.
+func (j *JSONText) Scan(src interface{}) error {
+	var source []byte
+	switch t := src.(type) {
+	case string:
+		source = []byte(t)
+	case []byte:
+		if len(t) == 0 {
+			source = emptyJSON
+		} else {
+			source = t
+		}
+	case nil:
+		*j = emptyJSON
+	default:
+		return errors.New("Incompatible type for JSONText")
+	}
+	*j = JSONText(append((*j)[0:0], source...))
+	return nil
+}
+
+// Unmarshal unmarshal's the json in j to v, as in json.Unmarshal.
+func (j *JSONText) Unmarshal(v interface{}) error {
+	if len(*j) == 0 {
+		*j = emptyJSON
+	}
+	return json.Unmarshal([]byte(*j), v)
+}
+
+// String supports pretty printing for JSONText types.
+func (j JSONText) String() string {
+	return string(j)
+}
+
+// NullJSONText represents a JSONText that may be null.
+// NullJSONText implements the scanner interface so
+// it can be used as a scan destination, similar to NullString.
+type NullJSONText struct {
+	JSONText
+	Valid bool // Valid is true if JSONText is not NULL
+}
+
+// Scan implements the Scanner interface.
+func (n *NullJSONText) Scan(value interface{}) error {
+	if value == nil {
+		n.JSONText, n.Valid = emptyJSON, false
+		return nil
+	}
+	n.Valid = true
+	return n.JSONText.Scan(value)
+}
+
+// Value implements the driver Valuer interface.
+func (n NullJSONText) Value() (driver.Value, error) {
+	if !n.Valid {
+		return nil, nil
+	}
+	return n.JSONText.Value()
+}

+ 59 - 0
types/numeric.go

@@ -0,0 +1,59 @@
+package types
+
+import (
+	"database/sql/driver"
+)
+
+// IntNull is an implementation of a int for the MySQL type int/tinyint ....
+type IntNull int
+
+// Value implements the driver.Valuer interface,
+// and turns the bytes into a integer for MySQL storage.
+func (n IntNull) Value() (driver.Value, error) {
+	return IntNull(n), nil
+}
+
+// Scan implements the sql.Scanner interface,
+// and turns the bytes incoming from MySQL into a integer
+func (n *IntNull) Scan(src interface{}) error {
+	if src != nil {
+		v, ok := src.(int64)
+		if !ok {
+			*n = IntNull(0)
+			return nil
+			//return errors.New("bad []byte type assertion")
+		}
+
+		*n = IntNull(v)
+		return nil
+	}
+	*n = IntNull(0)
+	return nil
+}
+
+// FloatNull is an implementation of a int for the MySQL type numeric ....
+type FloatNull float64
+
+// Value implements the driver.Valuer interface,
+// and turns the bytes into a integer for MySQL storage.
+func (n FloatNull) Value() (driver.Value, error) {
+	return FloatNull(n), nil
+}
+
+// Scan implements the sql.Scanner interface,
+// and turns the bytes incoming from MySQL into a numeric
+func (n *FloatNull) Scan(src interface{}) error {
+	if src != nil {
+		v, ok := src.(float64)
+		if !ok {
+			*n = FloatNull(0)
+			return nil
+			//return errors.New("bad []byte type assertion (not float64)")
+		}
+
+		*n = FloatNull(v)
+		return nil
+	}
+	*n = FloatNull(0)
+	return nil
+}

+ 32 - 0
types/string.go

@@ -0,0 +1,32 @@
+package types
+
+import (
+	"database/sql/driver"
+	"errors"
+)
+
+// TextNull is an implementation of a string for the MySQL type char/varchar/text ....
+type TextNull string
+
+// Value implements the driver.Valuer interface,
+// and turns the string into a bytes for MySQL storage.
+func (s TextNull) Value() (driver.Value, error) {
+	return []byte(s), nil
+}
+
+// Scan implements the sql.Scanner interface,
+// and turns the bytes incoming from MySQL into a string
+func (s *TextNull) Scan(src interface{}) error {
+	if src != nil {
+		v, ok := src.([]byte)
+		if !ok {
+			return errors.New("bad []byte type assertion")
+		}
+
+		*s = TextNull(v)
+		return nil
+	}
+
+	*s = TextNull("")
+	return nil
+}

+ 8 - 0
util/string.go

@@ -0,0 +1,8 @@
+package util
+
+import "unicode/utf8"
+
+// StrLen utf8 string length
+func StrLen(s string) int {
+	return utf8.RuneCountInString(s)
+}

+ 61 - 0
vars.go

@@ -0,0 +1,61 @@
+package myth
+
+import (
+	"context"
+	"net/http"
+)
+
+type contextKey string
+
+const (
+	varsKey  contextKey = `httprouter_route_vars`
+	routeKey            = `httprouter_route_path`
+)
+
+// ContextVars returns the route variables for the current Context, if any.
+func ContextVars(ctx context.Context) map[string]string {
+	if rv := ctx.Value(varsKey); rv != nil {
+		return rv.(map[string]string)
+	}
+	return nil
+}
+
+// ContextRoutePath current route
+func ContextRoutePath(ctx context.Context) string {
+	if rv := ctx.Value(routeKey); rv != nil {
+		return rv.(string)
+	}
+	return ""
+}
+
+// Vars returns the route variables for the current request, if any.
+func Vars(r *http.Request) map[string]string {
+	if rv := contextGet(r, varsKey); rv != nil {
+		return rv.(map[string]string)
+	}
+	return nil
+}
+
+func setVars(r *http.Request, val interface{}) *http.Request {
+	return contextSet(r, varsKey, val)
+}
+
+func setCurrentPath(r *http.Request, val interface{}) *http.Request {
+	return contextSet(r, routeKey, val)
+}
+
+func contextGet(r *http.Request, key interface{}) interface{} {
+	return r.Context().Value(key)
+}
+
+func contextSet(r *http.Request, key, val interface{}) *http.Request {
+	if val == nil {
+		return r
+	}
+
+	return r.WithContext(context.WithValue(r.Context(), key, val))
+}
+
+func contextClear(r *http.Request) {
+	return
+}

+ 7 - 0
version.go

@@ -0,0 +1,7 @@
+package myth
+
+// Library constant
+const (
+	LibName    = `myth`  // LibName toolkit name Go micro service
+	LibVersion = `0.6.0` // LibVersion toolkit version
+)