From 17a803836ad2182e80a9aa558061fb19c3f7aff0 Mon Sep 17 00:00:00 2001 From: Repinoid Date: Tue, 22 Sep 2026 21:10:13 +0300 Subject: [PATCH] =?UTF-8?q?refactor(core):=20=D0=B2=D1=8B=D0=BD=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=B8=20JSON-=D1=8D=D0=BA=D0=B2=D0=B8=D0=B2=D0=B0?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=82=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=B2?= =?UTF-8?q?=20core/jsonutil=20(+=D1=80=D0=B5=D1=8D=D0=BA=D1=81=D0=BF=D0=BE?= =?UTF-8?q?=D1=80=D1=82=20=D0=B2=20resources=5Fcore)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- provider/internal/core/jsonutil/jsonutil.go | 126 ++++++++++++++++++ .../internal/resources_core/json_normalize.go | 121 +---------------- 2 files changed, 132 insertions(+), 115 deletions(-) create mode 100644 provider/internal/core/jsonutil/jsonutil.go diff --git a/provider/internal/core/jsonutil/jsonutil.go b/provider/internal/core/jsonutil/jsonutil.go new file mode 100644 index 0000000..ede1782 --- /dev/null +++ b/provider/internal/core/jsonutil/jsonutil.go @@ -0,0 +1,126 @@ +// Package jsonutil — нейтральный пакет каноничного JSON-сравнения. +// +// Вынесен из resources_core, чтобы его мог импортировать core (иначе был бы +// цикл импорта: core → resources_core → core). Сравнивает JSON-строки, +// игнорируя пробелы и порядок ключей объектов (порядок элементов массивов сохраняется). +package jsonutil + +import ( + "bytes" + "encoding/json" + "sort" + "strconv" + "strings" +) + +// JSONStringsEquivalent compares JSON strings ignoring whitespace and map key order. +func JSONStringsEquivalent(a, b string) bool { + na, oka := normalizeJSONIfPossible(a) + if !oka { + return false + } + nb, okb := normalizeJSONIfPossible(b) + if !okb { + return false + } + return na == nb +} + +func normalizeJSONIfPossible(value string) (string, bool) { + trimmed := strings.TrimSpace(value) + if trimmed == "" { + return "", false + } + var v interface{} + if err := json.Unmarshal([]byte(trimmed), &v); err != nil { + return "", false + } + return encodeCanonicalJSON(normalizeJSONScalarsToStrings(v)), true +} + +func encodeCanonicalJSON(v interface{}) string { + var buf bytes.Buffer + writeCanonicalJSON(&buf, v) + return buf.String() +} + +func writeCanonicalJSON(buf *bytes.Buffer, v interface{}) { + switch val := v.(type) { + case map[string]interface{}: + keys := make([]string, 0, len(val)) + for k := range val { + keys = append(keys, k) + } + sort.Strings(keys) + buf.WriteByte('{') + for i, k := range keys { + if i > 0 { + buf.WriteByte(',') + } + buf.WriteString(strconv.Quote(k)) + buf.WriteByte(':') + writeCanonicalJSON(buf, val[k]) + } + buf.WriteByte('}') + case []interface{}: + buf.WriteByte('[') + for i, item := range val { + if i > 0 { + buf.WriteByte(',') + } + writeCanonicalJSON(buf, item) + } + buf.WriteByte(']') + case string: + buf.WriteString(strconv.Quote(val)) + case float64: + buf.WriteString(strconv.FormatFloat(val, 'g', -1, 64)) + case bool: + if val { + buf.WriteString("true") + } else { + buf.WriteString("false") + } + case nil: + buf.WriteString("null") + default: + if encoded, err := json.Marshal(val); err == nil { + buf.Write(encoded) + } else { + buf.WriteString("null") + } + } +} + +func normalizeJSONScalarsToStrings(v interface{}) interface{} { + switch val := v.(type) { + case map[string]interface{}: + out := make(map[string]interface{}, len(val)) + for k, child := range val { + out[k] = normalizeJSONScalarsToStrings(child) + } + return out + case []interface{}: + out := make([]interface{}, len(val)) + for i, child := range val { + out[i] = normalizeJSONScalarsToStrings(child) + } + return out + case string: + return val + case float64: + return strconv.FormatFloat(val, 'g', -1, 64) + case bool: + if val { + return "true" + } + return "false" + case nil: + return "null" + default: + if encoded, err := json.Marshal(val); err == nil { + return strings.TrimSpace(string(encoded)) + } + return "null" + } +} diff --git a/provider/internal/resources_core/json_normalize.go b/provider/internal/resources_core/json_normalize.go index 4dc7c23..5b15cbe 100644 --- a/provider/internal/resources_core/json_normalize.go +++ b/provider/internal/resources_core/json_normalize.go @@ -1,121 +1,12 @@ package resources_core -import ( - "bytes" - "encoding/json" - "sort" - "strconv" - "strings" -) +import "terraform-provider-nubes/internal/core/jsonutil" // JSONStringsEquivalent compares JSON strings ignoring whitespace and map key order. +// +// Реэкспорт каноничной реализации из core/jsonutil: функции вынесены в нейтральный +// пакет, чтобы их мог импортировать и core (без цикла core → resources_core → core). +// Оставляем обёртку, чтобы не трогать существующие вызовы в resources_core. func JSONStringsEquivalent(a, b string) bool { - na, oka := normalizeJSONIfPossible(a) - if !oka { - return false - } - nb, okb := normalizeJSONIfPossible(b) - if !okb { - return false - } - return na == nb -} - -func normalizeJSONIfPossible(value string) (string, bool) { - trimmed := strings.TrimSpace(value) - if trimmed == "" { - return "", false - } - var v interface{} - if err := json.Unmarshal([]byte(trimmed), &v); err != nil { - return "", false - } - return encodeCanonicalJSON(normalizeJSONScalarsToStrings(v)), true -} - -func encodeCanonicalJSON(v interface{}) string { - var buf bytes.Buffer - writeCanonicalJSON(&buf, v) - return buf.String() -} - -func writeCanonicalJSON(buf *bytes.Buffer, v interface{}) { - switch val := v.(type) { - case map[string]interface{}: - keys := make([]string, 0, len(val)) - for k := range val { - keys = append(keys, k) - } - sort.Strings(keys) - buf.WriteByte('{') - for i, k := range keys { - if i > 0 { - buf.WriteByte(',') - } - buf.WriteString(strconv.Quote(k)) - buf.WriteByte(':') - writeCanonicalJSON(buf, val[k]) - } - buf.WriteByte('}') - case []interface{}: - buf.WriteByte('[') - for i, item := range val { - if i > 0 { - buf.WriteByte(',') - } - writeCanonicalJSON(buf, item) - } - buf.WriteByte(']') - case string: - buf.WriteString(strconv.Quote(val)) - case float64: - buf.WriteString(strconv.FormatFloat(val, 'g', -1, 64)) - case bool: - if val { - buf.WriteString("true") - } else { - buf.WriteString("false") - } - case nil: - buf.WriteString("null") - default: - if encoded, err := json.Marshal(val); err == nil { - buf.Write(encoded) - } else { - buf.WriteString("null") - } - } -} - -func normalizeJSONScalarsToStrings(v interface{}) interface{} { - switch val := v.(type) { - case map[string]interface{}: - out := make(map[string]interface{}, len(val)) - for k, child := range val { - out[k] = normalizeJSONScalarsToStrings(child) - } - return out - case []interface{}: - out := make([]interface{}, len(val)) - for i, child := range val { - out[i] = normalizeJSONScalarsToStrings(child) - } - return out - case string: - return val - case float64: - return strconv.FormatFloat(val, 'g', -1, 64) - case bool: - if val { - return "true" - } - return "false" - case nil: - return "null" - default: - if encoded, err := json.Marshal(val); err == nil { - return strings.TrimSpace(string(encoded)) - } - return "null" - } + return jsonutil.JSONStringsEquivalent(a, b) }