blob: 1d314cf28acd8396390c7e4cfdb2dbb4fd5c5c92 [file] [log] [blame] [edit]
// Package codesearch wraps up the codesearch JSON API.
//
// Notes about the codesearch REST API:
//
// The query needs to be bracketed by search_request=[b|e] query parameters.
//
// For example:
//
// https://cs.chromium.org/codesearch/json/search_request:1
// ?search_request=b
// &query=file%3A.md+file%3A%5Esrc%2Fthird_party%2Fskia%2F+package%3A%5Echromium%24
// &max_num_results=20
// &results_offset=0
// &search_request=e
//
// The URL for a file.name of "src/third_party/skia/site/roles.md" is
//
// https://cs.chromium.org/chromium/src/third_party/skia/site/roles.md
//
// To jump to a specific line:
//
// https://cs.chromium.org/chromium/src/third_party/skia/site/roles.md?l=2
package codesearch
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/util"
)
const (
// origin is the site hosting codesearch.
origin = "https://cs.chromium.org"
// The location of the JSON codesearch API.
jsonQueryPath = "/codesearch/json/search_request"
// SkiaAllCode is the text to add to a search to restrict it to the Skia
// codebase.
SkiaAllCode = "file:^src/third_party/skia/ package:^chromium$"
// SkiaInfraBaseQuery is the text to add to a search to restrict it to the
// Skia Infra codebase.
SkiaInfraBaseQuery = "file:^skia/buildbot/ package:^chromium$"
// SkiaAllMarkdown is the text to add to a search to restrict it to Markdown
// files in either the buildbot or skia proper repo.
SkiaAllMarkdown = "lang:^markdown$ AND (file:^skia/buildbot/ OR file:^src/third_party/skia/) AND package:^chromium$ "
)
var (
defaultQueryParams = url.Values{
"search_request": []string{""},
}
)
// CodeSearch searches code.
type CodeSearch struct {
c *http.Client
origin string
}
// New creates a new CodeSearch instance.
func New(client *http.Client) *CodeSearch {
return &CodeSearch{
c: client,
origin: origin,
}
}
// Origin allows over-riding the default origin.
func (cs *CodeSearch) Origin(origin string) {
cs.origin = origin
}
// File is a file in a search result.
type File struct {
Name string `json:"name"`
PackageName string `json:"package_name"`
}
// TopFile is the best matching file in a search result.
type TopFile struct {
File File `json:"file"`
Size int `json:"size"`
}
// Text is the text of a snippet.
type Text struct {
Text string `json:"text"`
}
// Snippet is a matching snippet of code in a search result.
type Snippet struct {
Text Text `json:"text"`
FirstLineNumber int `json:"first_line_number"`
}
// SearchResult is a single result from a search.
type SearchResult struct {
TopFile TopFile `json:"top_file"`
NumDuplicates int `json:"num_duplicates"`
NumMatches int `json:"num_matches"`
Language string `json:"language"`
BestMatchingLineNumber int `json:"best_matching_line_number"`
Snippet []Snippet `json:"snippet"`
}
// SearchResponse is the response from CodeSearch.Query.
type SearchResponse struct {
Status int `json:"status"`
StatusMessage string `json:"status_message"`
EstimatedTotalNumberOfResults int `json:"estimated_total_number_of_results"`
ResultsOffset int `json:"results_offset"`
NextPageToken string `json:"next_page_token"`
SearchResult []SearchResult `json:"search_result"`
}
// CompoundSearchResponse represents multiple search responses.
type CompoundSearchResponse struct {
Response []SearchResponse `json:"search_response"`
}
func (cs *CodeSearch) urlForQuery(q string, params url.Values) string {
if params == nil {
params = url.Values{}
}
params["query"] = []string{q}
encodedQuery := params.Encode()
return fmt.Sprintf("%s%s?search_request=b&%s&search_request=e", cs.origin, jsonQueryPath, encodedQuery)
}
// Query runs a search against the given search service.
//
// The query string should conform to any query you would use on
// https://cs.chromium.org/.
func (cs CodeSearch) Query(ctx context.Context, q string, params url.Values) (SearchResponse, error) {
resp, err := httputils.GetWithContext(ctx, cs.c, cs.urlForQuery(q, params))
if err != nil {
return SearchResponse{}, err
}
defer util.Close(resp.Body)
if resp.StatusCode != http.StatusOK {
return SearchResponse{}, skerr.Fmt("Bad status code: %d", resp.StatusCode)
}
var csr CompoundSearchResponse
if err := json.NewDecoder(resp.Body).Decode(&csr); err != nil {
return SearchResponse{}, err
}
if len(csr.Response) < 1 {
return SearchResponse{}, skerr.Fmt("No results body found")
}
return csr.Response[0], nil
}
// URL returns the link to display the TopFile in the SearchResult.
func (cs CodeSearch) URL(r SearchResult) string {
return fmt.Sprintf("%s/%s/%s", cs.origin, r.TopFile.File.PackageName, r.TopFile.File.Name)
}