Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,20 @@ import org.codehaus.groovy.runtime.typehandling.GroovyCastException
@CompileStatic
class ProcessEntryHandler {

/**
* Declared numeric input types a param value is converted to (see {@link #asNumberType}).
* Boolean and Path are handled separately: Boolean accepts only the explicit `true`/`false`
* spellings rather than Groovy truthiness, and Path resolution also validates existence.
*/
private static final List<Class> NUMBER_TYPES = [
Integer, Long, Short, Byte, Float, Double, BigInteger, BigDecimal, Number ]

/** Numeric types that cannot represent a fractional value. */
private static final List<Class> INTEGRAL_TYPES = [ Integer, Long, Short, Byte, BigInteger ]

/** Non-numeric declared types converted with a plain cast. */
private static final List<Class> SCALAR_TYPES = [ String, Character ]

private final BaseScript script
private final Session session
private final ProcessDef processDef
Expand Down Expand Up @@ -374,40 +388,57 @@ class ProcessEntryHandler {
if( value instanceof Collection || value instanceof Map )
return asType(value, param)

if( value !instanceof CharSequence )
return value

final str = value.toString()

if( type == Boolean ) {
if( str.toLowerCase() == 'true' ) return Boolean.TRUE
if( str.toLowerCase() == 'false' ) return Boolean.FALSE
if( value instanceof Boolean )
return value
final str = value.toString().toLowerCase()
if( str == 'true' ) return Boolean.TRUE
if( str == 'false' ) return Boolean.FALSE
return value
}

if( type == Integer || type == Float ) {
if( str.isInteger() ) return str.toInteger()
if( str.isLong() ) return str.toLong()
if( str.isBigInteger() ) return str.toBigInteger()
}
if( type == Path )
return value instanceof Path ? value : TypeHelper.asPathType(value.toString())

if( type == Float ) {
if( str.isFloat() ) return str.toFloat()
if( str.isDouble() ) return str.toDouble()
if( str.isBigDecimal() ) return str.toBigDecimal()
}
// Coerce a scalar to the DECLARED type. A value reaches here either as text (the CLI
// passes every param as a String) or as another scalar when supplied programmatically
// (JSON, for instance, yields BigDecimal for `96.4` and Integer for `40`), and neither
// necessarily matches the declaration. Converting to "whatever the text looks like"
// instead left `Double` unhandled altogether and could hand a `Float` input an Integer,
// so the wrong type reached the task and was reported as an invalid argument type.
if( type in NUMBER_TYPES )
return asNumberType(value, param)

if( type == Path ) {
return TypeHelper.asPathType(str)
}
if( type in SCALAR_TYPES )
return asType(value, param)

return value
}

/**
* Convert a param value to the declared numeric type. Text is parsed as a decimal number
* first (Groovy's cast does not parse strings into numbers), and an integral declared type
* rejects fractional text rather than silently truncating it.
*/
private static Object asNumberType(Object value, ProcessInput param) {
try {
final number = value instanceof Number
? (Number) value
: new BigDecimal(value.toString().trim())
if( param.type in INTEGRAL_TYPES && new BigDecimal(number.toString()).stripTrailingZeros().scale() > 0 )
throw new NumberFormatException("Not an integral value: ${number}")
return TypeHelper.asType(number, param.type)
}
catch( NumberFormatException | GroovyCastException | UnsupportedOperationException e ) {
throw new IllegalArgumentException("Parameter `--${param.name}` with type ${Types.getName(param.type)} cannot be assigned to ${value} [${Types.getName(value.getClass())}]")
}
}

private static Object asType(Object value, ProcessInput param) {
try {
return TypeHelper.asType(value, param.type)
}
catch( GroovyCastException | UnsupportedOperationException e ) {
catch( GroovyCastException | UnsupportedOperationException | NumberFormatException e ) {
final actualType = value.getClass()
throw new IllegalArgumentException("Parameter `--${param.name}` with type ${Types.getName(param.type)} cannot be assigned to ${value} [${Types.getName(actualType)}]")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import nextflow.script.params.v2.ProcessTupleInput
import nextflow.script.types.Record
import nextflow.util.RecordMap
import spock.lang.Specification
import spock.lang.Unroll

/**
* Tests for ProcessEntryHandler parameter mapping functionality
Expand Down Expand Up @@ -384,6 +385,76 @@ class ProcessEntryHandlerTest extends Specification {
result == null
}

@Unroll
def 'should convert a #value.class.simpleName param to the declared #type.simpleName input type (v2)' () {
given: 'a typed input whose declared type differs from the supplied value type'
def meta = Mock(ScriptMeta) { getLocalProcessNames() >> [ 'hello' ] }
def handler = new ProcessEntryHandler(Mock(BaseScript), Mock(Session), meta)

when:
def result = handler.getValueForInputV2(new ProcessInput('x', type, false), [x: value])

then: 'the value is converted to the DECLARED type, not to whatever the value looks like'
result.class == type
result == expected

where:
type | value || expected
// a JSON/programmatic value: `96.4` parses as BigDecimal, `40` as Integer
Double | 96.4G || 96.4d
Double | 40 || 40.0d
Float | 96.4G || 96.4f
Integer | 40G || 40
Long | 40 || 40L
// a CLI value: every param arrives as text
Double | '96.4' || 96.4d
Double | '40' || 40.0d
Float | '40' || 40.0f
Integer | '40' || 40
Long | '40' || 40L
BigDecimal | '96.4' || 96.4G
String | 96.4G || '96.4'
}

@Unroll
def 'should reject the param value #value for a declared #type.simpleName input (v2)' () {
given:
def meta = Mock(ScriptMeta) { getLocalProcessNames() >> [ 'hello' ] }
def handler = new ProcessEntryHandler(Mock(BaseScript), Mock(Session), meta)

when:
handler.getValueForInputV2(new ProcessInput('n50', type, false), [n50: value])

then: 'the mismatch is reported up front instead of reaching the task'
def e = thrown(IllegalArgumentException)
e.message == "Parameter `--n50` with type ${type.simpleName} cannot be assigned to ${value} [String]"

where:
type | value
Integer | 'abc'
Integer | '3.7' // fractional text is not silently truncated
Long | '1e'
Double | 'abc'
}

@Unroll
def 'should keep boolean and path input handling (v2)' () {
given:
def meta = Mock(ScriptMeta) { getLocalProcessNames() >> [ 'hello' ] }
def handler = new ProcessEntryHandler(Mock(BaseScript), Mock(Session), meta)

expect:
handler.getValueForInputV2(new ProcessInput('x', type, false), [x: value]) == expected

where:
type | value || expected
Boolean | 'true' || true
Boolean | 'TRUE' || true
Boolean | 'false' || false
Boolean | true || true
Boolean | 'yes' || 'yes' // unchanged: no Groovy truthiness for an unknown spelling
}

def 'should throw error for missing required input (v2)' () {
given:
def session = Mock(Session)
Expand Down
Loading