Parameter parsing
Some aspects of this page is still under development. The conversion mechanics in this document may be incomplete or subject to change. See also: DataConverterUtils.
When developing tasks, you probably want them to be parameterizable from the build script. In order to achieve this, your Task
implementation needs to implement the ParameterizableTask
interface, and you can override the initParameters
method to handle the user provided parameters.
The saker.build system provides the default functionality for converting the user provided parameters and assigning them to the appropriate fields of your Task
object. In order to use this, you need to annotate your fields with @SakerInput
and related annotations.
A simplest example for the above is the following:
@SakerInput
public String MyValue;
Given that the above field is declared in your ParameterizableTask
object, it can be used the following way:
example.task(MyValue: abcd123)
This way the MyValue
field will have the "abcd123"
value when the run(TaskContext)
method is called.
Conversion mechanics
The exact conversion mechanics work on a best effort basis. The type converter will try to convert the value of a parameter to the target type by examining both objects and finding a conversion method for proper interpretation. The class DataConverterUtils
is used to convert the types and you can read more information about it in the Javadoc.
In general, it is strongly recommended to use only interfaces or primitive types and their compositions on lists and maps for the parameter types. The build system will attempt to convert the values and will throw an exception if it failed. It is strongly recommended to test the possible input values and properly document the types a task parameter accepts.
If a task needs to specially handle some input parameter, override the initParameters
function and act accordingly.
@DataContext
The @DataContext
annotation can be used to encapsulate a set of parameters into a custom class. This can be beneficial if the parameters will be reused in other places. The parsing facility will assign the annotated fields of the data context object accordingly.
public class MyParameters {
@SakerInput
public String MyValue;
}
@DataContext
public MyParameters params;
In the above example the MyValue
parameter of the params
data context will be assigned the same way as if it was declared directly in the task object.
The @DataContext
annotated fields will be recursively visited by the implementation. If the data context field is null
, the implementation will instantiate the data context type using the no-arg constructor.
Examples
The following will provide some examples and guidelines about how the parameters should be declared for a task.
Primitive types
If the target type of the task parameter is a primitive of the Java language (also including String
), the build system will use the default parsing methods for assigning the parameter. (E.g. Integer.parseInt
and others)
Boxed primitive types work the same way.
Types with valueOf
If the parameter field has a type that declares at least one static
valueOf
method with a single parameter, the build system will attempt to use that method to convert the value to the given type. An example for this is SakerPath
, where the valueOf(String)
is used to convert the value.
@SakerInput
public SakerPath PathParam;
example.task(PathParam: res/images)
Will result in the PathParam
be assigned to a SakerPath
instance representing res/images
.
The valueOf
methods are considered for conversion even if the target type is an interface.
Lists and Maps
The parameter type can be declared to be Java Collection interfaces. We strongly recommend to only use the List
and Map
, but using Collection
or Iterable
can be also acceptable. Set
and other collections that provide guarantees about the enclosed elements should not be used. It should also be noted that using Map
as the parameter type might result in keys that occur multiple times and no guarantees are made about the uniqueness of the keys. Developers should iterate over the entries in the map rather than looking up the values for a given key.
These interfaces should always be used with type parameters, and their declaration should not be raw. (I.e. use List<MyElementType>
rather than plain List
)
When a value is being converted to a collection type, the build system will attempt to convert each element to the target type at an appropriate time. This can happen right away when the task parameters are assigned, or lazily when the elements are actually accessed.
If the build system encounters a non-collection value for the given target, then it will attempt to enclose it into a singleton collection when appropriate.
@SakerInput
public List<Integer> IntsParam;
example.task(IntsParam: 123)
example.task(IntsParam: [1, 2, 3])
Both of the above parameterization of the tasks are valid, in the first case the IntsParam
will have a value of [123]
, while in the second case it will have [1, 2, 3]
accordingly.
Maps can be used similarly, but be noted that the key of the declared type should only be String
or plain Object
. Any other types may cause the map to work incorrectly.
@SakerInput
public Map<String, Integer> StringIntsParam;
example.task(StringIntsParam: {
First: 1,
Second: 2,
})
In the above example the value of StringIntsParam
will be the same as it was specified in the build script.
All collection and map instances assigned by the build system are immutable.
Enums
The parameters of a task can be declared as enum
types. The build system will convert them by looking up the enum value with the corresponding name.
public enum MyEnum {
VAL1, VAL2;
}
@SakerInput
public MyEnum EnumParam;
example.task(EnumParam: VAL2)
The parameter field will be correctly assigned with the enum instance of VAL2
.
Interfaces
The task parameter types may be interface
declarations which the build system will automatically implement based on the parameter value. This is usually advantageous when the parameter is expected to be a map, but the user code wants to handle it in a convenient way.
public interface MyParamType {
public String getValue();
public int getNumber();
public default Double getFloating() {
return 1.0;
}
}
@SakerInput
public MyParamType MyParam;
example.task(MyParam: {
Value: str,
Number: 123,
})
In the above example the build system will automatically implement the MyParamType
interface and create an object that forwards its getX()
calls to the underlying map. In this case calling getValue()
on the assigned parameter object will return "str"
, and calling getNumber()
will return 123
.
The conversion also supports the default
methods, therefore if the underlying map doesn't contain the associated entry, the default implementation of the method is called. In the above case calling getFloating()
will return 1.0
as seen in the default implementation.