Skip to content

Commit d8694ec

Browse files
author
Zach Vonler
authored
Added search using qualifier[:=]value syntax (#2373)
1 parent ad5dacc commit d8694ec

File tree

6 files changed

+461
-16
lines changed

6 files changed

+461
-16
lines changed

Diff for: commands/lib/search.go

+3-11
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import (
2323
"github.com/arduino/arduino-cli/arduino"
2424
"github.com/arduino/arduino-cli/arduino/libraries/librariesindex"
2525
"github.com/arduino/arduino-cli/arduino/libraries/librariesmanager"
26-
"github.com/arduino/arduino-cli/arduino/utils"
2726
"github.com/arduino/arduino-cli/commands/internal/instances"
2827
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
2928
semver "go.bug.st/relaxed-semver"
@@ -44,18 +43,11 @@ func searchLibrary(req *rpc.LibrarySearchRequest, lm *librariesmanager.Libraries
4443
if query == "" {
4544
query = req.GetQuery()
4645
}
47-
queryTerms := utils.SearchTermsFromQueryString(query)
4846

49-
for _, lib := range lm.Index.Libraries {
50-
toTest := lib.Name + " " +
51-
lib.Latest.Paragraph + " " +
52-
lib.Latest.Sentence + " " +
53-
lib.Latest.Author + " "
54-
for _, include := range lib.Latest.ProvidesIncludes {
55-
toTest += include + " "
56-
}
47+
matcher := MatcherFromQueryString(query)
5748

58-
if utils.Match(toTest, queryTerms) {
49+
for _, lib := range lm.Index.Libraries {
50+
if matcher(lib) {
5951
res = append(res, indexLibraryToRPCSearchLibrary(lib, req.GetOmitReleasesDetails()))
6052
}
6153
}

Diff for: commands/lib/search_matcher.go

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2023 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to [email protected].
15+
16+
package lib
17+
18+
import (
19+
"strings"
20+
21+
"github.com/arduino/arduino-cli/arduino/libraries/librariesindex"
22+
"github.com/arduino/arduino-cli/arduino/utils"
23+
)
24+
25+
// matcherTokensFromQueryString parses the query string into tokens of interest
26+
// for the qualifier-value pattern matching.
27+
func matcherTokensFromQueryString(query string) []string {
28+
escaped := false
29+
quoted := false
30+
tokens := []string{}
31+
sb := &strings.Builder{}
32+
33+
for _, r := range query {
34+
// Short circuit the loop on backslash so that all other paths can clear
35+
// the escaped flag.
36+
if !escaped && r == '\\' {
37+
escaped = true
38+
continue
39+
}
40+
41+
if r == '"' {
42+
if !escaped {
43+
quoted = !quoted
44+
} else {
45+
sb.WriteRune(r)
46+
}
47+
} else if !quoted && r == ' ' {
48+
tokens = append(tokens, strings.ToLower(sb.String()))
49+
sb.Reset()
50+
} else {
51+
sb.WriteRune(r)
52+
}
53+
escaped = false
54+
}
55+
if sb.Len() > 0 {
56+
tokens = append(tokens, strings.ToLower(sb.String()))
57+
}
58+
59+
return tokens
60+
}
61+
62+
// defaulLibraryMatchExtractor returns a string describing the library that
63+
// is used for the simple search.
64+
func defaultLibraryMatchExtractor(lib *librariesindex.Library) string {
65+
res := lib.Name + " " +
66+
lib.Latest.Paragraph + " " +
67+
lib.Latest.Sentence + " " +
68+
lib.Latest.Author + " "
69+
for _, include := range lib.Latest.ProvidesIncludes {
70+
res += include + " "
71+
}
72+
return res
73+
}
74+
75+
var qualifiers map[string]func(lib *librariesindex.Library) string = map[string]func(lib *librariesindex.Library) string{
76+
"name": func(lib *librariesindex.Library) string { return lib.Name },
77+
"architectures": func(lib *librariesindex.Library) string { return strings.Join(lib.Latest.Architectures, " ") },
78+
"author": func(lib *librariesindex.Library) string { return lib.Latest.Author },
79+
"category": func(lib *librariesindex.Library) string { return lib.Latest.Category },
80+
"dependencies": func(lib *librariesindex.Library) string {
81+
names := make([]string, len(lib.Latest.Dependencies))
82+
for i, dep := range lib.Latest.Dependencies {
83+
names[i] = dep.GetName()
84+
}
85+
return strings.Join(names, " ")
86+
},
87+
"license": func(lib *librariesindex.Library) string { return lib.Latest.License },
88+
"maintainer": func(lib *librariesindex.Library) string { return lib.Latest.Maintainer },
89+
"paragraph": func(lib *librariesindex.Library) string { return lib.Latest.Paragraph },
90+
"provides": func(lib *librariesindex.Library) string { return strings.Join(lib.Latest.ProvidesIncludes, " ") },
91+
"sentence": func(lib *librariesindex.Library) string { return lib.Latest.Sentence },
92+
"types": func(lib *librariesindex.Library) string { return strings.Join(lib.Latest.Types, " ") },
93+
"version": func(lib *librariesindex.Library) string { return lib.Latest.Version.String() },
94+
"website": func(lib *librariesindex.Library) string { return lib.Latest.Website },
95+
}
96+
97+
// MatcherFromQueryString returns a closure that takes a library as a
98+
// parameter and returns true if the library matches the query.
99+
func MatcherFromQueryString(query string) func(*librariesindex.Library) bool {
100+
// A qv-query is one using <qualifier>[:=]<value> syntax.
101+
qvQuery := strings.Contains(query, ":") || strings.Contains(query, "=")
102+
103+
if !qvQuery {
104+
queryTerms := utils.SearchTermsFromQueryString(query)
105+
return func(lib *librariesindex.Library) bool {
106+
return utils.Match(defaultLibraryMatchExtractor(lib), queryTerms)
107+
}
108+
}
109+
110+
queryTerms := matcherTokensFromQueryString(query)
111+
112+
return func(lib *librariesindex.Library) bool {
113+
matched := true
114+
for _, term := range queryTerms {
115+
if sepIdx := strings.IndexAny(term, ":="); sepIdx != -1 {
116+
qualifier, separator, target := term[:sepIdx], term[sepIdx], term[sepIdx+1:]
117+
if extractor, ok := qualifiers[qualifier]; ok {
118+
switch separator {
119+
case ':':
120+
matched = (matched && utils.Match(extractor(lib), []string{target}))
121+
continue
122+
case '=':
123+
matched = (matched && strings.ToLower(extractor(lib)) == target)
124+
continue
125+
}
126+
}
127+
}
128+
// We perform the usual match in the following cases:
129+
// 1. Unknown qualifier names revert to basic search terms.
130+
// 2. Terms that do not use qv-syntax.
131+
matched = (matched && utils.Match(defaultLibraryMatchExtractor(lib), []string{term}))
132+
}
133+
return matched
134+
}
135+
}

Diff for: commands/lib/search_test.go

+110
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828

2929
var customIndexPath = paths.New("testdata", "test1")
3030
var fullIndexPath = paths.New("testdata", "full")
31+
var qualifiedSearchIndexPath = paths.New("testdata", "qualified_search")
3132

3233
func TestSearchLibrary(t *testing.T) {
3334
lm := librariesmanager.NewLibraryManager(customIndexPath, nil)
@@ -94,3 +95,112 @@ func TestSearchLibraryFields(t *testing.T) {
9495
require.Len(t, res, 19)
9596
require.Equal(t, "FlashStorage", res[0])
9697
}
98+
99+
func TestSearchLibraryWithQualifiers(t *testing.T) {
100+
lm := librariesmanager.NewLibraryManager(qualifiedSearchIndexPath, nil)
101+
lm.LoadIndex()
102+
103+
query := func(q string) []string {
104+
libs := []string{}
105+
for _, lib := range searchLibrary(&rpc.LibrarySearchRequest{SearchArgs: q}, lm).Libraries {
106+
libs = append(libs, lib.Name)
107+
}
108+
return libs
109+
}
110+
111+
res := query("mesh")
112+
require.Len(t, res, 4)
113+
114+
res = query("name:Mesh")
115+
require.Len(t, res, 3)
116+
117+
res = query("name=Mesh")
118+
require.Len(t, res, 0)
119+
120+
// Space not in double-quoted string
121+
res = query("name=Painless Mesh")
122+
require.Len(t, res, 0)
123+
124+
// Embedded space in double-quoted string
125+
res = query("name=\"Painless Mesh\"")
126+
require.Len(t, res, 1)
127+
require.Equal(t, "Painless Mesh", res[0])
128+
129+
// No closing double-quote - still tokenizes with embedded space
130+
res = query("name:\"Painless Mesh")
131+
require.Len(t, res, 1)
132+
133+
// Malformed double-quoted string with escaped first double-quote
134+
res = query("name:\\\"Painless Mesh\"")
135+
require.Len(t, res, 0)
136+
137+
res = query("name:mesh author:TMRh20")
138+
require.Len(t, res, 1)
139+
require.Equal(t, "RF24Mesh", res[0])
140+
141+
res = query("mesh dependencies:ArduinoJson")
142+
require.Len(t, res, 1)
143+
require.Equal(t, "Painless Mesh", res[0])
144+
145+
res = query("architectures:esp author=\"Suraj I.\"")
146+
require.Len(t, res, 1)
147+
require.Equal(t, "esp8266-framework", res[0])
148+
149+
res = query("mesh esp")
150+
require.Len(t, res, 2)
151+
152+
res = query("mesh esp paragraph:wifi")
153+
require.Len(t, res, 1)
154+
require.Equal(t, "esp8266-framework", res[0])
155+
156+
// Unknown qualifier should revert to original matching
157+
res = query("std::array")
158+
require.Len(t, res, 1)
159+
require.Equal(t, "Array", res[0])
160+
161+
res = query("data storage")
162+
require.Len(t, res, 1)
163+
require.Equal(t, "Pushdata_ESP8266_SSL", res[0])
164+
165+
res = query("category:\"data storage\"")
166+
require.Len(t, res, 1)
167+
require.Equal(t, "Array", res[0])
168+
169+
res = query("maintainer:@")
170+
require.Len(t, res, 4)
171+
172+
res = query("sentence:\"A library for NRF24L01(+) devices mesh.\"")
173+
require.Len(t, res, 1)
174+
require.Equal(t, "RF24Mesh", res[0])
175+
176+
res = query("types=contributed")
177+
require.Len(t, res, 7)
178+
179+
res = query("version:1.0")
180+
require.Len(t, res, 3)
181+
182+
res = query("version=1.2.1")
183+
require.Len(t, res, 1)
184+
require.Equal(t, "Array", res[0])
185+
186+
// Non-SSL URLs
187+
res = query("website:http://")
188+
require.Len(t, res, 1)
189+
require.Equal(t, "RF24Mesh", res[0])
190+
191+
// Literal double-quote
192+
res = query("sentence:\\\"")
193+
require.Len(t, res, 1)
194+
require.Equal(t, "RTCtime", res[0])
195+
196+
res = query("license=MIT")
197+
require.Len(t, res, 2)
198+
199+
// Empty string
200+
res = query("license=\"\"")
201+
require.Len(t, res, 5)
202+
203+
res = query("provides:painlessmesh.h")
204+
require.Len(t, res, 1)
205+
require.Equal(t, "Painless Mesh", res[0])
206+
}

0 commit comments

Comments
 (0)