Skip to content

Implement argument and option type casting#146

Open
gustavothecoder wants to merge 1 commit into
dry-rb:mainfrom
gustavothecoder:type-casting
Open

Implement argument and option type casting#146
gustavothecoder wants to merge 1 commit into
dry-rb:mainfrom
gustavothecoder:type-casting

Conversation

@gustavothecoder
Copy link
Copy Markdown
Contributor

As reported in the issue #129, although we have the type config when defining arguments and options, we don't type cast the value for the user, which for some may seem like a bug.

This PR implement a simple type casting logic for some types. Since some types that users currently receive as string will start to be received as another type (e.g. integer), this is a breaking change.

The CLI bellow demonstrates all available types:

require 'dry/cli'

module Demo
  extend Dry::CLI::Registry

  class TypeCast < Dry::CLI::Command
    argument :a_int, type: :integer
    argument :a_flo, type: :float
    argument :a_boo, type: :boolean
    argument :a_fla, type: :flag
    argument :a_str, type: :string
    argument :a_arr, type: :array
    option :o_int, type: :integer
    option :o_flo, type: :float
    option :o_boo, type: :boolean
    option :o_fla, type: :flag
    option :o_str, type: :string
    option :o_arr, type: :array

    def call(a_int:, a_flo:, a_boo:, a_fla:, a_str:, a_arr:, o_int:, o_flo:, o_boo:, o_fla:, o_str:, o_arr:, **)
      puts <<~HEREDOC
        Arguments:
          #{a_int}\t\t\t#{a_int.class}
          #{a_flo}\t\t\t#{a_flo.class}
          #{a_boo}\t\t\t#{a_boo.class}
          #{a_fla}\t\t\t#{a_fla.class}
          #{a_str}\t#{a_str.class}
          #{a_arr}\t#{a_arr.class}
        Options:
          #{o_int}\t\t\t#{o_int.class}
          #{o_flo}\t\t\t#{o_flo.class}
          #{o_boo}\t\t\t#{o_boo.class}
          #{o_fla}\t\t\t#{o_fla.class}
          #{o_str}\t#{o_str.class}
          #{o_arr}\t#{o_arr.class}
      HEREDOC
    end
  end

  register "type-cast", TypeCast
end

Dry::CLI.new(Demo).call

The output:

$ demo type-cast 42 42.42 t off 'This is a string' 1 2 3 --o_int=42 --o_flo=42.42 --no-o_boo --o_fla --o_str='this is a string' --o_arr=1,2,3

# Before:
Arguments:
  42                    String
  42.42                 String
  t                     String
  off                   String
  This is a string      String
  ["1", "2", "3"]       Array
Options:
  42                    String
  42.42                 String
  false                 FalseClass
  true                  TrueClass
  this is a string      String
  ["1", "2", "3"]       Array

# After:
Arguments:
  42                    Integer
  42.42                 Float
  true                  TrueClass
  false                 FalseClass
  This is a string      String
  ["1", "2", "3"]       Array
Options:
  42                    Integer
  42.42                 Float
  false                 FalseClass
  true                  TrueClass
  this is a string      String
  ["1", "2", "3"]       Array

Comment thread lib/dry/cli/option.rb
elsif float?
value.to_f
elsif argument? && (boolean? || flag?)
%w[0 f false off].include?(value.downcase) ? false : true
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used rails as a base.

Comment thread lib/dry/cli/option.rb
return value if value.nil?

if integer?
value.to_i
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue with this is that casting "abc" to 0 might be considered too frivolous by many. Maybe something like Dry::Types::Params could work better here, but I'm not sure we want to pull a dependency to dry-cli.

If not, using Integer(value) would be a better choice here IMO, but we'd need to handle the exceptions in a better way than just propagating the ArgumentError.

@katafrakt
Copy link
Copy Markdown
Contributor

I'm more and more convinced that this should be some kind of Dry Types plugin.

@timriley I noticed that Dry CLI does not have Dry Core in dependencies. Is this a design decision that Dry CLI should not have any dependencies, or is this just that it was not needed yet? Would be nice to use Dry::Core::Extensions for it, but on the other hand it's not too much code to just vendor.

@noraj
Copy link
Copy Markdown
Contributor

noraj commented Apr 26, 2026

This would be really nice, because I have tons of options where I have to define booleans as string default: 'ture', values: %w[true false] and then call a custom .to_bool on the option options[:advanced].to_bool because user input always comes as a string.

As an alternative to having pre-defined casting, user could also provide a custom method for casting.

Maybe something like Dry::Types::Params could work better here, but I'm not sure we want to pull a dependency to dry-cli.

Then could dry-types be an optional dependency of dry-cli? Needed only for using type_cast: on arguments and options. That way it won't pull a dependency to dry-cli by default, only for people implementing advanced use cases. Also dry-types already supports custom types.

dry-types itself can be extended with maybe and monads, so how comes dry-cli couldn't be extended with dry-types? Couldn't it do Dry::CLI.load_extensions(:types)?

TL;DR: two options to not add default dependencies to dry-cli:

  1. Support only user-defined casting, the user has to provide its own method
  2. Add dry-types as an extension (so optional dependency)

@katafrakt
Copy link
Copy Markdown
Contributor

dry-types itself can be extended with maybe and monads, so how comes dry-cli couldn't be extended with dry-types? Couldn't it do Dry::CLI.load_extensions(:types)?

The reason is that Dry CLI is kept dependency-free and the extension mechanics is coming from Dry Core.

But I like the idea of a Dry Types extension for CLI. The code is rather small so we could even just pull it in and keep Dry CLI without Dry Core dependency.

@katafrakt katafrakt mentioned this pull request May 4, 2026
@katafrakt
Copy link
Copy Markdown
Contributor

FYI the Dry Types implementation PR is here: #157
Feedback welcome.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants