Package param implements parameter parsing into a target struct (in much the same way as encoding/json parses JSON into a struct). It targets the common jQuery.param / Ruby on Rails style parameter serialization format.
| @ -0,0 +1,56 @@ | |||
| package param | |||
| import ( | |||
| "net/url" | |||
| "testing" | |||
| ) | |||
| type Crazy struct { | |||
| A *Crazy | |||
| B *Crazy | |||
| Value int | |||
| Slice []int | |||
| Map map[string]Crazy | |||
| } | |||
| func TestCrazy(t *testing.T) { | |||
| t.Parallel() | |||
| c := Crazy{} | |||
| err := Parse(url.Values{ | |||
| "A[B][B][A][Value]": {"1"}, | |||
| "B[A][A][Slice][]": {"3", "1", "4"}, | |||
| "B[Map][hello][A][Value]": {"8"}, | |||
| "A[Value]": {"2"}, | |||
| "A[Slice][]": {"9", "1", "1"}, | |||
| "Value": {"42"}, | |||
| }, &c) | |||
| if err != nil { | |||
| t.Error("Error parsing craziness: ", err) | |||
| } | |||
| // Exhaustively checking everything here is going to be a huge pain, so | |||
| // let's just hope for the best, pretend NPEs don't exist, and hope that | |||
| // this test covers enough stuff that it's actually useful. | |||
| assertEqual(t, "c.A.B.B.A.Value", 1, c.A.B.B.A.Value) | |||
| assertEqual(t, "c.A.Value", 2, c.A.Value) | |||
| assertEqual(t, "c.Value", 42, c.Value) | |||
| assertEqual(t, `c.B.Map["hello"].A.Value`, 8, c.B.Map["hello"].A.Value) | |||
| assertEqual(t, "c.A.B.B.B", (*Crazy)(nil), c.A.B.B.B) | |||
| assertEqual(t, "c.A.B.A", (*Crazy)(nil), c.A.B.A) | |||
| assertEqual(t, "c.A.A", (*Crazy)(nil), c.A.A) | |||
| if c.Slice != nil || c.Map != nil { | |||
| t.Error("Map and Slice should not be set") | |||
| } | |||
| sl := c.B.A.A.Slice | |||
| if len(sl) != 3 || sl[0] != 3 || sl[1] != 1 || sl[2] != 4 { | |||
| t.Error("Something is wrong with c.B.A.A.Slice") | |||
| } | |||
| sl = c.A.Slice | |||
| if len(sl) != 3 || sl[0] != 9 || sl[1] != 1 || sl[2] != 1 { | |||
| t.Error("Something is wrong with c.A.Slice") | |||
| } | |||
| } | |||
| @ -0,0 +1,34 @@ | |||
| package param | |||
| import ( | |||
| "errors" | |||
| "fmt" | |||
| "log" | |||
| ) | |||
| // TODO: someday it might be nice to throw typed errors instead of weird strings | |||
| // Testing log.Fatal in tests is... not a thing. Allow tests to stub it out. | |||
| var pebkacTesting bool | |||
| const errPrefix = "param/parse: " | |||
| const yourFault = " This is a bug in your use of the param library." | |||
| // Panic with a formatted error. The param library uses panics to quickly unwind | |||
| // the call stack and return a user error | |||
| func perr(format string, a ...interface{}) { | |||
| err := errors.New(errPrefix + fmt.Sprintf(format, a...)) | |||
| panic(err) | |||
| } | |||
| // Problem exists between keyboard and chair. This function is used in cases of | |||
| // programmer error, i.e. an inappripriate use of the param library, to | |||
| // immediately force the program to halt with a hopefully helpful error message. | |||
| func pebkac(format string, a ...interface{}) { | |||
| err := errors.New(errPrefix + fmt.Sprintf(format, a...) + yourFault) | |||
| if pebkacTesting { | |||
| panic(err) | |||
| } else { | |||
| log.Fatal(err) | |||
| } | |||
| } | |||
| @ -0,0 +1,60 @@ | |||
| /* | |||
| Package param deserializes parameter values into the given struct using magical | |||
| reflection ponies. Inspired by gorilla/schema, but uses Rails/jQuery style param | |||
| encoding instead of their weird dotted syntax. In particular, this package was | |||
| written with the intent of parsing the output of jQuery.param. | |||
| This package uses struct tags to guess what names things ought to have. If a | |||
| struct value has a "param" tag defined, it will use that. If there is no "param" | |||
| tag defined, the name part of the "json" tag will be used. If that is not | |||
| defined, the name of the field itself will be used (no case transformation is | |||
| performed). | |||
| If the name derived in this way is the string "-", param will refuse to set that | |||
| value. | |||
| The parser is extremely strict, and will return an error if it has any | |||
| difficulty whatsoever in parsing any parameter, or if there is any kind of type | |||
| mismatch. | |||
| */ | |||
| package param | |||
| import ( | |||
| "net/url" | |||
| "reflect" | |||
| "strings" | |||
| ) | |||
| // Parse the given arguments into the the given pointer to a struct object. | |||
| func Parse(params url.Values, target interface{}) (err error) { | |||
| v := reflect.ValueOf(target) | |||
| defer func() { | |||
| if r := recover(); r != nil { | |||
| var ok bool | |||
| err, ok = r.(error) | |||
| if !ok { | |||
| panic(err) | |||
| } | |||
| } | |||
| }() | |||
| if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct { | |||
| pebkac("Target of param.Parse must be a pointer to a struct. "+ | |||
| "We instead were passed a %v", v.Type()) | |||
| } | |||
| el := v.Elem() | |||
| t := el.Type() | |||
| cache := cacheStruct(t) | |||
| for key, values := range params { | |||
| sk, keytail := key, "" | |||
| if i := strings.IndexRune(key, '['); i != -1 { | |||
| sk, keytail = sk[:i], sk[i:] | |||
| } | |||
| parseStructField(cache, key, sk, keytail, values, el) | |||
| } | |||
| return nil | |||
| } | |||
| @ -0,0 +1,505 @@ | |||
| package param | |||
| import ( | |||
| "net/url" | |||
| "reflect" | |||
| "strings" | |||
| "testing" | |||
| "time" | |||
| ) | |||
| type Everything struct { | |||
| Bool bool | |||
| Int int | |||
| Uint uint | |||
| Float float64 | |||
| Map map[string]int | |||
| Slice []int | |||
| String string | |||
| Struct Sub | |||
| Time time.Time | |||
| PBool *bool | |||
| PInt *int | |||
| PUint *uint | |||
| PFloat *float64 | |||
| PMap *map[string]int | |||
| PSlice *[]int | |||
| PString *string | |||
| PStruct *Sub | |||
| PTime *time.Time | |||
| PPInt **int | |||
| ABool MyBool | |||
| AInt MyInt | |||
| AUint MyUint | |||
| AFloat MyFloat | |||
| AMap MyMap | |||
| APtr MyPtr | |||
| ASlice MySlice | |||
| AString MyString | |||
| } | |||
| type Sub struct { | |||
| A int | |||
| B int | |||
| } | |||
| type MyBool bool | |||
| type MyInt int | |||
| type MyUint uint | |||
| type MyFloat float64 | |||
| type MyMap map[MyString]MyInt | |||
| type MyPtr *MyInt | |||
| type MySlice []MyInt | |||
| type MyString string | |||
| var boolAnswers = map[string]bool{ | |||
| "true": true, | |||
| "false": false, | |||
| "0": false, | |||
| "1": true, | |||
| "on": true, | |||
| "": false, | |||
| } | |||
| var testTimeString = "1996-12-19T16:39:57-08:00" | |||
| var testTime time.Time | |||
| func init() { | |||
| testTime, _ = time.Parse(time.RFC3339, testTimeString) | |||
| } | |||
| func singletonErrors(t *testing.T, field, valid, invalid string) { | |||
| e := Everything{} | |||
| err := Parse(url.Values{field: {invalid}}, &e) | |||
| if err == nil { | |||
| t.Errorf("Expected error parsing %q as %s", invalid, field) | |||
| } | |||
| err = Parse(url.Values{field + "[]": {valid}}, &e) | |||
| if err == nil { | |||
| t.Errorf("Expected error parsing nested %s", field) | |||
| } | |||
| err = Parse(url.Values{field + "[nested]": {valid}}, &e) | |||
| if err == nil { | |||
| t.Errorf("Expected error parsing nested %s", field) | |||
| } | |||
| err = Parse(url.Values{field: {valid, valid}}, &e) | |||
| if err == nil { | |||
| t.Errorf("Expected error passing %s twice", field) | |||
| } | |||
| } | |||
| func TestBool(t *testing.T) { | |||
| t.Parallel() | |||
| for val, correct := range boolAnswers { | |||
| e := Everything{} | |||
| e.Bool = !correct | |||
| err := Parse(url.Values{"Bool": {val}}, &e) | |||
| if err != nil { | |||
| t.Error("Parse error on key: ", val) | |||
| } | |||
| assertEqual(t, "e.Bool", correct, e.Bool) | |||
| } | |||
| } | |||
| func TestBoolTyped(t *testing.T) { | |||
| t.Parallel() | |||
| e := Everything{} | |||
| err := Parse(url.Values{"ABool": {"true"}}, &e) | |||
| if err != nil { | |||
| t.Error("Parse error for typed bool") | |||
| } | |||
| assertEqual(t, "e.ABool", MyBool(true), e.ABool) | |||
| } | |||
| func TestBoolErrors(t *testing.T) { | |||
| t.Parallel() | |||
| singletonErrors(t, "Bool", "true", "llama") | |||
| } | |||
| var intAnswers = map[string]int{ | |||
| "0": 0, | |||
| "9001": 9001, | |||
| "-42": -42, | |||
| } | |||
| func TestInt(t *testing.T) { | |||
| t.Parallel() | |||
| for val, correct := range intAnswers { | |||
| e := Everything{} | |||
| e.Int = 1 | |||
| err := Parse(url.Values{"Int": {val}}, &e) | |||
| if err != nil { | |||
| t.Error("Parse error on key: ", val) | |||
| } | |||
| assertEqual(t, "e.Int", correct, e.Int) | |||
| } | |||
| } | |||
| func TestIntTyped(t *testing.T) { | |||
| t.Parallel() | |||
| e := Everything{} | |||
| err := Parse(url.Values{"AInt": {"1"}}, &e) | |||
| if err != nil { | |||
| t.Error("Parse error for typed int") | |||
| } | |||
| assertEqual(t, "e.AInt", MyInt(1), e.AInt) | |||
| } | |||
| func TestIntErrors(t *testing.T) { | |||
| t.Parallel() | |||
| singletonErrors(t, "Int", "1", "llama") | |||
| e := Everything{} | |||
| err := Parse(url.Values{"Int": {"4.2"}}, &e) | |||
| if err == nil { | |||
| t.Error("Expected error parsing float as int") | |||
| } | |||
| } | |||
| var uintAnswers = map[string]uint{ | |||
| "0": 0, | |||
| "9001": 9001, | |||
| } | |||
| func TestUint(t *testing.T) { | |||
| t.Parallel() | |||
| for val, correct := range uintAnswers { | |||
| e := Everything{} | |||
| e.Uint = 1 | |||
| err := Parse(url.Values{"Uint": {val}}, &e) | |||
| if err != nil { | |||
| t.Error("Parse error on key: ", val) | |||
| } | |||
| assertEqual(t, "e.Uint", correct, e.Uint) | |||
| } | |||
| } | |||
| func TestUintTyped(t *testing.T) { | |||
| t.Parallel() | |||
| e := Everything{} | |||
| err := Parse(url.Values{"AUint": {"1"}}, &e) | |||
| if err != nil { | |||
| t.Error("Parse error for typed uint") | |||
| } | |||
| assertEqual(t, "e.AUint", MyUint(1), e.AUint) | |||
| } | |||
| func TestUintErrors(t *testing.T) { | |||
| t.Parallel() | |||
| singletonErrors(t, "Uint", "1", "llama") | |||
| e := Everything{} | |||
| err := Parse(url.Values{"Uint": {"4.2"}}, &e) | |||
| if err == nil { | |||
| t.Error("Expected error parsing float as uint") | |||
| } | |||
| err = Parse(url.Values{"Uint": {"-42"}}, &e) | |||
| if err == nil { | |||
| t.Error("Expected error parsing negative number as uint") | |||
| } | |||
| } | |||
| var floatAnswers = map[string]float64{ | |||
| "0": 0, | |||
| "9001": 9001, | |||
| "-42": -42, | |||
| "9001.0": 9001.0, | |||
| "4.2": 4.2, | |||
| "-9.000001": -9.000001, | |||
| } | |||
| func TestFloat(t *testing.T) { | |||
| t.Parallel() | |||
| for val, correct := range floatAnswers { | |||
| e := Everything{} | |||
| e.Float = 1 | |||
| err := Parse(url.Values{"Float": {val}}, &e) | |||
| if err != nil { | |||
| t.Error("Parse error on key: ", val) | |||
| } | |||
| assertEqual(t, "e.Float", correct, e.Float) | |||
| } | |||
| } | |||
| func TestFloatTyped(t *testing.T) { | |||
| t.Parallel() | |||
| e := Everything{} | |||
| err := Parse(url.Values{"AFloat": {"1.0"}}, &e) | |||
| if err != nil { | |||
| t.Error("Parse error for typed float") | |||
| } | |||
| assertEqual(t, "e.AFloat", MyFloat(1.0), e.AFloat) | |||
| } | |||
| func TestFloatErrors(t *testing.T) { | |||
| t.Parallel() | |||
| singletonErrors(t, "Float", "1.0", "llama") | |||
| } | |||
| func TestMap(t *testing.T) { | |||
| t.Parallel() | |||
| e := Everything{} | |||
| err := Parse(url.Values{ | |||
| "Map[one]": {"1"}, | |||
| "Map[two]": {"2"}, | |||
| "Map[three]": {"3"}, | |||
| }, &e) | |||
| if err != nil { | |||
| t.Error("Parse error in map: ", err) | |||
| } | |||
| for k, v := range map[string]int{"one": 1, "two": 2, "three": 3} { | |||
| if mv, ok := e.Map[k]; !ok { | |||
| t.Errorf("Key %q not in map", k) | |||
| } else { | |||
| assertEqual(t, "Map["+k+"]", v, mv) | |||
| } | |||
| } | |||
| } | |||
| func TestMapTyped(t *testing.T) { | |||
| t.Parallel() | |||
| e := Everything{} | |||
| err := Parse(url.Values{"AMap[one]": {"1"}}, &e) | |||
| if err != nil { | |||
| t.Error("Parse error for typed map") | |||
| } | |||
| assertEqual(t, "e.AMap[one]", MyInt(1), e.AMap[MyString("one")]) | |||
| } | |||
| func TestMapErrors(t *testing.T) { | |||
| t.Parallel() | |||
| e := Everything{} | |||
| err := Parse(url.Values{"Map[]": {"llama"}}, &e) | |||
| if err == nil { | |||
| t.Error("expected error parsing empty map key") | |||
| } | |||
| err = Parse(url.Values{"Map": {"llama"}}, &e) | |||
| if err == nil { | |||
| t.Error("expected error parsing map without key") | |||
| } | |||
| err = Parse(url.Values{"Map[": {"llama"}}, &e) | |||
| if err == nil { | |||
| t.Error("expected error parsing map with malformed key") | |||
| } | |||
| } | |||
| func testPtr(t *testing.T, key, in string, out interface{}) { | |||
| e := Everything{} | |||
| err := Parse(url.Values{key: {in}}, &e) | |||
| if err != nil { | |||
| t.Errorf("Parse error while parsing pointer e.%s: %v", key, err) | |||
| } | |||
| fieldKey := key | |||
| if i := strings.IndexRune(fieldKey, '['); i >= 0 { | |||
| fieldKey = fieldKey[:i] | |||
| } | |||
| v := reflect.ValueOf(e).FieldByName(fieldKey) | |||
| if v.IsNil() { | |||
| t.Errorf("Expected param to allocate pointer for e.%s", key) | |||
| } else { | |||
| assertEqual(t, "*e."+key, out, v.Elem().Interface()) | |||
| } | |||
| } | |||
| func TestPtr(t *testing.T) { | |||
| t.Parallel() | |||
| testPtr(t, "PBool", "true", true) | |||
| testPtr(t, "PInt", "2", 2) | |||
| testPtr(t, "PUint", "2", uint(2)) | |||
| testPtr(t, "PFloat", "2.0", 2.0) | |||
| testPtr(t, "PMap[llama]", "4", map[string]int{"llama": 4}) | |||
| testPtr(t, "PSlice[]", "4", []int{4}) | |||
| testPtr(t, "PString", "llama", "llama") | |||
| testPtr(t, "PStruct[B]", "2", Sub{0, 2}) | |||
| testPtr(t, "PTime", testTimeString, testTime) | |||
| foo := 2 | |||
| testPtr(t, "PPInt", "2", &foo) | |||
| } | |||
| func TestPtrTyped(t *testing.T) { | |||
| t.Parallel() | |||
| e := Everything{} | |||
| err := Parse(url.Values{"APtr": {"1"}}, &e) | |||
| if err != nil { | |||
| t.Error("Parse error for typed pointer") | |||
| } | |||
| assertEqual(t, "e.APtr", MyInt(1), *e.APtr) | |||
| } | |||
| func TestSlice(t *testing.T) { | |||
| t.Parallel() | |||
| e := Everything{} | |||
| err := Parse(url.Values{"Slice[]": {"3", "1", "4"}}, &e) | |||
| if err != nil { | |||
| t.Error("Parse error for slice") | |||
| } | |||
| if e.Slice == nil { | |||
| t.Fatal("Expected param to allocate a slice") | |||
| } | |||
| if len(e.Slice) != 3 { | |||
| t.Fatal("Expected a slice of length 3") | |||
| } | |||
| assertEqual(t, "e.Slice[0]", 3, e.Slice[0]) | |||
| assertEqual(t, "e.Slice[1]", 1, e.Slice[1]) | |||
| assertEqual(t, "e.Slice[2]", 4, e.Slice[2]) | |||
| } | |||
| func TestSliceTyped(t *testing.T) { | |||
| t.Parallel() | |||
| e := Everything{} | |||
| err := Parse(url.Values{"ASlice[]": {"3", "1", "4"}}, &e) | |||
| if err != nil { | |||
| t.Error("Parse error for typed slice") | |||
| } | |||
| if e.ASlice == nil { | |||
| t.Fatal("Expected param to allocate a slice") | |||
| } | |||
| if len(e.ASlice) != 3 { | |||
| t.Fatal("Expected a slice of length 3") | |||
| } | |||
| assertEqual(t, "e.ASlice[0]", MyInt(3), e.ASlice[0]) | |||
| assertEqual(t, "e.ASlice[1]", MyInt(1), e.ASlice[1]) | |||
| assertEqual(t, "e.ASlice[2]", MyInt(4), e.ASlice[2]) | |||
| } | |||
| func TestSliceErrors(t *testing.T) { | |||
| t.Parallel() | |||
| e := Everything{} | |||
| err := Parse(url.Values{"Slice": {"1"}}, &e) | |||
| if err == nil { | |||
| t.Error("expected error parsing slice without key") | |||
| } | |||
| err = Parse(url.Values{"Slice[llama]": {"1"}}, &e) | |||
| if err == nil { | |||
| t.Error("expected error parsing slice with string key") | |||
| } | |||
| err = Parse(url.Values{"Slice[": {"1"}}, &e) | |||
| if err == nil { | |||
| t.Error("expected error parsing malformed slice key") | |||
| } | |||
| } | |||
| var stringAnswer = "This is the world's best string" | |||
| func TestString(t *testing.T) { | |||
| t.Parallel() | |||
| e := Everything{} | |||
| err := Parse(url.Values{"String": {stringAnswer}}, &e) | |||
| if err != nil { | |||
| t.Error("Parse error in string: ", err) | |||
| } | |||
| assertEqual(t, "e.String", stringAnswer, e.String) | |||
| } | |||
| func TestStringTyped(t *testing.T) { | |||
| t.Parallel() | |||
| e := Everything{} | |||
| err := Parse(url.Values{"AString": {"llama"}}, &e) | |||
| if err != nil { | |||
| t.Error("Parse error for typed string") | |||
| } | |||
| assertEqual(t, "e.AString", MyString("llama"), e.AString) | |||
| } | |||
| func TestStruct(t *testing.T) { | |||
| t.Parallel() | |||
| e := Everything{} | |||
| err := Parse(url.Values{ | |||
| "Struct[A]": {"1"}, | |||
| }, &e) | |||
| if err != nil { | |||
| t.Error("Parse error in struct: ", err) | |||
| } | |||
| assertEqual(t, "e.Struct.A", 1, e.Struct.A) | |||
| assertEqual(t, "e.Struct.B", 0, e.Struct.B) | |||
| err = Parse(url.Values{ | |||
| "Struct[A]": {"4"}, | |||
| "Struct[B]": {"2"}, | |||
| }, &e) | |||
| if err != nil { | |||
| t.Error("Parse error in struct: ", err) | |||
| } | |||
| assertEqual(t, "e.Struct.A", 4, e.Struct.A) | |||
| assertEqual(t, "e.Struct.B", 2, e.Struct.B) | |||
| } | |||
| func TestStructErrors(t *testing.T) { | |||
| t.Parallel() | |||
| e := Everything{} | |||
| err := Parse(url.Values{"Struct[]": {"llama"}}, &e) | |||
| if err == nil { | |||
| t.Error("expected error parsing empty struct key") | |||
| } | |||
| err = Parse(url.Values{"Struct": {"llama"}}, &e) | |||
| if err == nil { | |||
| t.Error("expected error parsing struct without key") | |||
| } | |||
| err = Parse(url.Values{"Struct[": {"llama"}}, &e) | |||
| if err == nil { | |||
| t.Error("expected error parsing malformed struct key") | |||
| } | |||
| err = Parse(url.Values{"Struct[C]": {"llama"}}, &e) | |||
| if err == nil { | |||
| t.Error("expected error parsing unknown") | |||
| } | |||
| } | |||
| func TestTextUnmarshaler(t *testing.T) { | |||
| t.Parallel() | |||
| e := Everything{} | |||
| err := Parse(url.Values{"Time": {testTimeString}}, &e) | |||
| if err != nil { | |||
| t.Error("parse error for TextUnmarshaler (Time): ", err) | |||
| } | |||
| assertEqual(t, "e.Time", testTime, e.Time) | |||
| } | |||
| func TestTextUnmarshalerError(t *testing.T) { | |||
| t.Parallel() | |||
| e := Everything{} | |||
| err := Parse(url.Values{"Time": {"llama"}}, &e) | |||
| if err == nil { | |||
| t.Error("expected error parsing llama as time") | |||
| } | |||
| } | |||
| @ -0,0 +1,214 @@ | |||
| package param | |||
| import ( | |||
| "encoding" | |||
| "fmt" | |||
| "reflect" | |||
| "strconv" | |||
| "strings" | |||
| ) | |||
| var textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() | |||
| // Generic parse dispatcher. This function's signature is the interface of all | |||
| // parse functions. `key` is the entire key that is currently being parsed, such | |||
| // as "foo[bar][]". `keytail` is the portion of the string that the current | |||
| // parser is responsible for, for instance "[bar][]". `values` is the list of | |||
| // values assigned to this key, and `target` is where the resulting typed value | |||
| // should be Set() to. | |||
| func parse(key, keytail string, values []string, target reflect.Value) { | |||
| t := target.Type() | |||
| if reflect.PtrTo(t).Implements(textUnmarshalerType) { | |||
| parseTextUnmarshaler(key, keytail, values, target) | |||
| return | |||
| } | |||
| switch k := target.Kind(); k { | |||
| case reflect.Bool: | |||
| parseBool(key, keytail, values, target) | |||
| case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: | |||
| parseInt(key, keytail, values, target) | |||
| case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: | |||
| parseUint(key, keytail, values, target) | |||
| case reflect.Float32, reflect.Float64: | |||
| parseFloat(key, keytail, values, target) | |||
| case reflect.Map: | |||
| parseMap(key, keytail, values, target) | |||
| case reflect.Ptr: | |||
| parsePtr(key, keytail, values, target) | |||
| case reflect.Slice: | |||
| parseSlice(key, keytail, values, target) | |||
| case reflect.String: | |||
| parseString(key, keytail, values, target) | |||
| case reflect.Struct: | |||
| parseStruct(key, keytail, values, target) | |||
| default: | |||
| pebkac("unsupported object of type %v and kind %v.", | |||
| target.Type(), k) | |||
| } | |||
| } | |||
| // We pass down both the full key ("foo[bar][]") and the part the current layer | |||
| // is responsible for making sense of ("[bar][]"). This computes the other thing | |||
| // you probably want to know, which is the path you took to get here ("foo"). | |||
| func kpath(key, keytail string) string { | |||
| l, t := len(key), len(keytail) | |||
| return key[:l-t] | |||
| } | |||
| // Helper for validating that a value has been passed exactly once, and that the | |||
| // user is not attempting to nest on the key. | |||
| func primitive(tipe, key, keytail string, values []string) { | |||
| if keytail != "" { | |||
| perr("expected %s for key %q, got nested value", tipe, | |||
| kpath(key, keytail)) | |||
| } | |||
| if len(values) != 1 { | |||
| perr("expected %s for key %q, but key passed %v times", tipe, | |||
| kpath(key, keytail), len(values)) | |||
| } | |||
| } | |||
| func keyed(tipe reflect.Type, key, keytail string) (string, string) { | |||
| idx := strings.IndexRune(keytail, ']') | |||
| // Keys must be at least 1 rune wide: we refuse to use the empty string | |||
| // as the key | |||
| if len(keytail) < 3 || keytail[0] != '[' || idx < 2 { | |||
| perr("expected a square bracket delimited index for %q "+ | |||
| "(of type %v)", kpath(key, keytail), tipe) | |||
| } | |||
| return keytail[1:idx], keytail[idx+1:] | |||
| } | |||
| func parseTextUnmarshaler(key, keytail string, values []string, target reflect.Value) { | |||
| primitive("encoding.TextUnmarshaler", key, keytail, values) | |||
| tu := target.Addr().Interface().(encoding.TextUnmarshaler) | |||
| err := tu.UnmarshalText([]byte(values[0])) | |||
| if err != nil { | |||
| perr("error while calling UnmarshalText on %v for key %q: %v", | |||
| target.Type(), kpath(key, keytail), err) | |||
| } | |||
| } | |||
| func parseBool(key, keytail string, values []string, target reflect.Value) { | |||
| primitive("bool", key, keytail, values) | |||
| switch values[0] { | |||
| case "true", "1", "on": | |||
| target.SetBool(true) | |||
| case "false", "0", "": | |||
| target.SetBool(false) | |||
| default: | |||
| perr("could not parse key %q as bool", kpath(key, keytail)) | |||
| } | |||
| } | |||
| func parseInt(key, keytail string, values []string, target reflect.Value) { | |||
| primitive("int", key, keytail, values) | |||
| t := target.Type() | |||
| i, err := strconv.ParseInt(values[0], 10, t.Bits()) | |||
| if err != nil { | |||
| perr("error parsing key %q as int: %v", kpath(key, keytail), | |||
| err) | |||
| } | |||
| target.SetInt(i) | |||
| } | |||
| func parseUint(key, keytail string, values []string, target reflect.Value) { | |||
| primitive("uint", key, keytail, values) | |||
| t := target.Type() | |||
| i, err := strconv.ParseUint(values[0], 10, t.Bits()) | |||
| if err != nil { | |||
| perr("error parsing key %q as uint: %v", kpath(key, keytail), | |||
| err) | |||
| } | |||
| target.SetUint(i) | |||
| } | |||
| func parseFloat(key, keytail string, values []string, target reflect.Value) { | |||
| primitive("float", key, keytail, values) | |||
| t := target.Type() | |||
| f, err := strconv.ParseFloat(values[0], t.Bits()) | |||
| if err != nil { | |||
| perr("error parsing key %q as float: %v", kpath(key, keytail), | |||
| err) | |||
| } | |||
| target.SetFloat(f) | |||
| } | |||
| func parseString(key, keytail string, values []string, target reflect.Value) { | |||
| primitive("string", key, keytail, values) | |||
| target.SetString(values[0]) | |||
| } | |||
| func parseSlice(key, keytail string, values []string, target reflect.Value) { | |||
| // BUG(carl): We currently do not handle slices of nested types. If | |||
| // support is needed, the implementation probably could be fleshed out. | |||
| if keytail != "[]" { | |||
| perr("unexpected array nesting for key %q: %q", | |||
| kpath(key, keytail), keytail) | |||
| } | |||
| t := target.Type() | |||
| slice := reflect.MakeSlice(t, len(values), len(values)) | |||
| kp := kpath(key, keytail) | |||
| for i, _ := range values { | |||
| // We actually cheat a little bit and modify the key so we can | |||
| // generate better debugging messages later | |||
| key := fmt.Sprintf("%s[%d]", kp, i) | |||
| parse(key, "", values[i:i+1], slice.Index(i)) | |||
| } | |||
| target.Set(slice) | |||
| } | |||
| func parseMap(key, keytail string, values []string, target reflect.Value) { | |||
| t := target.Type() | |||
| mapkey, maptail := keyed(t, key, keytail) | |||
| // BUG(carl): We don't support any map keys except strings, although | |||
| // there's no reason we shouldn't be able to throw the value through our | |||
| // unparsing stack. | |||
| var mk reflect.Value | |||
| if t.Key().Kind() == reflect.String { | |||
| mk = reflect.ValueOf(mapkey).Convert(t.Key()) | |||
| } else { | |||
| pebkac("key for map %v isn't a string (it's a %v).", t, t.Key()) | |||
| } | |||
| if target.IsNil() { | |||
| target.Set(reflect.MakeMap(t)) | |||
| } | |||
| val := target.MapIndex(mk) | |||
| if !val.IsValid() || !val.CanSet() { | |||
| // It's a teensy bit annoying that the value returned by | |||
| // MapIndex isn't Set()table if the key exists. | |||
| val = reflect.New(t.Elem()).Elem() | |||
| } | |||
| parse(key, maptail, values, val) | |||
| target.SetMapIndex(mk, val) | |||
| } | |||
| func parseStruct(key, keytail string, values []string, target reflect.Value) { | |||
| t := target.Type() | |||
| sk, skt := keyed(t, key, keytail) | |||
| cache := cacheStruct(t) | |||
| parseStructField(cache, key, sk, skt, values, target) | |||
| } | |||
| func parsePtr(key, keytail string, values []string, target reflect.Value) { | |||
| t := target.Type() | |||
| if target.IsNil() { | |||
| target.Set(reflect.New(t.Elem())) | |||
| } | |||
| parse(key, keytail, values, target.Elem()) | |||
| } | |||
| @ -0,0 +1,58 @@ | |||
| package param | |||
| import ( | |||
| "net/url" | |||
| "strings" | |||
| "testing" | |||
| ) | |||
| type Bad struct { | |||
| Unknown interface{} | |||
| } | |||
| type Bad2 struct { | |||
| Unknown *interface{} | |||
| } | |||
| type Bad3 struct { | |||
| BadMap map[int]int | |||
| } | |||
| // These tests are not parallel so we can frob pebkac behavior in an isolated | |||
| // way | |||
| func assertPebkac(t *testing.T, err error) { | |||
| if err == nil { | |||
| t.Error("Expected PEBKAC error message") | |||
| } else if !strings.HasSuffix(err.Error(), yourFault) { | |||
| t.Errorf("Expected PEBKAC error, but got: %v", err) | |||
| } | |||
| } | |||
| func TestBadInputs(t *testing.T) { | |||
| pebkacTesting = true | |||
| err := Parse(url.Values{"Unknown": {"4"}}, Bad{}) | |||
| assertPebkac(t, err) | |||
| b := &Bad{} | |||
| err = Parse(url.Values{"Unknown": {"4"}}, &b) | |||
| assertPebkac(t, err) | |||
| pebkacTesting = false | |||
| } | |||
| func TestBadTypes(t *testing.T) { | |||
| pebkacTesting = true | |||
| err := Parse(url.Values{"Unknown": {"4"}}, &Bad{}) | |||
| assertPebkac(t, err) | |||
| err = Parse(url.Values{"Unknown": {"4"}}, &Bad2{}) | |||
| assertPebkac(t, err) | |||
| err = Parse(url.Values{"BadMap[llama]": {"4"}}, &Bad3{}) | |||
| assertPebkac(t, err) | |||
| pebkacTesting = false | |||
| } | |||
| @ -0,0 +1,117 @@ | |||
| package param | |||
| import ( | |||
| "reflect" | |||
| "strings" | |||
| "sync" | |||
| ) | |||
| // We decode a lot of structs (since it's the top-level thing this library | |||
| // decodes) and it takes a fair bit of work to reflect upon the struct to figure | |||
| // out what we want to do. Instead of doing this on every invocation, we cache | |||
| // metadata about each struct the first time we see it. The upshot is that we | |||
| // save some work every time. The downside is we are forced to briefly acquire | |||
| // a lock to access the cache in a thread-safe way. If this ever becomes a | |||
| // bottleneck, both the lock and the cache can be sharded or something. | |||
| type structCache map[string]cacheLine | |||
| type cacheLine struct { | |||
| offset int | |||
| parse func(string, string, []string, reflect.Value) | |||
| } | |||
| var cacheLock sync.RWMutex | |||
| var cache = make(map[reflect.Type]structCache) | |||
| func cacheStruct(t reflect.Type) structCache { | |||
| cacheLock.RLock() | |||
| sc, ok := cache[t] | |||
| cacheLock.RUnlock() | |||
| if ok { | |||
| return sc | |||
| } | |||
| // It's okay if two people build struct caches simultaneously | |||
| sc = make(structCache) | |||
| for i := 0; i < t.NumField(); i++ { | |||
| sf := t.Field(i) | |||
| // Only unexported fields have a PkgPath; we want to only cache | |||
| // exported fields. | |||
| if sf.PkgPath != "" { | |||
| continue | |||
| } | |||
| name := extractName(sf) | |||
| if name != "-" { | |||
| sc[name] = cacheLine{i, extractHandler(t, sf)} | |||
| } | |||
| } | |||
| cacheLock.Lock() | |||
| cache[t] = sc | |||
| cacheLock.Unlock() | |||
| return sc | |||
| } | |||
| // Extract the name of the given struct field, looking at struct tags as | |||
| // appropriate. | |||
| func extractName(sf reflect.StructField) string { | |||
| name := sf.Tag.Get("param") | |||
| if name == "" { | |||
| name = sf.Tag.Get("json") | |||
| idx := strings.IndexRune(name, ',') | |||
| if idx >= 0 { | |||
| name = name[:idx] | |||
| } | |||
| } | |||
| if name == "" { | |||
| name = sf.Name | |||
| } | |||
| return name | |||
| } | |||
| func extractHandler(s reflect.Type, sf reflect.StructField) func(string, string, []string, reflect.Value) { | |||
| if reflect.PtrTo(sf.Type).Implements(textUnmarshalerType) { | |||
| return parseTextUnmarshaler | |||
| } | |||
| switch sf.Type.Kind() { | |||
| case reflect.Bool: | |||
| return parseBool | |||
| case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: | |||
| return parseInt | |||
| case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: | |||
| return parseUint | |||
| case reflect.Float32, reflect.Float64: | |||
| return parseFloat | |||
| case reflect.Map: | |||
| return parseMap | |||
| case reflect.Ptr: | |||
| return parsePtr | |||
| case reflect.Slice: | |||
| return parseSlice | |||
| case reflect.String: | |||
| return parseString | |||
| case reflect.Struct: | |||
| return parseStruct | |||
| default: | |||
| pebkac("struct %v has illegal field %q (type %v, kind %v).", | |||
| s, sf.Name, sf.Type, sf.Type.Kind()) | |||
| return nil | |||
| } | |||
| } | |||
| // We have to parse two types of structs: ones at the top level, whose keys | |||
| // don't have square brackets around them, and nested structs, which do. | |||
| func parseStructField(cache structCache, key, sk, keytail string, values []string, target reflect.Value) { | |||
| l, ok := cache[sk] | |||
| if !ok { | |||
| perr("unknown key %q for struct at key %q", sk, | |||
| kpath(key, keytail)) | |||
| } | |||
| f := target.Field(l.offset) | |||
| l.parse(key, keytail, values, f) | |||
| } | |||
| @ -0,0 +1,106 @@ | |||
| package param | |||
| import ( | |||
| "reflect" | |||
| "testing" | |||
| ) | |||
| type Fruity struct { | |||
| A bool | |||
| B int `json:"banana"` | |||
| C uint `param:"cherry"` | |||
| D float64 `json:"durian" param:"dragonfruit"` | |||
| E int `json:"elderberry" param:"-"` | |||
| F map[string]int `json:"-" param:"fig"` | |||
| G *int `json:"grape,omitempty"` | |||
| H []int `param:"honeydew" json:"huckleberry"` | |||
| I string `foobar:"iyokan"` | |||
| J Cheesy `param:"jackfruit" cheese:"jarlsberg"` | |||
| } | |||
| type Cheesy struct { | |||
| A int `param:"affinois"` | |||
| B int `param:"brie"` | |||
| C int `param:"camembert"` | |||
| D int `param:"delice d'argental"` | |||
| } | |||
| type Private struct { | |||
| Public, private int | |||
| } | |||
| var fruityType = reflect.TypeOf(Fruity{}) | |||
| var cheesyType = reflect.TypeOf(Cheesy{}) | |||
| var privateType = reflect.TypeOf(Private{}) | |||
| var fruityNames = []string{ | |||
| "A", "banana", "cherry", "dragonfruit", "-", "fig", "grape", "honeydew", | |||
| "I", "jackfruit", | |||
| } | |||
| var fruityCache = map[string]cacheLine{ | |||
| "A": {0, parseBool}, | |||
| "banana": {1, parseInt}, | |||
| "cherry": {2, parseUint}, | |||
| "dragonfruit": {3, parseFloat}, | |||
| "fig": {5, parseMap}, | |||
| "grape": {6, parsePtr}, | |||
| "honeydew": {7, parseSlice}, | |||
| "I": {8, parseString}, | |||
| "jackfruit": {9, parseStruct}, | |||
| } | |||
| func assertEqual(t *testing.T, what string, e, a interface{}) { | |||
| if !reflect.DeepEqual(e, a) { | |||
| t.Errorf("Expected %s to be %v, was actually %v", what, e, a) | |||
| } | |||
| } | |||
| func TestNames(t *testing.T) { | |||
| t.Parallel() | |||
| for i, val := range fruityNames { | |||
| name := extractName(fruityType.Field(i)) | |||
| assertEqual(t, "tag", val, name) | |||
| } | |||
| } | |||
| func TestCacheStruct(t *testing.T) { | |||
| t.Parallel() | |||
| sc := cacheStruct(fruityType) | |||
| if len(sc) != len(fruityCache) { | |||
| t.Errorf("Cache has %d keys, but expected %d", len(sc), | |||
| len(fruityCache)) | |||
| } | |||
| for k, v := range fruityCache { | |||
| sck, ok := sc[k] | |||
| if !ok { | |||
| t.Errorf("Could not find key %q in cache", k) | |||
| continue | |||
| } | |||
| if sck.offset != v.offset { | |||
| t.Errorf("Cache for %q: expected offset %d but got %d", | |||
| k, sck.offset, v.offset) | |||
| } | |||
| // We want to compare function pointer equality, and this | |||
| // appears to be the only way | |||
| a := reflect.ValueOf(sck.parse) | |||
| b := reflect.ValueOf(v.parse) | |||
| if a.Pointer() != b.Pointer() { | |||
| t.Errorf("Parse mismatch for %q: %v, expected %v", k, a, | |||
| b) | |||
| } | |||
| } | |||
| } | |||
| func TestPrivate(t *testing.T) { | |||
| t.Parallel() | |||
| sc := cacheStruct(privateType) | |||
| if len(sc) != 1 { | |||
| t.Error("Expected Private{} to have one cachable field") | |||
| } | |||
| } | |||