Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
160 lines
3.6 KiB
Go
160 lines
3.6 KiB
Go
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))
|
|
}
|
|
}
|