diff --git a/bin/mindee.rb b/bin/mindee.rb index f6900dbe..f0abeaf5 100755 --- a/bin/mindee.rb +++ b/bin/mindee.rb @@ -6,24 +6,29 @@ require_relative 'v1/parser' require_relative 'v2/parser' -def setup_main_parser - v1_parser = MindeeCLI::V1Parser.new(ARGV) - v2_parser = MindeeCLI::V2Parser.new(ARGV) - main_parser = OptionParser.new do |opts| - opts.banner = "Usage: mindee [command]" - opts.separator "Commands:" - opts.separator " v1 Use Version 1 of the Mindee API" - opts.separator " v2 Use Version 2 of the Mindee API" +def root_help + help = "Usage: mindee command [options]\n\nAvailable commands:\n" + help += " #{'v1'.ljust(50)}Use Version 1 of the Mindee API\n" + help += " #{'search-models'.ljust(50)}Search for available models for this API key\n" + + V2_PRODUCTS.each do |product_key, product_values| + help += " #{product_key.ljust(50)}#{product_values[:description]}\n" end - main_command = ARGV.shift - case main_command - when 'v1' - v1_parser.execute - when 'v2' - v2_parser.execute + help +end + +def setup_main_parser + main_command = ARGV.first + + if main_command == 'v1' + ARGV.shift + MindeeCLI::V1Parser.new(ARGV).execute + elsif main_command.nil? || %w[help -h --help].include?(main_command) + abort(root_help) else - abort(main_parser.help) + ARGV.shift if main_command == 'v2' + MindeeCLI::V2Parser.new(ARGV, command_prefix: 'mindee').execute end end diff --git a/bin/v2/parser.rb b/bin/v2/parser.rb index 4f388d5b..a96e4dcf 100644 --- a/bin/v2/parser.rb +++ b/bin/v2/parser.rb @@ -19,10 +19,11 @@ class V2Parser # @return [Parser] attr_reader :search_parser - def initialize(arguments) + def initialize(arguments, command_prefix: 'mindee v2') @arguments = arguments + @command_prefix = command_prefix @options_parser = OptionParser.new do |opts| - opts.banner = 'Usage: mindee v2 command [options]' + opts.banner = "Usage: #{@command_prefix} command [options]" end @product_parser = init_product_parser @search_parser = init_search_parser @@ -67,6 +68,8 @@ def execute else abort("#{e.message}\n\n#{@product_parser[command].help}") end + rescue Mindee::Error::MindeeError => e + abort(format_cli_error(e)) end private @@ -83,9 +86,21 @@ def validate_command!(command) abort(error_msg) end + def format_cli_error(error) + if error.is_a?(Mindee::Error::MindeeHTTPErrorV2) && error.status.to_i == 401 + "CLI error: Missing credentials. Provide an API key using '--key' or " \ + "the 'MINDEE_V2_API_KEY' environment variable." + elsif error.is_a?(Mindee::Error::MindeeAPIError) && error.message.include?('Missing API key') + "CLI error: Missing API key. Provide it using '--key' or " \ + "the 'MINDEE_V2_API_KEY' environment variable." + else + "CLI error: #{error.message}" + end + end + def init_search_parser OptionParser.new do |options_parser| - options_parser.banner = 'Usage: mindee v2 search-models [options]' + options_parser.banner = "Usage: #{@command_prefix} search-models [options]" init_common_options(options_parser) options_parser.on('-n [NAME]', '--name [NAME]', 'Search for partial matches in model name. Note: case insensitive') do |v| @@ -159,7 +174,7 @@ def init_product_parser v2_product_parser = {} V2_PRODUCTS.each do |product_key, product_values| v2_product_parser[product_key] = OptionParser.new do |options_parser| - options_parser.banner = "Usage: mindee v2 #{product_key} [options] file" + options_parser.banner = "Usage: #{@command_prefix} #{product_key} [options] file" options_parser.on('-m MODEL_ID', '--model-id MODEL_ID', 'Model ID') { |v| @options[:model_id] = v } options_parser.on('-a ALIAS', '--alias ALIAS', 'Add a file alias to the response') do |v| @options[:alias] = v diff --git a/spec/bin/cli_integration.rb b/spec/bin/cli_integration.rb index ede5c4af..acf58699 100644 --- a/spec/bin/cli_integration.rb +++ b/spec/bin/cli_integration.rb @@ -21,32 +21,32 @@ def run_cli(*args) context 'search-models command' do ['classification', 'crop', 'extraction', 'ocr', 'split'].each do |model_type| it "returns model list for type #{model_type}" do - stdout, stderr, status = run_cli('v2', 'search-models', '-t', model_type) + stdout, stderr, status = run_cli('search-models', '-t', model_type) expect(status.success?).to eq(true), stderr expect(stdout.strip).not_to be_empty end end it 'returns no models for non-existent name' do - stdout, stderr, status = run_cli('v2', 'search-models', '-n', 'supercalifragilisticexpialidocious') + stdout, stderr, status = run_cli('search-models', '-n', 'supercalifragilisticexpialidocious') expect(status.success?).to eq(true), stderr expect(stdout.strip).to eq('') end it 'returns models for name filter' do - stdout, stderr, status = run_cli('v2', 'search-models', '-n', 'findoc') + stdout, stderr, status = run_cli('search-models', '-n', 'findoc') expect(status.success?).to eq(true), stderr expect(stdout.strip).not_to be_empty end it 'returns models for name and model_type filters' do - stdout, stderr, status = run_cli('v2', 'search-models', '-n', 'findoc', '-t', 'extraction') + stdout, stderr, status = run_cli('search-models', '-n', 'findoc', '-t', 'extraction') expect(status.success?).to eq(true), stderr expect(stdout.strip).not_to be_empty end it 'returns HTTP 422 on invalid model type' do - stdout, stderr, status = run_cli('v2', 'search-models', '-t', 'invalid') + stdout, stderr, status = run_cli('search-models', '-t', 'invalid') expect(status.success?).to eq(false) expect("#{stdout}\n#{stderr}").to include('HTTP 422') end @@ -54,7 +54,7 @@ def run_cli(*args) context 'product commands' do it 'runs extraction from an URL source' do - stdout, stderr, status = run_cli('v2', 'extraction', '-m', findoc_model_id, blank_pdf_url) + stdout, stderr, status = run_cli('extraction', '-m', findoc_model_id, blank_pdf_url) expect(status.success?).to eq(true), stderr expect(stdout.strip).not_to be_empty end @@ -66,7 +66,7 @@ def run_cli(*args) 'split' => -> { split_model_id }, }.each do |command, model_id_proc| it "runs #{command} with default args" do - stdout, stderr, status = run_cli('v2', command, '-m', instance_exec(&model_id_proc), test_file) + stdout, stderr, status = run_cli(command, '-m', instance_exec(&model_id_proc), test_file) expect(status.success?).to eq(true), stderr expect(stdout.strip).not_to be_empty end @@ -82,7 +82,7 @@ def run_cli(*args) ['-t', 'toto'], ].each do |option_args| it "runs extraction with #{option_args.join(' ')}" do - stdout, stderr, status = run_cli('v2', 'extraction', '-m', findoc_model_id, test_file, *option_args) + stdout, stderr, status = run_cli('extraction', '-m', findoc_model_id, test_file, *option_args) expect(status.success?).to eq(true), stderr expect(stdout.strip).not_to be_empty end diff --git a/spec/test_v1_cli.sh b/spec/test_v1_cli.sh index 166d9e7d..e5f2cf13 100755 --- a/spec/test_v1_cli.sh +++ b/spec/test_v1_cli.sh @@ -40,6 +40,15 @@ if [ "$RID" = "win-x64" ]; then CLI_PATH="${CLI_PATH}.exe" fi +echo "--- Test main menu includes v1 command" +HELP_OUTPUT=$("$CLI_PATH" 2>&1 || true) +if echo "$HELP_OUTPUT" | grep -q "v1"; then + echo "Main menu includes v1" +else + echo "Error: v1 command missing from main menu" + exit 1 +fi + PRODUCTS="financial-document receipt invoice invoice-splitter" PRODUCTS_SIZE=4 i=1 diff --git a/spec/test_v2_cli.sh b/spec/test_v2_cli.sh index 89c4b406..b2feff66 100755 --- a/spec/test_v2_cli.sh +++ b/spec/test_v2_cli.sh @@ -36,7 +36,7 @@ else fi echo "--- Test model list retrieval (all models)" -MODELS=$("$CLI_PATH" v2 search-models) +MODELS=$("$CLI_PATH" search-models) if [ -z "$MODELS" ]; then echo "Error: no models found" exit 1 @@ -45,7 +45,7 @@ else fi echo "--- Test extraction with no additional args" -SUMMARY_OUTPUT=$("$CLI_PATH" v2 extraction -m "$MINDEE_V2_SE_TESTS_FINDOC_MODEL_ID" "$TEST_FILE") +SUMMARY_OUTPUT=$("$CLI_PATH" extraction -m "$MINDEE_V2_SE_TESTS_FINDOC_MODEL_ID" "$TEST_FILE") if [ -z "$SUMMARY_OUTPUT" ]; then echo "Error: no extraction output" exit 1 diff --git a/spec/v2/parser_spec.rb b/spec/v2/parser_spec.rb index 97d2dc81..1d0f9be5 100644 --- a/spec/v2/parser_spec.rb +++ b/spec/v2/parser_spec.rb @@ -21,4 +21,35 @@ double_encoded = JSON.generate(JSON.generate(payload)) expect(parser.__send__(:raw_payload, double_encoded)).to eq(payload) end + it 'formats auth API errors as a CLI credential message' do + cli_parser = described_class.new(['search-models']) + error = Mindee::Error::MindeeHTTPErrorV2.new( + { + 'status' => 401, + 'title' => 'Missing credentials', + 'code' => '401-008', + 'detail' => 'Credentials are required.', + 'errors' => [], + } + ) + allow(cli_parser).to receive(:validate_command!) + allow(cli_parser).to receive(:print_result).and_raise(error) + + expect(cli_parser).to receive(:abort).with( + "CLI error: Missing credentials. Provide an API key using '--key' or " \ + "the 'MINDEE_V2_API_KEY' environment variable." + ).and_raise(SystemExit.new(1)) + + expect { cli_parser.execute }.to raise_error(SystemExit) + end + + it 'prefixes generic Mindee errors as CLI errors' do + cli_parser = described_class.new(['search-models']) + allow(cli_parser).to receive(:validate_command!) + allow(cli_parser).to receive(:print_result).and_raise(Mindee::Error::MindeeAPIError, 'boom') + + expect(cli_parser).to receive(:abort).with('CLI error: boom').and_raise(SystemExit.new(1)) + + expect { cli_parser.execute }.to raise_error(SystemExit) + end end