Made changes:

- Updated .gitignore to include specs
- Re-licensed to MPL-2.0 as I want any modifications to be pushed back
  to the main codebase so that the ecosystem can benefit from developer
  adoption
- Updated LICENSE & shard.yml with new license
- Changed author to personal email as this is not being done for my
  company
This commit is contained in:
2026-08-30 12:52:16 -05:00
parent fe6112795a
commit f2d5c84f2b
27 changed files with 1024 additions and 29 deletions
+94
View File
@@ -0,0 +1,94 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
require "../spec_helper"
def make_arg(val, index = 0) : CliGen::Arg
CliGen::Arg.new(value: val, index: index)
end
describe CliGen::Arg do
describe "#processed" do
it "will raise a ArgReprocessedError exception when re-processed" do
arg = make_arg("abc")
arg.processed
expect_raises(CliGen::ArgReprocessedError) do
arg.processed
end
end
end
describe "#flag?" do
it "will return true when value is in long flag format --[a-zA-Z0-9-_]+" do
arg = make_arg("--help")
arg.flag?.should be_true
end
it "will return true when value is in short flag format -[a-zA-Z0-9]" do
arg = make_arg("-h")
arg.flag?.should be_true
end
it "will return false when value is literally anything else" do
arg = make_arg("abc")
arg.flag?.should be_false
end
end
describe "#uint?" do
it "will return true on a value in an unigned int format ^[[:digit:]]+$" do
make_arg("3").uint?.should be_true
end
it "will return false on a value in an signed int format ^[+-][[:digit:]]+$" do
make_arg("-3").uint?.should be_false
make_arg("+3").uint?.should be_false
end
it "will return false on a value in a mixed format" do
make_arg("a2").uint?.should be_false
end
it "will return false on a value in any other format" do
make_arg("a").uint?.should be_false
end
end
describe "#int?" do
it "will return true on a value in int format ^[[:digit:]]+$" do
make_arg("3").int?.should be_true
end
it "will return true on a value in an signed int format ^[+-][[:digit:]]+$" do
make_arg("-3").int?.should be_true
make_arg("+3").int?.should be_true
end
it "will return false on a value in a mixed format" do
make_arg("a2").int?.should be_false
end
it "will return false on a value in any other format" do
make_arg("a").int?.should be_false
end
end
describe "#float?" do
it "will return true on a value in a float format ^[[:digit:]]+\.[[:digit:]]+$" do
make_arg("1.1").float?.should be_true
end
it "will return true on a value in a int format ^[[:digit:]]$" do
make_arg("1").float?.should be_true
end
it "will return false on a value in any other format" do
make_arg("a").float?.should be_false
end
it "will return false on a value that isn't fully int's" do
make_arg("a1").float?.should be_false
end
end
end
+438
View File
@@ -0,0 +1,438 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
require "../spec_helper"
module CliGen
class Flag(T)
def test_coerce(var : String)
coerce(var)
end
def process(args : Array(String))
process(make_args(args))
end
end
end
enum TestEnum
ABC
DEF
GHI
end
class MyGoodData
extend CliGen::Coercable
extend CliGen::Parsable
getter value : TestEnum
def initialize(@value)
end
def self.to_s
nil
end
def to_s
@value.to_s
end
def to_s(io : IO)
io << @value.to_s
end
def self.coerce(arg : String)
case arg
when "abc"
new(TestEnum::ABC)
when "def"
new(TestEnum::DEF)
when "ghi"
new(TestEnum::GHI)
else
raise "WTF?"
end
end
def self.parse_args(args : Array(CliGen::Arg))
raise "ERROR" if args.empty?
arg = args.first
arg.processed
coerce(arg.value)
end
end
class MyBadData < MyGoodData
def self.parse_args(args : Array(CliGen::Arg))
raise "ERROR" if args.empty?
arg = args.first
coerce(arg.value)
end
end
def make_arg(value, index : Int32 = 0) : CliGen::Arg
CliGen::Arg.new(value: value, index: index)
end
def make_args(values : Array(String)) : Array(CliGen::Arg)
output = [] of CliGen::Arg
values.each_with_index{|v, i| output << make_arg(v,i)}
output
end
macro make_flag(type, long = "--test", env_var = "TEST", var = "", description = "", short = nil, delimiter = ",", default = nil, options = nil, validate = nil, on_match = nil, format = nil)
CliGen::Flag({{type}}).new(
var: {{var}},
env_var: {{env_var}},
short: {{short}},
long: {{long}},
description: {{description}},
delimiter: {{delimiter}},
default: {{default}},
options: {{options}},
validate: {{validate}},
on_match: {{on_match}},
format: {{format}}
)
end
describe CliGen::Flag do
before_each { ENV.delete("TEST") }
describe "#check!" do
it "will throw CliGen::ReservedFlagError if -h is used as short" do
expect_raises(CliGen::ReservedFlagError) do
make_flag(type: Bool, short: "-h", long: "--not-help").check!
end
end
it "will throw CliGen::ReservedFlagError if --help is used in long" do
expect_raises(CliGen::ReservedFlagError) do
make_flag(type: Bool, short: "-b", long: "--help TOPIC").check!
end
end
it "will throw CliGen::ReservedFlagError if --help is used as long_key" do
expect_raises(CliGen::ReservedFlagError) do
make_flag(type: Bool, short: "-b", long: "--help").check!
end
end
end
describe "#value!" do
it "will throw CliGen::MissingRequiredFlagError if a flag without a default value is called with no way of determining a value" do
flg = make_flag(type: String)
expect_raises(CliGen::MissingRequiredFlagError) do
flg.value!
end
end
it "will fallback to provided default if no data is provided by the user" do
make_flag(type: String, default: "abc").value!.should eq "abc"
end
it "will resolve values from ENV VAR if not directly supplied with one" do
flg = make_flag(type: String, env_var: "MYTESTVAR")
ENV["MYTESTVAR"] = "abc"
flg.value!.should eq "abc"
end
describe "Flag(Array(String))" do
it "returns a valid array when resolving from ENV VAR" do
flg = make_flag(type: Array(String), env_var: "ABCDEF")
ENV["ABCDEF"]="a,b,c"
flg.value!.should eq %w[ a b c ]
end
end
end
describe "#coerce" do
{% for int in Int.subclasses %}
{% uint = int.stringify =~ /^UInt/ ? true : false %}
describe "Flag({{int}})" do
it "can correctly coerce {{int}}" do
make_flag(type: {{int}}).test_coerce("1").should be_a {{int}}
end
it "will raise CliGen::InvalidFlagValueError if provided a value that is not an {{int}}" do
expect_raises(CliGen::InvalidFlagValueError) do
make_flag(type: {{int}}).test_coerce("abc")
end
end
{% if uint %}
it "will raise CliGen::InvalidFlagValueError if provided a value in a signed int" do
expect_raises(CliGen::InvalidFlagValueError) do
make_flag(type: {{int}}).test_coerce("-1")
end
end
{% end %}
end
{% if uint %}
describe "Flag(Array({{int}}))" do
it "Will be correctly parsed when providing an unsigned int" do
abc = "1,2,3"
make_flag(type: Array({{int}})).test_coerce(abc).should eq([ 1, 2, 3 ] of {{int}})
end
it "will raise CliGen::InvalidFlagValueError if an signed int is provided" do
abc = "-1,2,3"
expect_raises(CliGen::InvalidFlagValueError) do
make_flag(type: Array({{int}})).test_coerce(abc)
end
end
end
{% else %}
describe "Flag(Array({{int}}))" do
it "Will be correctly parsed when providing a signed int" do
abc = "-1,2,3"
make_flag(type: Array({{int}})).test_coerce(abc).should eq([ -1, 2, 3 ] of {{int}})
end
end
{% end %}
{% end %}
{% for float in Float.subclasses %}
describe "Flag({{float}})" do
it "can correctly coerce {{float}}" do
make_flag(type: {{float}}).test_coerce("1.1").should be_a {{float}}
end
it "will raise CliGen::InvalidFlagValueError if provided value isn't a {{float}}" do
expect_raises(CliGen::InvalidFlagValueError) do
make_flag(type: {{float}}).test_coerce("why")
end
end
end
describe "Flag(Array({{float}}))" do
it "will correctly coerce data in a unsigned int format" do
abc = "1,2,3"
make_flag(type: Array({{float}})).test_coerce(abc).should eq([1,2,3] of {{float}})
end
it "will correctly coerce data in a signed int format" do
abc = "-1,2,3"
make_flag(type: Array({{float}})).test_coerce(abc).should eq([-1,2,3] of {{float}})
end
it "will correctly coerce data in a mixed int & float format" do
abc = "-1,2,3.2"
make_flag(type: Array({{float}})).test_coerce(abc).should eq([-1,2,3.2] of {{float}})
end
end
{% end %}
describe "Flag(Bool)" do
it "correctly coerces with valid data" do
%w[ t true 1 y yes ].each do |opt|
make_flag(type: Bool).test_coerce(opt).should be_true
end
%w[ f false 0 n no ].each do |opt|
make_flag(type: Bool).test_coerce(opt).should be_false
end
end
it "raises CliGen::InvalidFlagValueError if provided an invalid value" do
expect_raises(CliGen::InvalidFlagValueError) do
make_flag(type: Bool).test_coerce("why?")
end
end
end
describe "Flag(Array(CustomObj))" do
it "can correctly coerce data from the custom object" do
flg = make_flag(type: Array(MyGoodData))
flg.test_coerce("abc,def").map(&.to_s.downcase).should eq %w[ abc def ]
end
end
describe "Flag(Array(String))" do
it "correctly splits based on delimiter" do
flg = make_flag(type: Array(String), long: "--mytest", delimiter: "|")
flg.test_coerce("a|b|c").should eq %w[ a b c ]
end
it "will raise CliGen::InvalidFlagValueError if an element of the array doesn't match the format defined" do
flg = make_flag(type: Array(String), format: /^test[A-Z]$/)
expect_raises(CliGen::InvalidFlagValueError) do
flg.test_coerce("testA,testB,test1")
end
end
end
end
describe "#validate!" do
it "will raise CliGen::InvalidOptionError if the provided value isn't in the static list of options" do
flg = make_flag(type: String, long: "--test", env_var: "TESTVAR", options: %w[ a b c ])
ENV["TESTVAR"] = "d"
expect_raises(CliGen::InvalidOptionError) do
flg.validate!
end
end
it "will raise CliGen::ValidationError if the dev provided validation fails" do
flg = make_flag(type: String, long: "--test", env_var: "TESTVAR", validate: ->(v : String) : Bool { v == "abc" })
ENV["TESTVAR"] = "def"
expect_raises(CliGen::ValidationError) do
flg.validate!
end
end
describe "Flag(Array(String))" do
flg = make_flag(type: Array(String), long: "--test", env_var: "TESTVAR2", options: [%w[ a b c ]])
it "will raise CliGen::InvalidOptionError if the provided value isn't in the static list of options" do
end
end
end
describe "#process" do
describe "Flag(String)" do
it "will raise CliGen::InvalidFlagValueError if provided string doesn't match format" do
flg = make_flag(type: String, env_var: "TEST", short: "-t", long: "--test", format: /^abc$/)
expect_raises(CliGen::InvalidFlagValueError) do
flg.process(%w[ def ])
end
end
end
{% for int in Int.subclasses %}
{% uint = int.stringify =~ /^UInt/ ? true : false %}
describe "Flag({{int}})" do
it "coerces a valid {{int}} from provided arguments" do
{% if uint %}
val = 1
{% else %}
val = -1
{% end %}
flg = make_flag(type: {{int}})
flg.process(make_args([val.to_s]))
flg.value!.should eq val
end
{% if uint %}
it "will raise CliGen::InvalidFlagValueError if provided a signed int" do
expect_raises(CliGen::InvalidFlagValueError) do
make_flag(type: {{int}}).process([make_arg("-1", 0)])
end
end
{% end %}
end
describe "Flag(Array({{int}}))" do
{% if uint %}
it "will raise when a signed int is provided" do
args = make_args(%w[ 1 2 -3 ])
flg = make_flag(type: Array({{int}}))
expect_raises(CliGen::InvalidFlagValueError) do
flg.process(args)
end
end
it "will raise when a signed int is provided in a delimited string" do
args = [make_arg("1,2,-3")]
flg = make_flag(type: Array({{int}}))
expect_raises(CliGen::InvalidFlagValueError) do
flg.process(args)
end
end
{% end %}
it "can consume positive ints" do
args = make_args(%w[ 1 2 3 ])
flg = make_flag(type: Array({{int}}))
flg.process(args)
flg.value!.should eq([1, 2, 3] of {{int}})
end
it "can consume positive ints in a delimited string" do
args = [make_arg("1,2,3", 0)]
flg = make_flag(type: Array({{int}}))
flg.process(args)
flg.value!.should eq([1, 2, 3] of {{int}})
end
it "can consume positive ints in a delimited string and will strip whitespace" do
args = [make_arg("1 ,2,3", 0)]
flg = make_flag(type: Array({{int}}))
flg.process(args)
flg.value!.should eq([1, 2, 3] of {{int}})
end
it "can consume positive ints in multiple arguments" do
args = make_args(%w[ 1,2,3 4 ])
flg = make_flag(type: Array({{int}}))
flg.process(args)
flg.value!.should eq([1, 2, 3, 4] of {{int}})
end
{% unless uint %}
it "can consume signed ints" do
args = make_args(%w[ 1 2 -3 ])
flg = make_flag(type: Array({{int}}))
flg.process(args)
flg.value!.should eq([1, 2, -3] of {{int}})
end
it "can consume signed ints in a delimited string" do
args = [make_arg("1,2,-3")]
flg = make_flag(type: Array({{int}}))
flg.process(args)
flg.value!.should eq([1, 2, -3] of {{int}})
end
{% end %}
end
{% end %}
{% for float in Float.subclasses %}
describe "Flag({{float}})" do
it "coerces a valid {{float}} from provided arguments" do
val = {{float}}.new("1.1")
flg = make_flag(type: {{float}})
flg.process(make_args([val.to_s]))
flg.value!.should eq val
end
end
describe "Flag(Array({{float}}))" do
it "can handle an array of int/float formatted values" do
args = make_args(%w[ 1 2 3 1.1 -1])
flg = make_flag(type: Array({{float}}))
flg.process(args)
flg.value!.should eq([1, 2, 3, 1.1, -1] of {{float}})
end
end
{% end %}
describe "Flag(Array(String))" do
it "will throw CliGen::InvalidFlagValueError if a deserialized var/item is an invalid format" do
flg = make_flag(type: Array(String), format: /^(a|b|c)$/)
expect_raises(CliGen::InvalidFlagValueError) do
flg.process([ "a,b,c,d" ])
end
end
end
describe "Flag(CustomObj)" do
it "will throw a CliGen::ParseableInvariantError if object doesn't mark data as processed" do
flg = make_flag(type: MyBadData)
expect_raises(CliGen::ParseableInvariantError) do
flg.process(["abc"])
end
end
it "will correctly set the value if parsed" do
flg = make_flag(type: MyGoodData)
flg.process(["abc"])
flg.value!.value.should eq TestEnum::ABC
end
end
end
end
+7
View File
@@ -0,0 +1,7 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
require "spec"
require "../src/cligen"