flexJSONToString accepts a raw JSON value and returns it as a canonical JSON-encoded string. Acceptable inbound shapes: - absent / empty / null → nil (caller leaves the field unchanged) - JSON-encoded string whose INNER content is either empty (the legacy empty-string sentinel handled downstream by
(raw json.RawMessage, expectedStart byte, errInvalid error)
| 854 | // is supposed to add. The pre-existing ItemUpdate fields/tags path |
| 855 | // inherits the same tightening because it routes through this helper. |
| 856 | func flexJSONToString(raw json.RawMessage, expectedStart byte, errInvalid error) (*string, error) { |
| 857 | trimmed := bytes.TrimSpace(raw) |
| 858 | if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) { |
| 859 | return nil, nil |
| 860 | } |
| 861 | switch trimmed[0] { |
| 862 | case '"': |
| 863 | // JSON-encoded string envelope — unmarshal to the underlying Go |
| 864 | // string and then validate the inner content's shape matches |
| 865 | // expectedStart. Subsequent code that does |
| 866 | // json.Unmarshal([]byte(*s), ...) sees the inner JSON, not a |
| 867 | // re-quoted string. |
| 868 | var s string |
| 869 | if err := json.Unmarshal(trimmed, &s); err != nil { |
| 870 | return nil, errInvalid |
| 871 | } |
| 872 | // Empty inner string is the empty-string sentinel; store-layer |
| 873 | // coercion handles it (IDEA-1486). Don't reject here so callers |
| 874 | // retain the legacy "" → default normalization shape. |
| 875 | innerTrimmed := bytes.TrimSpace([]byte(s)) |
| 876 | if len(innerTrimmed) == 0 { |
| 877 | return &s, nil |
| 878 | } |
| 879 | if innerTrimmed[0] != expectedStart { |
| 880 | return nil, errInvalid |
| 881 | } |
| 882 | // Confirm the inner content actually parses as JSON of the |
| 883 | // expected shape. Catches strings that start with the right |
| 884 | // brace but are otherwise garbage (e.g. `"{not valid"`). |
| 885 | var inner any |
| 886 | if err := json.Unmarshal(innerTrimmed, &inner); err != nil { |
| 887 | return nil, errInvalid |
| 888 | } |
| 889 | return &s, nil |
| 890 | case expectedStart: |
| 891 | // Object or array — re-marshal to a canonical JSON string so |
| 892 | // the downstream string-typed pipeline can json.Unmarshal it |
| 893 | // back to a map/slice exactly as if the caller had stringified. |
| 894 | var v any |
| 895 | if err := json.Unmarshal(trimmed, &v); err != nil { |
| 896 | return nil, errInvalid |
| 897 | } |
| 898 | b, err := json.Marshal(v) |
| 899 | if err != nil { |
| 900 | return nil, errInvalid |
| 901 | } |
| 902 | s := string(b) |
| 903 | return &s, nil |
| 904 | default: |
| 905 | return nil, errInvalid |
| 906 | } |
| 907 | } |
| 908 | |
| 909 | type ItemListParams struct { |
| 910 | CollectionSlug string |
no outgoing calls
no test coverage detected