kafka/internal/contracts/main_result.go
ashisgreat22 fcd9be16df refactor: remove SearXNG references and rename binary to kafka
- Rename cmd/searxng-go to cmd/kafka
- Remove all SearXNG references from source comments while keeping
  "SearXNG-compatible API" in user-facing docs
- Update binary paths in README, CLAUDE.md, and Dockerfile
- Update log message to "kafka starting"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 01:47:03 +01:00

193 lines
4 KiB
Go

package contracts
import (
"bytes"
"encoding/json"
)
// MainResult represents one element of the `results` array.
//
// The API returns many additional keys beyond what templates use. To keep the
// contract stable for proxying/merging, we preserve all unknown keys in
// `raw` and re-emit them via MarshalJSON.
type MainResult struct {
raw map[string]any
// Common fields used by templates (RSS uses: title, url, content, pubdate).
Template string `json:"template"`
Title string `json:"title"`
Content string `json:"content"`
URL *string `json:"url"`
Pubdate *string `json:"pubdate"`
Engine string `json:"engine"`
Score float64 `json:"score"`
Category string `json:"category"`
Priority string `json:"priority"`
Positions []int `json:"positions"`
Engines []string `json:"engines"`
// These fields exist in the MainResult base; keep them so downstream
// callers can generate richer output later.
OpenGroup bool `json:"open_group"`
CloseGroup bool `json:"close_group"`
// parsed_url is emitted as a tuple; we preserve it as-is.
ParsedURL any `json:"parsed_url"`
}
func (mr *MainResult) UnmarshalJSON(data []byte) error {
// Preserve the full object.
dec := json.NewDecoder(bytes.NewReader(data))
dec.UseNumber()
var m map[string]any
if err := dec.Decode(&m); err != nil {
return err
}
mr.raw = m
// Fill the typed/common fields (best-effort; don't fail if types differ).
mr.Template = stringOrEmpty(m["template"])
mr.Title = stringOrEmpty(m["title"])
mr.Content = stringOrEmpty(m["content"])
mr.Engine = stringOrEmpty(m["engine"])
mr.Category = stringOrEmpty(m["category"])
mr.Priority = stringOrEmpty(m["priority"])
if s, ok := stringOrNullable(m["url"]); ok {
mr.URL = &s
}
if s, ok := stringOrNullable(m["pubdate"]); ok {
mr.Pubdate = &s
}
mr.Score = floatOrZero(m["score"])
if v, ok := sliceOfStrings(m["engines"]); ok {
mr.Engines = v
}
if v, ok := sliceOfInts(m["positions"]); ok {
mr.Positions = v
}
if v, ok := boolOrFalse(m["open_group"]); ok {
mr.OpenGroup = v
}
if v, ok := boolOrFalse(m["close_group"]); ok {
mr.CloseGroup = v
}
mr.ParsedURL = m["parsed_url"]
return nil
}
func (mr MainResult) MarshalJSON() ([]byte, error) {
// If we came from upstream JSON, preserve all keys exactly.
if mr.raw != nil {
return json.Marshal(mr.raw)
}
// Otherwise, marshal the known fields.
m := map[string]any{
"template": mr.Template,
"title": mr.Title,
"content": mr.Content,
"url": mr.URL,
"pubdate": mr.Pubdate,
"engine": mr.Engine,
"score": mr.Score,
"category": mr.Category,
"priority": mr.Priority,
"positions": mr.Positions,
"engines": mr.Engines,
"open_group": mr.OpenGroup,
"close_group": mr.CloseGroup,
"parsed_url": mr.ParsedURL,
}
return json.Marshal(m)
}
func stringOrEmpty(v any) string {
s, _ := v.(string)
return s
}
func stringOrNullable(v any) (string, bool) {
if v == nil {
return "", false
}
s, ok := v.(string)
return s, ok
}
func floatOrZero(v any) float64 {
switch t := v.(type) {
case float64:
return t
case float32:
return float64(t)
case int:
return float64(t)
case int64:
return float64(t)
case json.Number:
f, _ := t.Float64()
return f
default:
return 0
}
}
func boolOrFalse(v any) (bool, bool) {
b, ok := v.(bool)
if !ok {
return false, false
}
return b, true
}
func sliceOfStrings(v any) ([]string, bool) {
raw, ok := v.([]any)
if !ok {
return nil, false
}
out := make([]string, 0, len(raw))
for _, item := range raw {
s, ok := item.(string)
if !ok {
return nil, false
}
out = append(out, s)
}
return out, true
}
func sliceOfInts(v any) ([]int, bool) {
raw, ok := v.([]any)
if !ok {
return nil, false
}
out := make([]int, 0, len(raw))
for _, item := range raw {
switch t := item.(type) {
case float64:
out = append(out, int(t))
case int:
out = append(out, t)
case json.Number:
i64, err := t.Int64()
if err != nil {
return nil, false
}
out = append(out, int(i64))
default:
return nil, false
}
}
return out, true
}