From c9877a7bcd2be3034a71779d7652656cc2a1ed75 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Thu, 14 Jul 2016 13:30:20 -0400 Subject: [PATCH] adding field type for performing logical time operations --- fields.go | 72 +++++++++++++++++++++++++++++++ fields_test.go | 112 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 fields.go create mode 100644 fields_test.go diff --git a/fields.go b/fields.go new file mode 100644 index 0000000..c50a2bd --- /dev/null +++ b/fields.go @@ -0,0 +1,72 @@ +package qstring + +import ( + "errors" + "fmt" + "strings" + "time" +) + +// parseOperator parses a leading logical operator out of the provided string +func parseOperator(s string) string { + switch s[0] { + case 60: // "<" + switch s[1] { + case 61: // "=" + return "<=" + default: + return "<" + } + case 62: // ">" + switch s[1] { + case 61: // "=" + return ">=" + default: + return ">" + } + default: + // no operator found, default to "=" + return "=" + } +} + +// ComparativeTime is a field that can be used for specifying a query parameter +// which includes a conditional operator and a timestamp +type ComparativeTime struct { + Operator string + Time time.Time +} + +// NewComparativeTime returns a new ComparativeTime instance with a default +// operator of "=" +func NewComparativeTime() *ComparativeTime { + return &ComparativeTime{Operator: "="} +} + +// Parse is used to parse a query string into a ComparativeTime instance +func (c *ComparativeTime) Parse(query string) error { + if len(query) <= 2 { + return errors.New("qstring: Invalid Timestamp Query") + } + + c.Operator = parseOperator(query) + + // if no operator was provided and we defaulted to an equality operator + if !strings.HasPrefix(query, c.Operator) { + query = fmt.Sprintf("=%s", query) + } + + var err error + c.Time, err = time.Parse(time.RFC3339, query[len(c.Operator):]) + if err != nil { + return err + } + + return nil +} + +// String returns this ComparativeTime instance in the form of the query +// parameter that it came in on +func (c ComparativeTime) String() string { + return fmt.Sprintf("%s%s", c.Operator, c.Time.Format(time.RFC3339)) +} diff --git a/fields_test.go b/fields_test.go new file mode 100644 index 0000000..b97b1e8 --- /dev/null +++ b/fields_test.go @@ -0,0 +1,112 @@ +package qstring + +import ( + "fmt" + "net/url" + "strings" + "testing" +) + +func TestComparativeTimeParse(t *testing.T) { + tme := "2006-01-02T15:04:05Z" + testio := []struct { + inp string + operator string + errString string + }{ + {inp: tme, operator: "=", errString: ""}, + {inp: ">" + tme, operator: ">", errString: ""}, + {inp: "<" + tme, operator: "<", errString: ""}, + {inp: ">=" + tme, operator: ">=", errString: ""}, + {inp: "<=" + tme, operator: "<=", errString: ""}, + {inp: "<=" + tme, operator: "<=", errString: ""}, + {inp: "", operator: "=", errString: "qstring: Invalid Timestamp Query"}, + {inp: ">=", operator: "=", errString: "qstring: Invalid Timestamp Query"}, + {inp: ">=" + "foobar", operator: ">=", + errString: `parsing time "foobar" as "2006-01-02T15:04:05Z07:00": cannot parse "foobar" as "2006"`}, + } + + var ct *ComparativeTime + var err error + for _, test := range testio { + ct = NewComparativeTime() + err = ct.Parse(test.inp) + + if ct.Operator != test.operator { + t.Errorf("Expected operator %q, got %q", test.operator, ct.Operator) + } + + if err == nil && len(test.errString) != 0 { + t.Errorf("Expected error %q, got nil", test.errString) + } + + if err != nil && err.Error() != test.errString { + t.Errorf("Expected error %q, got %q", test.errString, err.Error()) + } + } +} + +func TestComparativeTimeUnmarshal(t *testing.T) { + type Query struct { + Created ComparativeTime + Modified ComparativeTime + } + + createdTS := ">2006-01-02T15:04:05Z" + updatedTS := "<=2016-01-02T15:04:05-07:00" + + query := url.Values{ + "created": []string{createdTS}, + "modified": []string{updatedTS}, + } + + params := &Query{} + err := Unmarshal(query, params) + if err != nil { + t.Fatal(err.Error()) + } + + created := params.Created.String() + if created != createdTS { + t.Errorf("Expected created ts of %s, got %s instead.", createdTS, created) + } + + modified := params.Modified.String() + if modified != updatedTS { + t.Errorf("Expected update ts of %s, got %s instead.", updatedTS, modified) + } +} + +func TestComparativeTimeMarshal(t *testing.T) { + type Query struct { + Created ComparativeTime + Modified ComparativeTime + } + + createdTS := ">2006-01-02T15:04:05Z" + created := NewComparativeTime() + created.Parse(createdTS) + updatedTS := "<=2016-01-02T15:04:05-07:00" + updated := NewComparativeTime() + updated.Parse(updatedTS) + + q := &Query{*created, *updated} + result, err := Marshal(q) + if err != nil { + t.Fatalf("Unable to marshal comparative timestamp: %s", err.Error()) + } + + var unescaped string + unescaped, err = url.QueryUnescape(result) + if err != nil { + t.Fatalf("Unable to unescape query string %q: %q", result, err.Error()) + } + expected := []string{"created=>2006-01-02T15:04:05Z", + "modified=<=2016-01-02T15:04:05-07:00"} + for _, ts := range expected { + fmt.Println(ts) + if !strings.Contains(unescaped, ts) { + t.Errorf("Expected query string %s to contain %s", unescaped, ts) + } + } +}