blob: d5b24ea31a890131280088315a1f3b41461dbd02 [file] [log] [blame]
// Copyright 2016 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rest
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"github.com/google/zoekt"
"github.com/google/zoekt/query"
"golang.org/x/net/context"
)
const jsonContentType = "application/json; charset=utf-8"
type httpError struct {
msg string
status int
}
func (e *httpError) Error() string { return fmt.Sprintf("%d: %s", e.status, e.msg) }
func Search(s zoekt.Searcher, w http.ResponseWriter, r *http.Request) {
if err := serveSearchAPIErr(s, w, r); err != nil {
if e, ok := err.(*httpError); ok {
http.Error(w, e.msg, e.status)
}
http.Error(w, err.Error(), http.StatusTeapot)
}
}
func serveSearchAPIErr(s zoekt.Searcher, w http.ResponseWriter, r *http.Request) error {
if r.Method != http.MethodPost {
return &httpError{"must use POST", http.StatusMethodNotAllowed}
}
if got := r.Header.Get("Content-Type"); got != jsonContentType {
return &httpError{"must use " + jsonContentType, http.StatusNotAcceptable}
}
content, err := ioutil.ReadAll(r.Body)
if err != nil {
return &httpError{err.Error(), http.StatusBadRequest}
}
var req SearchRequest
if err := json.Unmarshal(content, &req); err != nil {
return &httpError{err.Error(), http.StatusBadRequest}
}
rep, err := serveSearchAPIStructured(s, &req)
if err != nil {
return err
}
content, err = json.Marshal(rep)
if err != nil {
return &httpError{err.Error(), http.StatusInternalServerError}
}
w.Header().Set("Content-Type", jsonContentType)
if _, err := w.Write(content); err != nil {
return &httpError{err.Error(), http.StatusInternalServerError}
}
return nil
}
func serveSearchAPIStructured(searcher zoekt.Searcher, req *SearchRequest) (*SearchResponse, error) {
q, err := query.Parse(req.Query)
if err != nil {
msg := "parse error: " + err.Error()
return &SearchResponse{Error: &msg}, nil
}
var restrictions []query.Q
for _, r := range req.Restrict {
var branchQs []query.Q
for _, b := range r.Branches {
branchQs = append(branchQs, &query.Branch{b})
}
restrictions = append(restrictions,
query.NewAnd(&query.Repo{r.Repo}, query.NewOr(branchQs...)))
}
finalQ := query.NewAnd(q, query.NewOr(restrictions...))
var options zoekt.SearchOptions
options.SetDefaults()
ctx := context.Background()
result, err := searcher.Search(ctx, finalQ, &options)
if err != nil {
return nil, &httpError{err.Error(), http.StatusInternalServerError}
}
// TODO - make this tunable. Use a query param or a JSON struct?
num := 50
if len(result.Files) > num {
result.Files = result.Files[:num]
}
var resp SearchResponse
for _, f := range result.Files {
srf := SearchResponseFile{
Repo: f.Repository,
Branches: f.Branches,
FileName: f.FileName,
// TODO - set version
}
for _, m := range f.LineMatches {
srl := &SearchResponseLine{
LineNumber: m.LineNumber,
Line: string(m.Line),
}
// Convert to unicode indices.
charOffsets := make([]int, len(m.Line), len(m.Line)+1)
j := 0
for i := range srl.Line {
charOffsets[i] = j
j++
}
charOffsets = append(charOffsets, j)
for _, fr := range m.LineFragments {
srfr := SearchResponseMatch{
Start: charOffsets[fr.LineOffset],
End: charOffsets[fr.LineOffset+fr.MatchLength],
}
srl.Matches = append(srl.Matches, &srfr)
}
srf.Lines = append(srf.Lines, srl)
}
resp.Files = append(resp.Files, &srf)
}
return &resp, nil
}