Files
descrybe/apps/api/internal/catalog/link_feed_specs.go
T

160 lines
3.6 KiB
Go
Raw Normal View History

package catalog
import (
"fmt"
"strings"
"github.com/descrybe/descrybe-v2/apps/api/internal/feeds"
)
// linkFeedSpecificationsIntoProduct expands mapped_data.specifications HTML/flat
// blobs into attribute_key→value maps and merges them into attributes when empty.
// Mutates item in place for GET product responses (does not persist).
func linkFeedSpecificationsIntoProduct(item map[string]any) {
if item == nil {
return
}
mapped, _ := item["mapped_data"].(map[string]any)
if mapped == nil {
return
}
linked := extractLinkedSpecs(mapped)
if len(linked) == 0 {
return
}
// Prefer structured object in response mapped_data for the Attributes/Feed UI.
if specs := mapped["specifications"]; isLooseSpecBlob(specs) {
mapped["specifications"] = linked
item["mapped_data"] = mapped
} else if specs := mapped["specs"]; isLooseSpecBlob(specs) {
mapped["specs"] = linked
item["mapped_data"] = mapped
}
attrs := asStringAnyMap(item["attributes"])
if len(attrs) == 0 {
item["attributes"] = linked
item["has_attributes"] = true
return
}
merged := make(map[string]any, len(attrs)+len(linked))
for k, v := range attrs {
merged[k] = v
}
for k, v := range linked {
if existing, ok := merged[k]; ok && strings.TrimSpace(stringifyAny(existing)) != "" {
continue
}
merged[k] = v
}
item["attributes"] = merged
item["has_attributes"] = true
}
func extractLinkedSpecs(mapped map[string]any) map[string]any {
out := map[string]any{}
put := func(label, value string) {
k := feeds.CanonicalAttributeKey(label)
if k == "" || strings.TrimSpace(value) == "" {
return
}
if _, exists := out[k]; exists {
return
}
out[k] = value
}
for _, key := range []string{"specifications", "specs"} {
v, ok := mapped[key]
if !ok || v == nil {
continue
}
switch t := v.(type) {
case string:
for _, p := range feeds.ParseSpecifications(t) {
put(p.Label, p.Value)
}
case map[string]any:
for k, val := range t {
if strings.EqualFold(k, "_raw") {
if s, ok := val.(string); ok {
for _, p := range feeds.ParseSpecifications(s) {
put(p.Label, p.Value)
}
}
continue
}
put(k, stringifyAny(val))
}
case map[string]string:
for k, val := range t {
if strings.EqualFold(k, "_raw") {
for _, p := range feeds.ParseSpecifications(val) {
put(p.Label, p.Value)
}
continue
}
put(k, val)
}
}
}
// Scalar mapped fields → standard attribute keys (net_height, eprel_id, …).
for _, src := range []string{
"warranty", "eprel_id", "eprel",
"netwidth", "net_width", "netheight", "net_height",
"netdepth", "net_depth", "netmass", "net_mass",
"productmodel", "product_model",
"visina", "sirina", "globina", "teza",
} {
if s := stringifyAny(mapped[src]); s != "" {
put(src, s)
}
}
if len(out) == 0 {
return nil
}
return out
}
func isLooseSpecBlob(v any) bool {
switch t := v.(type) {
case string:
return strings.TrimSpace(t) != ""
case map[string]any:
_, hasRaw := t["_raw"]
return hasRaw
case map[string]string:
_, hasRaw := t["_raw"]
return hasRaw
default:
return false
}
}
func asStringAnyMap(v any) map[string]any {
switch t := v.(type) {
case map[string]any:
return t
case map[string]string:
out := make(map[string]any, len(t))
for k, val := range t {
out[k] = val
}
return out
default:
return nil
}
}
func stringifyAny(v any) string {
if v == nil {
return ""
}
switch t := v.(type) {
case string:
return strings.TrimSpace(t)
case float64, float32, int, int64, bool:
return strings.TrimSpace(fmt.Sprint(t))
default:
return strings.TrimSpace(fmt.Sprint(t))
}
}