|
6 | 6 | "fmt"
|
7 | 7 | "math"
|
8 | 8 | "strconv"
|
| 9 | + "strings" |
9 | 10 | )
|
10 | 11 |
|
11 | 12 | // Errors
|
@@ -49,6 +50,20 @@ func nextToken(data []byte) int {
|
49 | 50 | return -1
|
50 | 51 | }
|
51 | 52 |
|
| 53 | +// Find position of last character which is not whitespace |
| 54 | +func lastToken(data []byte) int { |
| 55 | + for i := len(data) - 1; i >= 0; i-- { |
| 56 | + switch data[i] { |
| 57 | + case ' ', '\n', '\r', '\t': |
| 58 | + continue |
| 59 | + default: |
| 60 | + return i |
| 61 | + } |
| 62 | + } |
| 63 | + |
| 64 | + return -1 |
| 65 | +} |
| 66 | + |
52 | 67 | // Tries to find the end of string
|
53 | 68 | // Support if string contains escaped quote symbols.
|
54 | 69 | func stringEnd(data []byte) (int, bool) {
|
@@ -460,34 +475,114 @@ var (
|
460 | 475 | nullLiteral = []byte("null")
|
461 | 476 | )
|
462 | 477 |
|
| 478 | +func createInsertComponent(keys []string, setValue []byte, comma, object bool) []byte { |
| 479 | + var buffer bytes.Buffer |
| 480 | + if comma { |
| 481 | + buffer.WriteString(",") |
| 482 | + } |
| 483 | + if object { |
| 484 | + buffer.WriteString("{") |
| 485 | + } |
| 486 | + buffer.WriteString("\"") |
| 487 | + buffer.WriteString(keys[0]) |
| 488 | + buffer.WriteString("\":") |
| 489 | + for i := 1; i < len(keys); i++ { |
| 490 | + buffer.WriteString("{\"") |
| 491 | + buffer.WriteString(keys[i]) |
| 492 | + buffer.WriteString("\":") |
| 493 | + } |
| 494 | + buffer.Write(setValue) |
| 495 | + buffer.WriteString(strings.Repeat("}", len(keys)-1)) |
| 496 | + if object { |
| 497 | + buffer.WriteString("}") |
| 498 | + } |
| 499 | + return buffer.Bytes() |
| 500 | +} |
| 501 | + |
463 | 502 | /*
|
464 |
| -Get - Receives data structure, and key path to extract value from. |
| 503 | +
|
| 504 | +Set - Receives existing data structure, path to set, and data to set at that key. |
465 | 505 |
|
466 | 506 | Returns:
|
467 |
| -`value` - Pointer to original data structure containing key value, or just empty slice if nothing found or error |
468 |
| -`dataType` - Can be: `NotExist`, `String`, `Number`, `Object`, `Array`, `Boolean` or `Null` |
469 |
| -`offset` - Offset from provided data structure where key value ends. Used mostly internally, for example for `ArrayEach` helper. |
470 |
| -`err` - If key not found or any other parsing issue it should return error. If key not found it also sets `dataType` to `NotExist` |
| 507 | +`value` - modified byte array |
| 508 | +`err` - On any parsing error |
471 | 509 |
|
472 |
| -Accept multiple keys to specify path to JSON value (in case of quering nested structures). |
473 |
| -If no keys provided it will try to extract closest JSON value (simple ones or object/array), useful for reading streams or arrays, see `ArrayEach` implementation. |
474 | 510 | */
|
475 |
| -func Get(data []byte, keys ...string) (value []byte, dataType ValueType, offset int, err error) { |
476 |
| - if len(keys) > 0 { |
477 |
| - if offset = searchKeys(data, keys...); offset == -1 { |
478 |
| - return []byte{}, NotExist, -1, KeyPathNotFoundError |
| 511 | +func Set(data []byte, setValue []byte, keys ...string) (value []byte, err error) { |
| 512 | + // ensure keys are set |
| 513 | + if len(keys) == 0 { |
| 514 | + return nil, KeyPathNotFoundError |
| 515 | + } |
| 516 | + |
| 517 | + _, _, startOffset, endOffset, err := internalGet(data, keys...) |
| 518 | + if err != nil { |
| 519 | + if err != KeyPathNotFoundError { |
| 520 | + // problem parsing the data |
| 521 | + return []byte{}, err |
| 522 | + } |
| 523 | + // full path doesnt exist |
| 524 | + // does any subpath exist? |
| 525 | + var depth int |
| 526 | + for i := range keys { |
| 527 | + _, _, start, end, sErr := internalGet(data, keys[:i+1]...) |
| 528 | + if sErr != nil { |
| 529 | + break |
| 530 | + } else { |
| 531 | + endOffset = end |
| 532 | + startOffset = start |
| 533 | + depth++ |
| 534 | + } |
479 | 535 | }
|
480 |
| - } |
| 536 | + comma := true |
| 537 | + object := false |
| 538 | + if endOffset == -1 { |
| 539 | + firstToken := nextToken(data) |
| 540 | + // We can't set a top-level key if data isn't an object |
| 541 | + if len(data) == 0 || data[firstToken] != '{' { |
| 542 | + return nil, KeyPathNotFoundError |
| 543 | + } |
| 544 | + // Don't need a comma if the input is an empty object |
| 545 | + secondToken := firstToken + 1 + nextToken(data[firstToken+1:]) |
| 546 | + if data[secondToken] == '}' { |
| 547 | + comma = false |
| 548 | + } |
| 549 | + // Set the top level key at the end (accounting for any trailing whitespace) |
| 550 | + // This assumes last token is valid like '}', could check and return error |
| 551 | + endOffset = lastToken(data) |
| 552 | + } |
| 553 | + depthOffset := endOffset |
| 554 | + if depth != 0 { |
| 555 | + // if subpath is a non-empty object, add to it |
| 556 | + if data[startOffset] == '{' && data[startOffset+1+nextToken(data[startOffset+1:])]!='}' { |
| 557 | + depthOffset-- |
| 558 | + startOffset = depthOffset |
| 559 | + // otherwise, over-write it with a new object |
| 560 | + } else { |
| 561 | + comma = false |
| 562 | + object = true |
| 563 | + } |
| 564 | + } else { |
| 565 | + startOffset = depthOffset |
| 566 | + } |
| 567 | + value = append(data[:startOffset], append(createInsertComponent(keys[depth:], setValue, comma, object), data[depthOffset:]...)...) |
| 568 | + } else { |
| 569 | + // path currently exists |
| 570 | + startComponent := data[:startOffset] |
| 571 | + endComponent := data[endOffset:] |
481 | 572 |
|
482 |
| - // Go to closest value |
483 |
| - nO := nextToken(data[offset:]) |
484 |
| - if nO == -1 { |
485 |
| - return []byte{}, NotExist, -1, MalformedJsonError |
| 573 | + value = make([]byte, len(startComponent)+len(endComponent)+len(setValue)) |
| 574 | + newEndOffset := startOffset + len(setValue) |
| 575 | + copy(value[0:startOffset], startComponent) |
| 576 | + copy(value[startOffset:newEndOffset], setValue) |
| 577 | + copy(value[newEndOffset:], endComponent) |
486 | 578 | }
|
| 579 | + return value, nil |
| 580 | +} |
487 | 581 |
|
488 |
| - offset += nO |
489 |
| - |
| 582 | +func getType(data []byte, offset int) ([]byte, ValueType, int, error) { |
| 583 | + var dataType ValueType |
490 | 584 | endOffset := offset
|
| 585 | + |
491 | 586 | // if string value
|
492 | 587 | if data[offset] == '"' {
|
493 | 588 | dataType = String
|
@@ -547,15 +642,51 @@ func Get(data []byte, keys ...string) (value []byte, dataType ValueType, offset
|
547 | 642 |
|
548 | 643 | endOffset += end
|
549 | 644 | }
|
| 645 | + return data[offset:endOffset], dataType, endOffset, nil |
| 646 | +} |
| 647 | + |
| 648 | +/* |
| 649 | +Get - Receives data structure, and key path to extract value from. |
| 650 | +
|
| 651 | +Returns: |
| 652 | +`value` - Pointer to original data structure containing key value, or just empty slice if nothing found or error |
| 653 | +`dataType` - Can be: `NotExist`, `String`, `Number`, `Object`, `Array`, `Boolean` or `Null` |
| 654 | +`offset` - Offset from provided data structure where key value ends. Used mostly internally, for example for `ArrayEach` helper. |
| 655 | +`err` - If key not found or any other parsing issue it should return error. If key not found it also sets `dataType` to `NotExist` |
550 | 656 |
|
551 |
| - value = data[offset:endOffset] |
| 657 | +Accept multiple keys to specify path to JSON value (in case of quering nested structures). |
| 658 | +If no keys provided it will try to extract closest JSON value (simple ones or object/array), useful for reading streams or arrays, see `ArrayEach` implementation. |
| 659 | +*/ |
| 660 | +func Get(data []byte, keys ...string) (value []byte, dataType ValueType, offset int, err error) { |
| 661 | + a, b, _, d, e := internalGet(data, keys...) |
| 662 | + return a, b, d, e |
| 663 | +} |
| 664 | + |
| 665 | +func internalGet(data []byte, keys ...string) (value []byte, dataType ValueType, offset, endOffset int, err error) { |
| 666 | + if len(keys) > 0 { |
| 667 | + if offset = searchKeys(data, keys...); offset == -1 { |
| 668 | + return []byte{}, NotExist, -1, -1, KeyPathNotFoundError |
| 669 | + } |
| 670 | + } |
| 671 | + |
| 672 | + // Go to closest value |
| 673 | + nO := nextToken(data[offset:]) |
| 674 | + if nO == -1 { |
| 675 | + return []byte{}, NotExist, offset, -1, MalformedJsonError |
| 676 | + } |
| 677 | + |
| 678 | + offset += nO |
| 679 | + value, dataType, endOffset, err = getType(data, offset) |
| 680 | + if err != nil { |
| 681 | + return value, dataType, offset, endOffset, err |
| 682 | + } |
552 | 683 |
|
553 | 684 | // Strip quotes from string values
|
554 | 685 | if dataType == String {
|
555 | 686 | value = value[1 : len(value)-1]
|
556 | 687 | }
|
557 | 688 |
|
558 |
| - return value, dataType, endOffset, nil |
| 689 | + return value, dataType, offset, endOffset, nil |
559 | 690 | }
|
560 | 691 |
|
561 | 692 | // ArrayEach is used when iterating arrays, accepts a callback function with the same return arguments as `Get`.
|
|
0 commit comments