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)
| 823 | // is supposed to add. The pre-existing ItemUpdate fields/tags path |
| 824 | // inherits the same tightening because it routes through this helper. |
| 825 | func flexJSONToString(raw json.RawMessage, expectedStart byte, errInvalid error) (*string, error) { |
| 826 | trimmed := bytes.TrimSpace(raw) |
| 827 | if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) { |
| 828 | return nil, nil |
| 829 | } |
| 830 | switch trimmed[0] { |
| 831 | case '"': |
| 832 | // JSON-encoded string envelope — unmarshal to the underlying Go |
| 833 | // string and then validate the inner content's shape matches |
| 834 | // expectedStart. Subsequent code that does |
| 835 | // json.Unmarshal([]byte(*s), ...) sees the inner JSON, not a |
| 836 | // re-quoted string. |
| 837 | var s string |
| 838 | if err := json.Unmarshal(trimmed, &s); err != nil { |
| 839 | return nil, errInvalid |
| 840 | } |
| 841 | // Empty inner string is the empty-string sentinel; store-layer |
| 842 | // coercion handles it (IDEA-1486). Don't reject here so callers |
| 843 | // retain the legacy "" → default normalization shape. |
| 844 | innerTrimmed := bytes.TrimSpace([]byte(s)) |
| 845 | if len(innerTrimmed) == 0 { |
| 846 | return &s, nil |
| 847 | } |
| 848 | if innerTrimmed[0] != expectedStart { |
| 849 | return nil, errInvalid |
| 850 | } |
| 851 | // Confirm the inner content actually parses as JSON of the |
| 852 | // expected shape. Catches strings that start with the right |
| 853 | // brace but are otherwise garbage (e.g. `"{not valid"`). |
| 854 | var inner any |
| 855 | if err := json.Unmarshal(innerTrimmed, &inner); err != nil { |
| 856 | return nil, errInvalid |
| 857 | } |
| 858 | return &s, nil |
| 859 | case expectedStart: |
| 860 | // Object or array — re-marshal to a canonical JSON string so |
| 861 | // the downstream string-typed pipeline can json.Unmarshal it |
| 862 | // back to a map/slice exactly as if the caller had stringified. |
| 863 | var v any |
| 864 | if err := json.Unmarshal(trimmed, &v); err != nil { |
| 865 | return nil, errInvalid |
| 866 | } |
| 867 | b, err := json.Marshal(v) |
| 868 | if err != nil { |
| 869 | return nil, errInvalid |
| 870 | } |
| 871 | s := string(b) |
| 872 | return &s, nil |
| 873 | default: |
| 874 | return nil, errInvalid |
| 875 | } |
| 876 | } |
| 877 | |
| 878 | type ItemListParams struct { |
| 879 | CollectionSlug string |
no outgoing calls
no test coverage detected