🐦‍⬛ Snapshot testing in Gleam

:sparkles: Suggest and run similar command if detected typo

+5
CHANGELOG.md
··· 1 1 # Changelog 2 2 3 + ## v1.2.0 - 2024-08-12 4 + 5 + - ✨ Birdie can now suggest and run the correct command if it can tell you've 6 + made a typo. 7 + 3 8 ## v1.1.8 - 2024-05-28 4 9 5 10 - ⬆️ Update `stdlib` to `>= 0.39.0 and < 1.0.0`
+1
gleam.toml
··· 17 17 filepath = ">= 1.0.0 and < 2.0.0" 18 18 trie_again = ">= 1.1.2 and < 2.0.0" 19 19 simplifile = ">= 2.0.1 and < 3.0.0" 20 + edit_distance = ">= 2.0.1 and < 3.0.0" 20 21 21 22 [dev-dependencies] 22 23 gleeunit = ">= 1.2.0 and < 2.0.0"
+2
manifest.toml
··· 3 3 4 4 packages = [ 5 5 { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, 6 + { name = "edit_distance", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "edit_distance", source = "hex", outer_checksum = "A1E485C69A70210223E46E63985FA1008B8B2DDA9848B7897469171B29020C05" }, 6 7 { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, 7 8 { name = "glance", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glexer"], otp_app = "glance", source = "hex", outer_checksum = "8F3314D27773B7C3B9FB58D8C02C634290422CE531988C0394FA0DF8676B964D" }, 8 9 { name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" }, ··· 21 22 22 23 [requirements] 23 24 argv = { version = ">= 1.0.2 and < 2.0.0" } 25 + edit_distance = { version = ">= 2.0.1 and < 3.0.0" } 24 26 filepath = { version = ">= 1.0.0 and < 2.0.0" } 25 27 glance = { version = ">= 0.11.0 and < 1.0.0" } 26 28 gleam_community_ansi = { version = ">= 1.4.0 and < 2.0.0" }
+79 -7
src/birdie.gleam
··· 2 2 import birdie/internal/diff.{type DiffLine, DiffLine} 3 3 import birdie/internal/project 4 4 import birdie/internal/titles 5 + import edit_distance/levenshtein 5 6 import filepath 6 7 import gleam/bool 7 8 import gleam/erlang ··· 730 731 731 732 // --- CLI COMMAND ------------------------------------------------------------- 732 733 734 + type Command { 735 + Review 736 + AcceptAll 737 + RejectAll 738 + Help 739 + } 740 + 741 + fn command_to_string(command: Command) -> String { 742 + case command { 743 + Review -> "review" 744 + AcceptAll -> "accept-all" 745 + RejectAll -> "reject-all" 746 + Help -> "help" 747 + } 748 + } 749 + 733 750 /// Reviews the snapshots in the project's folder. 734 751 /// This function will behave differently depending on the command line 735 752 /// arguments provided to the program. ··· 744 761 /// > and checked with the vcs you're using. 745 762 /// 746 763 pub fn main() -> Nil { 747 - case argv.load().arguments { 748 - [] | ["review"] -> report_status(review()) 749 - ["accept-all"] | ["accept", "all"] -> report_status(accept_all()) 750 - ["reject-all"] | ["reject", "all"] -> report_status(reject_all()) 751 - ["help"] -> help() 752 - [subcommand] -> unexpected_subcommand(subcommand) 753 - subcommands -> more_than_one_command(subcommands) 764 + let args = argv.load().arguments 765 + case parse_command(args) { 766 + Ok(command) -> run_command(command) 767 + Error(_) -> 768 + case args { 769 + [subcommand] -> 770 + case closest_command(subcommand) { 771 + Ok(command) -> suggest_run_command(subcommand, command) 772 + Error(Nil) -> unexpected_subcommand(subcommand) 773 + } 774 + subcommands -> more_than_one_command(subcommands) 775 + } 776 + } 777 + } 778 + 779 + fn parse_command(arguments: List(String)) { 780 + case arguments { 781 + [] | ["review"] -> Ok(Review) 782 + ["accept-all"] | ["accept", "all"] -> Ok(AcceptAll) 783 + ["reject-all"] | ["reject", "all"] -> Ok(RejectAll) 784 + ["help"] -> Ok(Help) 785 + _ -> Error(Nil) 786 + } 787 + } 788 + 789 + fn run_command(command: Command) -> Nil { 790 + case command { 791 + Review -> report_status(review()) 792 + AcceptAll -> report_status(accept_all()) 793 + RejectAll -> report_status(reject_all()) 794 + Help -> help() 754 795 } 796 + } 797 + 798 + fn suggest_run_command(invalid: String, command: Command) -> Nil { 799 + let error_message = 800 + ansi.bold("Error: ") <> "\"" <> invalid <> "\" isn't a valid subcommand." 801 + 802 + io.println(ansi.red(error_message)) 803 + let msg = 804 + "I think you misspelled `" 805 + <> command_to_string(command) 806 + <> "`, would you like me to run it instead? [Y/n] " 807 + 808 + case erlang.get_line(msg) { 809 + Error(_) -> Nil 810 + Ok(line) -> 811 + case string.lowercase(line) |> string.trim { 812 + "yes" | "y" | "" -> run_command(command) 813 + _ -> io.println("\n" <> help_text()) 814 + } 815 + } 816 + } 817 + 818 + fn closest_command(to string: String) -> Result(Command, Nil) { 819 + let distance = fn(c) { command_to_string(c) |> levenshtein.distance(string) } 820 + 821 + [Review, AcceptAll, RejectAll, Help] 822 + |> list.map(fn(command) { #(command, distance(command)) }) 823 + |> list.filter(keeping: fn(command) { command.1 <= 3 }) 824 + |> list.sort(fn(one, other) { int.compare(one.1, other.1) }) 825 + |> list.first 826 + |> result.map(fn(pair) { pair.0 }) 755 827 } 756 828 757 829 fn review() -> Result(Nil, Error) {
+3 -3
test/titles_test.gleam
··· 81 81 titles 82 82 } 83 83 84 - fn pretty_titles(titles: titles.Titles) -> String { 84 + fn pretty_titles(ts: titles.Titles) -> String { 85 85 let pretty = fn(title, info) { 86 86 let titles.TestInfo(file: file, test_name: test_name) = info 87 87 let title = string.pad_right(title, to: 40, with: " ") ··· 90 90 } 91 91 92 92 let literals = 93 - dict.to_list(titles.literals(titles)) 93 + dict.to_list(titles.literals(ts)) 94 94 |> list.map(fn(pair) { pretty(pair.0, pair.1) }) 95 95 |> string.join(with: "\n") 96 96 97 97 let prefixes = 98 - dict.to_list(titles.prefixes(titles)) 98 + dict.to_list(titles.prefixes(ts)) 99 99 |> list.map(fn(pair) { pretty(pair.0, pair.1) }) 100 100 |> string.join(with: "\n") 101 101