~samwhited/query

ref: v0.0.1 query/query.go -rw-r--r-- 3.6 KiB
0e3d4fc2Sam Whited Initial public release 1 year, 5 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
// Package query is used to parse the simple query language used by Soquee.
package query // import "code.soquee.net/query"

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"strconv"
	"strings"
	"unicode"
	"unicode/utf8"

	"golang.org/x/text/secure/precis"
)

// IssueStatus represents the status of an issue (closed, open or any).
type IssueStatus int

// A collection of issue statuses.
// Issues may be open or closed, and, in this special case "Any" which means
// "either of those".
const (
	StatusAny IssueStatus = iota
	StatusClosed
	StatusOpen
)

// Query contains the parsed query string split into fields.
// This struct may grow over time and the field order is not part the package
// stability guarantee.
//
// TSVector is a PostgreSQL compatible full text search string.
// It is not guaranteed to be safe from SQL injection and should always be
// parameterized.
type Query struct {
	Status   IssueStatus
	TSVector string
	Assignee string
	Limit    int
	Labels   []string
}

// String parses a query from a string.
func String(q string) *Query {
	/* #nosec */
	parsed, _ := Parse(strings.NewReader(q))
	return parsed
}

// Bytes parses a query from a byte slice.
func Bytes(q []byte) *Query {
	/* #nosec */
	parsed, _ := Parse(bytes.NewReader(q))
	return parsed
}

func isSkipable(r rune) bool {
	return unicode.IsSpace(r) ||
		r == '!' ||
		r == '|' ||
		r == '&' ||
		r == '(' ||
		r == ')'
}

// scanTokens is a copy of bufio.ScanWords except with the isSpace function
// replaced by one that also skips various tsquery operators.
func scanTokens(data []byte, atEOF bool) (advance int, token []byte, err error) {
	start := 0

	// Skip leading spaces.
	for width := 0; start < len(data); start += width {
		var r rune
		r, width = utf8.DecodeRune(data[start:])
		if !isSkipable(r) {
			break
		}
	}
	// Scan until space, marking end of word.
	for width, i := 0, start; i < len(data); i += width {
		var r rune
		r, width = utf8.DecodeRune(data[i:])
		if isSkipable(r) {
			return i + width, data[start:i], nil
		}
	}
	// If we're at EOF, we have a final, non-empty, non-terminated word. Return it.
	if atEOF && len(data) > start {
		return len(data), data[start:], nil
	}
	// Request more data.
	return start, nil, nil
}

const (
	prefixStatus   = "status:"
	prefixLabel    = "label:"
	prefixLimit    = "limit:"
	prefixAssignee = "assignee:"
)

// Parse parses the query string from r and returns a parsed representation.
func Parse(r io.Reader) (*Query, error) {
	parsed := &Query{}
	w := new(strings.Builder)
	s := bufio.NewScanner(r)
	s.Split(scanTokens)

	sep := ""
	no := ""
	for s.Scan() {
		tok := s.Text()
		if idx := strings.IndexByte(tok, ':'); idx > -1 {
			switch tok[:idx+1] {
			case prefixAssignee:
				/* #nosec */
				parsed.Assignee, _ = precis.UsernameCaseMapped.String(tok[len(prefixAssignee):])
				continue
			case prefixLimit:
				/* #nosec */
				parsed.Limit, _ = strconv.Atoi(tok[len(prefixLimit):])
				continue
			case prefixLabel:
				l := tok[len(prefixLabel):]
				if l == "" {
					continue
				}
				parsed.Labels = append(parsed.Labels, l)
				continue
			case prefixStatus:
				switch tok[len(prefixStatus):] {
				case "open":
					parsed.Status = StatusOpen
				case "closed":
					parsed.Status = StatusClosed
				case "any":
					parsed.Status = StatusAny
				default:
				}
				continue
			}
			continue
		}
		t := strings.TrimPrefix(tok, "-")
		if t == "" {
			continue
		}
		if tok != t {
			no = "!"
		}
		fmt.Fprintf(w, "%s%s%s", sep, no, t)
		no = ""
		sep = "&"
	}
	parsed.TSVector = w.String()
	switch {
	case parsed.Limit == 0:
		parsed.Limit = 15
	case parsed.Limit < 10:
		parsed.Limit = 10
	case parsed.Limit > 100:
		parsed.Limit = 100
	}
	return parsed, s.Err()
}