Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ eval_gemfile "Gemfile.devtools"
gemspec

gem "backports", "~> 3.15.0", require: false
gem "dry-types", require: false

unless ENV["CI"]
gem "yard", require: false
Expand Down
7 changes: 7 additions & 0 deletions lib/dry/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ class CLI
require "dry/cli/spell_checker"
require "dry/cli/banner"
require "dry/cli/inflector"
require "dry/cli/extensions"

extend Dry::CLI::Extensions

register_extension(:dry_types) do
require "dry/types"
end

# Check if command
#
Expand Down
44 changes: 44 additions & 0 deletions lib/dry/cli/extensions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

module Dry
class CLI
# Provides extension support based on Dry::Core::Extension.
# This module should only ever be used internally.
module Extensions
# @api private
def self.extended(obj)
super
obj.instance_variable_set(:@__available_extensions__, {})
obj.instance_variable_set(:@__loaded_extensions__, ::Set.new)
end

def register_extension(name, &block)
@__available_extensions__[name] = block
end

def loaded_extension?(name)
@__loaded_extensions__&.include?(name)
end

def unload_extension(name)
@__loaded_extensions__.delete(name)
end

def available_extension?(name)
@__available_extensions__.key?(name)
end

def load_extensions(*extensions)
extensions.each do |ext|
block = @__available_extensions__.fetch(ext) do
raise ::ArgumentError, "Unknown extension: #{ext.inspect}"
end
unless @__loaded_extensions__.include?(ext)
block.call
@__loaded_extensions__ << ext
end
end
end
end
end
end
13 changes: 13 additions & 0 deletions lib/dry/cli/option.rb
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,19 @@ def valid_value?(value)
available_values.map(&:to_s).include?(value.to_s)
end
end

def type_cast(value)
if Dry::CLI.loaded_extension?(:dry_types) &&
type.is_a?(Dry::Types::Type)
begin
type.call(value)
rescue Dry::Types::CoercionError
raise ValueError
end
else
value
end
end
end

# Command line argument
Expand Down
8 changes: 4 additions & 4 deletions lib/dry/cli/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def self.call(command, arguments, prog_name)
OptionParser.new do |opts|
command.options.each do |option|
opts.on(*option.parser_options) do |value|
parsed_options[option.name.to_sym] = value
parsed_options[option.name.to_sym] = option.type_cast(value)
end
end

Expand Down Expand Up @@ -81,10 +81,10 @@ def self.match_arguments(command_arguments, arguments, default_values)
result[cmd_arg.name] = arg
break
else
arg = arguments.at(index) || default_values[cmd_arg.name]
raise ValueError unless cmd_arg.valid_value?(arg)
value = arguments.at(index) || default_values[cmd_arg.name]
raise ValueError unless cmd_arg.valid_value?(value)

result[cmd_arg.name] = arg
result[cmd_arg.name] = cmd_arg.type_cast(value)
end
end

Expand Down
32 changes: 32 additions & 0 deletions spec/integration/dry_types_extension_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

require "open3"
require "json"

RSpec.describe "Dry Types extension" do
it "works with default values" do
output, _, = Open3.capture3("with_dry_types info system")

expect(JSON.parse(output.chomp)).to eq(
{"lines" => 10, "sudo_mode" => false, "type" => "system"}
)
end

it "casts values according to types" do
output, _, = Open3.capture3("with_dry_types info system 15 --sudo_mode true")

expect(JSON.parse(output.chomp)).to eq(
{"args" => ["15"], "lines" => 15, "sudo_mode" => true, "type" => "system"}
)
end

it "raises a standar Dry CLI error when casting fails" do
_, stderr, = Open3.capture3("with_dry_types info system --sudo_mode not_false")
expect(stderr).to eq("ERROR: \"with_dry_types info\" was called with arguments \"system --sudo_mode not_false\"\n")
end

it "does not use Dry Types extension if it's not loaded" do
output, _, = Open3.capture3("DONT_LOAD_EXTENSION=1 with_dry_types info system 17 --sudo_mode true")
expect(JSON.parse(output.chomp)).to eq({"args" => ["17"], "lines" => "17", "sudo_mode" => "true", "type" => "system"})
end
end
35 changes: 35 additions & 0 deletions spec/support/fixtures/with_dry_types
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

$LOAD_PATH.unshift File.expand_path("#{__dir__}/../../../lib")
require "dry/cli"
require_relative "../../../lib/dry/cli/command"
require "json"

if ENV["DONT_LOAD_EXTENSION"]
require "dry/types"
else
Dry::CLI.load_extensions(:dry_types)
end

Types = Dry.Types()

module WithDryTypes
extend Dry::CLI::Registry

class Info < Dry::CLI::Command
desc "Display info"

argument :type, desc: "Type of the information", required: true
argument :lines, desc: "Number of lines to show", type: Types::Coercible::Integer, default: 10
option :sudo_mode, desc: "Enable sudo mode?", type: Types::Params::Bool, default: false

def call(**options)
puts JSON.dump(options)
end
end

register "info", Info
end

Dry.CLI(WithDryTypes).call
4 changes: 4 additions & 0 deletions spec/support/rspec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
meta[:aggregate_failures] = true
end

config.after(:each) do
Dry::CLI.unload_extension(:dry_types) if Dry::CLI.available_extension?(:dry_types)
end

if ENV["CI"]
# No focused specs should be committed. This ensures
# builds fail when this happens.
Expand Down