Commit 9d7818c

mo khan <mo@mokhan.ca>
2026-08-05 12:43:11
refactor: confine thor to the CLI entry point cli
cli
1 parent c44db3e
lib/scim/kit/cli/app.rb
@@ -108,7 +108,7 @@ module Scim
         end
 
         def reporter
-          @reporter ||= Reporter.new(shell)
+          @reporter ||= Reporter.new
         end
 
         def settings
lib/scim/kit/cli/reporter.rb
@@ -7,8 +7,9 @@ module Scim
         SUCCESS = 0
         FAILURE = 1
 
-        def initialize(shell)
-          @shell = shell
+        def initialize(out = $stdout, err = $stderr)
+          @out = out
+          @err = err
         end
 
         def report(result)
@@ -16,7 +17,7 @@ module Scim
         end
 
         def report_validation(result, errors)
-          shell.say(pretty(result.body))
+          out.puts(pretty(result.body))
           return SUCCESS if errors.empty?
 
           failure(validation_errors: errors)
@@ -25,27 +26,27 @@ module Scim
         # --validate was asked for and could not be carried out, so the
         # response is reported but the command still fails.
         def report_unvalidated(result)
-          shell.say(pretty(result.body))
+          out.puts(pretty(result.body))
           FAILURE
         end
 
         def success(body)
-          shell.say(pretty(body))
+          out.puts(pretty(body))
           SUCCESS
         end
 
         def failure(body)
-          shell.say_error(pretty(body))
+          err.puts(pretty(body))
           FAILURE
         end
 
         def warn(message)
-          shell.say_error("warning: #{message}")
+          err.puts("warning: #{message}")
         end
 
         private
 
-        attr_reader :shell
+        attr_reader :out, :err
 
         def pretty(body)
           JSON.pretty_generate(body)
lib/scim/kit/cli/settings.rb
@@ -47,10 +47,10 @@ module Scim
         attr_reader :options, :env
 
         def validate(url)
-          raise Thor::Error, '--url is required' if url.to_s.empty?
+          raise InvalidOption, '--url is required' if url.to_s.empty?
 
           unless absolute_http?(url)
-            raise Thor::Error,
+            raise InvalidOption,
               "--url must be an absolute http(s) URL, got #{url.inspect}"
           end
 
@@ -67,7 +67,7 @@ module Scim
         def split_header(header)
           name, value = header.split(':', 2)
           if value.nil?
-            raise Thor::Error,
+            raise InvalidOption,
               "malformed --header #{header.inspect} " \
               '(expected "Name: Value")'
           end
lib/scim/kit/cli.rb
@@ -46,6 +46,8 @@ module Scim
 
       class InvalidResponse < Error; end
 
+      class InvalidOption < Error; end
+
       # SCIM collection endpoints (/Schemas, /ResourceTypes) return a
       # ListResponse per RFC 7644 section 4, but some servers return a
       # bare array. Return the underlying array for either shape, or nil.
spec/scim/kit/cli/app_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe Scim::Kit::Cli::App do
       end
 
       it 'exits 1' do
-        allow($stderr).to receive(:print)
+        allow($stderr).to receive(:puts)
 
         expect(exit_status { call.call(app, 'Nope') }).to eq(1)
       end
@@ -47,7 +47,7 @@ RSpec.describe Scim::Kit::Cli::App do
       end
 
       it 'exits 1' do
-        allow($stderr).to receive(:print)
+        allow($stderr).to receive(:puts)
 
         expect(exit_status { call.call(app, 'User') }).to eq(1)
       end
@@ -80,7 +80,7 @@ RSpec.describe Scim::Kit::Cli::App do
       end
 
       it 'exits 0' do
-        allow($stdout).to receive(:print)
+        allow($stdout).to receive(:puts)
 
         expect(exit_status { app.discover }).to eq(0)
       end
@@ -93,14 +93,14 @@ RSpec.describe Scim::Kit::Cli::App do
       end
 
       it 'reports the failure without requesting Schemas' do
-        allow($stderr).to receive(:print)
+        allow($stderr).to receive(:puts)
         exit_status { app.discover }
 
         expect(a_request(:get, "#{base_url}/Schemas")).not_to have_been_made
       end
 
       it 'exits 1' do
-        allow($stderr).to receive(:print)
+        allow($stderr).to receive(:puts)
 
         expect(exit_status { app.discover }).to eq(1)
       end
@@ -166,7 +166,7 @@ RSpec.describe Scim::Kit::Cli::App do
       end
 
       it 'exits 0' do
-        allow($stdout).to receive(:print)
+        allow($stdout).to receive(:puts)
         instance = app('validate' => true)
 
         expect(exit_status { instance.discover }).to eq(0)
@@ -193,7 +193,7 @@ RSpec.describe Scim::Kit::Cli::App do
       end
 
       it 'flags the bare array as non-compliant with ListResponse format' do
-        allow($stdout).to receive(:print)
+        allow($stdout).to receive(:puts)
         instance = app('validate' => true)
 
         expect { exit_status { instance.discover } }
@@ -214,7 +214,7 @@ RSpec.describe Scim::Kit::Cli::App do
       end
 
       it 'prints validation errors to stderr' do
-        allow($stdout).to receive(:print)
+        allow($stdout).to receive(:puts)
         instance = app('validate' => true)
 
         expect { exit_status { instance.discover } }
@@ -222,8 +222,8 @@ RSpec.describe Scim::Kit::Cli::App do
       end
 
       it 'exits 1' do
-        allow($stdout).to receive(:print)
-        allow($stderr).to receive(:print)
+        allow($stdout).to receive(:puts)
+        allow($stderr).to receive(:puts)
         instance = app('validate' => true)
 
         expect(exit_status { instance.discover }).to eq(1)
@@ -268,7 +268,7 @@ RSpec.describe Scim::Kit::Cli::App do
       end
 
       it 'exits 0' do
-        allow($stdout).to receive(:print)
+        allow($stdout).to receive(:puts)
 
         expect(exit_status { instance.list('User') }).to eq(0)
       end
@@ -286,7 +286,7 @@ RSpec.describe Scim::Kit::Cli::App do
       end
 
       it 'exits 1' do
-        allow($stderr).to receive(:print)
+        allow($stderr).to receive(:puts)
 
         expect(exit_status { app.list('User') }).to eq(1)
       end
@@ -330,7 +330,7 @@ RSpec.describe Scim::Kit::Cli::App do
         end
 
         it 'exits 0' do
-          allow($stdout).to receive(:print)
+          allow($stdout).to receive(:puts)
           instance = app('validate' => true)
 
           expect(exit_status { instance.list('User') }).to eq(0)
@@ -351,7 +351,7 @@ RSpec.describe Scim::Kit::Cli::App do
         end
 
         it 'exits 0 without demanding attributes the server was not asked for' do
-          allow($stdout).to receive(:print)
+          allow($stdout).to receive(:puts)
           instance = app('validate' => true, 'attributes' => 'id')
 
           expect(exit_status { instance.list('User') }).to eq(0)
@@ -381,7 +381,7 @@ RSpec.describe Scim::Kit::Cli::App do
         end
 
         it 'warns about the undeclared extension' do
-          allow($stdout).to receive(:print)
+          allow($stdout).to receive(:puts)
           instance = app('validate' => true)
 
           expect { exit_status { instance.list('User') } }
@@ -389,8 +389,8 @@ RSpec.describe Scim::Kit::Cli::App do
         end
 
         it 'still exits 0' do
-          allow($stdout).to receive(:print)
-          allow($stderr).to receive(:print)
+          allow($stdout).to receive(:puts)
+          allow($stderr).to receive(:puts)
           instance = app('validate' => true)
 
           expect(exit_status { instance.list('User') }).to eq(0)
@@ -411,7 +411,7 @@ RSpec.describe Scim::Kit::Cli::App do
         end
 
         it 'prints validation errors to stderr' do
-          allow($stdout).to receive(:print)
+          allow($stdout).to receive(:puts)
           instance = app('validate' => true)
 
           expect { exit_status { instance.list('User') } }
@@ -419,8 +419,8 @@ RSpec.describe Scim::Kit::Cli::App do
         end
 
         it 'exits 1' do
-          allow($stdout).to receive(:print)
-          allow($stderr).to receive(:print)
+          allow($stdout).to receive(:puts)
+          allow($stderr).to receive(:puts)
           instance = app('validate' => true)
 
           expect(exit_status { instance.list('User') }).to eq(1)
@@ -441,7 +441,7 @@ RSpec.describe Scim::Kit::Cli::App do
         end
 
         it 'warns that it could not validate' do
-          allow($stdout).to receive(:print)
+          allow($stdout).to receive(:puts)
           instance = app('validate' => true)
 
           expect { exit_status { instance.list('User') } }
@@ -449,8 +449,8 @@ RSpec.describe Scim::Kit::Cli::App do
         end
 
         it 'exits 1 rather than reporting an unvalidated success' do
-          allow($stdout).to receive(:print)
-          allow($stderr).to receive(:print)
+          allow($stdout).to receive(:puts)
+          allow($stderr).to receive(:puts)
           instance = app('validate' => true)
 
           expect(exit_status { instance.list('User') }).to eq(1)
@@ -478,7 +478,7 @@ RSpec.describe Scim::Kit::Cli::App do
       end
 
       it 'exits 0' do
-        allow($stdout).to receive(:print)
+        allow($stdout).to receive(:puts)
 
         expect(exit_status { instance.get('User', '123') }).to eq(0)
       end
@@ -488,7 +488,7 @@ RSpec.describe Scim::Kit::Cli::App do
       it 'escapes a space rather than raising URI::InvalidURIError' do
         stub = stub_request(:get, "#{base_url}/Users/mo%20khan")
           .to_return(status: 200, body: {}.to_json)
-        allow($stdout).to receive(:print)
+        allow($stdout).to receive(:puts)
 
         exit_status { app.get('User', 'mo khan') }
 
@@ -498,7 +498,7 @@ RSpec.describe Scim::Kit::Cli::App do
       it 'escapes separators so an id cannot traverse the endpoint' do
         stub = stub_request(:get, "#{base_url}/Users/..%2Fadmin%23x%3Fy")
           .to_return(status: 200, body: {}.to_json)
-        allow($stdout).to receive(:print)
+        allow($stdout).to receive(:puts)
 
         exit_status { app.get('User', '../admin#x?y') }
 
@@ -518,7 +518,7 @@ RSpec.describe Scim::Kit::Cli::App do
       end
 
       it 'exits 1' do
-        allow($stderr).to receive(:print)
+        allow($stderr).to receive(:puts)
 
         expect(exit_status { app.get('User', '123') }).to eq(1)
       end
@@ -556,7 +556,7 @@ RSpec.describe Scim::Kit::Cli::App do
         end
 
         it 'exits 0' do
-          allow($stdout).to receive(:print)
+          allow($stdout).to receive(:puts)
           instance = app('validate' => true)
 
           expect(exit_status { instance.get('User', '123') }).to eq(0)
@@ -571,7 +571,7 @@ RSpec.describe Scim::Kit::Cli::App do
         end
 
         it 'prints validation errors to stderr' do
-          allow($stdout).to receive(:print)
+          allow($stdout).to receive(:puts)
           instance = app('validate' => true)
 
           expect { exit_status { instance.get('User', '123') } }
@@ -579,8 +579,8 @@ RSpec.describe Scim::Kit::Cli::App do
         end
 
         it 'exits 1' do
-          allow($stdout).to receive(:print)
-          allow($stderr).to receive(:print)
+          allow($stdout).to receive(:puts)
+          allow($stderr).to receive(:puts)
           instance = app('validate' => true)
 
           expect(exit_status { instance.get('User', '123') }).to eq(1)
@@ -598,7 +598,7 @@ RSpec.describe Scim::Kit::Cli::App do
     end
 
     it 'sends repeated --header flags as request headers' do
-      allow($stdout).to receive(:print)
+      allow($stdout).to receive(:puts)
       instance = app('header' => ['Authorization: Bearer xyz', 'X-Test: value'])
 
       exit_status { instance.list('User') }
@@ -620,7 +620,7 @@ RSpec.describe Scim::Kit::Cli::App do
     end
 
     it 'exits 1 with a usage message when RESOURCE_TYPE is missing' do
-      allow($stderr).to receive(:print)
+      allow($stderr).to receive(:puts)
 
       expect { exit_status { described_class.start(['list', '--url', base_url]) } }
         .to output(/no arguments/).to_stderr
@@ -636,7 +636,7 @@ RSpec.describe Scim::Kit::Cli::App do
 
     it 'reads --url from SCIM_KIT_URL when --url is omitted' do
       ENV['SCIM_KIT_URL'] = base_url
-      allow($stdout).to receive(:print)
+      allow($stdout).to receive(:puts)
 
       status = exit_status { described_class.start(%w[list User]) }
 
@@ -652,7 +652,7 @@ RSpec.describe Scim::Kit::Cli::App do
       end
 
       before do
-        allow($stdout).to receive(:print)
+        allow($stdout).to receive(:puts)
         stub_request(:get, "#{base_url}/Users")
           .with(headers: headers).to_return(status: 200, body: '{}')
       end
@@ -667,7 +667,7 @@ RSpec.describe Scim::Kit::Cli::App do
     end
 
     it 'exits 1 with a usage message for a malformed --header' do
-      allow($stderr).to receive(:print)
+      allow($stderr).to receive(:puts)
       argv = ['list', 'User', '--url', base_url, '--header', 'BearerXYZ']
 
       expect { exit_status { described_class.start(argv) } }
spec/scim/kit/cli/reporter_spec.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 RSpec.describe Scim::Kit::Cli::Reporter do
-  subject { described_class.new(Thor::Shell::Basic.new) }
+  subject { described_class.new }
 
   let(:body) { { id: '123' } }
   let(:pretty) { JSON.pretty_generate(body) }
@@ -15,7 +15,7 @@ RSpec.describe Scim::Kit::Cli::Reporter do
       end
 
       it 'returns a success status' do
-        allow($stdout).to receive(:print)
+        allow($stdout).to receive(:puts)
 
         expect(subject.report(result)).to eq(0)
       end
@@ -29,7 +29,7 @@ RSpec.describe Scim::Kit::Cli::Reporter do
       end
 
       it 'returns a failure status' do
-        allow($stderr).to receive(:print)
+        allow($stderr).to receive(:puts)
 
         expect(subject.report(result)).to eq(1)
       end
@@ -46,7 +46,7 @@ RSpec.describe Scim::Kit::Cli::Reporter do
       end
 
       it 'returns a success status' do
-        allow($stdout).to receive(:print)
+        allow($stdout).to receive(:puts)
 
         expect(subject.report_validation(result, [])).to eq(0)
       end
@@ -56,22 +56,22 @@ RSpec.describe Scim::Kit::Cli::Reporter do
       let(:errors) { ['root is missing required keys: id'] }
 
       it 'prints the errors to stderr' do
-        allow($stdout).to receive(:print)
+        allow($stdout).to receive(:puts)
 
         expect { subject.report_validation(result, errors) }
           .to output(/validation_errors/).to_stderr
       end
 
       it 'still prints the body to stdout' do
-        allow($stderr).to receive(:print)
+        allow($stderr).to receive(:puts)
 
         expect { subject.report_validation(result, errors) }
           .to output(/#{pretty}/).to_stdout
       end
 
       it 'returns a failure status' do
-        allow($stdout).to receive(:print)
-        allow($stderr).to receive(:print)
+        allow($stdout).to receive(:puts)
+        allow($stderr).to receive(:puts)
 
         expect(subject.report_validation(result, errors)).to eq(1)
       end
@@ -85,7 +85,7 @@ RSpec.describe Scim::Kit::Cli::Reporter do
     end
 
     it 'returns a failure status' do
-      allow($stderr).to receive(:print)
+      allow($stderr).to receive(:puts)
 
       expect(subject.failure(detail: 'boom')).to eq(1)
     end
@@ -97,7 +97,7 @@ RSpec.describe Scim::Kit::Cli::Reporter do
     end
 
     it 'returns a success status' do
-      allow($stdout).to receive(:print)
+      allow($stdout).to receive(:puts)
 
       expect(subject.success(body)).to eq(0)
     end
@@ -112,7 +112,7 @@ RSpec.describe Scim::Kit::Cli::Reporter do
     end
 
     it 'returns a failure status' do
-      allow($stdout).to receive(:print)
+      allow($stdout).to receive(:puts)
 
       expect(subject.report_unvalidated(result)).to eq(1)
     end
spec/scim/kit/cli/resource_validation_spec.rb
@@ -3,7 +3,7 @@
 RSpec.describe Scim::Kit::Cli::ResourceValidation do
   subject { described_class.new(resolver, reporter) }
 
-  let(:reporter) { Scim::Kit::Cli::Reporter.new(Thor::Shell::Basic.new) }
+  let(:reporter) { Scim::Kit::Cli::Reporter.new }
   let(:entry) { { name: 'User', schema: core_urn } }
   let(:core_urn) { 'urn:ietf:params:scim:schemas:core:2.0:User' }
   let(:schema) do
@@ -62,7 +62,7 @@ RSpec.describe Scim::Kit::Cli::ResourceValidation do
       end
 
       it 'returns nil so the caller can flag it as unvalidated' do
-        allow($stderr).to receive(:print)
+        allow($stderr).to receive(:puts)
 
         expect(subject.errors_for(entry, resource)).to be_nil
       end
@@ -87,7 +87,7 @@ RSpec.describe Scim::Kit::Cli::ResourceValidation do
       end
 
       it 'still validates the resource' do
-        allow($stderr).to receive(:print)
+        allow($stderr).to receive(:puts)
 
         expect(subject.errors_for(entry, resource)).to eql([])
       end
spec/scim/kit/cli/settings_spec.rb
@@ -18,27 +18,27 @@ RSpec.describe Scim::Kit::Cli::Settings do
 
     it 'raises when neither is given' do
       expect { settings({}).url }
-        .to raise_error(Thor::Error, /--url is required/)
+        .to raise_error(Scim::Kit::Cli::InvalidOption, /--url is required/)
     end
 
     it 'raises when the url is blank' do
       expect { settings({ url: '' }).url }
-        .to raise_error(Thor::Error, /--url is required/)
+        .to raise_error(Scim::Kit::Cli::InvalidOption, /--url is required/)
     end
 
     it 'raises when the url has no scheme' do
       expect { settings({ url: 'example.com/scim/v2' }).url }
-        .to raise_error(Thor::Error, /--url must be an absolute http/)
+        .to raise_error(Scim::Kit::Cli::InvalidOption, /--url must be an absolute http/)
     end
 
     it 'raises when the url scheme is not http(s)' do
       expect { settings({ url: 'ftp://example.com' }).url }
-        .to raise_error(Thor::Error, /--url must be an absolute http/)
+        .to raise_error(Scim::Kit::Cli::InvalidOption, /--url must be an absolute http/)
     end
 
     it 'raises when the url is unparseable' do
       expect { settings({ url: 'http://exa mple.com' }).url }
-        .to raise_error(Thor::Error, /--url must be an absolute http/)
+        .to raise_error(Scim::Kit::Cli::InvalidOption, /--url must be an absolute http/)
     end
 
     it 'accepts an http url' do
@@ -71,7 +71,7 @@ RSpec.describe Scim::Kit::Cli::Settings do
 
     it 'raises on a header without a colon' do
       expect { settings({ header: ['nope'] }).headers }
-        .to raise_error(Thor::Error, /malformed --header/)
+        .to raise_error(Scim::Kit::Cli::InvalidOption, /malformed --header/)
     end
   end