From 0ae91884e2273c098bdf34d5c8cda387d60f0e38 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Wed, 17 Nov 2021 00:07:48 -0800 Subject: [PATCH] Add `--dump-format json` (#992) --- Cargo.lock | 87 +++-- Cargo.toml | 22 +- README.adoc | 4 + completions/just.bash | 6 +- completions/just.elvish | 3 +- completions/just.fish | 3 +- completions/just.powershell | 3 +- completions/just.zsh | 3 +- src/alias.rs | 6 +- src/analyzer.rs | 10 + src/binding.rs | 2 +- src/common.rs | 30 +- src/config.rs | 75 +++- src/dependency.rs | 5 +- src/dump_format.rs | 5 + src/error.rs | 6 + src/expression.rs | 48 +++ src/fragment.rs | 16 + src/justfile.rs | 26 +- src/keyed.rs | 22 ++ src/lib.rs | 25 +- src/line.rs | 3 +- src/name.rs | 9 + src/parameter.rs | 10 +- src/parameter_kind.rs | 3 +- src/recipe.rs | 16 +- src/setting.rs | 4 +- src/settings.rs | 2 +- src/string_literal.rs | 9 + src/subcommand.rs | 15 +- src/table.rs | 3 +- src/thunk.rs | 41 +++ src/warning.rs | 13 + tests/command.rs | 2 +- tests/common.rs | 23 +- tests/json.rs | 683 ++++++++++++++++++++++++++++++++++++ tests/lib.rs | 1 + 37 files changed, 1106 insertions(+), 138 deletions(-) create mode 100644 src/dump_format.rs create mode 100644 tests/json.rs diff --git a/Cargo.lock b/Cargo.lock index d6f664a..fcef7de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,7 +1,5 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 - [[package]] name = "aho-corasick" version = "0.7.18" @@ -71,9 +69,9 @@ checksum = "52d74260d9bf6944e2208aa46841b4b8f0d7ffc0849a06837b2f510337f86b2b" [[package]] name = "cc" -version = "1.0.71" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" +checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0" [[package]] name = "cfg-if" @@ -118,9 +116,9 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.2.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19c6cedffdc8c03a3346d723eb20bd85a13362bb96dc2ac000842c6381ec7bf" +checksum = "377c9b002a72a0b2c1a18c62e2f3864bdfea4a015e3683a96e24aa45dd6c02d1" dependencies = [ "nix", "winapi", @@ -221,6 +219,12 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + [[package]] name = "just" version = "0.10.3" @@ -242,6 +246,8 @@ dependencies = [ "log", "pretty_assertions", "regex", + "serde", + "serde_json", "similar", "snafu", "strum", @@ -269,9 +275,9 @@ checksum = "441225017b106b9f902e97947a6d31e44ebcf274b91bdbfb51e5c477fcd468e5" [[package]] name = "libc" -version = "0.2.106" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a60553f9a9e039a333b4e9b20573b9e9b9c0bb3a11e201ccc48ef4283456d673" +checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6" [[package]] name = "linked-hash-map" @@ -305,9 +311,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.23.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f305c2c2e4c39a82f7bf0bf65fb557f9070ce06781d4f2454295cc34b1c43188" +checksum = "cf1e25ee6b412c2a1e3fcb6a4499a5c1bfe7f43e014bdce9a6b6666e5aa2d187" dependencies = [ "bitflags", "cc", @@ -327,9 +333,9 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.15" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" [[package]] name = "pretty_assertions" @@ -369,18 +375,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.32" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" +checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" dependencies = [ "unicode-xid", ] [[package]] name = "quote" -version = "1.0.10" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" dependencies = [ "proc-macro2", ] @@ -481,6 +487,43 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "serde" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "similar" version = "2.1.0" @@ -520,9 +563,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "structopt" -version = "0.3.25" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b9788f4202aa75c240ecc9c15c65185e6a39ccdeb0fd5d008b98825464c87c" +checksum = "bf9d950ef167e25e0bdb073cf1d68e9ad2795ac826f2f3f59647817cf23c0bfa" dependencies = [ "clap", "lazy_static", @@ -531,9 +574,9 @@ dependencies = [ [[package]] name = "structopt-derive" -version = "0.4.18" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" +checksum = "134d838a2c9943ac3125cf6df165eda53493451b719f3255b2a26b85f772d0ba" dependencies = [ "heck", "proc-macro-error", @@ -565,9 +608,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.81" +version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" +checksum = "5239bc68e0fef57495900cfea4e8dc75596d9a319d7e16b1e0a440d24e6fe0a0" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 85739c7..f16b1ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,8 @@ members = [".", "bin/ref-type"] ansi_term = "0.12.0" atty = "0.2.0" camino = "1.0.4" +clap = { version = "2.33.0", features = ["wrap_help"] } +ctrlc = { version = "3.1.1", features = ["termination"] } derivative = "2.0.0" dotenv = "0.15.0" edit-distance = "2.0.0" @@ -28,29 +30,17 @@ lexiclean = "0.0.1" libc = "0.2.0" log = "0.4.4" regex = "1.5.4" +serde = { version = "1.0.130", features = ["derive", "rc"] } +serde_json = "1.0.68" +similar = { version = "2.1.0", features = ["unicode"] } snafu = "0.6.0" +strum = { version = "0.22.0", features = ["derive"] } strum_macros = "0.22.0" target = "2.0.0" tempfile = "3.0.0" typed-arena = "2.0.1" unicode-width = "0.1.0" -[dependencies.clap] -version = "2.33.0" -features = ["wrap_help"] - -[dependencies.ctrlc] -version = "3.1.1" -features = ["termination"] - -[dependencies.similar] -version = "2.1.0" -features = ["unicode"] - -[dependencies.strum] -version = "0.22.0" -features = ["derive"] - [dev-dependencies] cradle = "0.2.0" executable-path = "1.0.0" diff --git a/README.adoc b/README.adoc index 5496a32..3d433c1 100644 --- a/README.adoc +++ b/README.adoc @@ -1730,6 +1730,10 @@ default: echo foo ``` +=== Dumping Justfiles as JSON + +The `--dump` command can be used with `--dump-format json` to print a JSON representation of a justfile. The JSON format is currently unstable, so the `--unstable` flag is required. + === Changelog A changelog for the latest release is available in link:CHANGELOG.md[]. Changelogs for previous releases are available on https://github.com/casey/just/releases[the releases page]. `just --changelog` can also be used to make a `just` binary print its changelog. diff --git a/completions/just.bash b/completions/just.bash index cae30cc..102ac55 100644 --- a/completions/just.bash +++ b/completions/just.bash @@ -20,7 +20,7 @@ _just() { case "${cmd}" in just) - opts=" -q -u -v -e -l -h -V -f -d -c -s --check --dry-run --highlight --no-dotenv --no-highlight --quiet --shell-command --clear-shell-args --unsorted --unstable --verbose --changelog --choose --dump --edit --evaluate --fmt --init --list --summary --variables --help --version --chooser --color --list-heading --list-prefix --justfile --set --shell --shell-arg --working-directory --command --completions --show --dotenv-filename --dotenv-path ... " + opts=" -q -u -v -e -l -h -V -f -d -c -s --check --dry-run --highlight --no-dotenv --no-highlight --quiet --shell-command --clear-shell-args --unsorted --unstable --verbose --changelog --choose --dump --edit --evaluate --fmt --init --list --summary --variables --help --version --chooser --color --dump-format --list-heading --list-prefix --justfile --set --shell --shell-arg --working-directory --command --completions --show --dotenv-filename --dotenv-path ... " if [[ ${cur} == -* ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -41,6 +41,10 @@ _just() { COMPREPLY=($(compgen -W "auto always never" -- "${cur}")) return 0 ;; + --dump-format) + COMPREPLY=($(compgen -W "just json" -- "${cur}")) + return 0 + ;; --list-heading) COMPREPLY=($(compgen -f "${cur}")) return 0 diff --git a/completions/just.elvish b/completions/just.elvish index 531dcb4..ef2f326 100644 --- a/completions/just.elvish +++ b/completions/just.elvish @@ -16,6 +16,7 @@ edit:completion:arg-completer[just] = [@words]{ &'just'= { cand --chooser 'Override binary invoked by `--choose`' cand --color 'Print colorful output' + cand --dump-format 'Dump justfile as ' cand --list-heading 'Print before list' cand --list-prefix 'Print before each list item' cand -f 'Use as justfile' @@ -48,7 +49,7 @@ edit:completion:arg-completer[just] = [@words]{ cand --verbose 'Use verbose output' cand --changelog 'Print changelog' cand --choose 'Select one or more recipes to run using a binary. If `--chooser` is not passed the chooser defaults to the value of $JUST_CHOOSER, falling back to `fzf`' - cand --dump 'Print entire justfile' + cand --dump 'Print justfile' cand -e 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`' cand --edit 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`' cand --evaluate 'Evaluate and print all variables. If a variable name is given as an argument, only print that variable''s value.' diff --git a/completions/just.fish b/completions/just.fish index 7a17f6c..9b55069 100644 --- a/completions/just.fish +++ b/completions/just.fish @@ -11,6 +11,7 @@ complete -c just -a '(__fish_just_complete_recipes)' # autogenerated completions complete -c just -n "__fish_use_subcommand" -l chooser -d 'Override binary invoked by `--choose`' complete -c just -n "__fish_use_subcommand" -l color -d 'Print colorful output' -r -f -a "auto always never" +complete -c just -n "__fish_use_subcommand" -l dump-format -d 'Dump justfile as ' -r -f -a "just json" complete -c just -n "__fish_use_subcommand" -l list-heading -d 'Print before list' complete -c just -n "__fish_use_subcommand" -l list-prefix -d 'Print before each list item' complete -c just -n "__fish_use_subcommand" -s f -l justfile -d 'Use as justfile' @@ -36,7 +37,7 @@ complete -c just -n "__fish_use_subcommand" -l unstable -d 'Enable unstable feat complete -c just -n "__fish_use_subcommand" -s v -l verbose -d 'Use verbose output' complete -c just -n "__fish_use_subcommand" -l changelog -d 'Print changelog' complete -c just -n "__fish_use_subcommand" -l choose -d 'Select one or more recipes to run using a binary. If `--chooser` is not passed the chooser defaults to the value of $JUST_CHOOSER, falling back to `fzf`' -complete -c just -n "__fish_use_subcommand" -l dump -d 'Print entire justfile' +complete -c just -n "__fish_use_subcommand" -l dump -d 'Print justfile' complete -c just -n "__fish_use_subcommand" -s e -l edit -d 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`' complete -c just -n "__fish_use_subcommand" -l evaluate -d 'Evaluate and print all variables. If a variable name is given as an argument, only print that variable\'s value.' complete -c just -n "__fish_use_subcommand" -l fmt -d 'Format and overwrite justfile' diff --git a/completions/just.powershell b/completions/just.powershell index f2f68d2..02c55c1 100644 --- a/completions/just.powershell +++ b/completions/just.powershell @@ -21,6 +21,7 @@ Register-ArgumentCompleter -Native -CommandName 'just' -ScriptBlock { 'just' { [CompletionResult]::new('--chooser', 'chooser', [CompletionResultType]::ParameterName, 'Override binary invoked by `--choose`') [CompletionResult]::new('--color', 'color', [CompletionResultType]::ParameterName, 'Print colorful output') + [CompletionResult]::new('--dump-format', 'dump-format', [CompletionResultType]::ParameterName, 'Dump justfile as ') [CompletionResult]::new('--list-heading', 'list-heading', [CompletionResultType]::ParameterName, 'Print before list') [CompletionResult]::new('--list-prefix', 'list-prefix', [CompletionResultType]::ParameterName, 'Print before each list item') [CompletionResult]::new('-f', 'f', [CompletionResultType]::ParameterName, 'Use as justfile') @@ -53,7 +54,7 @@ Register-ArgumentCompleter -Native -CommandName 'just' -ScriptBlock { [CompletionResult]::new('--verbose', 'verbose', [CompletionResultType]::ParameterName, 'Use verbose output') [CompletionResult]::new('--changelog', 'changelog', [CompletionResultType]::ParameterName, 'Print changelog') [CompletionResult]::new('--choose', 'choose', [CompletionResultType]::ParameterName, 'Select one or more recipes to run using a binary. If `--chooser` is not passed the chooser defaults to the value of $JUST_CHOOSER, falling back to `fzf`') - [CompletionResult]::new('--dump', 'dump', [CompletionResultType]::ParameterName, 'Print entire justfile') + [CompletionResult]::new('--dump', 'dump', [CompletionResultType]::ParameterName, 'Print justfile') [CompletionResult]::new('-e', 'e', [CompletionResultType]::ParameterName, 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`') [CompletionResult]::new('--edit', 'edit', [CompletionResultType]::ParameterName, 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`') [CompletionResult]::new('--evaluate', 'evaluate', [CompletionResultType]::ParameterName, 'Evaluate and print all variables. If a variable name is given as an argument, only print that variable''s value.') diff --git a/completions/just.zsh b/completions/just.zsh index 1efbadc..7815920 100644 --- a/completions/just.zsh +++ b/completions/just.zsh @@ -17,6 +17,7 @@ _just() { local common=( '--chooser=[Override binary invoked by `--choose`]' \ '--color=[Print colorful output]: :(auto always never)' \ +'--dump-format=[Dump justfile as ]: :(just json)' \ '--list-heading=[Print before list]' \ '--list-prefix=[Print before each list item]' \ '-f+[Use as justfile]' \ @@ -49,7 +50,7 @@ _just() { '*--verbose[Use verbose output]' \ '--changelog[Print changelog]' \ '--choose[Select one or more recipes to run using a binary. If `--chooser` is not passed the chooser defaults to the value of $JUST_CHOOSER, falling back to `fzf`]' \ -'--dump[Print entire justfile]' \ +'--dump[Print justfile]' \ '-e[Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`]' \ '--edit[Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`]' \ '--evaluate[Evaluate and print all variables. If a variable name is given as an argument, only print that variable'\''s value.]' \ diff --git a/src/alias.rs b/src/alias.rs index be00432..e198e5a 100644 --- a/src/alias.rs +++ b/src/alias.rs @@ -1,9 +1,13 @@ use crate::common::*; /// An alias, e.g. `name := target` -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone, Serialize)] pub(crate) struct Alias<'src, T = Rc>> { pub(crate) name: Name<'src>, + #[serde( + bound(serialize = "T: Keyed<'src>"), + serialize_with = "keyed::serialize" + )] pub(crate) target: T, } diff --git a/src/analyzer.rs b/src/analyzer.rs index 746781c..1e78a62 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -81,6 +81,16 @@ impl<'src> Analyzer<'src> { Ok(Justfile { warnings: ast.warnings, + first: recipes + .values() + .fold(None, |accumulator, next| match accumulator { + None => Some(Rc::clone(next)), + Some(previous) => Some(if previous.line_number() < next.line_number() { + previous + } else { + Rc::clone(next) + }), + }), aliases, assignments, recipes, diff --git a/src/binding.rs b/src/binding.rs index 817db2f..77c1f9c 100644 --- a/src/binding.rs +++ b/src/binding.rs @@ -1,7 +1,7 @@ use crate::common::*; /// A binding of `name` to `value` -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize)] pub(crate) struct Binding<'src, V = String> { /// Export binding as an environment variable to child processes pub(crate) export: bool, diff --git a/src/common.rs b/src/common.rs index c0adeb3..a88154a 100644 --- a/src/common.rs +++ b/src/common.rs @@ -27,6 +27,10 @@ pub(crate) use ::{ libc::EXIT_FAILURE, log::{info, warn}, regex::Regex, + serde::{ + ser::{SerializeMap, SerializeSeq}, + Serialize, Serializer, + }, snafu::{ResultExt, Snafu}, strum::{Display, EnumString, IntoStaticStr}, typed_arena::Arena, @@ -34,7 +38,7 @@ pub(crate) use ::{ }; // modules -pub(crate) use crate::{completions, config, config_error, setting}; +pub(crate) use crate::{completions, config, config_error, keyed, setting}; // functions pub(crate) use crate::{load_dotenv::load_dotenv, output::output, unindent::unindent}; @@ -51,18 +55,18 @@ pub(crate) use crate::{ assignment_resolver::AssignmentResolver, ast::Ast, binding::Binding, color::Color, compile_error::CompileError, compile_error_kind::CompileErrorKind, conditional_operator::ConditionalOperator, config::Config, config_error::ConfigError, - count::Count, delimiter::Delimiter, dependency::Dependency, enclosure::Enclosure, error::Error, - evaluator::Evaluator, expression::Expression, fragment::Fragment, function::Function, - function_context::FunctionContext, interrupt_guard::InterruptGuard, - interrupt_handler::InterruptHandler, item::Item, justfile::Justfile, keyword::Keyword, - lexer::Lexer, line::Line, list::List, loader::Loader, name::Name, output_error::OutputError, - parameter::Parameter, parameter_kind::ParameterKind, parser::Parser, platform::Platform, - position::Position, positional::Positional, recipe::Recipe, recipe_context::RecipeContext, - recipe_resolver::RecipeResolver, scope::Scope, search::Search, search_config::SearchConfig, - search_error::SearchError, set::Set, setting::Setting, settings::Settings, shebang::Shebang, - show_whitespace::ShowWhitespace, string_kind::StringKind, string_literal::StringLiteral, - subcommand::Subcommand, suggestion::Suggestion, table::Table, thunk::Thunk, token::Token, - token_kind::TokenKind, unresolved_dependency::UnresolvedDependency, + count::Count, delimiter::Delimiter, dependency::Dependency, dump_format::DumpFormat, + enclosure::Enclosure, error::Error, evaluator::Evaluator, expression::Expression, + fragment::Fragment, function::Function, function_context::FunctionContext, + interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item, + justfile::Justfile, keyword::Keyword, lexer::Lexer, line::Line, list::List, loader::Loader, + name::Name, output_error::OutputError, parameter::Parameter, parameter_kind::ParameterKind, + parser::Parser, platform::Platform, position::Position, positional::Positional, recipe::Recipe, + recipe_context::RecipeContext, recipe_resolver::RecipeResolver, scope::Scope, search::Search, + search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting, + settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace, string_kind::StringKind, + string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table, + thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency, unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables, verbosity::Verbosity, warning::Warning, }; diff --git a/src/config.rs b/src/config.rs index bed6e1e..44731cc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -14,9 +14,12 @@ pub(crate) const DEFAULT_SHELL_ARG: &str = "-cu"; #[derive(Debug, PartialEq)] pub(crate) struct Config { - pub(crate) color: Color, pub(crate) check: bool, + pub(crate) color: Color, + pub(crate) dotenv_filename: Option, + pub(crate) dotenv_path: Option, pub(crate) dry_run: bool, + pub(crate) dump_format: DumpFormat, pub(crate) highlight: bool, pub(crate) invocation_directory: PathBuf, pub(crate) list_heading: String, @@ -30,8 +33,6 @@ pub(crate) struct Config { pub(crate) subcommand: Subcommand, pub(crate) unsorted: bool, pub(crate) unstable: bool, - pub(crate) dotenv_filename: Option, - pub(crate) dotenv_path: Option, pub(crate) verbosity: Verbosity, } @@ -86,7 +87,10 @@ mod arg { pub(crate) const CHOOSER: &str = "CHOOSER"; pub(crate) const CLEAR_SHELL_ARGS: &str = "CLEAR-SHELL-ARGS"; pub(crate) const COLOR: &str = "COLOR"; + pub(crate) const DOTENV_FILENAME: &str = "DOTENV-FILENAME"; + pub(crate) const DOTENV_PATH: &str = "DOTENV-PATH"; pub(crate) const DRY_RUN: &str = "DRY-RUN"; + pub(crate) const DUMP_FORMAT: &str = "DUMP-FORMAT"; pub(crate) const HIGHLIGHT: &str = "HIGHLIGHT"; pub(crate) const JUSTFILE: &str = "JUSTFILE"; pub(crate) const LIST_HEADING: &str = "LIST-HEADING"; @@ -100,8 +104,6 @@ mod arg { pub(crate) const SHELL_COMMAND: &str = "SHELL-COMMAND"; pub(crate) const UNSORTED: &str = "UNSORTED"; pub(crate) const UNSTABLE: &str = "UNSTABLE"; - pub(crate) const DOTENV_FILENAME: &str = "DOTENV_FILENAME"; - pub(crate) const DOTENV_PATH: &str = "DOTENV_PATH"; pub(crate) const VERBOSE: &str = "VERBOSE"; pub(crate) const WORKING_DIRECTORY: &str = "WORKING-DIRECTORY"; @@ -109,6 +111,10 @@ mod arg { pub(crate) const COLOR_AUTO: &str = "auto"; pub(crate) const COLOR_NEVER: &str = "never"; pub(crate) const COLOR_VALUES: &[&str] = &[COLOR_AUTO, COLOR_ALWAYS, COLOR_NEVER]; + + pub(crate) const DUMP_FORMAT_JSON: &str = "json"; + pub(crate) const DUMP_FORMAT_JUST: &str = "just"; + pub(crate) const DUMP_FORMAT_VALUES: &[&str] = &[DUMP_FORMAT_JUST, DUMP_FORMAT_JSON]; } impl Config { @@ -144,6 +150,15 @@ impl Config { .help("Print what just would do without doing it") .conflicts_with(arg::QUIET), ) + .arg( + Arg::with_name(arg::DUMP_FORMAT) + .long("dump-format") + .takes_value(true) + .possible_values(arg::DUMP_FORMAT_VALUES) + .default_value(arg::DUMP_FORMAT_JUST) + .value_name("FORMAT") + .help("Dump justfile as "), + ) .arg( Arg::with_name(arg::HIGHLIGHT) .long("highlight") @@ -283,7 +298,7 @@ impl Config { .arg( Arg::with_name(cmd::DUMP) .long("dump") - .help("Print entire justfile"), + .help("Print justfile"), ) .arg( Arg::with_name(cmd::EDIT) @@ -367,7 +382,13 @@ impl Config { } } - fn color_from_value(value: &str) -> ConfigResult { + fn color_from_matches(matches: &ArgMatches) -> ConfigResult { + let value = matches + .value_of(arg::COLOR) + .ok_or_else(|| ConfigError::Internal { + message: "`--color` had no value".to_string(), + })?; + match value { arg::COLOR_AUTO => Ok(Color::auto()), arg::COLOR_ALWAYS => Ok(Color::always()), @@ -378,6 +399,22 @@ impl Config { } } + fn dump_format_from_matches(matches: &ArgMatches) -> ConfigResult { + let value = matches + .value_of(arg::DUMP_FORMAT) + .ok_or_else(|| ConfigError::Internal { + message: "`--dump-format` had no value".to_string(), + })?; + + match value { + arg::DUMP_FORMAT_JSON => Ok(DumpFormat::Json), + arg::DUMP_FORMAT_JUST => Ok(DumpFormat::Just), + _ => Err(ConfigError::Internal { + message: format!("Invalid argument `{}` to --dump-format.", value), + }), + } + } + pub(crate) fn from_matches(matches: &ArgMatches) -> ConfigResult { let invocation_directory = env::current_dir().context(config_error::CurrentDir)?; @@ -387,11 +424,7 @@ impl Config { Verbosity::from_flag_occurrences(matches.occurrences_of(arg::VERBOSE)) }; - let color = Self::color_from_value( - matches - .value_of(arg::COLOR) - .expect("`--color` had no value"), - )?; + let color = Self::color_from_matches(matches)?; let set_count = matches.occurrences_of(arg::SET); let mut overrides = BTreeMap::new(); @@ -542,6 +575,7 @@ impl Config { Ok(Self { check: matches.is_present(arg::CHECK), dry_run: matches.is_present(arg::DRY_RUN), + dump_format: Self::dump_format_from_matches(matches)?, highlight: !matches.is_present(arg::NO_HIGHLIGHT), shell: matches.value_of(arg::SHELL).unwrap().to_owned(), load_dotenv: !matches.is_present(arg::NO_DOTENV), @@ -612,7 +646,7 @@ FLAGS: `fzf` --clear-shell-args Clear shell arguments --dry-run Print what just would do without doing it - --dump Print entire justfile + --dump Print justfile -e, --edit Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim` --evaluate Evaluate and print all variables. If a variable @@ -646,12 +680,15 @@ OPTIONS: --completions Print shell completion script for [possible values: zsh, bash, fish, powershell, elvish] - --dotenv-filename + --dotenv-filename Search for environment file named instead of `.env` - --dotenv-path + --dotenv-path Load environment file at instead of searching for one + --dump-format + Dump justfile as [default: just] [possible values: just, + json] -f, --justfile Use as justfile --list-heading Print before list --list-prefix @@ -693,6 +730,7 @@ ARGS: args: [$($arg:expr),*], $(color: $color:expr,)? $(dry_run: $dry_run:expr,)? + $(dump_format: $dump_format:expr,)? $(highlight: $highlight:expr,)? $(search_config: $search_config:expr,)? $(shell: $shell:expr,)? @@ -712,6 +750,7 @@ ARGS: let want = Config { $(color: $color,)? $(dry_run: $dry_run,)? + $(dump_format: $dump_format,)? $(highlight: $highlight,)? $(search_config: $search_config,)? $(shell: $shell.to_owned(),)? @@ -1101,6 +1140,12 @@ ARGS: subcommand: Subcommand::Dump, } + test! { + name: dump_format, + args: ["--dump-format", "json"], + dump_format: DumpFormat::Json, + } + test! { name: subcommand_edit, args: ["--edit"], diff --git a/src/dependency.rs b/src/dependency.rs index aa620c4..d3c470f 100644 --- a/src/dependency.rs +++ b/src/dependency.rs @@ -1,9 +1,10 @@ use crate::common::*; -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Serialize)] pub(crate) struct Dependency<'src> { - pub(crate) recipe: Rc>, pub(crate) arguments: Vec>, + #[serde(serialize_with = "keyed::serialize")] + pub(crate) recipe: Rc>, } impl<'src> Display for Dependency<'src> { diff --git a/src/dump_format.rs b/src/dump_format.rs new file mode 100644 index 0000000..e560ae0 --- /dev/null +++ b/src/dump_format.rs @@ -0,0 +1,5 @@ +#[derive(Debug, PartialEq)] +pub(crate) enum DumpFormat { + Json, + Just, +} diff --git a/src/error.rs b/src/error.rs index b479570..c702549 100644 --- a/src/error.rs +++ b/src/error.rs @@ -63,6 +63,9 @@ pub(crate) enum Error<'src> { Dotenv { dotenv_error: dotenv::Error, }, + DumpJson { + serde_json_error: serde_json::Error, + }, EditorInvoke { editor: OsString, io_error: io::Error, @@ -434,6 +437,9 @@ impl<'src> ColorDisplay for Error<'src> { Dotenv { dotenv_error } => { write!(f, "Failed to load environment file: {}", dotenv_error)?; } + DumpJson { serde_json_error } => { + write!(f, "Failed to dump JSON to stdout: {}", serde_json_error)?; + } EditorInvoke { editor, io_error } => { write!( f, diff --git a/src/expression.rs b/src/expression.rs index 407ea36..4354ab2 100644 --- a/src/expression.rs +++ b/src/expression.rs @@ -65,3 +65,51 @@ impl<'src> Display for Expression<'src> { } } } + +impl<'src> Serialize for Expression<'src> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + Self::Backtick { contents, .. } => { + let mut seq = serializer.serialize_seq(None)?; + seq.serialize_element("evaluate")?; + seq.serialize_element(contents)?; + seq.end() + } + Self::Call { thunk } => thunk.serialize(serializer), + Self::Concatination { lhs, rhs } => { + let mut seq = serializer.serialize_seq(None)?; + seq.serialize_element("concatinate")?; + seq.serialize_element(lhs)?; + seq.serialize_element(rhs)?; + seq.end() + } + Self::Conditional { + lhs, + rhs, + then, + otherwise, + operator, + } => { + let mut seq = serializer.serialize_seq(None)?; + seq.serialize_element("if")?; + seq.serialize_element(&operator.to_string())?; + seq.serialize_element(lhs)?; + seq.serialize_element(rhs)?; + seq.serialize_element(then)?; + seq.serialize_element(otherwise)?; + seq.end() + } + Self::Group { contents } => contents.serialize(serializer), + Self::StringLiteral { string_literal } => string_literal.serialize(serializer), + Self::Variable { name } => { + let mut seq = serializer.serialize_seq(None)?; + seq.serialize_element("variable")?; + seq.serialize_element(name)?; + seq.end() + } + } + } +} diff --git a/src/fragment.rs b/src/fragment.rs index e43dfa2..e4d73e3 100644 --- a/src/fragment.rs +++ b/src/fragment.rs @@ -8,3 +8,19 @@ pub(crate) enum Fragment<'src> { /// …an interpolation containing `expression`. Interpolation { expression: Expression<'src> }, } + +impl<'src> Serialize for Fragment<'src> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + Self::Text { token } => serializer.serialize_str(token.lexeme()), + Self::Interpolation { expression } => { + let mut seq = serializer.serialize_seq(None)?; + seq.serialize_element(expression)?; + seq.end() + } + } + } +} diff --git a/src/justfile.rs b/src/justfile.rs index d6f2b3a..f9746ce 100644 --- a/src/justfile.rs +++ b/src/justfile.rs @@ -1,29 +1,19 @@ use crate::common::*; -#[derive(Debug, PartialEq)] +use serde::Serialize; + +#[derive(Debug, PartialEq, Serialize)] pub(crate) struct Justfile<'src> { - pub(crate) recipes: Table<'src, Rc>>, - pub(crate) assignments: Table<'src, Assignment<'src>>, pub(crate) aliases: Table<'src, Alias<'src>>, + pub(crate) assignments: Table<'src, Assignment<'src>>, + #[serde(serialize_with = "keyed::serialize_option")] + pub(crate) first: Option>>, + pub(crate) recipes: Table<'src, Rc>>, pub(crate) settings: Settings<'src>, pub(crate) warnings: Vec, } impl<'src> Justfile<'src> { - pub(crate) fn first(&self) -> Option<&Recipe<'src>> { - let mut first: Option<&Recipe> = None; - for recipe in self.recipes.values() { - if let Some(first_recipe) = first { - if recipe.line_number() < first_recipe.line_number() { - first = Some(recipe); - } - } else { - first = Some(recipe); - } - } - first - } - pub(crate) fn count(&self) -> usize { self.recipes.len() } @@ -206,7 +196,7 @@ impl<'src> Justfile<'src> { let argvec: Vec<&str> = if !arguments.is_empty() { arguments.iter().map(String::as_str).collect() - } else if let Some(recipe) = self.first() { + } else if let Some(recipe) = &self.first { let min_arguments = recipe.min_arguments(); if min_arguments > 0 { return Err(Error::DefaultRecipeRequiresArguments { diff --git a/src/keyed.rs b/src/keyed.rs index 4a914fd..edea597 100644 --- a/src/keyed.rs +++ b/src/keyed.rs @@ -9,3 +9,25 @@ impl<'key, T: Keyed<'key>> Keyed<'key> for Rc { self.as_ref().key() } } + +pub(crate) fn serialize<'src, S, K>(keyed: &K, serializer: S) -> Result +where + S: Serializer, + K: Keyed<'src>, +{ + serializer.serialize_str(&keyed.key()) +} + +pub(crate) fn serialize_option<'src, S, K>( + recipe: &Option, + serializer: S, +) -> Result +where + S: Serializer, + K: Keyed<'src>, +{ + match recipe { + None => serializer.serialize_none(), + Some(keyed) => serialize(keyed, serializer), + } +} diff --git a/src/lib.rs b/src/lib.rs index 166a91b..c7d64ec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,12 @@ clippy::wildcard_imports )] +pub use crate::run::run; + +// Used in integration tests. +#[doc(hidden)] +pub use unindent::unindent; + #[macro_use] extern crate lazy_static; @@ -29,6 +35,12 @@ pub mod node; #[cfg(fuzzing)] pub(crate) mod fuzzing; +// Used by Janus, https://github.com/casey/janus, a tool +// that analyses all public justfiles on GitHub to avoid +// breaking changes. +#[doc(hidden)] +pub mod summary; + mod alias; mod analyzer; mod assignment; @@ -49,6 +61,7 @@ mod config_error; mod count; mod delimiter; mod dependency; +mod dump_format; mod enclosure; mod error; mod evaluator; @@ -107,15 +120,3 @@ mod use_color; mod variables; mod verbosity; mod warning; - -pub use crate::run::run; - -// Used in integration tests. -#[doc(hidden)] -pub use unindent::unindent; - -// Used by Janus, https://github.com/casey/janus, a tool -// that analyses all public justfiles on GitHub to avoid -// breaking changes. -#[doc(hidden)] -pub mod summary; diff --git a/src/line.rs b/src/line.rs index 7b4b4c8..718e4af 100644 --- a/src/line.rs +++ b/src/line.rs @@ -1,7 +1,8 @@ use crate::common::*; /// A single line in a recipe body, consisting of any number of `Fragment`s. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize)] +#[serde(transparent)] pub(crate) struct Line<'src> { pub(crate) fragments: Vec>, } diff --git a/src/name.rs b/src/name.rs index 4f8c4f6..e9b93ec 100644 --- a/src/name.rs +++ b/src/name.rs @@ -50,3 +50,12 @@ impl Display for Name<'_> { write!(f, "{}", self.lexeme()) } } + +impl<'src> Serialize for Name<'src> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.lexeme()) + } +} diff --git a/src/parameter.rs b/src/parameter.rs index 506b930..257c83e 100644 --- a/src/parameter.rs +++ b/src/parameter.rs @@ -1,16 +1,16 @@ use crate::common::*; /// A single function parameter -#[derive(PartialEq, Debug, Clone)] +#[derive(PartialEq, Debug, Clone, Serialize)] pub(crate) struct Parameter<'src> { - /// The parameter name - pub(crate) name: Name<'src>, - /// The kind of parameter - pub(crate) kind: ParameterKind, /// An optional default expression pub(crate) default: Option>, /// Export parameter as environment variable pub(crate) export: bool, + /// The kind of parameter + pub(crate) kind: ParameterKind, + /// The parameter name + pub(crate) name: Name<'src>, } impl<'src> ColorDisplay for Parameter<'src> { diff --git a/src/parameter_kind.rs b/src/parameter_kind.rs index 0161ae1..8651209 100644 --- a/src/parameter_kind.rs +++ b/src/parameter_kind.rs @@ -1,7 +1,8 @@ use crate::common::*; /// Parameters can either be… -#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] pub(crate) enum ParameterKind { /// …singular, accepting a single argument Singular, diff --git a/src/recipe.rs b/src/recipe.rs index e8c2f90..9797751 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -19,17 +19,17 @@ fn error_from_signal(recipe: &str, line_number: Option, exit_status: Exit } /// A recipe, e.g. `foo: bar baz` -#[derive(PartialEq, Debug, Clone)] +#[derive(PartialEq, Debug, Clone, Serialize)] pub(crate) struct Recipe<'src, D = Dependency<'src>> { pub(crate) body: Vec>, pub(crate) dependencies: Vec, pub(crate) doc: Option<&'src str>, pub(crate) name: Name<'src>, pub(crate) parameters: Vec>, + pub(crate) priors: usize, pub(crate) private: bool, pub(crate) quiet: bool, pub(crate) shebang: bool, - pub(crate) priors: usize, } impl<'src, D> Recipe<'src, D> { @@ -302,12 +302,6 @@ impl<'src, D> Recipe<'src, D> { } } -impl<'src, D> Keyed<'src> for Recipe<'src, D> { - fn key(&self) -> &'src str { - self.name.lexeme() - } -} - impl<'src, D: Display> ColorDisplay for Recipe<'src, D> { fn fmt(&self, f: &mut Formatter, color: Color) -> Result<(), fmt::Error> { if let Some(doc) = self.doc { @@ -353,3 +347,9 @@ impl<'src, D: Display> ColorDisplay for Recipe<'src, D> { Ok(()) } } + +impl<'src, D> Keyed<'src> for Recipe<'src, D> { + fn key(&self) -> &'src str { + self.name.lexeme() + } +} diff --git a/src/setting.rs b/src/setting.rs index 08fe80c..71dfb36 100644 --- a/src/setting.rs +++ b/src/setting.rs @@ -8,10 +8,10 @@ pub(crate) enum Setting<'src> { Shell(Shell<'src>), } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize)] pub(crate) struct Shell<'src> { - pub(crate) command: StringLiteral<'src>, pub(crate) arguments: Vec>, + pub(crate) command: StringLiteral<'src>, } impl<'src> Display for Setting<'src> { diff --git a/src/settings.rs b/src/settings.rs index afa02b0..81d09c7 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,6 +1,6 @@ use crate::common::*; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Serialize)] pub(crate) struct Settings<'src> { pub(crate) dotenv_load: Option, pub(crate) export: bool, diff --git a/src/string_literal.rs b/src/string_literal.rs index 83f7ddd..272642b 100644 --- a/src/string_literal.rs +++ b/src/string_literal.rs @@ -18,3 +18,12 @@ impl Display for StringLiteral<'_> { ) } } + +impl<'src> Serialize for StringLiteral<'src> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.cooked) + } +} diff --git a/src/subcommand.rs b/src/subcommand.rs index f1430c1..b278db7 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -76,7 +76,7 @@ impl Subcommand { Command { overrides, .. } | Evaluate { overrides, .. } => { justfile.run(config, &search, overrides, &[])? } - Dump => Self::dump(ast), + Dump => Self::dump(config, ast, justfile)?, Format => Self::format(config, &search, &src, ast)?, List => Self::list(config, justfile), Run { @@ -229,8 +229,17 @@ impl Subcommand { Ok(()) } - fn dump(ast: Ast) { - print!("{}", ast); + fn dump(config: &Config, ast: Ast, justfile: Justfile) -> Result<(), Error<'static>> { + match config.dump_format { + DumpFormat::Json => { + config.require_unstable("The JSON dump format is currently unstable.")?; + serde_json::to_writer(io::stdout(), &justfile) + .map_err(|serde_json_error| Error::DumpJson { serde_json_error })?; + println!(); + } + DumpFormat::Just => print!("{}", ast), + } + Ok(()) } fn edit(search: &Search) -> Result<(), Error<'static>> { diff --git a/src/table.rs b/src/table.rs index 31c1242..06e8dc9 100644 --- a/src/table.rs +++ b/src/table.rs @@ -2,7 +2,8 @@ use crate::common::*; use std::collections::btree_map; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Serialize)] +#[serde(transparent)] pub(crate) struct Table<'key, V: Keyed<'key>> { map: BTreeMap<&'key str, V>, } diff --git a/src/thunk.rs b/src/thunk.rs index 44988ff..f18c389 100644 --- a/src/thunk.rs +++ b/src/thunk.rs @@ -35,6 +35,16 @@ pub(crate) enum Thunk<'src> { } impl<'src> Thunk<'src> { + fn name(&self) -> &Name<'src> { + match self { + Self::Nullary { name, .. } + | Self::Unary { name, .. } + | Self::Binary { name, .. } + | Self::BinaryPlus { name, .. } + | Self::Ternary { name, .. } => name, + } + } + pub(crate) fn resolve( name: Name<'src>, mut arguments: Vec>, @@ -120,3 +130,34 @@ impl Display for Thunk<'_> { } } } + +impl<'src> Serialize for Thunk<'src> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut seq = serializer.serialize_seq(None)?; + seq.serialize_element("call")?; + seq.serialize_element(self.name())?; + match self { + Self::Nullary { .. } => {} + Self::Unary { arg, .. } => seq.serialize_element(&arg)?, + Self::Binary { args, .. } => { + for arg in args { + seq.serialize_element(arg)?; + } + } + Self::BinaryPlus { args, .. } => { + for arg in args.0.iter().map(Box::as_ref).chain(&args.1) { + seq.serialize_element(arg)?; + } + } + Self::Ternary { args, .. } => { + for arg in args { + seq.serialize_element(arg)?; + } + } + } + seq.end() + } +} diff --git a/src/warning.rs b/src/warning.rs index e94b228..959d790 100644 --- a/src/warning.rs +++ b/src/warning.rs @@ -54,3 +54,16 @@ See https://github.com/casey/just/issues/469 for more details.")?; Ok(()) } } + +impl Serialize for Warning { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(None)?; + + map.serialize_entry("message", &self.color_display(Color::never()).to_string())?; + + map.end() + } +} diff --git a/tests/command.rs b/tests/command.rs index 7caf603..d139b5f 100644 --- a/tests/command.rs +++ b/tests/command.rs @@ -31,7 +31,7 @@ test! { error: The argument '--command ' requires a value but none was supplied USAGE: - just{} --color --shell --shell-arg ... \ + just{} --color --dump-format --shell --shell-arg ... \ <--changelog|--choose|--command |--completions |--dump|--edit|\ --evaluate|--fmt|--init|--list|--show |--summary|--variables> diff --git a/tests/common.rs b/tests/common.rs index 5f2a3dd..05f53c1 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -11,16 +11,19 @@ pub(crate) use std::{ str, }; -pub(crate) use cradle::input::Input; -pub(crate) use executable_path::executable_path; -pub(crate) use just::unindent; -pub(crate) use libc::{EXIT_FAILURE, EXIT_SUCCESS}; -pub(crate) use pretty_assertions::Comparison; -pub(crate) use regex::Regex; -pub(crate) use tempfile::TempDir; -pub(crate) use temptree::{temptree, tree, Tree}; -pub(crate) use which::which; -pub(crate) use yaml_rust::YamlLoader; +pub(crate) use ::{ + cradle::input::Input, + executable_path::executable_path, + just::unindent, + libc::{EXIT_FAILURE, EXIT_SUCCESS}, + pretty_assertions::Comparison, + regex::Regex, + serde_json::{json, Value}, + tempfile::TempDir, + temptree::{temptree, tree, Tree}, + which::which, + yaml_rust::YamlLoader, +}; pub(crate) use crate::{ assert_stdout::assert_stdout, assert_success::assert_success, tempdir::tempdir, test::Test, diff --git a/tests/json.rs b/tests/json.rs new file mode 100644 index 0000000..512623c --- /dev/null +++ b/tests/json.rs @@ -0,0 +1,683 @@ +use crate::common::*; + +fn test(justfile: &str, value: Value) { + Test::new() + .justfile(justfile) + .args(&["--dump", "--dump-format", "json", "--unstable"]) + .stdout(format!("{}\n", serde_json::to_string(&value).unwrap())) + .run(); +} + +#[test] +fn alias() { + test( + " + alias f := foo + + foo: + ", + json!({ + "first": "foo", + "aliases": { + "f": { + "name": "f", + "target": "foo", + } + }, + "assignments": {}, + "recipes": { + "foo": { + "body": [], + "dependencies": [], + "doc": null, + "name": "foo", + "parameters": [], + "priors": 0, + "private": false, + "quiet": false, + "shebang": false, + } + }, + "settings": { + "dotenv_load": null, + "export": false, + "positional_arguments": false, + "shell": null, + }, + "warnings": [], + }), + ); +} + +#[test] +fn assignment() { + test( + "foo := 'bar'", + json!({ + "aliases": {}, + "assignments": { + "foo": { + "export": false, + "name": "foo", + "value": "bar", + } + }, + "first": null, + "recipes": {}, + "settings": { + "dotenv_load": null, + "export": false, + "positional_arguments": false, + "shell": null, + }, + "warnings": [], + }), + ); +} + +#[test] +fn body() { + test( + " + foo: + bar + abc{{ 'xyz' }}def + ", + json!({ + "aliases": {}, + "assignments": {}, + "first": "foo", + "recipes": { + "foo": { + "body": [ + ["bar"], + ["abc", ["xyz"], "def"], + ], + "dependencies": [], + "doc": null, + "name": "foo", + "parameters": [], + "priors": 0, + "private": false, + "quiet": false, + "shebang": false, + } + }, + "settings": { + "dotenv_load": null, + "export": false, + "positional_arguments": false, + "shell": null, + }, + "warnings": [], + }), + ); +} + +#[test] +fn dependencies() { + test( + " + foo: + bar: foo + ", + json!({ + "aliases": {}, + "assignments": {}, + "first": "foo", + "recipes": { + "bar": { + "doc": null, + "name": "bar", + "body": [], + "dependencies": [{ + "arguments": [], + "recipe": "foo" + }], + "parameters": [], + "priors": 1, + "private": false, + "quiet": false, + "shebang": false, + }, + "foo": { + "body": [], + "dependencies": [], + "doc": null, + "name": "foo", + "parameters": [], + "priors": 0, + "private": false, + "quiet": false, + "shebang": false, + } + }, + "settings": { + "dotenv_load": null, + "export": false, + "positional_arguments": false, + "shell": null, + }, + "warnings": [], + }), + ); +} + +#[test] +fn dependency_argument() { + test( + " + x := 'foo' + foo *args: + bar: ( + foo + 'baz' + ('baz') + ('a' + 'b') + `echo` + x + if 'a' == 'b' { 'c' } else { 'd' } + arch() + env_var('foo') + join('a', 'b') + replace('a', 'b', 'c') + ) + ", + json!({ + "aliases": {}, + "first": "foo", + "assignments": { + "x": { + "export": false, + "name": "x", + "value": "foo", + }, + }, + "recipes": { + "bar": { + "doc": null, + "name": "bar", + "body": [], + "dependencies": [{ + "arguments": [ + "baz", + "baz", + ["concatinate", "a", "b"], + ["evaluate", "echo"], + ["variable", "x"], + ["if", "==", "a", "b", "c", "d"], + ["call", "arch"], + ["call", "env_var", "foo"], + ["call", "join", "a", "b"], + ["call", "replace", "a", "b", "c"], + ], + "recipe": "foo" + }], + "parameters": [], + "priors": 1, + "private": false, + "quiet": false, + "shebang": false, + }, + "foo": { + "body": [], + "dependencies": [], + "doc": null, + "name": "foo", + "parameters": [ + { + "name": "args", + "export": false, + "default": null, + "kind": "star", + } + ], + "priors": 0, + "private": false, + "quiet": false, + "shebang": false, + } + }, + "settings": { + "dotenv_load": null, + "export": false, + "positional_arguments": false, + "shell": null, + }, + "warnings": [], + }), + ); +} + +#[test] +fn doc_comment() { + test( + "# hello\nfoo:", + json!({ + "aliases": {}, + "first": "foo", + "assignments": {}, + "recipes": { + "foo": { + "body": [], + "dependencies": [], + "doc": "hello", + "name": "foo", + "parameters": [], + "priors": 0, + "private": false, + "quiet": false, + "shebang": false, + } + }, + "settings": { + "dotenv_load": null, + "export": false, + "positional_arguments": false, + "shell": null, + }, + "warnings": [], + }), + ); +} + +#[test] +fn empty_justfile() { + test( + "", + json!({ + "aliases": {}, + "assignments": {}, + "first": null, + "recipes": {}, + "settings": { + "dotenv_load": null, + "export": false, + "positional_arguments": false, + "shell": null, + }, + "warnings": [], + }), + ); +} + +#[test] +fn parameters() { + test( + " + a: + b x: + c x='y': + d +x: + e *x: + f $x: + ", + json!({ + "aliases": {}, + "first": "a", + "assignments": {}, + "recipes": { + "a": { + "body": [], + "dependencies": [], + "doc": null, + "name": "a", + "parameters": [], + "priors": 0, + "private": false, + "quiet": false, + "shebang": false, + }, + "b": { + "body": [], + "dependencies": [], + "doc": null, + "name": "b", + "parameters": [ + { + "name": "x", + "export": false, + "default": null, + "kind": "singular", + }, + ], + "priors": 0, + "private": false, + "quiet": false, + "shebang": false, + }, + "c": { + "body": [], + "dependencies": [], + "doc": null, + "name": "c", + "parameters": [ + { + "name": "x", + "export": false, + "default": "y", + "kind": "singular", + } + ], + "priors": 0, + "private": false, + "quiet": false, + "shebang": false, + }, + "d": { + "body": [], + "dependencies": [], + "doc": null, + "name": "d", + "parameters": [ + { + "name": "x", + "export": false, + "default": null, + "kind": "plus", + } + ], + "priors": 0, + "private": false, + "quiet": false, + "shebang": false, + }, + "e": { + "body": [], + "dependencies": [], + "doc": null, + "name": "e", + "parameters": [ + { + "name": "x", + "export": false, + "default": null, + "kind": "star", + } + ], + "priors": 0, + "private": false, + "quiet": false, + "shebang": false, + }, + "f": { + "body": [], + "dependencies": [], + "doc": null, + "name": "f", + "parameters": [ + { + "name": "x", + "export": true, + "default": null, + "kind": "singular", + } + ], + "priors": 0, + "private": false, + "quiet": false, + "shebang": false, + }, + }, + "settings": { + "dotenv_load": null, + "export": false, + "positional_arguments": false, + "shell": null, + }, + "warnings": [], + }), + ); +} + +#[test] +fn priors() { + test( + " + a: + b: a && c + c: + ", + json!({ + "aliases": {}, + "assignments": {}, + "first": "a", + "recipes": { + "a": { + "body": [], + "dependencies": [], + "doc": null, + "name": "a", + "parameters": [], + "priors": 0, + "private": false, + "quiet": false, + "shebang": false, + }, + "b": { + "body": [], + "dependencies": [ + { + "arguments": [], + "recipe": "a", + }, + { + "arguments": [], + "recipe": "c", + } + ], + "doc": null, + "name": "b", + "private": false, + "quiet": false, + "shebang": false, + "parameters": [], + "priors": 1, + }, + "c": { + "body": [], + "dependencies": [], + "doc": null, + "name": "c", + "parameters": [], + "private": false, + "quiet": false, + "shebang": false, + "parameters": [], + "priors": 0, + }, + }, + "settings": { + "dotenv_load": null, + "export": false, + "positional_arguments": false, + "shell": null, + }, + "warnings": [], + }), + ); +} + +#[test] +fn private() { + test( + "_foo:", + json!({ + "aliases": {}, + "assignments": {}, + "first": "_foo", + "recipes": { + "_foo": { + "body": [], + "dependencies": [], + "doc": null, + "name": "_foo", + "parameters": [], + "priors": 0, + "private": true, + "quiet": false, + "shebang": false, + } + }, + "settings": { + "dotenv_load": null, + "export": false, + "positional_arguments": false, + "shell": null, + }, + "warnings": [], + }), + ); +} + +#[test] +fn quiet() { + test( + "@foo:", + json!({ + "aliases": {}, + "assignments": {}, + "first": "foo", + "recipes": { + "foo": { + "body": [], + "dependencies": [], + "doc": null, + "name": "foo", + "parameters": [], + "priors": 0, + "private": false, + "quiet": true, + "shebang": false, + } + }, + "settings": { + "dotenv_load": null, + "export": false, + "positional_arguments": false, + "shell": null, + }, + "warnings": [], + }), + ); +} + +#[test] +fn requires_unstable() { + Test::new() + .justfile("foo:") + .args(&["--dump", "--dump-format", "json"]) + .stderr("error: The JSON dump format is currently unstable. Invoke `just` with the `--unstable` flag to enable unstable features.\n") + .status(EXIT_FAILURE) + .run(); +} + +#[test] +fn settings() { + test( + " + set dotenv-load + set export + set positional-arguments + set shell := ['a', 'b', 'c'] + + foo: + #!bar + ", + json!({ + "aliases": {}, + "assignments": {}, + "first": "foo", + "recipes": { + "foo": { + "body": [["#!bar"]], + "dependencies": [], + "doc": null, + "name": "foo", + "parameters": [], + "priors": 0, + "private": false, + "quiet": false, + "shebang": true, + } + }, + "settings": { + "dotenv_load": true, + "export": true, + "positional_arguments": true, + "shell": { + "arguments": ["b", "c"], + "command": "a", + }, + }, + "warnings": [], + }), + ); +} + +#[test] +fn shebang() { + test( + " + foo: + #!bar + ", + json!({ + "aliases": {}, + "assignments": {}, + "first": "foo", + "recipes": { + "foo": { + "body": [["#!bar"]], + "dependencies": [], + "doc": null, + "name": "foo", + "parameters": [], + "priors": 0, + "private": false, + "quiet": false, + "shebang": true, + } + }, + "settings": { + "dotenv_load": null, + "export": false, + "positional_arguments": false, + "shell": null, + }, + "warnings": [], + }), + ); +} + +#[test] +fn simple() { + test( + "foo:", + json!({ + "aliases": {}, + "assignments": {}, + "first": "foo", + "recipes": { + "foo": { + "body": [], + "dependencies": [], + "doc": null, + "name": "foo", + "parameters": [], + "priors": 0, + "private": false, + "quiet": false, + "shebang": false, + } + }, + "settings": { + "dotenv_load": null, + "export": false, + "positional_arguments": false, + "shell": null, + }, + "warnings": [], + }), + ); +} diff --git a/tests/lib.rs b/tests/lib.rs index 250eeaf..87b7595 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -23,6 +23,7 @@ mod functions; mod init; mod interrupts; mod invocation_directory; +mod json; mod misc; mod positional_arguments; mod quiet;