Skip to content

Commit

Permalink
refactored query_parser. Now each filter format is handled by an impl…
Browse files Browse the repository at this point in the history
…ementation of a filterquery interface. Added a query_parser_manager to manage the available parsing strategies, keep track of the active one or ones, etc. I also switched to using testify suties. This has had the unfortunate side effect of losing feedback on a per-test basis.
  • Loading branch information
jlgerber committed Jan 24, 2016
1 parent f8b422f commit a35de13
Show file tree
Hide file tree
Showing 11 changed files with 813 additions and 314 deletions.
111 changes: 111 additions & 0 deletions format1.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package main

/*
Implementation of QueryParserI interface
*/

import (
"encoding/json"
"net/http"
"regexp"
"strings"

log "github.com/Sirupsen/logrus"
)

// Format1 satisfies the QueryParserI interface for format1
// from the sg-query spec
type Format1 struct{}

var format1QueryRegexp = regexp.MustCompile(`^([\w]+)\((.*)\)`)

// CanParseString returns a boolean indicating whether or not the method can parse the supplied string. It is not a guarantee that parsing will be successful.
func (f *Format1) CanParseString(queryStr string) bool {
// gotta start with and/or
if !strings.HasPrefix(queryStr, "AND") &&
!strings.HasPrefix(queryStr, "and") &&
!strings.HasPrefix(queryStr, "OR") &&
!strings.HasPrefix(queryStr, "or") {
return false
}

matches := format1QueryRegexp.FindStringSubmatch(queryStr)
if matches != nil {
if len(matches) != 3 {
return false
}

if strings.HasPrefix(matches[2], "[[") || strings.HasPrefix(matches[2], "[") {
return true
}
}
return false
}

// ParseString parses the supplied string and returns readFilters and an error
func (f *Format1) ParseString(queryStr string) (readFilters, error) {
query := newReadFilters()

if !strings.HasPrefix(queryStr, "AND") &&
!strings.HasPrefix(queryStr, "and") &&
!strings.HasPrefix(queryStr, "OR") &&
!strings.HasPrefix(queryStr, "or") {
return query, queryParseError{
StatusCode: http.StatusBadRequest,
Message: "Invalid query format",
}
}

matches := queryRegexp.FindStringSubmatch(queryStr)
if matches != nil {
if len(matches) != 3 {
return query, queryParseError{
StatusCode: http.StatusBadRequest,
Message: "Invalid query format",
}
}

var filtersStr string
if strings.HasPrefix(matches[2], "[[") {
filtersStr = matches[2]
} else if strings.HasPrefix(matches[2], "[") {
filtersStr = "[" + matches[2] + "]"
} else {
return query, queryParseError{
StatusCode: http.StatusBadRequest,
Message: "Invalid query filter format",
}
}

var filters []interface{}
err := json.Unmarshal([]byte(filtersStr), &filters)
if err != nil {
return query, queryParseError{
StatusCode: http.StatusBadRequest,
Message: err.Error(),
}
}

queryData := map[string]interface{}{
"logical_operator": matches[1],
"conditions": filters,
}

err = mapToReadFilters(queryData, &query)

return query, err

}

return query, queryParseError{
StatusCode: http.StatusBadRequest,
Message: "Invalid query format",
}
}

// Register the format with the manager
func init() {
manager := GetQPManager()
log.Info("manager fetched", manager)
manager.AddParser("format1", &Format1{})
}
160 changes: 160 additions & 0 deletions format1_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package main

import (
"fmt"
"net/http"
"os"
"testing"

log "github.com/Sirupsen/logrus"

"github.com/stretchr/testify/suite"
)

func TestMain(m *testing.M) {
f := SetupLog()
log.Info("TestMain setup")
manager := GetQPManager()
res := m.Run()
f.Close()
os.Exit(res)
}

// Define the suite, and absorb the built-in basic suite
// functionality from testify - including a T() method which
// returns the current testing context
type Format1TestSuite struct {
suite.Suite
}

func SetupLog() *os.File {
f, err := os.OpenFile("sg-restful-test.log", os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666)
if err != nil {
panic(fmt.Sprintf("error opening file: %v", err))
}

log.SetLevel(log.DebugLevel)
log.SetOutput(f)
log.SetFormatter(&log.TextFormatter{})
return f

}

// Make sure that VariableThatShouldStartAtFive is set to five
// before each test
func (suite *Format1TestSuite) SetupSuite() {
log.Info(" -- Format1 Test Suite --\n")
log.Debug("Format1TestSuite.SetupSuite() - setting active parsers to format1")
manager.SetActiveParsers("format1")
}

// In order for 'go test' to run this suite, we need to create
// a normal test function and pass our suite to suite.Run
func TestFormat1TestSuite(t *testing.T) {
//ts := new(Format1TestSuite)
log.Info("TestFormat1TestSuite - Running test suite")
suite.Run(t, new(Format1TestSuite))
log.Info("TestFormat1TestSuite - Finished test suite")

}

func (suite *Format1TestSuite) TestActiveParsers() {
log.Info("Format1TestSuite.TestActiveParsers()")
manager := GetQPManager()
keys, active := manager.GetActiveParsers()
log.Infof("active parsers:%s %s", keys, active)
suite.Equal(len(active), 1, "Should be a single active parser")
}

func (suite *Format1TestSuite) TestParseQueryAndOrStatementRegExpError() {
_, err := parseQuery("()")

expectedError := queryParseError{
StatusCode: http.StatusBadRequest,
Message: "No QueryFormats can parse input",
}
suite.Equal(expectedError, err, "Should be formating error")
}

func (suite *Format1TestSuite) TestParseQueryAndOrStatementFilterFormatError() {
_, err := parseQuery("and(foo, bar)")

expectedError := queryParseError{
StatusCode: http.StatusBadRequest,
Message: "No QueryFormats can parse input",
}
suite.Equal(expectedError, err, "Should be formating error")
}

func (suite *Format1TestSuite) TestParseQueryAndOrStatementJsonError() {
_, err := parseQuery("and([foo, bar,])")

expectedError := queryParseError{
StatusCode: http.StatusBadRequest,
Message: "invalid character 'o' in literal false (expecting 'a')",
}
suite.Equal(expectedError, err, "Should be formating error")
}

func (suite *Format1TestSuite) TestParseQueryAndOrStatementNotAQuery() {
_, err := parseQuery("andwell_this_is_bad")

expectedError := queryParseError{
StatusCode: http.StatusBadRequest,
Message: "No QueryFormats can parse input",
}
suite.Equal(expectedError, err, "Should be formating error")
}

func (suite *Format1TestSuite) TestParseQueryAndOrStatementSliceOfFilters() {
rf, err := parseQuery(`and([["name", "is", "blorg"]])`)

rfExpected := newReadFilters()
rfExpected.AddCondition(newQueryCondition("name", "is", "blorg"))

suite.Equal(nil, err, "Error should be nil")
suite.Equal(rfExpected, rf, "Should be the same")
}

func (suite *Format1TestSuite) TestParseQueryAndOrStatementBasicAnd() {

rf, err := parseQuery(`and(["name", "is", "blorg"])`)

rfExpected := newReadFilters()
rfExpected.AddCondition(newQueryCondition("name", "is", "blorg"))

suite.Equal(nil, err, "Error should be nil")
suite.Equal(rfExpected, rf, "Should be the same")
}

func (suite *Format1TestSuite) TestParseQueryAndOrStatementBasicAndUpper() {
rf, err := parseQuery(`AND(["name", "is", "blorg"])`)

rfExpected := newReadFilters()
rfExpected.AddCondition(newQueryCondition("name", "is", "blorg"))

suite.Equal(nil, err, "Error should be nil")
suite.Equal(rfExpected, rf, "Should be the same")
}

func (suite *Format1TestSuite) TestParseQueryAndOrStatementBasicOr() {
rf, err := parseQuery(`or(["name", "is", "blorg"])`)

rfExpected := newReadFilters()
rfExpected.LogicalOperator = "or"
rfExpected.AddCondition(newQueryCondition("name", "is", "blorg"))

suite.Equal(nil, err, "Error should be nil")
suite.Equal(rfExpected, rf, "Should be the same")
}

func (suite *Format1TestSuite) TestParseQueryAndOrStatementBasicOrUpper() {
rf, err := parseQuery(`OR(["name", "is", "blorg"])`)

rfExpected := newReadFilters()
rfExpected.LogicalOperator = "or"
rfExpected.AddCondition(newQueryCondition("name", "is", "blorg"))

suite.Equal(nil, err, "Error should be nil")
suite.Equal(rfExpected, rf, "Should be the same")
}
76 changes: 76 additions & 0 deletions format2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package main

import (
"encoding/json"
"net/http"
"strings"
)

// Format2 satisfies the QueryParserI interface for format1
// from the sg-query spec
type Format2 struct{}

// CanParseString returns a boolean indicating whether or not the method can parse the supplied string. It is not a guarantee that parsing will be successful.
func (f *Format2) CanParseString(queryStr string) bool {
if !strings.HasPrefix(queryStr, "{") {
return false
}
// Format: {"logical_operator": "and", "conditions": [[key, comparitor, value], ...]}
var queryData map[string]interface{}
err := json.Unmarshal([]byte(queryStr), &queryData)
if err != nil {
return false
}

if _, ok := queryData["logical_operator"]; !ok {
return false
}

if _, ok := queryData["conditions"]; !ok {
return false
}
return true
}

// ParseString parses the supplied string and returns readFilters and an error
func (f *Format2) ParseString(queryStr string) (readFilters, error) {
query := newReadFilters()
if strings.HasPrefix(queryStr, "{") {
// Format: {"logical_operator": "and", "conditions": [[key, comparitor, value], ...]}
var queryData map[string]interface{}
err := json.Unmarshal([]byte(queryStr), &queryData)
if err != nil {
return query, queryParseError{
StatusCode: http.StatusBadRequest,
Message: err.Error(),
}
}

if _, ok := queryData["logical_operator"]; !ok {
return query, queryParseError{
StatusCode: http.StatusBadRequest,
Message: "Missing key: 'logical_operator'",
}
}

if _, ok := queryData["conditions"]; !ok {
return query, queryParseError{
StatusCode: http.StatusBadRequest,
Message: "Missing key: 'conditions'",
}
}
err = mapToReadFilters(queryData, &query)

return query, err
}
return query, queryParseError{
StatusCode: http.StatusBadRequest,
Message: "Missing Prefix: '{'",
}
}

// Register the format with the manager
func init() {
manager := GetQPManager()
manager.AddParser("format2", &Format2{})
}
Loading

0 comments on commit a35de13

Please sign in to comment.