You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

290 lines
7.5 KiB

6 years ago
  1. // Package query implements encoding of structs into http.Header fields.
  2. //
  3. // As a simple example:
  4. //
  5. // type Options struct {
  6. // ContentType string `header:"Content-Type"`
  7. // Length int
  8. // }
  9. //
  10. // opt := Options{"application/json", 2}
  11. // h, _ := httpheader.Header(opt)
  12. // fmt.Printf("%#v", h)
  13. // // will output:
  14. // // http.Header{"Content-Type":[]string{"application/json"},"Length":[]string{"2"}}
  15. //
  16. // The exact mapping between Go values and http.Header is described in the
  17. // documentation for the Header() function.
  18. package httpheader
  19. import (
  20. "fmt"
  21. "net/http"
  22. "reflect"
  23. "strconv"
  24. "strings"
  25. "time"
  26. )
  27. const tagName = "header"
  28. // Version ...
  29. const Version = "0.2.0"
  30. var timeType = reflect.TypeOf(time.Time{})
  31. var headerType = reflect.TypeOf(http.Header{})
  32. var encoderType = reflect.TypeOf(new(Encoder)).Elem()
  33. // Encoder is an interface implemented by any type that wishes to encode
  34. // itself into Header fields in a non-standard way.
  35. type Encoder interface {
  36. EncodeHeader(key string, v *http.Header) error
  37. }
  38. // Header returns the http.Header encoding of v.
  39. //
  40. // Header expects to be passed a struct, and traverses it recursively using the
  41. // following encoding rules.
  42. //
  43. // Each exported struct field is encoded as a Header field unless
  44. //
  45. // - the field's tag is "-", or
  46. // - the field is empty and its tag specifies the "omitempty" option
  47. //
  48. // The empty values are false, 0, any nil pointer or interface value, any array
  49. // slice, map, or string of length zero, and any time.Time that returns true
  50. // for IsZero().
  51. //
  52. // The Header field name defaults to the struct field name but can be
  53. // specified in the struct field's tag value. The "header" key in the struct
  54. // field's tag value is the key name, followed by an optional comma and
  55. // options. For example:
  56. //
  57. // // Field is ignored by this package.
  58. // Field int `header:"-"`
  59. //
  60. // // Field appears as Header field "X-Name".
  61. // Field int `header:"X-Name"`
  62. //
  63. // // Field appears as Header field "X-Name" and the field is omitted if
  64. // // its value is empty
  65. // Field int `header:"X-Name,omitempty"`
  66. //
  67. // // Field appears as Header field "Field" (the default), but the field
  68. // // is skipped if empty. Note the leading comma.
  69. // Field int `header:",omitempty"`
  70. //
  71. // For encoding individual field values, the following type-dependent rules
  72. // apply:
  73. //
  74. // Boolean values default to encoding as the strings "true" or "false".
  75. // Including the "int" option signals that the field should be encoded as the
  76. // strings "1" or "0".
  77. //
  78. // time.Time values default to encoding as RFC1123("Mon, 02 Jan 2006 15:04:05 GMT")
  79. // timestamps. Including the "unix" option signals that the field should be
  80. // encoded as a Unix time (see time.Unix())
  81. //
  82. // Slice and Array values default to encoding as multiple Header values of the
  83. // same name. example:
  84. // X-Name: []string{"Tom", "Jim"}, etc.
  85. //
  86. // http.Header values will be used to extend the Header fields.
  87. //
  88. // Anonymous struct fields are usually encoded as if their inner exported
  89. // fields were fields in the outer struct, subject to the standard Go
  90. // visibility rules. An anonymous struct field with a name given in its Header
  91. // tag is treated as having that name, rather than being anonymous.
  92. //
  93. // Non-nil pointer values are encoded as the value pointed to.
  94. //
  95. // All other values are encoded using their default string representation.
  96. //
  97. // Multiple fields that encode to the same Header filed name will be included
  98. // as multiple Header values of the same name.
  99. func Header(v interface{}) (http.Header, error) {
  100. h := make(http.Header)
  101. val := reflect.ValueOf(v)
  102. for val.Kind() == reflect.Ptr {
  103. if val.IsNil() {
  104. return h, nil
  105. }
  106. val = val.Elem()
  107. }
  108. if v == nil {
  109. return h, nil
  110. }
  111. if val.Kind() != reflect.Struct {
  112. return nil, fmt.Errorf("httpheader: Header() expects struct input. Got %v", val.Kind())
  113. }
  114. err := reflectValue(h, val)
  115. return h, err
  116. }
  117. // reflectValue populates the header fields from the struct fields in val.
  118. // Embedded structs are followed recursively (using the rules defined in the
  119. // Values function documentation) breadth-first.
  120. func reflectValue(header http.Header, val reflect.Value) error {
  121. var embedded []reflect.Value
  122. typ := val.Type()
  123. for i := 0; i < typ.NumField(); i++ {
  124. sf := typ.Field(i)
  125. if sf.PkgPath != "" && !sf.Anonymous { // unexported
  126. continue
  127. }
  128. sv := val.Field(i)
  129. tag := sf.Tag.Get(tagName)
  130. if tag == "-" {
  131. continue
  132. }
  133. name, opts := parseTag(tag)
  134. if name == "" {
  135. if sf.Anonymous && sv.Kind() == reflect.Struct {
  136. // save embedded struct for later processing
  137. embedded = append(embedded, sv)
  138. continue
  139. }
  140. name = sf.Name
  141. }
  142. if opts.Contains("omitempty") && isEmptyValue(sv) {
  143. continue
  144. }
  145. if sv.Type().Implements(encoderType) {
  146. if !reflect.Indirect(sv).IsValid() {
  147. sv = reflect.New(sv.Type().Elem())
  148. }
  149. m := sv.Interface().(Encoder)
  150. if err := m.EncodeHeader(name, &header); err != nil {
  151. return err
  152. }
  153. continue
  154. }
  155. if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array {
  156. for i := 0; i < sv.Len(); i++ {
  157. k := name
  158. header.Add(k, valueString(sv.Index(i), opts))
  159. }
  160. continue
  161. }
  162. for sv.Kind() == reflect.Ptr {
  163. if sv.IsNil() {
  164. break
  165. }
  166. sv = sv.Elem()
  167. }
  168. if sv.Type() == timeType {
  169. header.Add(name, valueString(sv, opts))
  170. continue
  171. }
  172. if sv.Type() == headerType {
  173. h := sv.Interface().(http.Header)
  174. for k, vs := range h {
  175. for _, v := range vs {
  176. header.Add(k, v)
  177. }
  178. }
  179. continue
  180. }
  181. if sv.Kind() == reflect.Struct {
  182. reflectValue(header, sv)
  183. continue
  184. }
  185. header.Add(name, valueString(sv, opts))
  186. }
  187. for _, f := range embedded {
  188. if err := reflectValue(header, f); err != nil {
  189. return err
  190. }
  191. }
  192. return nil
  193. }
  194. // valueString returns the string representation of a value.
  195. func valueString(v reflect.Value, opts tagOptions) string {
  196. for v.Kind() == reflect.Ptr {
  197. if v.IsNil() {
  198. return ""
  199. }
  200. v = v.Elem()
  201. }
  202. if v.Kind() == reflect.Bool && opts.Contains("int") {
  203. if v.Bool() {
  204. return "1"
  205. }
  206. return "0"
  207. }
  208. if v.Type() == timeType {
  209. t := v.Interface().(time.Time)
  210. if opts.Contains("unix") {
  211. return strconv.FormatInt(t.Unix(), 10)
  212. }
  213. return t.Format(http.TimeFormat)
  214. }
  215. return fmt.Sprint(v.Interface())
  216. }
  217. // isEmptyValue checks if a value should be considered empty for the purposes
  218. // of omitting fields with the "omitempty" option.
  219. func isEmptyValue(v reflect.Value) bool {
  220. switch v.Kind() {
  221. case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
  222. return v.Len() == 0
  223. case reflect.Bool:
  224. return !v.Bool()
  225. case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
  226. return v.Int() == 0
  227. case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
  228. return v.Uint() == 0
  229. case reflect.Float32, reflect.Float64:
  230. return v.Float() == 0
  231. case reflect.Interface, reflect.Ptr:
  232. return v.IsNil()
  233. }
  234. if v.Type() == timeType {
  235. return v.Interface().(time.Time).IsZero()
  236. }
  237. return false
  238. }
  239. // tagOptions is the string following a comma in a struct field's "header" tag, or
  240. // the empty string. It does not include the leading comma.
  241. type tagOptions []string
  242. // parseTag splits a struct field's header tag into its name and comma-separated
  243. // options.
  244. func parseTag(tag string) (string, tagOptions) {
  245. s := strings.Split(tag, ",")
  246. return s[0], s[1:]
  247. }
  248. // Contains checks whether the tagOptions contains the specified option.
  249. func (o tagOptions) Contains(option string) bool {
  250. for _, s := range o {
  251. if s == option {
  252. return true
  253. }
  254. }
  255. return false
  256. }