Skip to content
This repository has been archived by the owner on Mar 11, 2021. It is now read-only.

Commit

Permalink
List workitems (#120)
Browse files Browse the repository at this point in the history
Fixes #101

This change implements a very simple "query by example" functionality where you can add two parameters to the url "api/workitem"

- page: <start>,<length> implements paging. If only one value is given, it is taken as the length parameters
- filter: you can pass in a json object string. Only work items matching (with '=') the field values given in the example json will be selected. 
Example: 
    http://localhost:8080/api/workitem?filter={"system.owner": "tmaeder","Name": "First","Type": "1"}&page=1,2

This change has a query parser, which produces expression trees, which will in turn be compiled for use with Gorm. While the query system architecture is in place, only a small amount of expressions is implemented (for example, only "=", no other comparisons). The intention is to extend the system as needed.

* First cut of simple work item listing
* Extract work item storage to a repository struct
* Always start from original db
* Extracted transaction support and errors
* Cleanup, doc & minor fixes
* Support for paging parameters
* Conditional value rendering depending on json context or regular context
* Fix test, update doc
* Fixed some comments
* Added comment
* Prevent sql injection
* Remove unused function
  • Loading branch information
tsmaeder authored and kwk committed Aug 19, 2016
1 parent 72a5560 commit de975dc
Show file tree
Hide file tree
Showing 12 changed files with 681 additions and 0 deletions.
202 changes: 202 additions & 0 deletions criteria/criteria.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
// Package criteria holds a representation of expression trees and code for their manipulation
// This package serves to decouple the concrete query language from the execution of the queries against the database
package criteria

// Expression is used to express conditions for selecting an entity
type Expression interface {
// Accept calls the visitor callback of the appropriate type
Accept(visitor ExpressionVisitor) interface{}
// SetAnnotation puts the given annotation on the expression
SetAnnotation(key string, value interface{})
// Annotation reads back values set with SetAnnotation
Annotation(key string) interface{}
// Returns the parent expression or nil
Parent() Expression
setParent(parent Expression)
}

// IterateParents calls f for every member of the parent chain
// Stops iterating if f returns false
func IterateParents(exp Expression, f func(Expression) bool) {
if exp != nil {
exp = exp.Parent()
}
for exp != nil {
if !f(exp) {
return
}
exp = exp.Parent()
}
}

// BinaryExpression represents expressions with 2 children
// This could be generalized to n-ary expressions, but that is not neccessary right now
type BinaryExpression interface {
Expression
Left() Expression
Right() Expression
}

// ExpressionVisitor is an implementation of the visitor pattern for expressions
type ExpressionVisitor interface {
Field(t *FieldExpression) interface{}
And(a *AndExpression) interface{}
Or(a *OrExpression) interface{}
Equals(e *EqualsExpression) interface{}
Parameter(v *ParameterExpression) interface{}
Literal(c *LiteralExpression) interface{}
}

type expression struct {
parent Expression
annotations map[string]interface{}
}

func (exp *expression) SetAnnotation(key string, value interface{}) {
if exp.annotations == nil {
exp.annotations = map[string]interface{}{}
}
exp.annotations[key] = value
}

func (exp *expression) Annotation(key string) interface{} {
return exp.annotations[key]
}

func (exp *expression) Parent() Expression {
result := exp.parent
return result
}

func (exp *expression) setParent(parent Expression) {
exp.parent = parent
}

// access a Field

// FieldExpression represents access to a field of the tested object
type FieldExpression struct {
expression
FieldName string
}

// Accept implements ExpressionVisitor
func (t *FieldExpression) Accept(visitor ExpressionVisitor) interface{} {
return visitor.Field(t)
}

// Field constructs a FieldExpression
func Field(id string) Expression {
return &FieldExpression{expression{}, id}
}

// Parameter (free variable of the expression)

// A ParameterExpression represents a parameter to be passed upon evaluation of the expression
type ParameterExpression struct {
expression
}

// Accept implements ExpressionVisitor
func (t *ParameterExpression) Accept(visitor ExpressionVisitor) interface{} {
return visitor.Parameter(t)
}

// Parameter constructs a value expression.
func Parameter() Expression {
return &ParameterExpression{}
}

// literal value

// A LiteralExpression represents a single constant value in the expression, think "5" or "asdf"
// the type of literals is not restricted at this level, but compilers or interpreters will have limitations on what they handle
type LiteralExpression struct {
expression
Value interface{}
}

// Accept implements ExpressionVisitor
func (t *LiteralExpression) Accept(visitor ExpressionVisitor) interface{} {
return visitor.Literal(t)
}

// Literal constructs a literal expression
func Literal(value interface{}) Expression {
return &LiteralExpression{expression{}, value}
}

// binaryExpression is an "abstract" type for binary expressions.
type binaryExpression struct {
expression
left Expression
right Expression
}

// Left implements BinaryExpression
func (exp *binaryExpression) Left() Expression {
return exp.left
}

// Right implements BinaryExpression
func (exp *binaryExpression) Right() Expression {
return exp.right
}

// make sure the children have the correct parent
func reparent(parent BinaryExpression) Expression {
parent.Left().setParent(parent)
parent.Right().setParent(parent)
return parent
}

// And

// AndExpression represents the conjunction operation of two terms
type AndExpression struct {
binaryExpression
}

// Accept implements ExpressionVisitor
func (t *AndExpression) Accept(visitor ExpressionVisitor) interface{} {
return visitor.And(t)
}

// And constructs an AndExpression
func And(left Expression, right Expression) Expression {
return reparent(&AndExpression{binaryExpression{expression{}, left, right}})
}

// Or

// OrExpression represents the disjunction operation of two terms
type OrExpression struct {
binaryExpression
}

// Accept implements ExpressionVisitor
func (t *OrExpression) Accept(visitor ExpressionVisitor) interface{} {
return visitor.Or(t)
}

// Or constructs an OrExpression
func Or(left Expression, right Expression) Expression {
return reparent(&OrExpression{binaryExpression{expression{}, left, right}})
}

// ==

// EqualsExpression represents the equality operator
type EqualsExpression struct {
binaryExpression
}

// Accept implements ExpressionVisitor
func (t *EqualsExpression) Accept(visitor ExpressionVisitor) interface{} {
return visitor.Equals(t)
}

// Equals constructs an EqualsExpression
func Equals(left Expression, right Expression) Expression {
return reparent(&EqualsExpression{binaryExpression{expression{}, left, right}})
}
12 changes: 12 additions & 0 deletions criteria/criteria_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package criteria

import "testing"

func TestGetParent(t *testing.T) {
l := Field("a")
r := Literal(5)
expr := Equals(l, r)
if l.Parent() != expr {
t.Errorf("parent should be %v, but is %v", expr, l.Parent())
}
}
46 changes: 46 additions & 0 deletions criteria/iterator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package criteria

// IteratePostOrder walks the expression tree in depth-first, left to right order
// The iteration stops if visitorFunction returns false
func IteratePostOrder(exp Expression, visitorFunction func(exp Expression) bool) {
exp.Accept(&postOrderIterator{visitorFunction})
}

// implements ExpressionVisitor
type postOrderIterator struct {
visit func(exp Expression) bool
}

func (i *postOrderIterator) Field(exp *FieldExpression) interface{} {
return i.visit(exp)
}

func (i *postOrderIterator) And(exp *AndExpression) interface{} {
return i.binary(exp)
}

func (i *postOrderIterator) Or(exp *OrExpression) interface{} {
return i.binary(exp)
}

func (i *postOrderIterator) Equals(exp *EqualsExpression) interface{} {
return i.binary(exp)
}

func (i *postOrderIterator) Parameter(exp *ParameterExpression) interface{} {
return i.visit(exp)
}

func (i *postOrderIterator) Literal(exp *LiteralExpression) interface{} {
return i.visit(exp)
}

func (i *postOrderIterator) binary(exp BinaryExpression) bool {
if exp.Left().Accept(i) == false {
return false
}
if exp.Right().Accept(i) == false {
return false
}
return i.visit(exp)
}
39 changes: 39 additions & 0 deletions criteria/iterator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package criteria

import (
"reflect"
"testing"
)

func TestIterator(t *testing.T) {
// test left-to-right, depth first iteration
visited := []Expression{}
l := Field("a")
r := Literal(5)
expr := Equals(l, r)
expected := []Expression{l, r, expr}
recorder := func(expr Expression) bool {
visited = append(visited, expr)
return true
}
IteratePostOrder(expr, recorder)
if !reflect.DeepEqual(expected, visited) {
t.Errorf("Visited should be %v, but is %v", expected, visited)
}

// test early iteration cutoff with false return from iterator function
visited = []Expression{}
recorder = func(expr Expression) bool {
visited = append(visited, expr)
if expr == r {
return false
}
return true
}
IteratePostOrder(expr, recorder)
expected = []Expression{l, r}
if !reflect.DeepEqual(expected, visited) {
t.Errorf("Visited should be %v, but is %v", expected, visited)
}

}
18 changes: 18 additions & 0 deletions design/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,24 @@ var _ = Resource("workitem", func() {
Response(NotFound)
})

Action("list", func() {
Routing(
GET(""),
)
Description("List work items.")
Params(func() {
Param("filter", String, "a query language expression restricting the set of found items")
Param("page", String, "Paging in the format <start>,<limit>")
})
Response(OK, func() {
Media(CollectionOf(WorkItem))
})
Response(BadRequest, func() {
Media(ErrorMedia)
})
Response(InternalServerError)
})

Action("create", func() {
Routing(
POST(""),
Expand Down

0 comments on commit de975dc

Please sign in to comment.