Skip to content

Commit b74462e

Browse files
authored
Support undefined parameter in function callback (#15)
* helper functions * swap out incompatible parameter with default value * add source and javadoc for mavenlocal * add unit test
1 parent 0d29726 commit b74462e

File tree

5 files changed

+151
-2
lines changed

5 files changed

+151
-2
lines changed

gradle/mvn-publish.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ publishing {
1212
version getVersionName()
1313

1414
artifact("$buildDir/outputs/aar/${project.getName()}-release.aar")
15+
if (gradle.startParameter.taskNames.any { it.contains('publishToMavenLocal') }) {
16+
artifact sourcesJar
17+
artifact javadocJar
18+
}
1519

1620
// Self-explanatory metadata for the most part
1721
pom {

substrata-kotlin/build.gradle

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,31 @@ dependencies {
5151
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
5252
implementation "org.jetbrains.kotlin:kotlin-reflect:1.6.21"
5353
}
54+
task sourcesJar(type: Jar) {
55+
archiveClassifier.set('sources')
56+
from android.sourceSets.main.java.srcDirs
57+
}
58+
59+
task javadoc(type: Javadoc) {
60+
configurations.implementation.setCanBeResolved(true)
61+
62+
failOnError false
63+
source = android.sourceSets.main.java.sourceFiles
64+
classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
65+
classpath += configurations.implementation
66+
}
67+
68+
// build a jar with javadoc
69+
task javadocJar(type: Jar, dependsOn: javadoc) {
70+
archiveClassifier.set('javadoc')
71+
from javadoc.destinationDir
72+
}
73+
74+
// Attach Javadocs and Sources jar
75+
artifacts {
76+
archives sourcesJar
77+
archives javadocJar
78+
}
5479

5580
apply from: rootProject.file('gradle/mvn-publish.gradle')
5681
tasks.named("signReleasePublication") {

substrata-kotlin/src/androidTest/java/com/segment/analytics/substrata/kotlin/EngineTests.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,29 @@ class EngineTests {
599599
assertNotNull(exception)
600600
}
601601

602+
@Test
603+
fun testIncompatibleParameters() {
604+
class MyJSClass {
605+
fun test(a: String?, b: String, c: JSValue, d: Int): Int {
606+
return d
607+
}
608+
}
609+
scope.sync {
610+
export( "MyJSClass", MyJSClass::class)
611+
var res: Int = evaluate("""
612+
let o = MyJSClass()
613+
o.test(null, undefined, {}, 1234)
614+
""") as Int
615+
assertEquals(1234, res)
616+
617+
res = evaluate("""
618+
o.test("a", "b", {}, 5678)
619+
""") as Int
620+
assertEquals(5678, res)
621+
}
622+
assertNotNull(exception)
623+
}
624+
602625
@Test
603626
fun testAwait() {
604627
val ret = scope.await {
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,89 @@
11
package com.segment.analytics.substrata.kotlin
22

3+
import kotlin.reflect.KClass
4+
import kotlin.reflect.KType
35

46

7+
fun KType.defaultValue(): Any? {
8+
val classifier = this.classifier as? KClass<*>
9+
10+
// For nullable types, decide whether to return null or a default value
11+
val shouldReturnNull = this.isMarkedNullable && when (classifier) {
12+
String::class -> false // Return "" even for String?
13+
// Add other types where you want defaults instead of null
14+
Int::class, Long::class, Double::class, Float::class, Boolean::class -> false
15+
else -> true // Return null for other nullable types
16+
}
17+
18+
if (shouldReturnNull) return null
19+
20+
return when (classifier) {
21+
// Primitives
22+
String::class -> ""
23+
Int::class -> 0
24+
Long::class -> 0L
25+
Double::class -> 0.0
26+
Float::class -> 0.0f
27+
Boolean::class -> false
28+
Byte::class -> 0.toByte()
29+
Short::class -> 0.toShort()
30+
Char::class -> '\u0000'
31+
32+
// Collections
33+
List::class -> emptyList<Any>()
34+
Set::class -> emptySet<Any>()
35+
Map::class -> emptyMap<Any, Any>()
36+
Array::class -> emptyArray<Any>()
37+
38+
// Try to create instance with default constructor
39+
else -> try {
40+
classifier?.constructors?.firstOrNull {
41+
it.parameters.all { param -> param.isOptional }
42+
}?.callBy(emptyMap())
43+
} catch (e: Exception) {
44+
null
45+
}
46+
}
47+
}
48+
49+
fun typesCompatible(parameterType: KType, actualValue: Any?): Boolean {
50+
// Handle null values
51+
if (actualValue == null) {
52+
return parameterType.isMarkedNullable
53+
}
54+
55+
val paramClassifier = parameterType.classifier as? KClass<*> ?: return false
56+
val actualClass = actualValue::class
57+
58+
return when {
59+
// Direct match
60+
paramClassifier == actualClass -> true
61+
62+
// Check inheritance/interface implementation
63+
paramClassifier.isInstance(actualValue) -> true
64+
65+
// Handle primitive boxing
66+
isPrimitiveCompatible(paramClassifier, actualClass) -> true
67+
68+
// Handle generics (basic check)
69+
paramClassifier.java.isAssignableFrom(actualClass.java) -> true
70+
71+
else -> false
72+
}
73+
}
74+
75+
private fun isPrimitiveCompatible(expected: KClass<*>, actual: KClass<*>): Boolean {
76+
val primitiveMap = mapOf(
77+
Int::class to setOf(java.lang.Integer::class, Int::class),
78+
Long::class to setOf(java.lang.Long::class, Long::class),
79+
Double::class to setOf(java.lang.Double::class, Double::class),
80+
Float::class to setOf(java.lang.Float::class, Float::class),
81+
Boolean::class to setOf(java.lang.Boolean::class, Boolean::class),
82+
Byte::class to setOf(java.lang.Byte::class, Byte::class),
83+
Short::class to setOf(java.lang.Short::class, Short::class),
84+
Char::class to setOf(java.lang.Character::class, Char::class)
85+
)
86+
87+
return primitiveMap[expected]?.contains(actual) == true ||
88+
primitiveMap[actual]?.contains(expected) == true
89+
}

substrata-kotlin/src/main/java/com/segment/analytics/substrata/kotlin/Types.kt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -297,13 +297,25 @@ open class JSClass(
297297
throw JSCallbackInvalidParametersException("Arguments does not match to Java method ${method.name}")
298298
}
299299

300+
val modifiableParams = params.toMutableList()
300301
for (i in methodParams.indices) {
301-
if (methodParams[i].type.classifier != params[i]!!::class) {
302+
if (modifiableParams[i] == context.JSNull) {
303+
if (methodParams[i].type.isMarkedNullable) {
304+
modifiableParams[i] = null
305+
}
306+
else {
307+
modifiableParams[i] = methodParams[i].type.defaultValue()
308+
}
309+
}
310+
else if (modifiableParams[i] == context.JSUndefined) {
311+
modifiableParams[i] = methodParams[i].type.defaultValue()
312+
}
313+
else if (!typesCompatible(methodParams[i].type, modifiableParams[i])) {
302314
throw JSCallbackInvalidParametersException("Wrong argument passed to Java method ${method.name}. Expecting ${methodParams[i]::class.simpleName}, but was ${params[i]!!::class.simpleName}")
303315
}
304316
}
305317

306-
method.call(instance ?: obj, *params.toTypedArray())
318+
method.call(instance ?: obj, *modifiableParams.toTypedArray())
307319
}
308320

309321
if (methods[method.name] == null) {

0 commit comments

Comments
 (0)