diff.gleam
1 //// Patch model and codec for rendered updates. 2 3 import gleam/int 4 import gleam/list 5 import gleam/string 6 7 pub const patch_stream_version = 1 8 9 /// Dynamic text values injected into static markup by slot name. 10 pub type DynamicSlot { 11 DynamicSlot(name: String, value: String) 12 } 13 14 /// Keyed nodes used for stable collection updates. 15 pub type KeyedNode { 16 KeyedNode(key: String, html: String) 17 } 18 19 /// Patch model. 20 pub type Patch { 21 Replace(target: String, html: String) 22 Append(target: String, html: String) 23 Prepend(target: String, html: String) 24 Remove(target: String) 25 ReplaceSegments( 26 target: String, 27 fingerprint: String, 28 static_html: String, 29 dynamic_slots: List(DynamicSlot), 30 ) 31 UpdateSegments( 32 target: String, 33 fingerprint: String, 34 dynamic_slots: List(DynamicSlot), 35 ) 36 UpsertKeyed(target: String, key: String, html: String) 37 RemoveKeyed(target: String, key: String) 38 } 39 40 /// Decode errors for patch streams. 41 pub type DecodeError { 42 EmptyPayload 43 UnknownPayloadTag(String) 44 BadFieldCount(stage: String, expected: Int, actual: Int) 45 InvalidInteger(String) 46 UnsupportedVersion(Int) 47 MissingDictionaryEntry(Int) 48 MalformedOperation(String) 49 InvalidEscapeSequence 50 } 51 52 /// Return the target selector or id for a patch. 53 pub fn target(patch: Patch) -> String { 54 case patch { 55 Replace(target, _) -> target 56 Append(target, _) -> target 57 Prepend(target, _) -> target 58 Remove(target) -> target 59 ReplaceSegments(target, _, _, _) -> target 60 UpdateSegments(target, _, _) -> target 61 UpsertKeyed(target, _, _) -> target 62 RemoveKeyed(target, _) -> target 63 } 64 } 65 66 /// Return the patch operation name. 67 pub fn operation(patch: Patch) -> String { 68 case patch { 69 Replace(_, _) -> "replace" 70 Append(_, _) -> "append" 71 Prepend(_, _) -> "prepend" 72 Remove(_) -> "remove" 73 ReplaceSegments(_, _, _, _) -> "replace_segments" 74 UpdateSegments(_, _, _) -> "update_segments" 75 UpsertKeyed(_, _, _) -> "upsert_keyed" 76 RemoveKeyed(_, _) -> "remove_keyed" 77 } 78 } 79 80 /// True when a patch carries HTML content. 81 pub fn carries_html(patch: Patch) -> Bool { 82 case patch { 83 Remove(_) -> False 84 UpdateSegments(_, _, _) -> False 85 RemoveKeyed(_, _) -> False 86 _ -> True 87 } 88 } 89 90 /// Build one dynamic slot value. 91 pub fn slot(name: String, value: String) -> DynamicSlot { 92 DynamicSlot(name: name, value: value) 93 } 94 95 /// Build one keyed node. 96 pub fn keyed_node(key: String, html: String) -> KeyedNode { 97 KeyedNode(key: key, html: html) 98 } 99 100 /// Build a keyed update plan from previous and current collections. 101 pub fn keyed_patch_plan( 102 target: String, 103 previous: List(KeyedNode), 104 current: List(KeyedNode), 105 ) -> List(Patch) { 106 concat( 107 keyed_upserts(target, previous, current), 108 keyed_removals(target, previous, current), 109 ) 110 } 111 112 /// Encode one patch into a versioned compressed stream. 113 pub fn encode(patch: Patch) -> String { 114 encode_stream([patch]) 115 } 116 117 /// Encode a patch stream with dictionary compression. 118 pub fn encode_stream(patches: List(Patch)) -> String { 119 let dictionary = build_dictionary(patches, []) 120 let operation_fields = encode_operations(patches, dictionary, []) 121 122 let fields = 123 concat( 124 concat( 125 concat( 126 [ 127 "ps", 128 int.to_string(patch_stream_version), 129 int.to_string(list.length(dictionary)), 130 ], 131 dictionary, 132 ), 133 [int.to_string(list.length(operation_fields))], 134 ), 135 operation_fields, 136 ) 137 138 join_fields(fields) 139 } 140 141 /// Decode one patch from a versioned compressed stream. 142 pub fn decode(payload: String) -> Result(Patch, DecodeError) { 143 case decode_stream(payload) { 144 Error(error) -> Error(error) 145 Ok(patches) -> 146 case patches { 147 [patch] -> Ok(patch) 148 _ -> 149 Error(MalformedOperation( 150 "expected_single_patch:" <> int.to_string(list.length(patches)), 151 )) 152 } 153 } 154 } 155 156 /// Decode a patch stream. 157 pub fn decode_stream(payload: String) -> Result(List(Patch), DecodeError) { 158 case payload { 159 "" -> Error(EmptyPayload) 160 _ -> 161 case split_fields(payload) { 162 Error(error) -> Error(error) 163 Ok(fields) -> decode_fields(fields) 164 } 165 } 166 } 167 168 /// Stable error labels for logs and adapter failures. 169 pub fn decode_error_to_string(error: DecodeError) -> String { 170 case error { 171 EmptyPayload -> "empty_payload" 172 UnknownPayloadTag(tag) -> "unknown_payload_tag:" <> tag 173 BadFieldCount(stage, expected, actual) -> 174 "bad_field_count:" 175 <> stage 176 <> ":" 177 <> int.to_string(expected) 178 <> ":" 179 <> int.to_string(actual) 180 InvalidInteger(value) -> "invalid_integer:" <> value 181 UnsupportedVersion(version) -> 182 "unsupported_version:" <> int.to_string(version) 183 MissingDictionaryEntry(index) -> 184 "missing_dictionary_entry:" <> int.to_string(index) 185 MalformedOperation(reason) -> "malformed_operation:" <> reason 186 InvalidEscapeSequence -> "invalid_escape_sequence" 187 } 188 } 189 190 fn keyed_upserts( 191 target: String, 192 previous: List(KeyedNode), 193 current: List(KeyedNode), 194 ) -> List(Patch) { 195 case current { 196 [] -> [] 197 [KeyedNode(key, html), ..rest] -> 198 case keyed_lookup(previous, key) { 199 Ok(previous_html) -> 200 case previous_html == html { 201 True -> keyed_upserts(target, previous, rest) 202 False -> [ 203 UpsertKeyed(target: target, key: key, html: html), 204 ..keyed_upserts(target, previous, rest) 205 ] 206 } 207 208 Error(Nil) -> [ 209 UpsertKeyed(target: target, key: key, html: html), 210 ..keyed_upserts(target, previous, rest) 211 ] 212 } 213 } 214 } 215 216 fn keyed_removals( 217 target: String, 218 previous: List(KeyedNode), 219 current: List(KeyedNode), 220 ) -> List(Patch) { 221 case previous { 222 [] -> [] 223 [KeyedNode(key, _), ..rest] -> 224 case keyed_lookup(current, key) { 225 Ok(_) -> keyed_removals(target, rest, current) 226 Error(Nil) -> [ 227 RemoveKeyed(target: target, key: key), 228 ..keyed_removals(target, rest, current) 229 ] 230 } 231 } 232 } 233 234 fn keyed_lookup(nodes: List(KeyedNode), key: String) -> Result(String, Nil) { 235 case nodes { 236 [] -> Error(Nil) 237 [KeyedNode(node_key, html), ..rest] -> 238 case node_key == key { 239 True -> Ok(html) 240 False -> keyed_lookup(rest, key) 241 } 242 } 243 } 244 245 fn build_dictionary( 246 patches: List(Patch), 247 dictionary: List(String), 248 ) -> List(String) { 249 case patches { 250 [] -> dictionary 251 [patch, ..rest] -> 252 build_dictionary(rest, add_all_unique(dictionary, patch_strings(patch))) 253 } 254 } 255 256 fn patch_strings(patch: Patch) -> List(String) { 257 case patch { 258 Replace(target, html) -> [target, html] 259 Append(target, html) -> [target, html] 260 Prepend(target, html) -> [target, html] 261 Remove(target) -> [target] 262 ReplaceSegments(target, fingerprint, static_html, dynamic_slots) -> 263 concat( 264 [target, fingerprint, static_html], 265 dynamic_slot_strings(dynamic_slots), 266 ) 267 UpdateSegments(target, fingerprint, dynamic_slots) -> 268 concat([target, fingerprint], dynamic_slot_strings(dynamic_slots)) 269 UpsertKeyed(target, key, html) -> [target, key, html] 270 RemoveKeyed(target, key) -> [target, key] 271 } 272 } 273 274 fn dynamic_slot_strings(dynamic_slots: List(DynamicSlot)) -> List(String) { 275 case dynamic_slots { 276 [] -> [] 277 [DynamicSlot(name, value), ..rest] -> [ 278 name, 279 value, 280 ..dynamic_slot_strings(rest) 281 ] 282 } 283 } 284 285 fn add_all_unique( 286 dictionary: List(String), 287 values: List(String), 288 ) -> List(String) { 289 case values { 290 [] -> dictionary 291 [value, ..rest] -> add_all_unique(add_unique(dictionary, value), rest) 292 } 293 } 294 295 fn add_unique(dictionary: List(String), value: String) -> List(String) { 296 case contains_string(dictionary, value) { 297 True -> dictionary 298 False -> concat(dictionary, [value]) 299 } 300 } 301 302 fn contains_string(values: List(String), target: String) -> Bool { 303 case values { 304 [] -> False 305 [value, ..rest] -> 306 case value == target { 307 True -> True 308 False -> contains_string(rest, target) 309 } 310 } 311 } 312 313 fn encode_operations( 314 patches: List(Patch), 315 dictionary: List(String), 316 operation_fields_rev: List(String), 317 ) -> List(String) { 318 case patches { 319 [] -> list.reverse(operation_fields_rev) 320 [patch, ..rest] -> 321 encode_operations(rest, dictionary, [ 322 encode_operation(patch, dictionary), 323 ..operation_fields_rev 324 ]) 325 } 326 } 327 328 fn encode_operation(patch: Patch, dictionary: List(String)) -> String { 329 case patch { 330 Replace(target, html) -> 331 join_tokens([ 332 "r", 333 encode_index(dictionary, target), 334 encode_index(dictionary, html), 335 ]) 336 337 Append(target, html) -> 338 join_tokens([ 339 "a", 340 encode_index(dictionary, target), 341 encode_index(dictionary, html), 342 ]) 343 344 Prepend(target, html) -> 345 join_tokens([ 346 "p", 347 encode_index(dictionary, target), 348 encode_index(dictionary, html), 349 ]) 350 351 Remove(target) -> join_tokens(["x", encode_index(dictionary, target)]) 352 353 ReplaceSegments(target, fingerprint, static_html, dynamic_slots) -> 354 join_tokens(concat( 355 [ 356 "s", 357 encode_index(dictionary, target), 358 encode_index(dictionary, fingerprint), 359 encode_index(dictionary, static_html), 360 int.to_string(list.length(dynamic_slots)), 361 ], 362 encode_dynamic_slot_tokens(dynamic_slots, dictionary, []), 363 )) 364 365 UpdateSegments(target, fingerprint, dynamic_slots) -> 366 join_tokens(concat( 367 [ 368 "u", 369 encode_index(dictionary, target), 370 encode_index(dictionary, fingerprint), 371 int.to_string(list.length(dynamic_slots)), 372 ], 373 encode_dynamic_slot_tokens(dynamic_slots, dictionary, []), 374 )) 375 376 UpsertKeyed(target, key, html) -> 377 join_tokens([ 378 "k", 379 encode_index(dictionary, target), 380 encode_index(dictionary, key), 381 encode_index(dictionary, html), 382 ]) 383 384 RemoveKeyed(target, key) -> 385 join_tokens([ 386 "q", 387 encode_index(dictionary, target), 388 encode_index(dictionary, key), 389 ]) 390 } 391 } 392 393 fn encode_dynamic_slot_tokens( 394 dynamic_slots: List(DynamicSlot), 395 dictionary: List(String), 396 tokens_rev: List(String), 397 ) -> List(String) { 398 case dynamic_slots { 399 [] -> list.reverse(tokens_rev) 400 [DynamicSlot(name, value), ..rest] -> 401 encode_dynamic_slot_tokens(rest, dictionary, [ 402 encode_index(dictionary, value), 403 encode_index(dictionary, name), 404 ..tokens_rev 405 ]) 406 } 407 } 408 409 fn encode_index(dictionary: List(String), value: String) -> String { 410 int.to_string(index_of(dictionary, value, 0)) 411 } 412 413 fn index_of(values: List(String), target: String, index: Int) -> Int { 414 case values { 415 [] -> 0 416 [value, ..rest] -> 417 case value == target { 418 True -> index 419 False -> index_of(rest, target, index + 1) 420 } 421 } 422 } 423 424 fn decode_fields(fields: List(String)) -> Result(List(Patch), DecodeError) { 425 case fields { 426 [tag, version_text, dictionary_count_text, ..rest] -> 427 case tag { 428 "ps" -> 429 case parse_int(version_text) { 430 Error(error) -> Error(error) 431 Ok(version) -> 432 case version == patch_stream_version { 433 False -> Error(UnsupportedVersion(version)) 434 True -> 435 decode_dictionary_and_operations(dictionary_count_text, rest) 436 } 437 } 438 439 _ -> Error(UnknownPayloadTag(tag)) 440 } 441 442 _ -> 443 Error(BadFieldCount( 444 stage: "stream_header", 445 expected: 3, 446 actual: list.length(fields), 447 )) 448 } 449 } 450 451 fn decode_dictionary_and_operations( 452 dictionary_count_text: String, 453 rest_fields: List(String), 454 ) -> Result(List(Patch), DecodeError) { 455 case parse_int(dictionary_count_text) { 456 Error(error) -> Error(error) 457 Ok(dictionary_count) -> 458 case split_n(rest_fields, dictionary_count, []) { 459 Error(error) -> Error(error) 460 Ok(#(dictionary, operation_count_and_fields)) -> 461 case operation_count_and_fields { 462 [operation_count_text, ..operation_fields] -> 463 case parse_int(operation_count_text) { 464 Error(error) -> Error(error) 465 Ok(operation_count) -> 466 case list.length(operation_fields) == operation_count { 467 False -> 468 Error(BadFieldCount( 469 stage: "operation_fields", 470 expected: operation_count, 471 actual: list.length(operation_fields), 472 )) 473 True -> decode_operations(operation_fields, dictionary, []) 474 } 475 } 476 477 _ -> 478 Error(BadFieldCount( 479 stage: "operation_count", 480 expected: 1, 481 actual: 0, 482 )) 483 } 484 } 485 } 486 } 487 488 fn split_n( 489 values: List(String), 490 count: Int, 491 acc_rev: List(String), 492 ) -> Result(#(List(String), List(String)), DecodeError) { 493 case count < 0 { 494 True -> Error(MalformedOperation("negative_count")) 495 False -> 496 case count { 497 0 -> Ok(#(list.reverse(acc_rev), values)) 498 _ -> 499 case values { 500 [] -> Error(MalformedOperation("insufficient_fields")) 501 [value, ..rest] -> split_n(rest, count - 1, [value, ..acc_rev]) 502 } 503 } 504 } 505 } 506 507 fn decode_operations( 508 operation_fields: List(String), 509 dictionary: List(String), 510 patches_rev: List(Patch), 511 ) -> Result(List(Patch), DecodeError) { 512 case operation_fields { 513 [] -> Ok(list.reverse(patches_rev)) 514 [operation_field, ..rest] -> 515 case decode_operation(operation_field, dictionary) { 516 Error(error) -> Error(error) 517 Ok(patch) -> decode_operations(rest, dictionary, [patch, ..patches_rev]) 518 } 519 } 520 } 521 522 fn decode_operation( 523 operation_field: String, 524 dictionary: List(String), 525 ) -> Result(Patch, DecodeError) { 526 let tokens = string.split(operation_field, ",") 527 528 case tokens { 529 ["r", target_index, html_index] -> 530 decode_binary_patch( 531 "replace", 532 target_index, 533 html_index, 534 dictionary, 535 Replace, 536 ) 537 538 ["a", target_index, html_index] -> 539 decode_binary_patch( 540 "append", 541 target_index, 542 html_index, 543 dictionary, 544 Append, 545 ) 546 547 ["p", target_index, html_index] -> 548 decode_binary_patch( 549 "prepend", 550 target_index, 551 html_index, 552 dictionary, 553 Prepend, 554 ) 555 556 ["x", target_index] -> 557 case decode_dictionary_value(target_index, dictionary) { 558 Error(error) -> Error(error) 559 Ok(target) -> Ok(Remove(target: target)) 560 } 561 562 ["k", target_index, key_index, html_index] -> 563 case decode_dictionary_value(target_index, dictionary) { 564 Error(error) -> Error(error) 565 Ok(target) -> 566 case decode_dictionary_value(key_index, dictionary) { 567 Error(error) -> Error(error) 568 Ok(key) -> 569 case decode_dictionary_value(html_index, dictionary) { 570 Error(error) -> Error(error) 571 Ok(html) -> 572 Ok(UpsertKeyed(target: target, key: key, html: html)) 573 } 574 } 575 } 576 577 ["q", target_index, key_index] -> 578 case decode_dictionary_value(target_index, dictionary) { 579 Error(error) -> Error(error) 580 Ok(target) -> 581 case decode_dictionary_value(key_index, dictionary) { 582 Error(error) -> Error(error) 583 Ok(key) -> Ok(RemoveKeyed(target: target, key: key)) 584 } 585 } 586 587 [ 588 "s", 589 target_index, 590 fingerprint_index, 591 static_index, 592 slot_count, 593 ..slot_tokens 594 ] -> 595 decode_segment_patch( 596 target_index, 597 fingerprint_index, 598 static_index, 599 slot_count, 600 slot_tokens, 601 dictionary, 602 ) 603 604 ["u", target_index, fingerprint_index, slot_count, ..slot_tokens] -> 605 decode_update_segment_patch( 606 target_index, 607 fingerprint_index, 608 slot_count, 609 slot_tokens, 610 dictionary, 611 ) 612 613 _ -> Error(MalformedOperation(operation_field)) 614 } 615 } 616 617 fn decode_binary_patch( 618 patch_name: String, 619 target_index: String, 620 html_index: String, 621 dictionary: List(String), 622 constructor: fn(String, String) -> Patch, 623 ) -> Result(Patch, DecodeError) { 624 case decode_dictionary_value(target_index, dictionary) { 625 Error(error) -> Error(error) 626 Ok(target) -> 627 case decode_dictionary_value(html_index, dictionary) { 628 Error(error) -> Error(error) 629 Ok(html) -> Ok(constructor(target, html)) 630 } 631 } 632 |> map_patch_name_error(patch_name) 633 } 634 635 fn map_patch_name_error( 636 result: Result(Patch, DecodeError), 637 patch_name: String, 638 ) -> Result(Patch, DecodeError) { 639 case result { 640 Ok(patch) -> Ok(patch) 641 Error(MalformedOperation(_)) -> Error(MalformedOperation(patch_name)) 642 Error(error) -> Error(error) 643 } 644 } 645 646 fn decode_segment_patch( 647 target_index: String, 648 fingerprint_index: String, 649 static_index: String, 650 slot_count_text: String, 651 slot_tokens: List(String), 652 dictionary: List(String), 653 ) -> Result(Patch, DecodeError) { 654 case decode_dictionary_value(target_index, dictionary) { 655 Error(error) -> Error(error) 656 Ok(target) -> 657 case decode_dictionary_value(fingerprint_index, dictionary) { 658 Error(error) -> Error(error) 659 Ok(fingerprint) -> 660 case decode_dictionary_value(static_index, dictionary) { 661 Error(error) -> Error(error) 662 Ok(static_html) -> 663 case 664 decode_dynamic_slots(slot_count_text, slot_tokens, dictionary) 665 { 666 Error(error) -> Error(error) 667 Ok(dynamic_slots) -> 668 Ok(ReplaceSegments( 669 target: target, 670 fingerprint: fingerprint, 671 static_html: static_html, 672 dynamic_slots: dynamic_slots, 673 )) 674 } 675 } 676 } 677 } 678 } 679 680 fn decode_update_segment_patch( 681 target_index: String, 682 fingerprint_index: String, 683 slot_count_text: String, 684 slot_tokens: List(String), 685 dictionary: List(String), 686 ) -> Result(Patch, DecodeError) { 687 case decode_dictionary_value(target_index, dictionary) { 688 Error(error) -> Error(error) 689 Ok(target) -> 690 case decode_dictionary_value(fingerprint_index, dictionary) { 691 Error(error) -> Error(error) 692 Ok(fingerprint) -> 693 case decode_dynamic_slots(slot_count_text, slot_tokens, dictionary) { 694 Error(error) -> Error(error) 695 Ok(dynamic_slots) -> 696 Ok(UpdateSegments( 697 target: target, 698 fingerprint: fingerprint, 699 dynamic_slots: dynamic_slots, 700 )) 701 } 702 } 703 } 704 } 705 706 fn decode_dynamic_slots( 707 slot_count_text: String, 708 slot_tokens: List(String), 709 dictionary: List(String), 710 ) -> Result(List(DynamicSlot), DecodeError) { 711 case parse_int(slot_count_text) { 712 Error(error) -> Error(error) 713 Ok(slot_count) -> 714 case list.length(slot_tokens) == slot_count * 2 { 715 False -> 716 Error(BadFieldCount( 717 stage: "dynamic_slots", 718 expected: slot_count * 2, 719 actual: list.length(slot_tokens), 720 )) 721 True -> decode_dynamic_slot_pairs(slot_tokens, dictionary, []) 722 } 723 } 724 } 725 726 fn decode_dynamic_slot_pairs( 727 slot_tokens: List(String), 728 dictionary: List(String), 729 dynamic_slots_rev: List(DynamicSlot), 730 ) -> Result(List(DynamicSlot), DecodeError) { 731 case slot_tokens { 732 [] -> Ok(list.reverse(dynamic_slots_rev)) 733 734 [name_index, value_index, ..rest] -> 735 case decode_dictionary_value(name_index, dictionary) { 736 Error(error) -> Error(error) 737 Ok(name) -> 738 case decode_dictionary_value(value_index, dictionary) { 739 Error(error) -> Error(error) 740 Ok(value) -> 741 decode_dynamic_slot_pairs(rest, dictionary, [ 742 DynamicSlot(name: name, value: value), 743 ..dynamic_slots_rev 744 ]) 745 } 746 } 747 748 _ -> Error(MalformedOperation("dynamic_slot_pair")) 749 } 750 } 751 752 fn decode_dictionary_value( 753 index_text: String, 754 dictionary: List(String), 755 ) -> Result(String, DecodeError) { 756 case parse_int(index_text) { 757 Error(error) -> Error(error) 758 Ok(index) -> dictionary_value(dictionary, index) 759 } 760 } 761 762 fn dictionary_value( 763 dictionary: List(String), 764 index: Int, 765 ) -> Result(String, DecodeError) { 766 case index < 0 { 767 True -> Error(MissingDictionaryEntry(index)) 768 False -> 769 case dictionary { 770 [] -> Error(MissingDictionaryEntry(index)) 771 [entry, ..rest] -> 772 case index == 0 { 773 True -> Ok(entry) 774 False -> dictionary_value(rest, index - 1) 775 } 776 } 777 } 778 } 779 780 fn parse_int(value: String) -> Result(Int, DecodeError) { 781 case int.parse(value) { 782 Ok(parsed) -> Ok(parsed) 783 Error(_) -> Error(InvalidInteger(value)) 784 } 785 } 786 787 fn concat(left: List(a), right: List(a)) -> List(a) { 788 case left { 789 [] -> right 790 [entry, ..rest] -> [entry, ..concat(rest, right)] 791 } 792 } 793 794 fn join_tokens(tokens: List(String)) -> String { 795 case tokens { 796 [] -> "" 797 [token, ..rest] -> join_tokens_loop(rest, token) 798 } 799 } 800 801 fn join_tokens_loop(tokens: List(String), acc: String) -> String { 802 case tokens { 803 [] -> acc 804 [token, ..rest] -> join_tokens_loop(rest, acc <> "," <> token) 805 } 806 } 807 808 fn join_fields(fields: List(String)) -> String { 809 case fields { 810 [] -> "" 811 [field, ..rest] -> join_fields_loop(rest, escape_field(field)) 812 } 813 } 814 815 fn join_fields_loop(fields: List(String), acc: String) -> String { 816 case fields { 817 [] -> acc 818 [field, ..rest] -> join_fields_loop(rest, acc <> "|" <> escape_field(field)) 819 } 820 } 821 822 fn escape_field(value: String) -> String { 823 escape_chars(string.to_graphemes(value), "") 824 } 825 826 fn escape_chars(chars: List(String), acc: String) -> String { 827 case chars { 828 [] -> acc 829 [char, ..rest] -> 830 case char { 831 "\\" -> escape_chars(rest, acc <> "\\\\") 832 "|" -> escape_chars(rest, acc <> "\\|") 833 _ -> escape_chars(rest, acc <> char) 834 } 835 } 836 } 837 838 fn split_fields(payload: String) -> Result(List(String), DecodeError) { 839 split_chars(string.to_graphemes(payload), "", [], False) 840 } 841 842 fn split_chars( 843 chars: List(String), 844 current: String, 845 fields_rev: List(String), 846 escaped: Bool, 847 ) -> Result(List(String), DecodeError) { 848 case chars { 849 [] -> 850 case escaped { 851 True -> Error(InvalidEscapeSequence) 852 False -> Ok(list.reverse([current, ..fields_rev])) 853 } 854 855 [char, ..rest] -> 856 case escaped { 857 True -> split_chars(rest, current <> char, fields_rev, False) 858 859 False -> 860 case char { 861 "\\" -> split_chars(rest, current, fields_rev, True) 862 "|" -> split_chars(rest, "", [current, ..fields_rev], False) 863 _ -> split_chars(rest, current <> char, fields_rev, False) 864 } 865 } 866 } 867 }