Files
descrybe/apps/api/internal/processing/fill.go
T

149 lines
3.9 KiB
Go
Raw Normal View History

package processing
import (
"regexp"
"strings"
)
var (
brandPrefixRe = regexp.MustCompile(`(?i)^([A-Za-z][A-Za-z0-9&.\-]{1,40})\b`)
dimTripleRe = regexp.MustCompile(`(?i)(\d+(?:[.,]\d+)?)\s*[x×]\s*(\d+(?:[.,]\d+)?)\s*[x×]\s*(\d+(?:[.,]\d+)?)`)
dimPairRe = regexp.MustCompile(`(?i)(\d+(?:[.,]\d+)?)\s*[x×]\s*(\d+(?:[.,]\d+)?)`)
weightRe = regexp.MustCompile(`(?i)(\d+(?:[.,]\d+)?)\s*(kg|g|lb|oz)\b`)
)
// FillMissingFields derives sensible standard fields from name/attrs when absent.
func FillMissingFields(mapped map[string]any, attrs map[string]any) map[string]any {
out := make(map[string]any, len(mapped)+8)
for k, v := range mapped {
out[k] = v
}
name := stringFromAny(out["name"])
if name == "" {
name = stringFromAny(out["title"])
}
if stringFromAny(out["brand"]) == "" {
if b := stringFromAny(attrs["brand"]); b != "" {
out["brand"] = b
} else if b := inferBrand(name); b != "" {
out["brand"] = b
}
}
if stringFromAny(out["gtin"]) == "" {
if g := stringFromAny(out["ean"]); g != "" {
out["gtin"] = g
}
}
blob := name + " " + stringFromAny(out["description"])
for _, m := range []map[string]any{attrs, out} {
for _, k := range []string{"dimensions", "size", "dimension"} {
blob += " " + stringFromAny(m[k])
}
}
if stringFromAny(out["width"]) == "" || stringFromAny(out["height"]) == "" || stringFromAny(out["depth"]) == "" {
if w, h, d, ok := parseDimensions(blob); ok {
if stringFromAny(out["width"]) == "" {
out["width"] = w
}
if stringFromAny(out["height"]) == "" {
out["height"] = h
}
if stringFromAny(out["depth"]) == "" && d != "" {
out["depth"] = d
}
}
}
if stringFromAny(out["weight"]) == "" {
if w := stringFromAny(attrs["weight"]); w != "" {
out["weight"] = w
} else if w, ok := parseWeight(blob); ok {
out["weight"] = w
}
}
2026-08-16 16:57:36 +02:00
if c := categoryUniqueIDFromAny(out["category"]); c != "" {
out["category"] = c
} else if c := categoryUniqueIDFromAny(attrs["category"]); c != "" {
out["category"] = c
} else if c := categoryUniqueIDFromAny(out["category_unique_id"]); c != "" {
out["category"] = c
}
if stringFromAny(out["stock_status"]) == "" {
if s := stringFromAny(out["stock"]); s != "" {
out["stock_status"] = normalizeStockStatusLabel(s)
}
} else {
out["stock_status"] = normalizeStockStatusLabel(stringFromAny(out["stock_status"]))
}
return out
}
func inferBrand(name string) string {
name = strings.TrimSpace(name)
if name == "" {
return ""
}
m := brandPrefixRe.FindStringSubmatch(name)
if len(m) < 2 {
return ""
}
b := strings.TrimSpace(m[1])
// Skip generic leading words
switch strings.ToLower(b) {
case "the", "new", "set", "pack", "pair", "product", "item":
return ""
}
return SanitizeOutput(b)
}
func parseDimensions(blob string) (w, h, d string, ok bool) {
if m := dimTripleRe.FindStringSubmatch(blob); len(m) == 4 {
return normalizeNum(m[1]), normalizeNum(m[2]), normalizeNum(m[3]), true
}
if m := dimPairRe.FindStringSubmatch(blob); len(m) == 3 {
return normalizeNum(m[1]), normalizeNum(m[2]), "", true
}
return "", "", "", false
}
func parseWeight(blob string) (string, bool) {
m := weightRe.FindStringSubmatch(blob)
if len(m) < 3 {
return "", false
}
return normalizeNum(m[1]) + " " + strings.ToLower(m[2]), true
}
func normalizeNum(s string) string {
return strings.ReplaceAll(strings.TrimSpace(s), ",", ".")
}
func normalizeStockStatusLabel(s string) string {
if mapped := MapStockStatus(s); mapped != "" {
return mapped
}
s = strings.ToLower(strings.TrimSpace(s))
switch {
case s == "" || s == "0" || strings.Contains(s, "out"):
return "out_of_stock"
case strings.Contains(s, "pre"):
return "preorder"
case strings.Contains(s, "back"):
return "backorder"
default:
return "in_stock"
}
}
func stringFromAny(v any) string {
2026-08-16 16:57:36 +02:00
// Flatten arrays / #text wrappers; callers that need HTML-stripped descriptions
// should use PlainDescriptionFromAny (normalize + V1 enforce do that).
return coerceScalarText(v)
}