add documentation/tests for the defaultValue option

Project: http://git-wip-us.apache.org/repos/asf/groovy/repo
Commit: http://git-wip-us.apache.org/repos/asf/groovy/commit/0eca1541
Tree: http://git-wip-us.apache.org/repos/asf/groovy/tree/0eca1541
Diff: http://git-wip-us.apache.org/repos/asf/groovy/diff/0eca1541

Branch: refs/heads/master
Commit: 0eca15411b7fe4e6a6499e70125fb19ff1aee903
Parents: e825754
Author: paulk <pa...@asert.com.au>
Authored: Sat Apr 9 21:46:20 2016 +1000
Committer: paulk <pa...@asert.com.au>
Committed: Wed Apr 13 20:38:50 2016 +1000

----------------------------------------------------------------------
 src/main/groovy/cli/Option.java                 |   5 +-
 src/main/groovy/util/CliBuilder.groovy          |  31 +++++-
 .../doc/core-domain-specific-languages.adoc     | 109 +++++++++++++++++--
 src/spec/test/builder/CliBuilderTest.groovy     |  79 ++++++++++++++
 4 files changed, 210 insertions(+), 14 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/groovy/blob/0eca1541/src/main/groovy/cli/Option.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/cli/Option.java b/src/main/groovy/cli/Option.java
index c5001a8..453a51a 100644
--- a/src/main/groovy/cli/Option.java
+++ b/src/main/groovy/cli/Option.java
@@ -25,7 +25,6 @@ import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
-// TODO: why setter methods?
 /**
  * Indicates that a method or field can be used to set a CLI option.
  */
@@ -46,7 +45,6 @@ public @interface Option {
      * @return the short name of this option
      */
     String shortName() default "";
-    // TODO: default to '_' or at least support this?
 
     /**
      * The long name of this option. Defaults to the name of member being 
annotated.
@@ -92,6 +90,7 @@ public @interface Option {
 
     /**
      * The default value for this option as a String; subject to type 
conversion and 'convert'.
+     * Ignored for Boolean options.
      *
      * @return the default value for this option
      */
@@ -102,5 +101,5 @@ public @interface Option {
      *
      * @return the closure to convert this option's argument(s)
      */
-    Class convert() default Undefined.CLASS.class; // TODO rename convert to 
handler?
+    Class convert() default Undefined.CLASS.class;
 }

http://git-wip-us.apache.org/repos/asf/groovy/blob/0eca1541/src/main/groovy/util/CliBuilder.groovy
----------------------------------------------------------------------
diff --git a/src/main/groovy/util/CliBuilder.groovy 
b/src/main/groovy/util/CliBuilder.groovy
index e71ac3f..30537b0 100644
--- a/src/main/groovy/util/CliBuilder.groovy
+++ b/src/main/groovy/util/CliBuilder.groovy
@@ -255,6 +255,13 @@ class CliBuilder {
     if (type != null) option.setType(type);
      */
 
+    public <T> TypedOption<T> option(Map args, Class<T> type, String 
description) {
+        def name = args.opt ?: '_'
+        args.type = type
+        args.remove('opt')
+        "$name"(args, description)
+    }
+
     /**
      * Internal method: Detect option specification method calls.
      */
@@ -274,6 +281,7 @@ class CliBuilder {
             if (args.size() == 2 && args[0] instanceof Map) {
                 def convert = args[0].remove('convert')
                 def type = args[0].remove('type')
+                def defaultValue = args[0].remove('defaultValue')
                 if (type && !(type instanceof Class)) {
                     throw new CliBuilderException("'type' must be a Class")
                 }
@@ -283,7 +291,7 @@ class CliBuilder {
                 }
                 def option = option(name, args[0], args[1])
                 options.addOption(option)
-                return create(option, type, null, convert)
+                return create(option, type, defaultValue, convert)
             }
         }
         return InvokerHelper.getMetaClass(this).invokeMethod(this, name, args)
@@ -382,7 +390,7 @@ class CliBuilder {
         if (convert == Undefined.CLASS) {
             convert = null
         }
-        Map names = calculateNames(annotation.longName(), 
annotation.shortName(), m, namesAreSetters)
+        Map names = calculateNames(annotation.longName(), shortName, m, 
namesAreSetters)
         def builder = names.short ? CliOption.builder(names.short) : 
CliOption.builder()
         if (names.long) {
             builder.longOpt(names.long)
@@ -433,7 +441,7 @@ class CliBuilder {
             if (opt != null) result.put("opt", opt)
             result.put("longOpt", longOpt)
             result.put("cliOption", o)
-            if (defaultValue && !defaultValue.isEmpty()) {
+            if (defaultValue) {
                 result.put("defaultValue", defaultValue)
             }
             if (convert) {
@@ -501,7 +509,7 @@ class CliBuilder {
             boolean isFlag = (isBoolRetType && !hasArg) || noArg
             t.put(m.getName(), cli.hasOption(name) ?
                     { -> isFlag ? true : optionValue(cli, name) } :
-                    { -> isFlag ? false : null })
+                    { -> isFlag ? false : cli.defaultValue(name) })
         }
     }
 
@@ -605,6 +613,12 @@ class OptionAccessor {
         commandLine.hasOption(typedOption.longOpt ?: typedOption.opt)
     }
 
+    public <T> T defaultValue(String name) {
+        Class<T> type = savedTypeOptions[name]?.type
+        String value = savedTypeOptions[name]?.defaultValue() ? 
savedTypeOptions[name].defaultValue() : null
+        return (T) value ? getTypedValue(type, name, value) : null
+    }
+
     public <T> T getOptionValue(TypedOption<T> typedOption) {
         getOptionValue(typedOption, null)
     }
@@ -659,6 +673,9 @@ class OptionAccessor {
     }
 
     private <T> T getTypedValue(Class<T> type, String optionName, String 
optionValue) {
+        if (!type) {
+            return (T) optionValue
+        }
         if (Closure.isAssignableFrom(type) && 
savedTypeOptions[optionName]?.convert) {
             return (T) savedTypeOptions[optionName].convert(optionValue)
         }
@@ -676,6 +693,10 @@ class OptionAccessor {
     }
 
     def getProperty(String name) {
+        if (!savedTypeOptions.containsKey(name)) {
+            def alt = savedTypeOptions.find{ it.value.opt == name }
+            if (alt) name = alt.key
+        }
         def methodname = 'getOptionValue'
         Class type = savedTypeOptions[name]?.type
         def foundArray = type?.isArray()
@@ -698,6 +719,8 @@ class OptionAccessor {
             } else {
                 if (type) result = getTypedValue(type, name, result)
             }
+        } else if (type?.simpleName != 'boolean' && 
savedTypeOptions[name]?.defaultValue) {
+            result = getTypedValue(type, name, 
savedTypeOptions[name].defaultValue)
         } else {
             result = commandLine.hasOption(name)
         }

http://git-wip-us.apache.org/repos/asf/groovy/blob/0eca1541/src/spec/doc/core-domain-specific-languages.adoc
----------------------------------------------------------------------
diff --git a/src/spec/doc/core-domain-specific-languages.adoc 
b/src/spec/doc/core-domain-specific-languages.adoc
index 4aa387b..6c5245a 100644
--- a/src/spec/doc/core-domain-specific-languages.adoc
+++ b/src/spec/doc/core-domain-specific-languages.adoc
@@ -1393,12 +1393,13 @@ 
include::{projectdir}/src/spec/test/builder/CliBuilderTest.groovy[tags=multipleA
 <6> An alternative syntax for specifying two arguments for the 'a' option
 <7> The arguments to the 'b' option supplied as a comma-separated value
 
-As an alternative to obtaining multiple arguments is to use an array-based 
type for the
-option. In this case, all options will always be returned via the array. We'll 
see an example
-of this next when discussing types. It is also the only approach to use when 
using the annotation style.
+As an alternative to accessing multiple arguments using the _plural name_ 
approach, you can use an
+array-based type for the option. In this case, all options will always be 
returned via the array
+which is accessed via the normal singular name. We'll see an example of this 
next when discussing
+types.
 
-Multiple arguments are also supported using the annotation style of option 
definition by using an array type for the
-annotated class member (method or property) as this example shows:
+Multiple arguments are also supported using the annotation style of option 
definition by using an
+array type for the annotated class member (method or property) as this example 
shows:
 
 [source,groovy]
 ----
@@ -1424,13 +1425,91 @@ 
include::{projectdir}/src/spec/test/builder/CliBuilderTest.groovy[tags=withTypeM
 
 ===== Setting a default value
 
+Groovy makes it easy using the Elvis operator to provide a default value at 
the point of usage of some variable,
+e.g. `String x = someVariable ?: 'some default'`. But sometimes you wish to 
make such a default part of the
+options specification to minimise the interrogators work in later stages. 
`CliBuilder` supports the `defaultValue`
+property to cater for this scenario.
+
+Here is how you could use it using the deynamic api style:
+
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/builder/CliBuilderTest.groovy[tags=withDefaultValue,indent=0]
+----
+
+Similarly, you might want such a specification using the annotation style. 
Here is an example using an interface
+specification:
+
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/builder/CliBuilderTest.groovy[tags=withDefaultValueInterfaceSpec,indent=0]
+----
+
+Which would be used like this:
+
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/builder/CliBuilderTest.groovy[tags=withDefaultValueInterface,indent=0]
+----
+
+===== Use with `TypeChecked`
+
+The dynamic api style of using `CliBuilder` is inherently dynamic but you have 
a few options
+should you want to make use of Groovy's static type checking capabilities. 
Firstly, consider using the
+annotation style, for example, here is an interface option specification:
+
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/builder/CliBuilderTest.groovy[tags=withTypeCheckedInterfaceSpec,indent=0]
+----
+
+And it can be used  in combination with `@TypeChecked` as shown here:
+
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/builder/CliBuilderTest.groovy[tags=withTypeCheckedInterface,indent=0]
+----
+
+Secondly, there is a feature of the dynamic api style which offers some 
support. The definition statements
+are inherently dynamic but actually return a value which we have ignored in 
earlier examples.
+The returned value is in fact a `TypedOption<Type>` and special `getAt` 
support allows the options
+to be interrogated using the typed option, e.g. `options[savedTypeOption]`. 
So, if you have statements
+similar to these in a non type checked part of your code:
+
+[source,groovy]
+----
+def cli = new CliBuilder()
+TypedOption<Integer> age = cli.a(longOpt: 'age', type: Integer, 'some age 
option')
+----
+
+Then, the following statements can be in a separate part of your code which is 
type checked:
+
+[source,groovy]
+----
+def args = '--age 21'.split()
+def options = cli.parse(args)
+int age = options[age]
+assert age == 21
+----
+
+Finally, there is one additional convenience method offered by `CliBuilder` to 
even allow the
+definition part to be type checked. It is a slightly more verbose method call. 
Instead of using
+the short name (the _opt_ name) in the method call, you use a fixed named of 
`option` and
+supply the `opt` value as a property. You must also specify the type directly 
as shown in
+the following example:
+
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/builder/CliBuilderTest.groovy[tags=withTypeChecked,indent=0]
+----
+
 ===== Advanced CLI Usage
 
 [NOTE]
 ===============================
 *NOTE* Advanced CLI features
 
-`CliBuilder` can be thought of a Groovy friendly wrapper on top of (currently) 
Apache Commons CLI.
+`CliBuilder` can be thought of as a Groovy friendly wrapper on top of 
(currently) Apache Commons CLI.
 If there is a feature not provided by `CliBuilder` that you know is supported 
in the underlying
 library, the current `CliBuilder` implementation (and various Groovy language 
features) make it easy for you
 to call the underlying library methods directly. Doing so is a pragmatic way 
to leverage the Groovy-friendly
@@ -1439,8 +1518,24 @@ A word of caution however; future versions of 
`CliBuilder` could potentially use
 and in that event, some porting work may be required for your Groovy classes 
and/or scripts.
 ===============================
 
+As an example, here is some code for making use of Apache Commons CLI's 
grouping mechanism:
 
-===== Use with `TypeChecked` and `CompileStatic`
+[source,groovy]
+----
+import org.apache.commons.cli.*
+
+def cli = new CliBuilder()
+cli.f longOpt: 'from', 'f option'
+cli.u longOpt: 'until', 'u option'
+def optionGroup = new OptionGroup()
+optionGroup.with {
+  addOption cli.option('o', [longOpt: 'output'], 'o option')
+  addOption cli.option('d', [longOpt: 'directory'], 'd option')
+}
+cli.options.addOptionGroup optionGroup
+assert !cli.parse('-d -o'.split()) // <1>
+----
+<1> The parse will fail since only one option from a group can be used at a 
time.
 
 ==== ObjectGraphBuilder
 

http://git-wip-us.apache.org/repos/asf/groovy/blob/0eca1541/src/spec/test/builder/CliBuilderTest.groovy
----------------------------------------------------------------------
diff --git a/src/spec/test/builder/CliBuilderTest.groovy 
b/src/spec/test/builder/CliBuilderTest.groovy
index 8284d04..1d039a1 100644
--- a/src/spec/test/builder/CliBuilderTest.groovy
+++ b/src/spec/test/builder/CliBuilderTest.groovy
@@ -19,7 +19,9 @@
 package builder
 
 import groovy.cli.Option
+import groovy.cli.TypedOption
 import groovy.cli.Unparsed
+import groovy.transform.TypeChecked
 
 import java.math.RoundingMode
 
@@ -288,4 +290,81 @@ class CliBuilderTest extends GroovyTestCase {
         assert options.remaining() == ['and', 'some', 'more']
         // end::withConvertInterface[]
     }
+
+    void testDefaultValue() {
+        // tag::withDefaultValue[]
+        def cli = new CliBuilder()
+        cli.f longOpt: 'from', type: String, args: 1, defaultValue: 'one', 'f 
option'
+        cli.t longOpt: 'to', type: int, defaultValue: '35', 't option'
+
+        def options = cli.parse('-f two'.split())
+        assert options.hasOption('f')
+        assert options.f == 'two'
+        assert !options.hasOption('t')
+        assert options.t == 35
+
+        options = cli.parse('-t 45'.split())
+        assert !options.hasOption('from')
+        assert options.from == 'one'
+        assert options.hasOption('to')
+        assert options.to == 45
+        // end::withDefaultValue[]
+    }
+
+    // tag::withDefaultValueInterfaceSpec[]
+    interface WithDefaultValueI {
+        @Option(shortName='f', defaultValue='one') String from()
+        @Option(shortName='t', defaultValue='35') int to()
+    }
+    // end::withDefaultValueInterfaceSpec[]
+
+    void testDefaultValueInterface() {
+        // tag::withDefaultValueInterface[]
+        def cli = new CliBuilder()
+
+        def options = cli.parseFromSpec(WithDefaultValueI, '-f two'.split())
+        assert options.from() == 'two'
+        assert options.to() == 35
+
+        options = cli.parseFromSpec(WithDefaultValueI, '-t 45'.split())
+        assert options.from() == 'one'
+        assert options.to() == 45
+        // end::withDefaultValueInterface[]
+    }
+
+    // tag::withTypeCheckedInterfaceSpec[]
+    interface TypeCheckedI{
+        @Option String name()
+        @Option int age()
+        @Unparsed List remaining()
+    }
+    // end::withTypeCheckedInterfaceSpec[]
+
+    // tag::withTypeCheckedInterface[]
+    @TypeChecked
+    void testTypeCheckedInterface() {
+        def argz = "--name John --age 21 and some more".split()
+        def cli = new CliBuilder()
+        def options = cli.parseFromSpec(TypeCheckedI, argz)
+        String n = options.name()
+        int a = options.age()
+        assert n == 'John' && a == 21
+        assert options.remaining() == ['and', 'some', 'more']
+    }
+    // end::withTypeCheckedInterface[]
+
+    // tag::withTypeChecked[]
+    @TypeChecked
+    void testTypeChecked() {
+        def cli = new CliBuilder()
+        TypedOption<String> name = cli.option(String, opt: 'n', longOpt: 
'name', 'name option')
+        TypedOption<Integer> age = cli.option(Integer, longOpt: 'age', 'age 
option')
+        def argz = "--name John -age 21 and some more".split()
+        def options = cli.parse(argz)
+        String n = options[name]
+        int a = options[age]
+        assert n == 'John' && a == 21
+        assert options.arguments() == ['and', 'some', 'more']
+    }
+    // end::withTypeChecked[]
 }

Reply via email to