about summary refs log tree commit diff stats
path: root/internal
diff options
context:
space:
mode:
authorAlan Pearce2024-06-08 20:31:47 +0200
committerAlan Pearce2024-06-08 20:42:48 +0200
commitd40c0e188a7fe1b36887f59c4a9958faa81b3d44 (patch)
treeb6c168adcc3336dd0f6de4b1cbb7bf671f212f53 /internal
parentef6c98da84c2327e0a0003fb3b1b64a5d1e2d550 (diff)
downloadsearchix-d40c0e188a7fe1b36887f59c4a9958faa81b3d44.tar.lz
searchix-d40c0e188a7fe1b36887f59c4a9958faa81b3d44.tar.zst
searchix-d40c0e188a7fe1b36887f59c4a9958faa81b3d44.zip
feat: add detail pages for packages/options
Diffstat (limited to 'internal')
-rw-r--r--internal/config/importer-type.go11
-rw-r--r--internal/index/search.go77
-rw-r--r--internal/nix/importable.go9
-rw-r--r--internal/server/mux.go62
4 files changed, 132 insertions, 27 deletions
diff --git a/internal/config/importer-type.go b/internal/config/importer-type.go
index 8f64d58..9bd50b6 100644
--- a/internal/config/importer-type.go
+++ b/internal/config/importer-type.go
@@ -25,6 +25,17 @@ func (i ImporterType) String() string {
 	return fmt.Sprintf("Type(%d)", i)
 }
 
+func (i ImporterType) Singular() string {
+	switch i {
+	case Packages:
+		return "package"
+	case Options:
+		return "option"
+	}
+
+	return fmt.Sprintf("Type(%d)", i)
+}
+
 func ParseImporterType(name string) (ImporterType, error) {
 	switch strcase.KebabCase(name) {
 	case "packages":
diff --git a/internal/index/search.go b/internal/index/search.go
index 47f8ac1..3198afb 100644
--- a/internal/index/search.go
+++ b/internal/index/search.go
@@ -82,6 +82,42 @@ func setField(
 	return q
 }
 
+func (index *ReadIndex) search(
+	ctx context.Context,
+	request *bleve.SearchRequest,
+) (*Result, error) {
+	request.Fields = []string{"_data"}
+
+	bleveResult, err := index.index.SearchInContext(ctx, request)
+	select {
+	case <-ctx.Done():
+		return nil, ctx.Err()
+	default:
+		if err != nil {
+			return nil, errors.WithMessage(err, "failed to execute search query")
+		}
+
+		results := make([]DocumentMatch, min(ResultsPerPage, bleveResult.Total))
+		var buf bytes.Buffer
+		for i, result := range bleveResult.Hits {
+			_, err = buf.WriteString(result.Fields["_data"].(string))
+			if err != nil {
+				return nil, errors.WithMessage(err, "error fetching result data")
+			}
+			err = gob.NewDecoder(&buf).Decode(&results[i].Data)
+			if err != nil {
+				return nil, errors.WithMessagef(err, "error decoding gob data: %s", buf.String())
+			}
+			buf.Reset()
+		}
+
+		return &Result{
+			SearchResult: bleveResult,
+			Hits:         results,
+		}, nil
+	}
+}
+
 func (index *ReadIndex) Search(
 	ctx context.Context,
 	source *config.Source,
@@ -127,38 +163,27 @@ func (index *ReadIndex) Search(
 
 	search := bleve.NewSearchRequest(query)
 	search.Size = ResultsPerPage
-	search.Fields = []string{"_data"}
 
 	if from != 0 {
 		search.From = int(from)
 	}
 
-	bleveResult, err := index.index.SearchInContext(ctx, search)
-	select {
-	case <-ctx.Done():
-		return nil, ctx.Err()
-	default:
-		if err != nil {
-			return nil, errors.WithMessage(err, "failed to execute search query")
-		}
+	return index.search(ctx, search)
+}
 
-		results := make([]DocumentMatch, min(ResultsPerPage, bleveResult.Total))
-		var buf bytes.Buffer
-		for i, result := range bleveResult.Hits {
-			_, err = buf.WriteString(result.Fields["_data"].(string))
-			if err != nil {
-				return nil, errors.WithMessage(err, "error fetching result data")
-			}
-			err = gob.NewDecoder(&buf).Decode(&results[i].Data)
-			if err != nil {
-				return nil, errors.WithMessagef(err, "error decoding gob data: %s", buf.String())
-			}
-			buf.Reset()
-		}
+func (index *ReadIndex) GetDocument(
+	ctx context.Context,
+	source *config.Source,
+	id string,
+) (*nix.Importable, error) {
+	key := nix.MakeKey(source, id)
+	query := bleve.NewDocIDQuery([]string{key})
+	search := bleve.NewSearchRequest(query)
 
-		return &Result{
-			SearchResult: bleveResult,
-			Hits:         results,
-		}, nil
+	result, err := index.search(ctx, search)
+	if err != nil {
+		return nil, err
 	}
+
+	return &result.Hits[0].Data, err
 }
diff --git a/internal/nix/importable.go b/internal/nix/importable.go
index 37532cd..67ba96d 100644
--- a/internal/nix/importable.go
+++ b/internal/nix/importable.go
@@ -1,6 +1,9 @@
 package nix
 
-import "encoding/gob"
+import (
+	"encoding/gob"
+	"searchix/internal/config"
+)
 
 type Importable interface {
 	BleveType() string
@@ -12,6 +15,10 @@ func GetKey(i Importable) string {
 	return i.BleveType() + "/" + i.GetSource() + "/" + i.GetName()
 }
 
+func MakeKey(source *config.Source, id string) string {
+	return source.Importer.Singular() + "/" + source.Key + "/" + id
+}
+
 func init() {
 	gob.Register(Option{})
 	gob.Register(Package{})
diff --git a/internal/server/mux.go b/internal/server/mux.go
index 7bddbe5..79e24cd 100644
--- a/internal/server/mux.go
+++ b/internal/server/mux.go
@@ -18,6 +18,7 @@ import (
 	"searchix/frontend"
 	"searchix/internal/config"
 	search "searchix/internal/index"
+	"searchix/internal/nix"
 
 	"github.com/blevesearch/bleve/v2"
 	sentryhttp "github.com/getsentry/sentry-go/http"
@@ -55,6 +56,12 @@ type ResultData struct {
 	Next           string
 }
 
+type DocumentData struct {
+	TemplateData
+	Document *nix.Importable
+	Children *search.Result
+}
+
 var templates TemplateCollection
 
 func applyDevModeOverrides(cfg *config.Config) {
@@ -222,6 +229,61 @@ func NewMux(
 	mux.HandleFunc("/options/{source}/search", createSearchHandler(config.Options))
 	mux.HandleFunc("/packages/{source}/search", createSearchHandler(config.Packages))
 
+	createSourceIDHandler := func(importerType config.ImporterType) http.HandlerFunc {
+		return func(w http.ResponseWriter, r *http.Request) {
+			source := cfg.Importer.Sources[r.PathValue("source")]
+			if source == nil || source.Importer != importerType {
+				errorHandler(w, r, http.StatusText(http.StatusNotFound), http.StatusNotFound)
+
+				return
+			}
+			importerSingular := importerType.Singular()
+
+			ctx, cancel := context.WithTimeout(r.Context(), searchTimeout)
+			defer cancel()
+
+			doc, err := index.GetDocument(ctx, source, r.PathValue("id"))
+			if err != nil {
+				errorHandler(
+					w,
+					r,
+					http.StatusText(http.StatusInternalServerError),
+					http.StatusInternalServerError,
+				)
+
+				return
+			}
+
+			if doc == nil {
+				errorHandler(w, r, http.StatusText(http.StatusNotFound), http.StatusNotFound)
+
+				return
+			}
+
+			tdata := DocumentData{
+				TemplateData: TemplateData{
+					ExtraHeadHTML: cfg.Web.ExtraHeadHTML,
+					Source:        *source,
+					Sources:       cfg.Importer.Sources,
+					Assets:        frontend.Assets,
+				},
+				Document: doc,
+			}
+			if r.Header.Get("Fetch") == "true" {
+				w.Header().Add("Content-Type", "text/html; charset=utf-8")
+				err = templates[importerSingular].ExecuteTemplate(w, "main", tdata)
+			} else {
+				err = templates[importerSingular].Execute(w, tdata)
+			}
+			if err != nil {
+				slog.Error("template error", "template", importerSingular, "error", err)
+				errorHandler(w, r, err.Error(), http.StatusInternalServerError)
+			}
+		}
+	}
+	mux.HandleFunc("/options/{source}/{id}", createSourceIDHandler(config.Options))
+	mux.HandleFunc("/packages/{source}/{id}", createSourceIDHandler(config.Packages))
+
 	createOpenSearchXMLHandler := func(importerType config.ImporterType) func(http.ResponseWriter, *http.Request) {
 		return func(w http.ResponseWriter, r *http.Request) {
 			type openSearchData struct {