Skip to content

Commit

Permalink
Moved MatcherTokensFromQueryString and supporting logic to their own …
Browse files Browse the repository at this point in the history
…file.
  • Loading branch information
zvonler committed Oct 20, 2023
1 parent 9284df8 commit b1e1fcb
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 116 deletions.
117 changes: 1 addition & 116 deletions commands/lib/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (
"github.com/arduino/arduino-cli/arduino"
"github.com/arduino/arduino-cli/arduino/libraries/librariesindex"
"github.com/arduino/arduino-cli/arduino/libraries/librariesmanager"
"github.com/arduino/arduino-cli/arduino/utils"
"github.com/arduino/arduino-cli/commands"
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
semver "go.bug.st/relaxed-semver"
Expand All @@ -38,128 +37,14 @@ func LibrarySearch(ctx context.Context, req *rpc.LibrarySearchRequest) (*rpc.Lib
return searchLibrary(req, lm), nil
}

// matcherTokensFromQueryString parses the query string into tokens of interest
// for the qualifier-value pattern matching.
func matcherTokensFromQueryString(query string) []string {
escaped := false
quoted := false
tokens := []string{}
sb := &strings.Builder{}

for _, r := range query {
// Short circuit the loop on backslash so that all other paths can clear
// the escaped flag.
if !escaped && r == '\\' {
escaped = true
continue
}

if r == '"' {
if !escaped {
quoted = !quoted
} else {
sb.WriteRune(r)
}
} else if !quoted && r == ' ' {
tokens = append(tokens, strings.ToLower(sb.String()))
sb.Reset()
} else {
sb.WriteRune(r)
}
escaped = false
}
if sb.Len() > 0 {
tokens = append(tokens, strings.ToLower(sb.String()))
}

return tokens
}

// defaulLibraryMatchExtractor returns a string describing the library that
// is used for the simple search.
func defaultLibraryMatchExtractor(lib *librariesindex.Library) string {
res := lib.Name + " " +
lib.Latest.Paragraph + " " +
lib.Latest.Sentence + " " +
lib.Latest.Author + " "
for _, include := range lib.Latest.ProvidesIncludes {
res += include + " "
}
return res
}

var qualifiers map[string]func(lib *librariesindex.Library) string = map[string]func(lib *librariesindex.Library) string{
"name": func(lib *librariesindex.Library) string { return lib.Name },
"architectures": func(lib *librariesindex.Library) string { return strings.Join(lib.Latest.Architectures, " ") },
"author": func(lib *librariesindex.Library) string { return lib.Latest.Author },
"category": func(lib *librariesindex.Library) string { return lib.Latest.Category },
"dependencies": func(lib *librariesindex.Library) string {
names := make([]string, len(lib.Latest.Dependencies))
for i, dep := range lib.Latest.Dependencies {
names[i] = dep.GetName()
}
return strings.Join(names, " ")
},
"maintainer": func(lib *librariesindex.Library) string { return lib.Latest.Maintainer },
"paragraph": func(lib *librariesindex.Library) string { return lib.Latest.Paragraph },
"sentence": func(lib *librariesindex.Library) string { return lib.Latest.Sentence },
"types": func(lib *librariesindex.Library) string { return strings.Join(lib.Latest.Types, " ") },
"version": func(lib *librariesindex.Library) string { return lib.Latest.Version.String() },
"website": func(lib *librariesindex.Library) string { return lib.Latest.Website },
}

// matcherFromQueryString returns a closure that takes a library as a
// parameter and returns true if the library matches the query.
func matcherFromQueryString(query string) func(*librariesindex.Library) bool {
// A qv-query is one using <qualifier>[:=]<value> syntax.
qvQuery := strings.Contains(query, ":") || strings.Contains(query, "=")

if !qvQuery {
queryTerms := utils.SearchTermsFromQueryString(query)
return func(lib *librariesindex.Library) bool {
return utils.Match(defaultLibraryMatchExtractor(lib), queryTerms)
}
}

queryTerms := matcherTokensFromQueryString(query)

return func(lib *librariesindex.Library) bool {
matched := true
for _, term := range queryTerms {

if sepIdx := strings.IndexAny(term, "=:"); sepIdx != -1 {
potentialKey := term[:sepIdx]
separator := term[sepIdx]

extractor, ok := qualifiers[potentialKey]
if ok {
target := term[sepIdx+1:]
if separator == ':' {
matched = (matched && utils.Match(extractor(lib), []string{target}))
} else { // "="
matched = (matched && strings.ToLower(extractor(lib)) == target)
}
} else {
// Unknown qualifier names revert to basic search terms.
matched = (matched && utils.Match(defaultLibraryMatchExtractor(lib), []string{term}))
}
} else {
// Terms that do not use qv-syntax are handled as usual.
matched = (matched && utils.Match(defaultLibraryMatchExtractor(lib), []string{term}))
}
}
return matched
}
}

func searchLibrary(req *rpc.LibrarySearchRequest, lm *librariesmanager.LibrariesManager) *rpc.LibrarySearchResponse {
res := []*rpc.SearchedLibrary{}
query := req.GetSearchArgs()
if query == "" {
query = req.GetQuery()
}

matcher := matcherFromQueryString(query)
matcher := MatcherFromQueryString(query)

for _, lib := range lm.Index.Libraries {
if matcher(lib) {
Expand Down
139 changes: 139 additions & 0 deletions commands/lib/search_matcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// This file is part of arduino-cli.
//
// Copyright 2023 ARDUINO SA (http://www.arduino.cc/)
//
// This software is released under the GNU General Public License version 3,
// which covers the main part of arduino-cli.
// The terms of this license can be found at:
// https://www.gnu.org/licenses/gpl-3.0.en.html
//
// You can be released from the requirements of the above licenses by purchasing
// a commercial license. Buying such a license is mandatory if you want to
// modify or otherwise use the software for commercial activities involving the
// Arduino software without disclosing the source code of your own applications.
// To purchase a commercial license, send an email to [email protected].

package lib

import (
"strings"

"github.com/arduino/arduino-cli/arduino/libraries/librariesindex"
"github.com/arduino/arduino-cli/arduino/utils"
)

// matcherTokensFromQueryString parses the query string into tokens of interest
// for the qualifier-value pattern matching.
func matcherTokensFromQueryString(query string) []string {
escaped := false
quoted := false
tokens := []string{}
sb := &strings.Builder{}

for _, r := range query {
// Short circuit the loop on backslash so that all other paths can clear
// the escaped flag.
if !escaped && r == '\\' {
escaped = true
continue
}

if r == '"' {
if !escaped {
quoted = !quoted
} else {
sb.WriteRune(r)
}
} else if !quoted && r == ' ' {
tokens = append(tokens, strings.ToLower(sb.String()))
sb.Reset()
} else {
sb.WriteRune(r)
}
escaped = false
}
if sb.Len() > 0 {
tokens = append(tokens, strings.ToLower(sb.String()))
}

return tokens
}

// defaulLibraryMatchExtractor returns a string describing the library that
// is used for the simple search.
func defaultLibraryMatchExtractor(lib *librariesindex.Library) string {
res := lib.Name + " " +
lib.Latest.Paragraph + " " +
lib.Latest.Sentence + " " +
lib.Latest.Author + " "
for _, include := range lib.Latest.ProvidesIncludes {
res += include + " "
}
return res
}

var qualifiers map[string]func(lib *librariesindex.Library) string = map[string]func(lib *librariesindex.Library) string{
"name": func(lib *librariesindex.Library) string { return lib.Name },
"architectures": func(lib *librariesindex.Library) string { return strings.Join(lib.Latest.Architectures, " ") },
"author": func(lib *librariesindex.Library) string { return lib.Latest.Author },
"category": func(lib *librariesindex.Library) string { return lib.Latest.Category },
"dependencies": func(lib *librariesindex.Library) string {
names := make([]string, len(lib.Latest.Dependencies))
for i, dep := range lib.Latest.Dependencies {
names[i] = dep.GetName()
}
return strings.Join(names, " ")
},
"maintainer": func(lib *librariesindex.Library) string { return lib.Latest.Maintainer },
"paragraph": func(lib *librariesindex.Library) string { return lib.Latest.Paragraph },
"sentence": func(lib *librariesindex.Library) string { return lib.Latest.Sentence },
"types": func(lib *librariesindex.Library) string { return strings.Join(lib.Latest.Types, " ") },
"version": func(lib *librariesindex.Library) string { return lib.Latest.Version.String() },
"website": func(lib *librariesindex.Library) string { return lib.Latest.Website },
}

// matcherFromQueryString returns a closure that takes a library as a
// parameter and returns true if the library matches the query.
func MatcherFromQueryString(query string) func(*librariesindex.Library) bool {
// A qv-query is one using <qualifier>[:=]<value> syntax.
qvQuery := strings.Contains(query, ":") || strings.Contains(query, "=")

if !qvQuery {
queryTerms := utils.SearchTermsFromQueryString(query)
return func(lib *librariesindex.Library) bool {
return utils.Match(defaultLibraryMatchExtractor(lib), queryTerms)
}
}

queryTerms := matcherTokensFromQueryString(query)

return func(lib *librariesindex.Library) bool {
matched := true
for _, term := range queryTerms {

if sepIdx := strings.IndexAny(term, "=:"); sepIdx != -1 {
potentialKey := term[:sepIdx]
separator := term[sepIdx]

extractor, ok := qualifiers[potentialKey]
if ok {
target := term[sepIdx+1:]
if separator == ':' {
matched = (matched && utils.Match(extractor(lib), []string{target}))
} else { // "="
matched = (matched && strings.ToLower(extractor(lib)) == target)
}
} else {
// Unknown qualifier names revert to basic search terms.
matched = (matched && utils.Match(defaultLibraryMatchExtractor(lib), []string{term}))
}
} else {
// Terms that do not use qv-syntax are handled as usual.
matched = (matched && utils.Match(defaultLibraryMatchExtractor(lib), []string{term}))
}
}
return matched
}
}


0 comments on commit b1e1fcb

Please sign in to comment.