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)
| 753 | // is supposed to add. The pre-existing ItemUpdate fields/tags path |
| 754 | // inherits the same tightening because it routes through this helper. |
| 755 | func flexJSONToString(raw json.RawMessage, expectedStart byte, errInvalid error) (*string, error) { |
| 756 | trimmed := bytes.TrimSpace(raw) |
| 757 | if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) { |
| 758 | return nil, nil |
| 759 | } |
| 760 | switch trimmed[0] { |
| 761 | case '"': |
| 762 | // JSON-encoded string envelope — unmarshal to the underlying Go |
| 763 | // string and then validate the inner content's shape matches |
| 764 | // expectedStart. Subsequent code that does |
| 765 | // json.Unmarshal([]byte(*s), ...) sees the inner JSON, not a |
| 766 | // re-quoted string. |
| 767 | var s string |
| 768 | if err := json.Unmarshal(trimmed, &s); err != nil { |
| 769 | return nil, errInvalid |
| 770 | } |
| 771 | // Empty inner string is the empty-string sentinel; store-layer |
| 772 | // coercion handles it (IDEA-1486). Don't reject here so callers |
| 773 | // retain the legacy "" → default normalization shape. |
| 774 | innerTrimmed := bytes.TrimSpace([]byte(s)) |
| 775 | if len(innerTrimmed) == 0 { |
| 776 | return &s, nil |
| 777 | } |
| 778 | if innerTrimmed[0] != expectedStart { |
| 779 | return nil, errInvalid |
| 780 | } |
| 781 | // Confirm the inner content actually parses as JSON of the |
| 782 | // expected shape. Catches strings that start with the right |
| 783 | // brace but are otherwise garbage (e.g. `"{not valid"`). |
| 784 | var inner any |
| 785 | if err := json.Unmarshal(innerTrimmed, &inner); err != nil { |
| 786 | return nil, errInvalid |
| 787 | } |
| 788 | return &s, nil |
| 789 | case expectedStart: |
| 790 | // Object or array — re-marshal to a canonical JSON string so |
| 791 | // the downstream string-typed pipeline can json.Unmarshal it |
| 792 | // back to a map/slice exactly as if the caller had stringified. |
| 793 | var v any |
| 794 | if err := json.Unmarshal(trimmed, &v); err != nil { |
| 795 | return nil, errInvalid |
| 796 | } |
| 797 | b, err := json.Marshal(v) |
| 798 | if err != nil { |
| 799 | return nil, errInvalid |
| 800 | } |
| 801 | s := string(b) |
| 802 | return &s, nil |
| 803 | default: |
| 804 | return nil, errInvalid |
| 805 | } |
| 806 | } |
| 807 | |
| 808 | type ItemListParams struct { |
| 809 | CollectionSlug string |
no outgoing calls
no test coverage detected