aboutsummaryrefslogtreecommitdiffstats
path: root/query
diff options
context:
space:
mode:
authorMichael Muré <batolettre@gmail.com>2021-02-27 20:39:27 +0100
committerGitHub <noreply@github.com>2021-02-27 20:39:27 +0100
commit22cc4cc043370bac945f92ecf343025ce7fdfe33 (patch)
treea0162d685c22d274dd4e5916d8cf46136c7dc0ea /query
parent10a259b6823e1234e5add1ee62935f259c39f803 (diff)
parentfab626a7a663a8fa6ef27848bb63e91af812ab8c (diff)
downloadgit-bug-22cc4cc043370bac945f92ecf343025ce7fdfe33.tar.gz
Merge pull request #568 from vmiklos/search-metadata
Add ability to search by arbitrary metadata
Diffstat (limited to 'query')
-rw-r--r--query/lexer.go108
-rw-r--r--query/lexer_test.go28
-rw-r--r--query/parser.go9
-rw-r--r--query/parser_test.go19
-rw-r--r--query/query.go7
5 files changed, 113 insertions, 58 deletions
diff --git a/query/lexer.go b/query/lexer.go
index 5ca700c7..77830a47 100644
--- a/query/lexer.go
+++ b/query/lexer.go
@@ -11,16 +11,20 @@ type tokenKind int
const (
_ tokenKind = iota
tokenKindKV
+ tokenKindKVV
tokenKindSearch
)
type token struct {
kind tokenKind
- // KV
+ // KV and KVV
qualifier string
value string
+ // KVV only
+ subQualifier string
+
// Search
term string
}
@@ -33,6 +37,15 @@ func newTokenKV(qualifier, value string) token {
}
}
+func newTokenKVV(qualifier, subQualifier, value string) token {
+ return token{
+ kind: tokenKindKVV,
+ qualifier: qualifier,
+ subQualifier: subQualifier,
+ value: value,
+ }
+}
+
func newTokenSearch(term string) token {
return token{
kind: tokenKindSearch,
@@ -43,44 +56,68 @@ func newTokenSearch(term string) token {
// tokenize parse and break a input into tokens ready to be
// interpreted later by a parser to get the semantic.
func tokenize(query string) ([]token, error) {
- fields, err := splitQuery(query)
+ fields, err := splitFunc(query, unicode.IsSpace)
if err != nil {
return nil, err
}
var tokens []token
for _, field := range fields {
- split := strings.Split(field, ":")
-
- // full text search
- if len(split) == 1 {
- tokens = append(tokens, newTokenSearch(removeQuote(field)))
- continue
+ chunks, err := splitFunc(field, func(r rune) bool { return r == ':' })
+ if err != nil {
+ return nil, err
}
- if len(split) != 2 {
- return nil, fmt.Errorf("can't tokenize \"%s\"", field)
+ if strings.HasPrefix(field, ":") || strings.HasSuffix(field, ":") {
+ return nil, fmt.Errorf("empty qualifier or value")
}
- if len(split[0]) == 0 {
- return nil, fmt.Errorf("can't tokenize \"%s\": empty qualifier", field)
- }
- if len(split[1]) == 0 {
- return nil, fmt.Errorf("empty value for qualifier \"%s\"", split[0])
+ // pre-process chunks
+ for i, chunk := range chunks {
+ if len(chunk) == 0 {
+ return nil, fmt.Errorf("empty qualifier or value")
+ }
+ chunks[i] = removeQuote(chunk)
}
- tokens = append(tokens, newTokenKV(split[0], removeQuote(split[1])))
+ switch len(chunks) {
+ case 1: // full text search
+ tokens = append(tokens, newTokenSearch(chunks[0]))
+
+ case 2: // KV
+ tokens = append(tokens, newTokenKV(chunks[0], chunks[1]))
+
+ case 3: // KVV
+ tokens = append(tokens, newTokenKVV(chunks[0], chunks[1], chunks[2]))
+
+ default:
+ return nil, fmt.Errorf("can't tokenize \"%s\": too many separators", field)
+ }
}
return tokens, nil
}
-// split the query into chunks by splitting on whitespaces but respecting
+func removeQuote(field string) string {
+ runes := []rune(field)
+ if len(runes) >= 2 {
+ r1 := runes[0]
+ r2 := runes[len(runes)-1]
+
+ if r1 == r2 && isQuote(r1) {
+ return string(runes[1 : len(runes)-1])
+ }
+ }
+ return field
+}
+
+// split the input into chunks by splitting according to separatorFunc but respecting
// quotes
-func splitQuery(query string) ([]string, error) {
+func splitFunc(input string, separatorFunc func(r rune) bool) ([]string, error) {
lastQuote := rune(0)
inQuote := false
- isToken := func(r rune) bool {
+ // return true if it's part of a chunk, or false if it's a rune that delimit one, as determined by the separatorFunc.
+ isChunk := func(r rune) bool {
switch {
case !inQuote && isQuote(r):
lastQuote = r
@@ -93,19 +130,19 @@ func splitQuery(query string) ([]string, error) {
case inQuote:
return true
default:
- return !unicode.IsSpace(r)
+ return !separatorFunc(r)
}
}
var result []string
- var token strings.Builder
- for _, r := range query {
- if isToken(r) {
- token.WriteRune(r)
+ var chunk strings.Builder
+ for _, r := range input {
+ if isChunk(r) {
+ chunk.WriteRune(r)
} else {
- if token.Len() > 0 {
- result = append(result, token.String())
- token.Reset()
+ if chunk.Len() > 0 {
+ result = append(result, chunk.String())
+ chunk.Reset()
}
}
}
@@ -114,8 +151,8 @@ func splitQuery(query string) ([]string, error) {
return nil, fmt.Errorf("unmatched quote")
}
- if token.Len() > 0 {
- result = append(result, token.String())
+ if chunk.Len() > 0 {
+ result = append(result, chunk.String())
}
return result, nil
@@ -124,16 +161,3 @@ func splitQuery(query string) ([]string, error) {
func isQuote(r rune) bool {
return r == '"' || r == '\''
}
-
-func removeQuote(field string) string {
- runes := []rune(field)
- if len(runes) >= 2 {
- r1 := runes[0]
- r2 := runes[len(runes)-1]
-
- if r1 == r2 && isQuote(r1) {
- return string(runes[1 : len(runes)-1])
- }
- }
- return field
-}
diff --git a/query/lexer_test.go b/query/lexer_test.go
index 59f17dec..6ef679d2 100644
--- a/query/lexer_test.go
+++ b/query/lexer_test.go
@@ -3,7 +3,7 @@ package query
import (
"testing"
- "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestTokenize(t *testing.T) {
@@ -37,6 +37,14 @@ func TestTokenize(t *testing.T) {
{`key:'value value`, nil},
{`key:value value'`, nil},
+ // sub-qualifier positive testing
+ {`key:subkey:"value:value"`, []token{newTokenKVV("key", "subkey", "value:value")}},
+
+ // sub-qualifier negative testing
+ {`key:subkey:value:value`, nil},
+ {`key:subkey:`, nil},
+ {`key:subkey:"value`, nil},
+
// full text search
{"search", []token{newTokenSearch("search")}},
{"search more terms", []token{
@@ -51,13 +59,15 @@ func TestTokenize(t *testing.T) {
}
for _, tc := range tests {
- tokens, err := tokenize(tc.input)
- if tc.tokens == nil {
- assert.Error(t, err)
- assert.Nil(t, tokens)
- } else {
- assert.NoError(t, err)
- assert.Equal(t, tc.tokens, tokens)
- }
+ t.Run(tc.input, func(t *testing.T) {
+ tokens, err := tokenize(tc.input)
+ if tc.tokens == nil {
+ require.Error(t, err)
+ require.Nil(t, tokens)
+ } else {
+ require.NoError(t, err)
+ require.Equal(t, tc.tokens, tokens)
+ }
+ })
}
}
diff --git a/query/parser.go b/query/parser.go
index 762a47e5..6fd5cd74 100644
--- a/query/parser.go
+++ b/query/parser.go
@@ -67,6 +67,15 @@ func Parse(query string) (*Query, error) {
default:
return nil, fmt.Errorf("unknown qualifier \"%s\"", t.qualifier)
}
+
+ case tokenKindKVV:
+ switch t.qualifier {
+ case "metadata":
+ q.Metadata = append(q.Metadata, StringPair{Key: t.subQualifier, Value: t.value})
+
+ default:
+ return nil, fmt.Errorf("unknown qualifier \"%s:%s\"", t.qualifier, t.subQualifier)
+ }
}
}
return q, nil
diff --git a/query/parser_test.go b/query/parser_test.go
index 87dd870a..cef01ffd 100644
--- a/query/parser_test.go
+++ b/query/parser_test.go
@@ -3,7 +3,7 @@ package query
import (
"testing"
- "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
"github.com/MichaelMure/git-bug/bug"
)
@@ -62,6 +62,11 @@ func TestParse(t *testing.T) {
}},
{"sort:unknown", nil},
+ // KVV
+ {`metadata:key:"https://www.example.com/"`, &Query{
+ Filters: Filters{Metadata: []StringPair{{"key", "https://www.example.com/"}}},
+ }},
+
// Search
{"search", &Query{
Search: []string{"search"},
@@ -90,17 +95,17 @@ func TestParse(t *testing.T) {
t.Run(tc.input, func(t *testing.T) {
query, err := Parse(tc.input)
if tc.output == nil {
- assert.Error(t, err)
- assert.Nil(t, query)
+ require.Error(t, err)
+ require.Nil(t, query)
} else {
- assert.NoError(t, err)
+ require.NoError(t, err)
if tc.output.OrderBy != 0 {
- assert.Equal(t, tc.output.OrderBy, query.OrderBy)
+ require.Equal(t, tc.output.OrderBy, query.OrderBy)
}
if tc.output.OrderDirection != 0 {
- assert.Equal(t, tc.output.OrderDirection, query.OrderDirection)
+ require.Equal(t, tc.output.OrderDirection, query.OrderDirection)
}
- assert.Equal(t, tc.output.Filters, query.Filters)
+ require.Equal(t, tc.output.Filters, query.Filters)
}
})
}
diff --git a/query/query.go b/query/query.go
index 816d6414..cce61a54 100644
--- a/query/query.go
+++ b/query/query.go
@@ -23,10 +23,17 @@ func NewQuery() *Query {
type Search []string
+// StringPair is a key/value pair of strings
+type StringPair struct {
+ Key string
+ Value string
+}
+
// Filters is a collection of Filter that implement a complex filter
type Filters struct {
Status []bug.Status
Author []string
+ Metadata []StringPair
Actor []string
Participant []string
Label []string