github.com/fufuok/utils@v1.0.10/xjson/sjson/sjson.go (about) 1 // Package sjson provides setting json values. 2 package sjson 3 4 import ( 5 jsongo "encoding/json" 6 "sort" 7 "strconv" 8 "unsafe" 9 10 "github.com/fufuok/utils/xjson/gjson" 11 ) 12 13 type errorType struct { 14 msg string 15 } 16 17 func (err *errorType) Error() string { 18 return err.msg 19 } 20 21 // Options represents additional options for the Set and Delete functions. 22 type Options struct { 23 // Optimistic is a hint that the value likely exists which 24 // allows for the sjson to perform a fast-track search and replace. 25 Optimistic bool 26 // ReplaceInPlace is a hint to replace the input json rather than 27 // allocate a new json byte slice. When this field is specified 28 // the input json will not longer be valid and it should not be used 29 // In the case when the destination slice doesn't have enough free 30 // bytes to replace the data in place, a new bytes slice will be 31 // created under the hood. 32 // The Optimistic flag must be set to true and the input must be a 33 // byte slice in order to use this field. 34 ReplaceInPlace bool 35 } 36 37 type pathResult struct { 38 part string // current key part 39 gpart string // gjson get part 40 path string // remaining path 41 force bool // force a string key 42 more bool // there is more path to parse 43 } 44 45 func isSimpleChar(ch byte) bool { 46 switch ch { 47 case '|', '#', '@', '*', '?': 48 return false 49 default: 50 return true 51 } 52 } 53 54 func parsePath(path string) (res pathResult, simple bool) { 55 var r pathResult 56 if len(path) > 0 && path[0] == ':' { 57 r.force = true 58 path = path[1:] 59 } 60 for i := 0; i < len(path); i++ { 61 if path[i] == '.' { 62 r.part = path[:i] 63 r.gpart = path[:i] 64 r.path = path[i+1:] 65 r.more = true 66 return r, true 67 } 68 if !isSimpleChar(path[i]) { 69 return r, false 70 } 71 if path[i] == '\\' { 72 // go into escape mode. this is a slower path that 73 // strips off the escape character from the part. 74 epart := []byte(path[:i]) 75 gpart := []byte(path[:i+1]) 76 i++ 77 if i < len(path) { 78 epart = append(epart, path[i]) 79 gpart = append(gpart, path[i]) 80 i++ 81 for ; i < len(path); i++ { 82 if path[i] == '\\' { 83 gpart = append(gpart, '\\') 84 i++ 85 if i < len(path) { 86 epart = append(epart, path[i]) 87 gpart = append(gpart, path[i]) 88 } 89 continue 90 } else if path[i] == '.' { 91 r.part = string(epart) 92 r.gpart = string(gpart) 93 r.path = path[i+1:] 94 r.more = true 95 return r, true 96 } else if !isSimpleChar(path[i]) { 97 return r, false 98 } 99 epart = append(epart, path[i]) 100 gpart = append(gpart, path[i]) 101 } 102 } 103 // append the last part 104 r.part = string(epart) 105 r.gpart = string(gpart) 106 return r, true 107 } 108 } 109 r.part = path 110 r.gpart = path 111 return r, true 112 } 113 114 func mustMarshalString(s string) bool { 115 for i := 0; i < len(s); i++ { 116 if s[i] < ' ' || s[i] > 0x7f || s[i] == '"' || s[i] == '\\' { 117 return true 118 } 119 } 120 return false 121 } 122 123 // appendStringify makes a json string and appends to buf. 124 func appendStringify(buf []byte, s string) []byte { 125 if mustMarshalString(s) { 126 b, _ := jsongo.Marshal(s) 127 return append(buf, b...) 128 } 129 buf = append(buf, '"') 130 buf = append(buf, s...) 131 buf = append(buf, '"') 132 return buf 133 } 134 135 // appendBuild builds a json block from a json path. 136 func appendBuild(buf []byte, array bool, paths []pathResult, raw string, 137 stringify bool, 138 ) []byte { 139 if !array { 140 buf = appendStringify(buf, paths[0].part) 141 buf = append(buf, ':') 142 } 143 if len(paths) > 1 { 144 n, numeric := atoui(paths[1]) 145 if numeric || (!paths[1].force && paths[1].part == "-1") { 146 buf = append(buf, '[') 147 buf = appendRepeat(buf, "null,", n) 148 buf = appendBuild(buf, true, paths[1:], raw, stringify) 149 buf = append(buf, ']') 150 } else { 151 buf = append(buf, '{') 152 buf = appendBuild(buf, false, paths[1:], raw, stringify) 153 buf = append(buf, '}') 154 } 155 } else { 156 if stringify { 157 buf = appendStringify(buf, raw) 158 } else { 159 buf = append(buf, raw...) 160 } 161 } 162 return buf 163 } 164 165 // atoui does a rip conversion of string -> unigned int. 166 func atoui(r pathResult) (n int, ok bool) { 167 if r.force { 168 return 0, false 169 } 170 for i := 0; i < len(r.part); i++ { 171 if r.part[i] < '0' || r.part[i] > '9' { 172 return 0, false 173 } 174 n = n*10 + int(r.part[i]-'0') 175 } 176 return n, true 177 } 178 179 // appendRepeat repeats string "n" times and appends to buf. 180 func appendRepeat(buf []byte, s string, n int) []byte { 181 for i := 0; i < n; i++ { 182 buf = append(buf, s...) 183 } 184 return buf 185 } 186 187 // trim does a rip trim 188 func trim(s string) string { 189 for len(s) > 0 { 190 if s[0] <= ' ' { 191 s = s[1:] 192 continue 193 } 194 break 195 } 196 for len(s) > 0 { 197 if s[len(s)-1] <= ' ' { 198 s = s[:len(s)-1] 199 continue 200 } 201 break 202 } 203 return s 204 } 205 206 // deleteTailItem deletes the previous key or comma. 207 func deleteTailItem(buf []byte) ([]byte, bool) { 208 loop: 209 for i := len(buf) - 1; i >= 0; i-- { 210 // look for either a ',',':','[' 211 switch buf[i] { 212 case '[': 213 return buf, true 214 case ',': 215 return buf[:i], false 216 case ':': 217 // delete tail string 218 i-- 219 for ; i >= 0; i-- { 220 if buf[i] == '"' { 221 i-- 222 for ; i >= 0; i-- { 223 if buf[i] == '"' { 224 i-- 225 if i >= 0 && buf[i] == '\\' { 226 i-- 227 continue 228 } 229 for ; i >= 0; i-- { 230 // look for either a ',','{' 231 switch buf[i] { 232 case '{': 233 return buf[:i+1], true 234 case ',': 235 return buf[:i], false 236 } 237 } 238 } 239 } 240 break 241 } 242 } 243 break loop 244 } 245 } 246 return buf, false 247 } 248 249 var errNoChange = &errorType{"no change"} 250 251 func appendRawPaths(buf []byte, jstr string, paths []pathResult, raw string, 252 stringify, del bool, 253 ) ([]byte, error) { 254 var err error 255 var res gjson.Result 256 var found bool 257 if del { 258 if paths[0].part == "-1" && !paths[0].force { 259 res = gjson.Get(jstr, "#") 260 if res.Int() > 0 { 261 res = gjson.Get(jstr, strconv.FormatInt(int64(res.Int()-1), 10)) 262 found = true 263 } 264 } 265 } 266 if !found { 267 res = gjson.Get(jstr, paths[0].gpart) 268 } 269 if res.Index > 0 { 270 if len(paths) > 1 { 271 buf = append(buf, jstr[:res.Index]...) 272 buf, err = appendRawPaths(buf, res.Raw, paths[1:], raw, 273 stringify, del) 274 if err != nil { 275 return nil, err 276 } 277 buf = append(buf, jstr[res.Index+len(res.Raw):]...) 278 return buf, nil 279 } 280 buf = append(buf, jstr[:res.Index]...) 281 var exidx int // additional forward stripping 282 if del { 283 var delNextComma bool 284 buf, delNextComma = deleteTailItem(buf) 285 if delNextComma { 286 i, j := res.Index+len(res.Raw), 0 287 for ; i < len(jstr); i, j = i+1, j+1 { 288 if jstr[i] <= ' ' { 289 continue 290 } 291 if jstr[i] == ',' { 292 exidx = j + 1 293 } 294 break 295 } 296 } 297 } else { 298 if stringify { 299 buf = appendStringify(buf, raw) 300 } else { 301 buf = append(buf, raw...) 302 } 303 } 304 buf = append(buf, jstr[res.Index+len(res.Raw)+exidx:]...) 305 return buf, nil 306 } 307 if del { 308 return nil, errNoChange 309 } 310 n, numeric := atoui(paths[0]) 311 isempty := true 312 for i := 0; i < len(jstr); i++ { 313 if jstr[i] > ' ' { 314 isempty = false 315 break 316 } 317 } 318 if isempty { 319 if numeric { 320 jstr = "[]" 321 } else { 322 jstr = "{}" 323 } 324 } 325 jsres := gjson.Parse(jstr) 326 if jsres.Type != gjson.JSON { 327 if numeric { 328 jstr = "[]" 329 } else { 330 jstr = "{}" 331 } 332 jsres = gjson.Parse(jstr) 333 } 334 var comma bool 335 for i := 1; i < len(jsres.Raw); i++ { 336 if jsres.Raw[i] <= ' ' { 337 continue 338 } 339 if jsres.Raw[i] == '}' || jsres.Raw[i] == ']' { 340 break 341 } 342 comma = true 343 break 344 } 345 switch jsres.Raw[0] { 346 default: 347 return nil, &errorType{"json must be an object or array"} 348 case '{': 349 end := len(jsres.Raw) - 1 350 for ; end > 0; end-- { 351 if jsres.Raw[end] == '}' { 352 break 353 } 354 } 355 buf = append(buf, jsres.Raw[:end]...) 356 if comma { 357 buf = append(buf, ',') 358 } 359 buf = appendBuild(buf, false, paths, raw, stringify) 360 buf = append(buf, '}') 361 return buf, nil 362 case '[': 363 var appendit bool 364 if !numeric { 365 if paths[0].part == "-1" && !paths[0].force { 366 appendit = true 367 } else { 368 return nil, &errorType{ 369 "cannot set array element for non-numeric key '" + 370 paths[0].part + "'", 371 } 372 } 373 } 374 if appendit { 375 njson := trim(jsres.Raw) 376 if njson[len(njson)-1] == ']' { 377 njson = njson[:len(njson)-1] 378 } 379 buf = append(buf, njson...) 380 if comma { 381 buf = append(buf, ',') 382 } 383 384 buf = appendBuild(buf, true, paths, raw, stringify) 385 buf = append(buf, ']') 386 return buf, nil 387 } 388 buf = append(buf, '[') 389 ress := jsres.Array() 390 for i := 0; i < len(ress); i++ { 391 if i > 0 { 392 buf = append(buf, ',') 393 } 394 buf = append(buf, ress[i].Raw...) 395 } 396 if len(ress) == 0 { 397 buf = appendRepeat(buf, "null,", n-len(ress)) 398 } else { 399 buf = appendRepeat(buf, ",null", n-len(ress)) 400 if comma { 401 buf = append(buf, ',') 402 } 403 } 404 buf = appendBuild(buf, true, paths, raw, stringify) 405 buf = append(buf, ']') 406 return buf, nil 407 } 408 } 409 410 func isOptimisticPath(path string) bool { 411 for i := 0; i < len(path); i++ { 412 if path[i] < '.' || path[i] > 'z' { 413 return false 414 } 415 if path[i] > '9' && path[i] < 'A' { 416 return false 417 } 418 if path[i] > 'z' { 419 return false 420 } 421 } 422 return true 423 } 424 425 // Set sets a json value for the specified path. 426 // A path is in dot syntax, such as "name.last" or "age". 427 // This function expects that the json is well-formed, and does not validate. 428 // Invalid json will not panic, but it may return back unexpected results. 429 // An error is returned if the path is not valid. 430 // 431 // A path is a series of keys separated by a dot. 432 // 433 // { 434 // "name": {"first": "Tom", "last": "Anderson"}, 435 // "age":37, 436 // "children": ["Sara","Alex","Jack"], 437 // "friends": [ 438 // {"first": "James", "last": "Murphy"}, 439 // {"first": "Roger", "last": "Craig"} 440 // ] 441 // } 442 // "name.last" >> "Anderson" 443 // "age" >> 37 444 // "children.1" >> "Alex" 445 func Set(json, path string, value interface{}) (string, error) { 446 return SetOptions(json, path, value, nil) 447 } 448 449 // SetBytes sets a json value for the specified path. 450 // If working with bytes, this method preferred over 451 // Set(string(data), path, value) 452 func SetBytes(json []byte, path string, value interface{}) ([]byte, error) { 453 return SetBytesOptions(json, path, value, nil) 454 } 455 456 // SetRaw sets a raw json value for the specified path. 457 // This function works the same as Set except that the value is set as a 458 // raw block of json. This allows for setting premarshalled json objects. 459 func SetRaw(json, path, value string) (string, error) { 460 return SetRawOptions(json, path, value, nil) 461 } 462 463 // SetRawOptions sets a raw json value for the specified path with options. 464 // This furnction works the same as SetOptions except that the value is set 465 // as a raw block of json. This allows for setting premarshalled json objects. 466 func SetRawOptions(json, path, value string, opts *Options) (string, error) { 467 var optimistic bool 468 if opts != nil { 469 optimistic = opts.Optimistic 470 } 471 res, err := set(json, path, value, false, false, optimistic, false) 472 if err == errNoChange { 473 return json, nil 474 } 475 return string(res), err 476 } 477 478 // SetRawBytes sets a raw json value for the specified path. 479 // If working with bytes, this method preferred over 480 // SetRaw(string(data), path, value) 481 func SetRawBytes(json []byte, path string, value []byte) ([]byte, error) { 482 return SetRawBytesOptions(json, path, value, nil) 483 } 484 485 type dtype struct{} 486 487 // Delete deletes a value from json for the specified path. 488 func Delete(json, path string) (string, error) { 489 return Set(json, path, dtype{}) 490 } 491 492 // DeleteBytes deletes a value from json for the specified path. 493 func DeleteBytes(json []byte, path string) ([]byte, error) { 494 return SetBytes(json, path, dtype{}) 495 } 496 497 type stringHeader struct { 498 data unsafe.Pointer 499 len int 500 } 501 502 type sliceHeader struct { 503 data unsafe.Pointer 504 len int 505 cap int 506 } 507 508 func set(jstr, path, raw string, 509 stringify, del, optimistic, inplace bool, 510 ) ([]byte, error) { 511 if path == "" { 512 return []byte(jstr), &errorType{"path cannot be empty"} 513 } 514 if !del && optimistic && isOptimisticPath(path) { 515 res := gjson.Get(jstr, path) 516 if res.Exists() && res.Index > 0 { 517 sz := len(jstr) - len(res.Raw) + len(raw) 518 if stringify { 519 sz += 2 520 } 521 if inplace && sz <= len(jstr) { 522 if !stringify || !mustMarshalString(raw) { 523 jsonh := *(*stringHeader)(unsafe.Pointer(&jstr)) 524 jsonbh := sliceHeader{ 525 data: jsonh.data, len: jsonh.len, cap: jsonh.len, 526 } 527 jbytes := *(*[]byte)(unsafe.Pointer(&jsonbh)) 528 if stringify { 529 jbytes[res.Index] = '"' 530 copy(jbytes[res.Index+1:], []byte(raw)) 531 jbytes[res.Index+1+len(raw)] = '"' 532 copy(jbytes[res.Index+1+len(raw)+1:], 533 jbytes[res.Index+len(res.Raw):]) 534 } else { 535 copy(jbytes[res.Index:], []byte(raw)) 536 copy(jbytes[res.Index+len(raw):], 537 jbytes[res.Index+len(res.Raw):]) 538 } 539 return jbytes[:sz], nil 540 } 541 return []byte(jstr), nil 542 } 543 buf := make([]byte, 0, sz) 544 buf = append(buf, jstr[:res.Index]...) 545 if stringify { 546 buf = appendStringify(buf, raw) 547 } else { 548 buf = append(buf, raw...) 549 } 550 buf = append(buf, jstr[res.Index+len(res.Raw):]...) 551 return buf, nil 552 } 553 } 554 var paths []pathResult 555 r, simple := parsePath(path) 556 if simple { 557 paths = append(paths, r) 558 for r.more { 559 r, simple = parsePath(r.path) 560 if !simple { 561 break 562 } 563 paths = append(paths, r) 564 } 565 } 566 if !simple { 567 if del { 568 return []byte(jstr), 569 &errorType{"cannot delete value from a complex path"} 570 } 571 return setComplexPath(jstr, path, raw, stringify) 572 } 573 njson, err := appendRawPaths(nil, jstr, paths, raw, stringify, del) 574 if err != nil { 575 return []byte(jstr), err 576 } 577 return njson, nil 578 } 579 580 func setComplexPath(jstr, path, raw string, stringify bool) ([]byte, error) { 581 res := gjson.Get(jstr, path) 582 if !res.Exists() || !(res.Index != 0 || len(res.Indexes) != 0) { 583 return []byte(jstr), errNoChange 584 } 585 if res.Index != 0 { 586 njson := []byte(jstr[:res.Index]) 587 if stringify { 588 njson = appendStringify(njson, raw) 589 } else { 590 njson = append(njson, raw...) 591 } 592 njson = append(njson, jstr[res.Index+len(res.Raw):]...) 593 jstr = string(njson) 594 } 595 if len(res.Indexes) > 0 { 596 type val struct { 597 index int 598 res gjson.Result 599 } 600 vals := make([]val, 0, len(res.Indexes)) 601 res.ForEach(func(_, vres gjson.Result) bool { 602 vals = append(vals, val{res: vres}) 603 return true 604 }) 605 if len(res.Indexes) != len(vals) { 606 return []byte(jstr), errNoChange 607 } 608 for i := 0; i < len(res.Indexes); i++ { 609 vals[i].index = res.Indexes[i] 610 } 611 sort.SliceStable(vals, func(i, j int) bool { 612 return vals[i].index > vals[j].index 613 }) 614 for _, val := range vals { 615 vres := val.res 616 index := val.index 617 njson := []byte(jstr[:index]) 618 if stringify { 619 njson = appendStringify(njson, raw) 620 } else { 621 njson = append(njson, raw...) 622 } 623 njson = append(njson, jstr[index+len(vres.Raw):]...) 624 jstr = string(njson) 625 } 626 } 627 return []byte(jstr), nil 628 } 629 630 // SetOptions sets a json value for the specified path with options. 631 // A path is in dot syntax, such as "name.last" or "age". 632 // This function expects that the json is well-formed, and does not validate. 633 // Invalid json will not panic, but it may return back unexpected results. 634 // An error is returned if the path is not valid. 635 func SetOptions(json, path string, value interface{}, 636 opts *Options, 637 ) (string, error) { 638 if opts != nil { 639 if opts.ReplaceInPlace { 640 // it's not safe to replace bytes in-place for strings 641 // copy the Options and set options.ReplaceInPlace to false. 642 nopts := *opts 643 opts = &nopts 644 opts.ReplaceInPlace = false 645 } 646 } 647 jsonh := *(*stringHeader)(unsafe.Pointer(&json)) 648 jsonbh := sliceHeader{data: jsonh.data, len: jsonh.len, cap: jsonh.len} 649 jsonb := *(*[]byte)(unsafe.Pointer(&jsonbh)) 650 res, err := SetBytesOptions(jsonb, path, value, opts) 651 return string(res), err 652 } 653 654 // SetBytesOptions sets a json value for the specified path with options. 655 // If working with bytes, this method preferred over 656 // SetOptions(string(data), path, value) 657 func SetBytesOptions(json []byte, path string, value interface{}, 658 opts *Options, 659 ) ([]byte, error) { 660 var optimistic, inplace bool 661 if opts != nil { 662 optimistic = opts.Optimistic 663 inplace = opts.ReplaceInPlace 664 } 665 jstr := *(*string)(unsafe.Pointer(&json)) 666 var res []byte 667 var err error 668 switch v := value.(type) { 669 default: 670 b, merr := jsongo.Marshal(value) 671 if merr != nil { 672 return nil, merr 673 } 674 raw := *(*string)(unsafe.Pointer(&b)) 675 res, err = set(jstr, path, raw, false, false, optimistic, inplace) 676 case dtype: 677 res, err = set(jstr, path, "", false, true, optimistic, inplace) 678 case string: 679 res, err = set(jstr, path, v, true, false, optimistic, inplace) 680 case []byte: 681 raw := *(*string)(unsafe.Pointer(&v)) 682 res, err = set(jstr, path, raw, true, false, optimistic, inplace) 683 case bool: 684 if v { 685 res, err = set(jstr, path, "true", false, false, optimistic, inplace) 686 } else { 687 res, err = set(jstr, path, "false", false, false, optimistic, inplace) 688 } 689 case int8: 690 res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), 691 false, false, optimistic, inplace) 692 case int16: 693 res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), 694 false, false, optimistic, inplace) 695 case int32: 696 res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), 697 false, false, optimistic, inplace) 698 case int64: 699 res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), 700 false, false, optimistic, inplace) 701 case uint8: 702 res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), 703 false, false, optimistic, inplace) 704 case uint16: 705 res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), 706 false, false, optimistic, inplace) 707 case uint32: 708 res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), 709 false, false, optimistic, inplace) 710 case uint64: 711 res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), 712 false, false, optimistic, inplace) 713 case float32: 714 res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64), 715 false, false, optimistic, inplace) 716 case float64: 717 res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64), 718 false, false, optimistic, inplace) 719 } 720 if err == errNoChange { 721 return json, nil 722 } 723 return res, err 724 } 725 726 // SetRawBytesOptions sets a raw json value for the specified path with options. 727 // If working with bytes, this method preferred over 728 // SetRawOptions(string(data), path, value, opts) 729 func SetRawBytesOptions(json []byte, path string, value []byte, 730 opts *Options, 731 ) ([]byte, error) { 732 jstr := *(*string)(unsafe.Pointer(&json)) 733 vstr := *(*string)(unsafe.Pointer(&value)) 734 var optimistic, inplace bool 735 if opts != nil { 736 optimistic = opts.Optimistic 737 inplace = opts.ReplaceInPlace 738 } 739 res, err := set(jstr, path, vstr, false, false, optimistic, inplace) 740 if err == errNoChange { 741 return json, nil 742 } 743 return res, err 744 }