From bccd3b0280537f1baea887478e1f9d2fdefc899a Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 14 Aug 2024 11:01:58 +0000 Subject: [PATCH 1/7] - Add support for environment variable validations --- lib/bashly/libraries/strings/strings.yml | 1 + lib/bashly/script/environment_variable.rb | 4 +++- lib/bashly/script/introspection/environment_variables.rb | 5 +++++ .../views/command/environment_variables_filter.gtx | 6 ++++++ lib/bashly/views/environment_variable/validations.gtx | 9 +++++++++ 5 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 lib/bashly/views/environment_variable/validations.gtx diff --git a/lib/bashly/libraries/strings/strings.yml b/lib/bashly/libraries/strings/strings.yml index 02f597ad..13834bed 100644 --- a/lib/bashly/libraries/strings/strings.yml +++ b/lib/bashly/libraries/strings/strings.yml @@ -42,3 +42,4 @@ disallowed_argument: "%{name} must be one of: %{allowed}" disallowed_environment_variable: "%{name} environment variable must be one of: %{allowed}" unsupported_bash_version: "bash version 4 or higher is required" validation_error: "validation error in %s:\\n%s" +environment_variable_validation_error: "validation error in environment variable %s:\\n%s" diff --git a/lib/bashly/script/environment_variable.rb b/lib/bashly/script/environment_variable.rb index b86ab274..c60a041b 100644 --- a/lib/bashly/script/environment_variable.rb +++ b/lib/bashly/script/environment_variable.rb @@ -3,7 +3,9 @@ module Script class EnvironmentVariable < Base class << self def option_keys - @option_keys ||= %i[allowed default help name required private] + @option_keys ||= %i[ + allowed default help name required private validate + ] end end diff --git a/lib/bashly/script/introspection/environment_variables.rb b/lib/bashly/script/introspection/environment_variables.rb index 0f2da004..0f5f6fbb 100644 --- a/lib/bashly/script/introspection/environment_variables.rb +++ b/lib/bashly/script/introspection/environment_variables.rb @@ -30,6 +30,11 @@ def required_environment_variables def whitelisted_environment_variables environment_variables.select(&:allowed) end + + # Returns an array of all the environment_variables with a validation + def validated_environment_variables + environment_variables.select(&:validate) + end end end end diff --git a/lib/bashly/views/command/environment_variables_filter.gtx b/lib/bashly/views/command/environment_variables_filter.gtx index 666d9377..f95fdf87 100644 --- a/lib/bashly/views/command/environment_variables_filter.gtx +++ b/lib/bashly/views/command/environment_variables_filter.gtx @@ -24,3 +24,9 @@ if whitelisted_environment_variables.any? > fi end end + +if validated_environment_variables.any? + validated_environment_variables.each do |env_var| + = env_var.render(:validations) + end +end diff --git a/lib/bashly/views/environment_variable/validations.gtx b/lib/bashly/views/environment_variable/validations.gtx new file mode 100644 index 00000000..805536cf --- /dev/null +++ b/lib/bashly/views/environment_variable/validations.gtx @@ -0,0 +1,9 @@ +if validate + = view_marker + + > if [[ -v {{ name.upcase }} && -n $(validate_{{ validate }} "${{ name.upcase }}") ]]; then + > printf "{{ strings[:environment_variable_validation_error] }}\n" "{{ usage_string }}" "$(validate_{{ validate }} "${{ name.upcase }}")" >&2 + > exit 1 + > fi + > +end From a1ebdcc4e9c267efa0c2bd57d77ea589fe70d990 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 14 Aug 2024 11:15:31 +0000 Subject: [PATCH 2/7] update json schemas with new environment variable validation --- schemas/bashly.json | 11 +++++++++++ schemas/strings.json | 7 +++++++ support/schema/bashly.yml | 11 +++++++++++ 3 files changed, 29 insertions(+) diff --git a/schemas/bashly.json b/schemas/bashly.json index c1a3f26b..441c42e7 100644 --- a/schemas/bashly.json +++ b/schemas/bashly.json @@ -334,6 +334,17 @@ "type": "boolean", "default": true }, + "validate": { + "title": "validate", + "description": "A validation function for the current environment variable\nhttps://bashly.dannyb.co/configuration/environment-variable/#validate", + "type": "string", + "examples": [ + "file_exists", + "dir_exists", + "integer", + "non_empty" + ] + }, "allowed": { "title": "allowed", "description": "Valid values of the current environment variable\nhttps://bashly.dannyb.co/configuration/environment-variable/#allowed", diff --git a/schemas/strings.json b/schemas/strings.json index b57df61a..55ee58dc 100644 --- a/schemas/strings.json +++ b/schemas/strings.json @@ -241,6 +241,13 @@ "type": "string", "minLength": 1, "default": "validation error in %s:\\n%s" + }, + "environment_variable_validation_error": { + "title": "validation error", + "description": "The error message template for failed custom validation for environment variables\nhttps://bashly.dannyb.co/advanced/strings/#custom-strings", + "type": "string", + "minLength": 1, + "default": "validation error in environment variable %s:\\n%s" } }, "additionalProperties": false diff --git a/support/schema/bashly.yml b/support/schema/bashly.yml index 70ac28a6..37fd6a01 100644 --- a/support/schema/bashly.yml +++ b/support/schema/bashly.yml @@ -300,6 +300,17 @@ definitions: https://bashly.dannyb.co/configuration/environment-variable/#required type: boolean default: true + validate: + title: validate + description: |- + A validation function for the current environment variable + https://bashly.dannyb.co/configuration/environment-variable/#validate + type: string + examples: + - file_exists + - dir_exists + - integer + - non_empty allowed: title: allowed description: |- From ee6a4c9973d9cf3398dd9fef8eae053244772444 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 14 Aug 2024 11:33:42 +0000 Subject: [PATCH 3/7] - Update validations example to show environment variable validation --- examples/README.md | 2 +- examples/needs/README.md | 4 +-- examples/validations/README.md | 32 +++++++++++++++++++++++ examples/validations/src/bashly.yml | 8 ++++++ examples/validations/src/build_command.sh | 4 +++ examples/validations/test.sh | 3 +++ spec/approvals/examples/validations | 13 +++++++++ 7 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 examples/validations/src/build_command.sh diff --git a/examples/README.md b/examples/README.md index c035d11f..08e92b7e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -61,7 +61,7 @@ Each of these examples demonstrates one aspect or feature of bashly. - [yaml](yaml#readme) - using the YAML reading functions - [colors](colors#readme) - using the color print feature - [completions](completions#readme) - adding bash completion functionality -- [validations](validations#readme) - adding argument validation functions +- [validations](validations#readme) - adding validation functions for arguments, flags or environment variables - [hooks](hooks#readme) - adding before/after hooks ## Real-world-like examples diff --git a/examples/needs/README.md b/examples/needs/README.md index a3ff0b50..d3825211 100644 --- a/examples/needs/README.md +++ b/examples/needs/README.md @@ -83,7 +83,7 @@ Options: ### `$ ./cli --add deploy` ````shell ---add requires --command +--add needs --command ```` @@ -91,7 +91,7 @@ Options: ### `$ ./cli --add deploy --command 'git push'` ````shell ---add requires --target +--add needs --target ```` diff --git a/examples/validations/README.md b/examples/validations/README.md index 9b436fb4..75c72c47 100644 --- a/examples/validations/README.md +++ b/examples/validations/README.md @@ -50,6 +50,15 @@ commands: # Validations also work on flags (when they have arguments) validate: file_exists + +- name: build + environment_variables: + - name: build_dir + help: Path to the build directory + default: release + + # Validations also work on environment variables + validate: dir_exists ```` @@ -97,5 +106,28 @@ must be an existing file ```` +### `$ ./validate build` + +````shell +validation error in environment variable BUILD_DIR: +must be an existing directory + + +```` + +### `$ BUILD_DIR=src ./validate build` + +````shell +# this file is located in 'src/build_command.sh' +# code for 'validate build' goes here +# you can edit it freely and regenerate (it will not be overwritten) +args: none + +environment variables: +- $BUILD_DIR = src + + +```` + diff --git a/examples/validations/src/bashly.yml b/examples/validations/src/bashly.yml index 058e6df2..ae6c3ce8 100644 --- a/examples/validations/src/bashly.yml +++ b/examples/validations/src/bashly.yml @@ -27,3 +27,11 @@ commands: # Validations also work on flags (when they have arguments) validate: file_exists +- name: build + environment_variables: + - name: build_dir + help: Path to the build directory + default: release + + # Validations also work on environment variables + validate: dir_exists diff --git a/examples/validations/src/build_command.sh b/examples/validations/src/build_command.sh new file mode 100644 index 00000000..ec2f6d2b --- /dev/null +++ b/examples/validations/src/build_command.sh @@ -0,0 +1,4 @@ +echo "# this file is located in 'src/build_command.sh'" +echo "# code for 'validate build' goes here" +echo "# you can edit it freely and regenerate (it will not be overwritten)" +inspect_args diff --git a/examples/validations/test.sh b/examples/validations/test.sh index 83adb4eb..e0574e17 100644 --- a/examples/validations/test.sh +++ b/examples/validations/test.sh @@ -14,3 +14,6 @@ bashly generate ./validate calc A ./validate calc 1 B ./validate calc 1 2 --save no-such-file.txt + +./validate build +BUILD_DIR=src ./validate build \ No newline at end of file diff --git a/spec/approvals/examples/validations b/spec/approvals/examples/validations index 76b2085c..ffe232f9 100644 --- a/spec/approvals/examples/validations +++ b/spec/approvals/examples/validations @@ -6,6 +6,7 @@ created src/lib/validations/validate_not_empty.sh + bashly generate creating user files in src created src/calc_command.sh +created src/build_command.sh created ./validate run ./validate --help to test your bash script + ./validate calc 1 2 --save README.md @@ -25,3 +26,15 @@ must be an integer + ./validate calc 1 2 --save no-such-file.txt validation error in --save PATH: must be an existing file ++ ./validate build +validation error in environment variable BUILD_DIR: +must be an existing directory ++ BUILD_DIR=src ++ ./validate build +# this file is located in 'src/build_command.sh' +# code for 'validate build' goes here +# you can edit it freely and regenerate (it will not be overwritten) +args: none + +environment variables: +- $BUILD_DIR = src From 3414743c562d943d4bad27536dd3ef82667e0c26 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 14 Aug 2024 11:37:18 +0000 Subject: [PATCH 4/7] add `environment_variable.validate` to `bashly doc` --- lib/bashly/docs/env.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/bashly/docs/env.yml b/lib/bashly/docs/env.yml index 21246489..096cc0d5 100644 --- a/lib/bashly/docs/env.yml +++ b/lib/bashly/docs/env.yml @@ -49,6 +49,16 @@ environment_variable.required: help: Specify that this variable is required. url: https://bashly.dannyb.co/configuration/environment-variable/#required example: |- + environment_variables: - name: api_key help: Your API key required: true + +environment_variable.validate: + help: Apply custom validation functions. + + url: https://bashly.dannyb.co/configuration/environment-variable/#validate + example: |- + environment_variables: + - name: build_dir + validate: dir_exists From 49188d2e4b4a95d68c917523c659d898eac71330 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 14 Aug 2024 11:55:50 +0000 Subject: [PATCH 5/7] move introspection specs to their respective folder --- .../introspection/environment_variables.rb | 10 +- lib/bashly/script/introspection/flags.rb | 10 +- spec/bashly/script/command_spec.rb | 270 ------------------ .../script/introspection/arguments_spec.rb | 58 ++++ .../script/introspection/commands_spec.rb | 100 +++++++ .../script/introspection/dependencies_spec.rb | 19 ++ .../environment_variables_spec.rb | 24 ++ .../script/introspection/examples_spec.rb | 36 +++ .../bashly/script/introspection/flags_spec.rb | 93 ++++++ 9 files changed, 340 insertions(+), 280 deletions(-) create mode 100644 spec/bashly/script/introspection/arguments_spec.rb create mode 100644 spec/bashly/script/introspection/commands_spec.rb create mode 100644 spec/bashly/script/introspection/dependencies_spec.rb create mode 100644 spec/bashly/script/introspection/environment_variables_spec.rb create mode 100644 spec/bashly/script/introspection/examples_spec.rb create mode 100644 spec/bashly/script/introspection/flags_spec.rb diff --git a/lib/bashly/script/introspection/environment_variables.rb b/lib/bashly/script/introspection/environment_variables.rb index 0f5f6fbb..6e99cc42 100644 --- a/lib/bashly/script/introspection/environment_variables.rb +++ b/lib/bashly/script/introspection/environment_variables.rb @@ -26,15 +26,15 @@ def required_environment_variables environment_variables.select(&:required) end - # Returns an array of all the environment_variables with a whitelist arg - def whitelisted_environment_variables - environment_variables.select(&:allowed) - end - # Returns an array of all the environment_variables with a validation def validated_environment_variables environment_variables.select(&:validate) end + + # Returns an array of all the environment_variables with a whitelist arg + def whitelisted_environment_variables + environment_variables.select(&:allowed) + end end end end diff --git a/lib/bashly/script/introspection/flags.rb b/lib/bashly/script/introspection/flags.rb index 07485664..81cea8cf 100644 --- a/lib/bashly/script/introspection/flags.rb +++ b/lib/bashly/script/introspection/flags.rb @@ -22,6 +22,11 @@ def global_flags? flags.any? and commands.any? end + # Returns an array of all fpags that need other flags + def needy_flags + flags.select(&:needs) + end + # Returns only flags that are not private def public_flags flags.reject(&:private) @@ -32,11 +37,6 @@ def required_flags flags.select(&:required) end - # Returns an array of all fpags that need other flags - def needy_flags - flags.select(&:needs) - end - # Returns true if one of the flags matches the provided short code def short_flag_exist?(flag) flags.any? { |f| f.short == flag } diff --git a/spec/bashly/script/command_spec.rb b/spec/bashly/script/command_spec.rb index a096bbdb..f7d3adce 100644 --- a/spec/bashly/script/command_spec.rb +++ b/spec/bashly/script/command_spec.rb @@ -48,13 +48,6 @@ end end - describe '#args' do - it 'returns an array of Argument objects' do - expect(subject.args).to be_an Array - expect(subject.args.first).to be_a Script::Argument - end - end - describe '#caption_string' do it 'returns a string containing the name and summary' do expect(subject.caption_string).to eq 'get - get something from somewhere' @@ -69,129 +62,6 @@ end end - describe '#command_aliases' do - let(:fixture) { :aliases } - - it 'returns an array of command aliases' do - expect(subject.command_aliases).to eq %w[download d pull upload u push update upgrade] - end - end - - describe '#command_help_data' do - let(:fixture) { :exposed } - - it 'returns a hash suitable for showing command and exposed subcommand help' do - expect(subject.command_help_data.to_yaml) - .to match_approval('script/command/exposed_commands') - end - end - - describe '#command_names' do - let(:fixture) { :docker } - - it 'returns an array of command names' do - expect(subject.command_names).to eq %w[container image] - end - end - - describe '#commands' do - let(:fixture) { :docker } - - it 'returns an array of Command objects' do - expect(subject.commands).to be_an Array - expect(subject.commands.first).to be_a described_class - end - - it 'sets the parents property of its commands' do - expect(subject.commands.first.parents).to eq ['docker'] - end - end - - describe '#deep_commands' do - let(:fixture) { :docker } - - it 'returns an array of all commands in the tree' do - expect(subject.deep_commands.map(&:full_name)) - .to eq ['docker container', 'docker container run', 'docker container stop', 'docker image'] - end - - context 'when include_self is true' do - it 'prepends the result with the command itself' do - expect(subject.deep_commands(include_self: true).map(&:full_name)) - .to eq ['docker', 'docker container', 'docker container run', 'docker container stop', 'docker image'] - end - end - end - - describe '#default_arguments' do - let(:fixture) { :default_values } - - it 'returns an array of only the Argument objects that have default' do - expect(subject.default_args.size).to eq 1 - expect(subject.default_args.first.name).to eq 'files' - end - end - - describe '#default_command' do - let(:fixture) { :default_command } - - it 'returns a Command object of the first default command' do - expect(subject.default_command).to be_a described_class - expect(subject.default_command.name).to eq 'get' - end - end - - describe '#default_flags' do - let(:fixture) { :default_values } - - it 'returns an array of only the Flags objects that have default' do - expect(subject.default_flags.size).to eq 1 - expect(subject.default_flags.first.long).to eq '--format' - end - end - - describe '#dependencies' do - let(:fixture) { :dependencies } - - it 'returns an array of Dependency objects' do - expect(subject.dependencies).to be_an Array - expect(subject.dependencies.first).to be_a Script::Dependency - end - end - - describe '#environment_cariables' do - it 'returns an array of EnvironmentVariable objects' do - expect(subject.environment_variables).to be_an Array - expect(subject.environment_variables.first).to be_a Script::EnvironmentVariable - end - end - - describe '#examples' do - context 'when there are no examples' do - it 'returns nil' do - expect(subject.examples).to be_nil - end - end - - context 'when there are examples as array' do - let(:fixture) { :examples_array } - - it 'returns the array' do - expect(subject.examples).to be_an Array - end - end - - context 'when there are examples as string' do - let(:fixture) { :examples_string } - - it 'returns an array with one item' do - expect(subject.examples).to be_an Array - expect(subject.examples.count).to eq 1 - expect(subject.examples.first).to start_with 'Download a file' - end - end - end - describe '#filename' do context 'when it is the root command' do it 'returns root_command.sh' do @@ -251,13 +121,6 @@ end end - describe '#flags' do - it 'returns an array of Flag objects' do - expect(subject.flags).to be_an Array - expect(subject.flags.first).to be_a Script::Flag - end - end - describe '#function_name' do let(:fixture) { :docker_container_run } @@ -290,32 +153,6 @@ end end - describe '#global_flags?' do - context 'when a command has flags and commands' do - let(:fixture) { :mode_global_flags } - - it 'returns true' do - expect(subject).to be_global_flags - end - end - - context 'when a command has flags but no commands' do - let(:fixture) { :mode_flags } - - it 'returns false' do - expect(subject).not_to be_global_flags - end - end - - context 'when a command has commands but no flags' do - let(:fixture) { :mode_commands } - - it 'returns false' do - expect(subject).not_to be_global_flags - end - end - end - describe '#group_string' do it 'returns a string suitable for showing the group in usage' do expect(subject.group_string).to eq 'Commands:' @@ -330,16 +167,6 @@ end end - describe '#grouped_commands' do - let(:fixture) { :exposed } - - it 'returns a hash with an array of Command objects per group key' do - expect(subject.grouped_commands.keys).to contain_exactly('Cluster Commands:', 'Commands:') - expect(subject.grouped_commands['Commands:'].count).to eq 4 - expect(subject.grouped_commands['Commands:']).to all(be_a described_class) - end - end - describe '#has_unique_args_or_flags?' do context 'when the command has any args that are unique' do let(:fixture) { :unique_args } @@ -455,69 +282,6 @@ end end - describe '#needy_flags' do - let(:fixture) { :needy_flags } - - it 'returns an array of only the needy Flag objects' do - expect(subject.needy_flags.size).to eq 2 - expect(subject.needy_flags.first.long).to eq '--add' - end - end - - describe '#public_commands' do - let(:fixture) { :private_commands } - - it 'returns an array of Command objects excluding private commands' do - expect(subject.public_commands.count).to eq 1 - expect(subject.public_commands.first.name).to eq 'connect' - end - end - - describe '#public_commands_aliases' do - let(:fixture) { :private_commands } - - it 'returns an array of command aliases of public subcommands' do - expect(subject.public_command_aliases).to eq %w[connect c] - end - end - - describe '#required_args' do - it 'returns an array of only the required Argument objects' do - expect(subject.required_args.size).to eq 1 - expect(subject.required_args.first.name).to eq 'source' - end - end - - describe '#required_environment_variables' do - it 'returns an array of only the required Argument objects' do - expect(subject.required_environment_variables.size).to eq 1 - expect(subject.required_environment_variables.first.name).to eq 'secret_key' - end - end - - describe '#required_flags' do - it 'returns an array of only the required Flag objects' do - expect(subject.required_flags.size).to eq 1 - expect(subject.required_flags.first.long).to eq '--force' - end - end - - describe '#repeatable_arg_exist?' do - context 'when the command does not have any repeatable flags' do - it 'returns false' do - expect(subject.repeatable_arg_exist?).to be false - end - end - - context 'when the command has at least one repeatable flag' do - let(:fixture) { :repeatable_arg } - - it 'returns true' do - expect(subject.repeatable_arg_exist?).to be true - end - end - end - describe '#root_command?' do context 'when the command has no parents' do it 'returns true' do @@ -534,22 +298,6 @@ end end - describe '#short_flag_exist?' do - let(:fixture) { :flag_hog } - - context 'when the command has this short flag' do - it 'returns true' do - expect(subject.short_flag_exist?('-h')).to be true - end - end - - context 'when the command does not have this short flag' do - it 'returns false' do - expect(subject.short_flag_exist?('-s')).to be false - end - end - end - describe '#summary_string' do it 'returns the user defined summary' do expect(subject.summary_string).to eq 'get something from somewhere' @@ -678,22 +426,4 @@ end end end - - describe '#whitelisted_args' do - let(:fixture) { :whitelist } - - it 'returns an array of args that have a whitelist' do - expect(subject.whitelisted_args.size).to eq 1 - expect(subject.whitelisted_args.first.name).to eq 'region' - end - end - - describe '#whitelisted_flags' do - let(:fixture) { :whitelist } - - it 'returns an array of flags that have a whitelist' do - expect(subject.whitelisted_flags.size).to eq 1 - expect(subject.whitelisted_flags.first.long).to eq '--user' - end - end end diff --git a/spec/bashly/script/introspection/arguments_spec.rb b/spec/bashly/script/introspection/arguments_spec.rb new file mode 100644 index 00000000..d27c04c5 --- /dev/null +++ b/spec/bashly/script/introspection/arguments_spec.rb @@ -0,0 +1,58 @@ +describe Script::Introspection::Arguments do + subject do + result = Script::Command.new fixtures[fixture] + result.parents = result.options['parents'] + result + end + + let(:fixtures) { load_fixture 'script/commands' } + let(:fixture) { :basic_command } + + describe '#args' do + it 'returns an array of Argument objects' do + expect(subject.args).to be_an Array + expect(subject.args.first).to be_a Script::Argument + end + end + + describe '#default_args' do + let(:fixture) { :default_values } + + it 'returns an array of only the Argument objects that have default' do + expect(subject.default_args.size).to eq 1 + expect(subject.default_args.first.name).to eq 'files' + end + end + + describe '#repeatable_arg_exist?' do + context 'when the command does not have any repeatable flags' do + it 'returns false' do + expect(subject.repeatable_arg_exist?).to be false + end + end + + context 'when the command has at least one repeatable flag' do + let(:fixture) { :repeatable_arg } + + it 'returns true' do + expect(subject.repeatable_arg_exist?).to be true + end + end + end + + describe '#required_args' do + it 'returns an array of only the required Argument objects' do + expect(subject.required_args.size).to eq 1 + expect(subject.required_args.first.name).to eq 'source' + end + end + + describe '#whitelisted_args' do + let(:fixture) { :whitelist } + + it 'returns an array of args that have a whitelist' do + expect(subject.whitelisted_args.size).to eq 1 + expect(subject.whitelisted_args.first.name).to eq 'region' + end + end +end diff --git a/spec/bashly/script/introspection/commands_spec.rb b/spec/bashly/script/introspection/commands_spec.rb new file mode 100644 index 00000000..9e9668d1 --- /dev/null +++ b/spec/bashly/script/introspection/commands_spec.rb @@ -0,0 +1,100 @@ +describe Script::Introspection::Commands do + subject do + result = Script::Command.new fixtures[fixture] + result.parents = result.options['parents'] + result + end + + let(:fixtures) { load_fixture 'script/commands' } + let(:fixture) { :basic_command } + + describe '#command_aliases' do + let(:fixture) { :aliases } + + it 'returns an array of command aliases' do + expect(subject.command_aliases).to eq %w[download d pull upload u push update upgrade] + end + end + + describe '#command_help_data' do + let(:fixture) { :exposed } + + it 'returns a hash suitable for showing command and exposed subcommand help' do + expect(subject.command_help_data.to_yaml) + .to match_approval('script/command/exposed_commands') + end + end + + describe '#command_names' do + let(:fixture) { :docker } + + it 'returns an array of command names' do + expect(subject.command_names).to eq %w[container image] + end + end + + describe '#commands' do + let(:fixture) { :docker } + + it 'returns an array of Command objects' do + expect(subject.commands).to be_an Array + expect(subject.commands.first).to be_a described_class + end + + it 'sets the parents property of its commands' do + expect(subject.commands.first.parents).to eq ['docker'] + end + end + + describe '#deep_commands' do + let(:fixture) { :docker } + + it 'returns an array of all commands in the tree' do + expect(subject.deep_commands.map(&:full_name)) + .to eq ['docker container', 'docker container run', 'docker container stop', 'docker image'] + end + + context 'when include_self is true' do + it 'prepends the result with the command itself' do + expect(subject.deep_commands(include_self: true).map(&:full_name)) + .to eq ['docker', 'docker container', 'docker container run', 'docker container stop', 'docker image'] + end + end + end + + describe '#default_command' do + let(:fixture) { :default_command } + + it 'returns a Command object of the first default command' do + expect(subject.default_command).to be_a described_class + expect(subject.default_command.name).to eq 'get' + end + end + + describe '#grouped_commands' do + let(:fixture) { :exposed } + + it 'returns a hash with an array of Command objects per group key' do + expect(subject.grouped_commands.keys).to contain_exactly('Cluster Commands:', 'Commands:') + expect(subject.grouped_commands['Commands:'].count).to eq 4 + expect(subject.grouped_commands['Commands:']).to all(be_a described_class) + end + end + + describe '#public_commands' do + let(:fixture) { :private_commands } + + it 'returns an array of Command objects excluding private commands' do + expect(subject.public_commands.count).to eq 1 + expect(subject.public_commands.first.name).to eq 'connect' + end + end + + describe '#public_commands_aliases' do + let(:fixture) { :private_commands } + + it 'returns an array of command aliases of public subcommands' do + expect(subject.public_command_aliases).to eq %w[connect c] + end + end +end diff --git a/spec/bashly/script/introspection/dependencies_spec.rb b/spec/bashly/script/introspection/dependencies_spec.rb new file mode 100644 index 00000000..389de2a8 --- /dev/null +++ b/spec/bashly/script/introspection/dependencies_spec.rb @@ -0,0 +1,19 @@ +describe Script::Introspection::Dependencies do + subject do + result = Script::Command.new fixtures[fixture] + result.parents = result.options['parents'] + result + end + + let(:fixtures) { load_fixture 'script/commands' } + let(:fixture) { :basic_command } + + describe '#dependencies' do + let(:fixture) { :dependencies } + + it 'returns an array of Dependency objects' do + expect(subject.dependencies).to be_an Array + expect(subject.dependencies.first).to be_a Script::Dependency + end + end +end diff --git a/spec/bashly/script/introspection/environment_variables_spec.rb b/spec/bashly/script/introspection/environment_variables_spec.rb new file mode 100644 index 00000000..f3854097 --- /dev/null +++ b/spec/bashly/script/introspection/environment_variables_spec.rb @@ -0,0 +1,24 @@ +describe Script::Introspection::EnvironmentVariables do + subject do + result = Script::Command.new fixtures[fixture] + result.parents = result.options['parents'] + result + end + + let(:fixtures) { load_fixture 'script/commands' } + let(:fixture) { :basic_command } + + describe '#environment_cariables' do + it 'returns an array of EnvironmentVariable objects' do + expect(subject.environment_variables).to be_an Array + expect(subject.environment_variables.first).to be_a Script::EnvironmentVariable + end + end + + describe '#required_environment_variables' do + it 'returns an array of only the required Argument objects' do + expect(subject.required_environment_variables.size).to eq 1 + expect(subject.required_environment_variables.first.name).to eq 'secret_key' + end + end +end diff --git a/spec/bashly/script/introspection/examples_spec.rb b/spec/bashly/script/introspection/examples_spec.rb new file mode 100644 index 00000000..ab08c068 --- /dev/null +++ b/spec/bashly/script/introspection/examples_spec.rb @@ -0,0 +1,36 @@ +describe Script::Introspection::Examples do + subject do + result = Script::Command.new fixtures[fixture] + result.parents = result.options['parents'] + result + end + + let(:fixtures) { load_fixture 'script/commands' } + let(:fixture) { :basic_command } + + describe '#examples' do + context 'when there are no examples' do + it 'returns nil' do + expect(subject.examples).to be_nil + end + end + + context 'when there are examples as array' do + let(:fixture) { :examples_array } + + it 'returns the array' do + expect(subject.examples).to be_an Array + end + end + + context 'when there are examples as string' do + let(:fixture) { :examples_string } + + it 'returns an array with one item' do + expect(subject.examples).to be_an Array + expect(subject.examples.count).to eq 1 + expect(subject.examples.first).to start_with 'Download a file' + end + end + end +end diff --git a/spec/bashly/script/introspection/flags_spec.rb b/spec/bashly/script/introspection/flags_spec.rb new file mode 100644 index 00000000..815a09fa --- /dev/null +++ b/spec/bashly/script/introspection/flags_spec.rb @@ -0,0 +1,93 @@ +describe Script::Introspection::Flags do + subject do + result = Script::Command.new fixtures[fixture] + result.parents = result.options['parents'] + result + end + + let(:fixtures) { load_fixture 'script/commands' } + let(:fixture) { :basic_command } + + describe '#default_flags' do + let(:fixture) { :default_values } + + it 'returns an array of only the Flags objects that have default' do + expect(subject.default_flags.size).to eq 1 + expect(subject.default_flags.first.long).to eq '--format' + end + end + + describe '#flags' do + it 'returns an array of Flag objects' do + expect(subject.flags).to be_an Array + expect(subject.flags.first).to be_a Script::Flag + end + end + + describe '#global_flags?' do + context 'when a command has flags and commands' do + let(:fixture) { :mode_global_flags } + + it 'returns true' do + expect(subject).to be_global_flags + end + end + + context 'when a command has flags but no commands' do + let(:fixture) { :mode_flags } + + it 'returns false' do + expect(subject).not_to be_global_flags + end + end + + context 'when a command has commands but no flags' do + let(:fixture) { :mode_commands } + + it 'returns false' do + expect(subject).not_to be_global_flags + end + end + end + + describe '#whitelisted_flags' do + let(:fixture) { :whitelist } + + it 'returns an array of flags that have a whitelist' do + expect(subject.whitelisted_flags.size).to eq 1 + expect(subject.whitelisted_flags.first.long).to eq '--user' + end + end + + describe '#short_flag_exist?' do + let(:fixture) { :flag_hog } + + context 'when the command has this short flag' do + it 'returns true' do + expect(subject.short_flag_exist?('-h')).to be true + end + end + + context 'when the command does not have this short flag' do + it 'returns false' do + expect(subject.short_flag_exist?('-s')).to be false + end + end + end + + describe '#needy_flags' do + let(:fixture) { :needy_flags } + + it 'returns an array of only the needy Flag objects' do + expect(subject.needy_flags.size).to eq 2 + expect(subject.needy_flags.first.long).to eq '--add' + end + end + + describe '#required_flags' do + it 'returns an array of only the required Flag objects' do + expect(subject.required_flags.size).to eq 1 + expect(subject.required_flags.first.long).to eq '--force' + end + end +end From 5467f7c803a417d472db6c65690dd880a88cf616 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 14 Aug 2024 12:43:54 +0000 Subject: [PATCH 6/7] backfill missing introspection specs --- .../introspection/environment_variables.rb | 2 +- spec/approvals/cli/doc/full | 11 +++++ spec/approvals/cli/doc/index | 1 + .../environment_variables_spec.rb | 36 ++++++++++++++-- .../bashly/script/introspection/flags_spec.rb | 43 +++++++++++-------- spec/fixtures/script/commands.yml | 22 ++++++++++ 6 files changed, 93 insertions(+), 22 deletions(-) diff --git a/lib/bashly/script/introspection/environment_variables.rb b/lib/bashly/script/introspection/environment_variables.rb index 6e99cc42..512204f9 100644 --- a/lib/bashly/script/introspection/environment_variables.rb +++ b/lib/bashly/script/introspection/environment_variables.rb @@ -2,7 +2,7 @@ module Bashly module Script module Introspection module EnvironmentVariables - # Returns an array of all the default Environment Variables + # Returns an array of all the Environment Variables with default values def default_environment_variables environment_variables.select(&:default) end diff --git a/spec/approvals/cli/doc/full b/spec/approvals/cli/doc/full index 18fe0697..67f56ae8 100644 --- a/spec/approvals/cli/doc/full +++ b/spec/approvals/cli/doc/full @@ -539,12 +539,23 @@ environment_variable.required Specify that this variable is required. + environment_variables: - name: api_key help: Your API key required: true See https://bashly.dannyb.co/configuration/environment-variable/#required +environment_variable.validate + + Apply custom validation functions. + + environment_variables: + - name: build_dir + validate: dir_exists + + See https://bashly.dannyb.co/configuration/environment-variable/#validate + flag Define option flags. diff --git a/spec/approvals/cli/doc/index b/spec/approvals/cli/doc/index index d9d7d9d6..00791e68 100644 --- a/spec/approvals/cli/doc/index +++ b/spec/approvals/cli/doc/index @@ -34,6 +34,7 @@ environment_variable.help environment_variable.name environment_variable.private environment_variable.required +environment_variable.validate flag flag.allowed flag.arg diff --git a/spec/bashly/script/introspection/environment_variables_spec.rb b/spec/bashly/script/introspection/environment_variables_spec.rb index f3854097..a400676c 100644 --- a/spec/bashly/script/introspection/environment_variables_spec.rb +++ b/spec/bashly/script/introspection/environment_variables_spec.rb @@ -6,19 +6,47 @@ end let(:fixtures) { load_fixture 'script/commands' } - let(:fixture) { :basic_command } + let(:fixture) { :environment_variable_enthusiast } - describe '#environment_cariables' do + describe '#default_environment_variables' do + it 'returns an array of all the Environment Variables with default values' do + expect(subject.default_environment_variables.size).to eq 1 + expect(subject.default_environment_variables.first.name).to eq 'file' + end + end + + describe '#environment_variables' do it 'returns an array of EnvironmentVariable objects' do expect(subject.environment_variables).to be_an Array - expect(subject.environment_variables.first).to be_a Script::EnvironmentVariable + expect(subject.environment_variables).to all(be_a Script::EnvironmentVariable) + end + end + + describe '#public_environment_variables' do + it 'returns only environment variables that are not private' do + expect(subject.public_environment_variables.size).to eq 4 + expect(subject.public_environment_variables.map(&:name)).not_to include 'secret' end end describe '#required_environment_variables' do it 'returns an array of only the required Argument objects' do expect(subject.required_environment_variables.size).to eq 1 - expect(subject.required_environment_variables.first.name).to eq 'secret_key' + expect(subject.required_environment_variables.first.name).to eq 'username' + end + end + + describe '#validated_environment_variables' do + it 'returns an array of all the environment variables with a validation' do + expect(subject.validated_environment_variables.size).to eq 1 + expect(subject.validated_environment_variables.first.name).to eq 'config_dir' + end + end + + describe '#whitelisted_environment_variables' do + it 'returns an array of all the environment_variables with a whitelist arg' do + expect(subject.whitelisted_environment_variables.size).to eq 1 + expect(subject.whitelisted_environment_variables.first.name).to eq 'protocol' end end end diff --git a/spec/bashly/script/introspection/flags_spec.rb b/spec/bashly/script/introspection/flags_spec.rb index 815a09fa..bdef65b1 100644 --- a/spec/bashly/script/introspection/flags_spec.rb +++ b/spec/bashly/script/introspection/flags_spec.rb @@ -50,12 +50,28 @@ end end - describe '#whitelisted_flags' do - let(:fixture) { :whitelist } + describe '#needy_flags' do + let(:fixture) { :needy_flags } - it 'returns an array of flags that have a whitelist' do - expect(subject.whitelisted_flags.size).to eq 1 - expect(subject.whitelisted_flags.first.long).to eq '--user' + it 'returns an array of only the needy Flag objects' do + expect(subject.needy_flags.size).to eq 2 + expect(subject.needy_flags.first.long).to eq '--add' + end + end + + describe '#public_flags' do + let(:fixture) { :private_flags } + + it 'returns an array of only the non private Flag objects' do + expect(subject.public_flags.size).to eq 1 + expect(subject.public_flags.first.long).to eq '--new' + end + end + + describe '#required_flags' do + it 'returns an array of only the required Flag objects' do + expect(subject.required_flags.size).to eq 1 + expect(subject.required_flags.first.long).to eq '--force' end end @@ -75,19 +91,12 @@ end end - describe '#needy_flags' do - let(:fixture) { :needy_flags } - - it 'returns an array of only the needy Flag objects' do - expect(subject.needy_flags.size).to eq 2 - expect(subject.needy_flags.first.long).to eq '--add' - end - end + describe '#whitelisted_flags' do + let(:fixture) { :whitelist } - describe '#required_flags' do - it 'returns an array of only the required Flag objects' do - expect(subject.required_flags.size).to eq 1 - expect(subject.required_flags.first.long).to eq '--force' + it 'returns an array of flags that have a whitelist' do + expect(subject.whitelisted_flags.size).to eq 1 + expect(subject.whitelisted_flags.first.long).to eq '--user' end end end diff --git a/spec/fixtures/script/commands.yml b/spec/fixtures/script/commands.yml index b4c49077..50a9c427 100644 --- a/spec/fixtures/script/commands.yml +++ b/spec/fixtures/script/commands.yml @@ -199,6 +199,20 @@ help: perform docker container run parents: [docker, container] +:environment_variable_enthusiast: + name: get + environment_variables: + - name: username + required: true + - name: file + default: README.md + - name: config_dir + validate: dir_exists + - name: protocol + allowed: [ssh, https] + - name: secret + private: true + :examples_array: name: get help: get something from somewhere @@ -404,6 +418,14 @@ alias: cf private: true +:private_flags: + name: status + help: perform git status + flags: + - long: --legacy + private: true + - long: --new + :repeatable_arg: name: get args: From 72efef785945d85d22f94034c2e4a9e69d75bae9 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 14 Aug 2024 17:19:23 +0300 Subject: [PATCH 7/7] Update schemas/strings.json Co-authored-by: Maisa Unbelievable --- schemas/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schemas/strings.json b/schemas/strings.json index 55ee58dc..60a864b9 100644 --- a/schemas/strings.json +++ b/schemas/strings.json @@ -243,7 +243,7 @@ "default": "validation error in %s:\\n%s" }, "environment_variable_validation_error": { - "title": "validation error", + "title": "environment variable validation error", "description": "The error message template for failed custom validation for environment variables\nhttps://bashly.dannyb.co/advanced/strings/#custom-strings", "type": "string", "minLength": 1,