github.com/felipejfc/helm@v2.1.2+incompatible/cmd/helm/search/search.go (about)

     1  /*
     2  Copyright 2016 The Kubernetes Authors All rights reserved.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  /*Package search provides client-side repository searching.
    18  
    19  This supports building an in-memory search index based on the contents of
    20  multiple repositories, and then using string matching or regular expressions
    21  to find matches.
    22  */
    23  package search
    24  
    25  import (
    26  	"errors"
    27  	"path"
    28  	"regexp"
    29  	"sort"
    30  	"strings"
    31  
    32  	"github.com/Masterminds/semver"
    33  
    34  	"k8s.io/helm/pkg/repo"
    35  )
    36  
    37  // Result is a search result.
    38  //
    39  // Score indicates how close it is to match. The higher the score, the longer
    40  // the distance.
    41  type Result struct {
    42  	Name  string
    43  	Score int
    44  	Chart *repo.ChartVersion
    45  }
    46  
    47  // Index is a searchable index of chart information.
    48  type Index struct {
    49  	lines  map[string]string
    50  	charts map[string]*repo.ChartVersion
    51  }
    52  
    53  const sep = "\v"
    54  
    55  // NewIndex creats a new Index.
    56  func NewIndex() *Index {
    57  	return &Index{lines: map[string]string{}, charts: map[string]*repo.ChartVersion{}}
    58  }
    59  
    60  // verSep is a separator for version fields in map keys.
    61  const verSep = "$$"
    62  
    63  // AddRepo adds a repository index to the search index.
    64  func (i *Index) AddRepo(rname string, ind *repo.IndexFile, all bool) {
    65  	for name, ref := range ind.Entries {
    66  		if len(ref) == 0 {
    67  			// Skip chart names that have zero releases.
    68  			continue
    69  		}
    70  		// By convention, an index file is supposed to have the newest at the
    71  		// 0 slot, so our best bet is to grab the 0 entry and build the index
    72  		// entry off of that.
    73  		// Note: Do not use filePath.Join since on Windows it will return \
    74  		//       which results in a repo name that cannot be understood.
    75  		fname := path.Join(rname, name)
    76  		if !all {
    77  			i.lines[fname] = indstr(rname, ref[0])
    78  			i.charts[fname] = ref[0]
    79  			continue
    80  		}
    81  
    82  		// If 'all' is set, then we go through all of the refs, and add them all
    83  		// to the index. This will generate a lot of near-duplicate entries.
    84  		for _, rr := range ref {
    85  			versionedName := fname + verSep + rr.Version
    86  			i.lines[versionedName] = indstr(rname, rr)
    87  			i.charts[versionedName] = rr
    88  		}
    89  	}
    90  }
    91  
    92  // All returns all charts in the index as if they were search results.
    93  //
    94  // Each will be given a score of 0.
    95  func (i *Index) All() []*Result {
    96  	res := make([]*Result, len(i.charts))
    97  	j := 0
    98  	for name, ch := range i.charts {
    99  		parts := strings.Split(name, verSep)
   100  		res[j] = &Result{
   101  			Name:  parts[0],
   102  			Chart: ch,
   103  		}
   104  		j++
   105  	}
   106  	return res
   107  }
   108  
   109  // Search searches an index for the given term.
   110  //
   111  // Threshold indicates the maximum score a term may have before being marked
   112  // irrelevant. (Low score means higher relevance. Golf, not bowling.)
   113  //
   114  // If regexp is true, the term is treated as a regular expression. Otherwise,
   115  // term is treated as a literal string.
   116  func (i *Index) Search(term string, threshold int, regexp bool) ([]*Result, error) {
   117  	if regexp {
   118  		return i.SearchRegexp(term, threshold)
   119  	}
   120  	return i.SearchLiteral(term, threshold), nil
   121  }
   122  
   123  // calcScore calculates a score for a match.
   124  func (i *Index) calcScore(index int, matchline string) int {
   125  
   126  	// This is currently tied to the fact that sep is a single char.
   127  	splits := []int{}
   128  	s := rune(sep[0])
   129  	for i, ch := range matchline {
   130  		if ch == s {
   131  			splits = append(splits, i)
   132  		}
   133  	}
   134  
   135  	for i, pos := range splits {
   136  		if index > pos {
   137  			continue
   138  		}
   139  		return i
   140  	}
   141  	return len(splits)
   142  }
   143  
   144  // SearchLiteral does a literal string search (no regexp).
   145  func (i *Index) SearchLiteral(term string, threshold int) []*Result {
   146  	term = strings.ToLower(term)
   147  	buf := []*Result{}
   148  	for k, v := range i.lines {
   149  		res := strings.Index(v, term)
   150  		if score := i.calcScore(res, v); res != -1 && score < threshold {
   151  			parts := strings.Split(k, verSep) // Remove version, if it is there.
   152  			buf = append(buf, &Result{Name: parts[0], Score: score, Chart: i.charts[k]})
   153  		}
   154  	}
   155  	return buf
   156  }
   157  
   158  // SearchRegexp searches using a regular expression.
   159  func (i *Index) SearchRegexp(re string, threshold int) ([]*Result, error) {
   160  	matcher, err := regexp.Compile(re)
   161  	if err != nil {
   162  		return []*Result{}, err
   163  	}
   164  	buf := []*Result{}
   165  	for k, v := range i.lines {
   166  		ind := matcher.FindStringIndex(v)
   167  		if len(ind) == 0 {
   168  			continue
   169  		}
   170  		if score := i.calcScore(ind[0], v); ind[0] >= 0 && score < threshold {
   171  			parts := strings.Split(k, verSep) // Remove version, if it is there.
   172  			buf = append(buf, &Result{Name: parts[0], Score: score, Chart: i.charts[k]})
   173  		}
   174  	}
   175  	return buf, nil
   176  }
   177  
   178  // Chart returns the ChartRef for a particular name.
   179  func (i *Index) Chart(name string) (*repo.ChartVersion, error) {
   180  	c, ok := i.charts[name]
   181  	if !ok {
   182  		return nil, errors.New("no such chart")
   183  	}
   184  	return c, nil
   185  }
   186  
   187  // SortScore does an in-place sort of the results.
   188  //
   189  // Lowest scores are highest on the list. Matching scores are subsorted alphabetically.
   190  func SortScore(r []*Result) {
   191  	sort.Sort(scoreSorter(r))
   192  }
   193  
   194  // scoreSorter sorts results by score, and subsorts by alpha Name.
   195  type scoreSorter []*Result
   196  
   197  // Len returns the length of this scoreSorter.
   198  func (s scoreSorter) Len() int { return len(s) }
   199  
   200  // Swap performs an in-place swap.
   201  func (s scoreSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
   202  
   203  // Less compares a to b, and returns true if a is less than b.
   204  func (s scoreSorter) Less(a, b int) bool {
   205  	first := s[a]
   206  	second := s[b]
   207  
   208  	if first.Score > second.Score {
   209  		return false
   210  	}
   211  	if first.Score < second.Score {
   212  		return true
   213  	}
   214  	if first.Name == second.Name {
   215  		v1, err := semver.NewVersion(first.Chart.Version)
   216  		if err != nil {
   217  			return true
   218  		}
   219  		v2, err := semver.NewVersion(second.Chart.Version)
   220  		if err != nil {
   221  			return true
   222  		}
   223  		return v1.GreaterThan(v2)
   224  	}
   225  	return first.Name < second.Name
   226  }
   227  
   228  func indstr(name string, ref *repo.ChartVersion) string {
   229  	i := ref.Name + sep + name + "/" + ref.Name + sep +
   230  		ref.Description + sep + strings.Join(ref.Keywords, " ")
   231  	return strings.ToLower(i)
   232  }