expo-modules-core 0.7.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +1 -1
  3. package/android/ExpoModulesCorePlugin.gradle +15 -0
  4. package/android/build.gradle +46 -32
  5. package/android/src/main/java/expo/modules/adapters/react/NativeModulesProxy.java +5 -5
  6. package/android/src/main/java/expo/modules/adapters/react/services/UIManagerModuleWrapper.java +13 -0
  7. package/android/src/main/java/expo/modules/core/ViewManager.java +9 -0
  8. package/android/src/main/java/expo/modules/core/interfaces/JavaScriptContextProvider.java +4 -0
  9. package/android/src/main/java/expo/modules/core/interfaces/ReactActivityHandler.java +37 -1
  10. package/android/src/main/java/expo/modules/core/interfaces/ReactNativeHostHandler.java +30 -0
  11. package/android/src/main/java/expo/modules/core/interfaces/services/UIManager.java +2 -0
  12. package/android/src/main/java/expo/modules/kotlin/AppContext.kt +13 -5
  13. package/android/src/main/java/expo/modules/kotlin/KPromiseWrapper.kt +2 -13
  14. package/android/src/main/java/expo/modules/kotlin/KotlinInteropModuleRegistry.kt +11 -5
  15. package/android/src/main/java/expo/modules/kotlin/ModuleHolder.kt +5 -1
  16. package/android/src/main/java/expo/modules/kotlin/ModuleRegistry.kt +17 -0
  17. package/android/src/main/java/expo/modules/kotlin/callbacks/ViewCallback.kt +14 -3
  18. package/android/src/main/java/expo/modules/kotlin/events/EventEmitter.kt +13 -0
  19. package/android/src/main/java/expo/modules/kotlin/events/KModuleEventEmitterWrapper.kt +102 -0
  20. package/android/src/main/java/expo/modules/kotlin/exception/CodedException.kt +25 -1
  21. package/android/src/main/java/expo/modules/kotlin/{methods/AnyMethod.kt → functions/AnyFunction.kt} +6 -5
  22. package/android/src/main/java/expo/modules/kotlin/functions/AsyncFunction.kt +15 -0
  23. package/android/src/main/java/expo/modules/kotlin/functions/AsyncFunctionBuilder.kt +170 -0
  24. package/android/src/main/java/expo/modules/kotlin/functions/AsyncFunctionWithPromise.kt +15 -0
  25. package/android/src/main/java/expo/modules/kotlin/functions/AsyncSuspendFunction.kt +36 -0
  26. package/android/src/main/java/expo/modules/kotlin/modules/Module.kt +14 -0
  27. package/android/src/main/java/expo/modules/kotlin/modules/ModuleDefinitionBuilder.kt +374 -41
  28. package/android/src/main/java/expo/modules/kotlin/modules/ModuleDefinitionData.kt +2 -2
  29. package/android/src/main/java/expo/modules/kotlin/records/FieldValidator.kt +139 -0
  30. package/android/src/main/java/expo/modules/kotlin/records/Record.kt +0 -39
  31. package/android/src/main/java/expo/modules/kotlin/records/RecordTypeConverter.kt +59 -10
  32. package/android/src/main/java/expo/modules/kotlin/records/Required.kt +5 -0
  33. package/android/src/main/java/expo/modules/kotlin/records/ValidationBinder.kt +110 -0
  34. package/android/src/main/java/expo/modules/kotlin/records/Validators.kt +61 -0
  35. package/android/src/main/java/expo/modules/kotlin/types/JSTypeConverter.kt +35 -0
  36. package/android/src/main/java/expo/modules/kotlin/types/JSTypeConverterHelper.kt +148 -0
  37. package/android/src/main/java/expo/modules/kotlin/types/TypeConverterProvider.kt +9 -1
  38. package/android/src/main/java/expo/modules/kotlin/views/GroupViewManagerWrapper.kt +49 -0
  39. package/android/src/main/java/expo/modules/kotlin/views/ViewGroupDefinition.kt +18 -0
  40. package/android/src/main/java/expo/modules/kotlin/views/ViewGroupDefinitionBuilder.kt +114 -0
  41. package/android/src/main/java/expo/modules/kotlin/views/ViewManagerDefinition.kt +4 -1
  42. package/android/src/main/java/expo/modules/kotlin/views/ViewManagerDefinitionBuilder.kt +56 -5
  43. package/android/src/main/java/expo/modules/kotlin/views/ViewManagerWrapperDelegate.kt +3 -0
  44. package/build/NativeModulesProxy.native.d.ts +0 -4
  45. package/build/NativeModulesProxy.native.d.ts.map +1 -1
  46. package/build/NativeModulesProxy.native.js +1 -14
  47. package/build/NativeModulesProxy.native.js.map +1 -1
  48. package/build/NativeModulesProxy.types.d.ts +0 -3
  49. package/build/NativeModulesProxy.types.d.ts.map +1 -1
  50. package/build/NativeModulesProxy.types.js.map +1 -1
  51. package/build/NativeViewManagerAdapter.native.d.ts.map +1 -1
  52. package/build/NativeViewManagerAdapter.native.js +9 -33
  53. package/build/NativeViewManagerAdapter.native.js.map +1 -1
  54. package/build/sweet/NativeErrorManager.js +1 -1
  55. package/build/sweet/NativeErrorManager.js.map +1 -1
  56. package/ios/AppDelegates/EXAppDelegatesLoader.m +4 -8
  57. package/ios/AppDelegates/ExpoAppDelegate.swift +4 -10
  58. package/ios/EXAppDefines.h +1 -0
  59. package/ios/EXAppDefines.m +6 -0
  60. package/ios/EXUtilities.h +2 -0
  61. package/ios/EXUtilities.m +12 -0
  62. package/ios/ExpoModulesCore.h +4 -0
  63. package/ios/ExpoModulesCore.podspec +4 -2
  64. package/ios/Interfaces/FileSystem/EXFileSystemInterface.h +1 -1
  65. package/ios/Interfaces/TaskManager/EXTaskServiceInterface.h +1 -0
  66. package/ios/JSI/{JSIConversions.h → EXJSIConversions.h} +4 -1
  67. package/ios/JSI/{JSIConversions.mm → EXJSIConversions.mm} +16 -5
  68. package/ios/JSI/{JSIInstaller.h → EXJSIInstaller.h} +3 -3
  69. package/ios/JSI/EXJSIInstaller.mm +17 -0
  70. package/ios/JSI/{ExpoModulesProxySpec.h → EXJSIUtils.h} +0 -9
  71. package/ios/JSI/{ExpoModulesProxySpec.mm → EXJSIUtils.mm} +4 -48
  72. package/ios/JSI/EXJavaScriptObject.h +97 -0
  73. package/ios/JSI/EXJavaScriptObject.mm +121 -0
  74. package/ios/JSI/{JavaScriptRuntime.h → EXJavaScriptRuntime.h} +27 -8
  75. package/ios/JSI/EXJavaScriptRuntime.mm +153 -0
  76. package/ios/JSI/EXJavaScriptValue.h +57 -0
  77. package/ios/JSI/EXJavaScriptValue.mm +166 -0
  78. package/ios/JSI/ExpoModulesHostObject.mm +2 -1
  79. package/ios/JSI/JavaScriptRuntime.swift +32 -0
  80. package/ios/JSI/JavaScriptValue.swift +94 -0
  81. package/ios/ModuleRegistryAdapter/EXModuleRegistryAdapter.m +1 -11
  82. package/ios/NativeModulesProxy/EXNativeModulesProxy.h +2 -2
  83. package/ios/NativeModulesProxy/EXNativeModulesProxy.mm +24 -22
  84. package/ios/ReactDelegates/EXReactCompatibleHelpers.h +18 -0
  85. package/ios/ReactDelegates/EXReactCompatibleHelpers.m +19 -0
  86. package/ios/ReactDelegates/ExpoReactDelegate.swift +2 -2
  87. package/ios/ReactDelegates/ExpoReactDelegateHandler.swift +1 -1
  88. package/ios/Swift/AppContext.swift +21 -2
  89. package/ios/Swift/Arguments/AnyArgumentType.swift +1 -1
  90. package/ios/Swift/Functions/AnyFunction.swift +5 -0
  91. package/ios/Swift/Functions/AsyncFunctionComponent.swift +182 -0
  92. package/ios/Swift/Functions/ConcreteFunction.swift +40 -68
  93. package/ios/Swift/Functions/SyncFunctionComponent.swift +181 -0
  94. package/ios/Swift/JavaScriptUtils.swift +56 -0
  95. package/ios/Swift/ModuleHolder.swift +17 -5
  96. package/ios/Swift/Modules/ModuleDefinitionComponents.swift +66 -44
  97. package/ios/Swift/ModulesProvider.swift +3 -10
  98. package/ios/Swift/Objects/ObjectDefinitionComponents.swift +49 -0
  99. package/ios/Swift/SwiftInteropBridge.swift +14 -5
  100. package/ios/Swift/Views/ComponentData.swift +2 -1
  101. package/ios/Swift/Views/ExpoView.swift +8 -0
  102. package/ios/Swift/Views/ViewManagerDefinitionComponents.swift +23 -0
  103. package/ios/Swift.h +5 -0
  104. package/ios/Tests/ArgumentTypeSpec.swift +2 -3
  105. package/ios/Tests/ConstantsSpec.swift +6 -7
  106. package/ios/Tests/ConvertiblesSpec.swift +2 -3
  107. package/ios/Tests/ExceptionsSpec.swift +2 -3
  108. package/ios/Tests/ExpoModulesSpec.swift +75 -0
  109. package/ios/Tests/FunctionSpec.swift +13 -15
  110. package/ios/Tests/FunctionWithConvertiblesSpec.swift +4 -5
  111. package/ios/Tests/JavaScriptObjectSpec.swift +97 -0
  112. package/ios/Tests/JavaScriptRuntimeSpec.swift +94 -0
  113. package/ios/Tests/ModuleEventListenersSpec.swift +15 -16
  114. package/ios/Tests/ModuleRegistrySpec.swift +2 -3
  115. package/ios/Tests/RecordSpec.swift +2 -3
  116. package/package.json +2 -2
  117. package/src/NativeModulesProxy.native.ts +2 -22
  118. package/src/NativeModulesProxy.types.ts +0 -8
  119. package/src/NativeViewManagerAdapter.native.tsx +12 -28
  120. package/src/sweet/NativeErrorManager.ts +1 -1
  121. package/android/src/main/java/expo/modules/kotlin/events/KEventEmitterWrapper.kt +0 -26
  122. package/android/src/main/java/expo/modules/kotlin/methods/Method.kt +0 -14
  123. package/android/src/main/java/expo/modules/kotlin/methods/PromiseMethod.kt +0 -15
  124. package/ios/JSI/JSIInstaller.mm +0 -34
  125. package/ios/JSI/JavaScriptObject.h +0 -60
  126. package/ios/JSI/JavaScriptObject.mm +0 -93
  127. package/ios/JSI/JavaScriptRuntime.mm +0 -102
  128. package/ios/NativeModulesProxy/EXComponentDataCompatibleWrapper.h +0 -16
  129. package/ios/NativeModulesProxy/EXComponentDataCompatibleWrapper.m +0 -28
@@ -0,0 +1,139 @@
1
+ package expo.modules.kotlin.records
2
+
3
+ import expo.modules.kotlin.exception.ValidationException
4
+
5
+ interface FieldValidator<T> {
6
+ fun validate(value: T)
7
+ }
8
+
9
+ class NumericRangeValidator<T : Comparable<T>>(
10
+ private val from: T,
11
+ private val to: T,
12
+ private val fromInclusive: Boolean,
13
+ private val toInclusive: Boolean
14
+ ) : FieldValidator<T> {
15
+ override fun validate(value: T) {
16
+ if (
17
+ value < from ||
18
+ to < value ||
19
+ value == from && !fromInclusive ||
20
+ value == to && !toInclusive
21
+ ) {
22
+ throw ValidationException("Value should be in range $from ${if (fromInclusive) "<=" else "<"} 'value' ${if (toInclusive) "<=" else "<"} $to, got $value")
23
+ }
24
+ }
25
+ }
26
+
27
+ class IsNotEmptyCollectionValidator : FieldValidator<Collection<*>> {
28
+ override fun validate(value: Collection<*>) {
29
+ if (value.isEmpty()) {
30
+ throw ValidationException("Collection is empty")
31
+ }
32
+ }
33
+ }
34
+
35
+ class IsNotEmptyIntArrayValidator : FieldValidator<IntArray> {
36
+ override fun validate(value: IntArray) {
37
+ if (value.isEmpty()) {
38
+ throw ValidationException("Array is empty")
39
+ }
40
+ }
41
+ }
42
+
43
+ class IsNotEmptyFloatArrayValidator : FieldValidator<FloatArray> {
44
+ override fun validate(value: FloatArray) {
45
+ if (value.isEmpty()) {
46
+ throw ValidationException("Array is empty")
47
+ }
48
+ }
49
+ }
50
+
51
+ class IsNotEmptyDoubleArrayValidator : FieldValidator<DoubleArray> {
52
+ override fun validate(value: DoubleArray) {
53
+ if (value.isEmpty()) {
54
+ throw ValidationException("Array is empty")
55
+ }
56
+ }
57
+ }
58
+
59
+ class IsNotEmptyArrayValidator : FieldValidator<Array<*>> {
60
+ override fun validate(value: Array<*>) {
61
+ if (value.isEmpty()) {
62
+ throw ValidationException("Array is empty")
63
+ }
64
+ }
65
+ }
66
+
67
+ class CollectionSizeValidator(
68
+ private val min: Int,
69
+ private val max: Int
70
+ ) : FieldValidator<Collection<*>> {
71
+ override fun validate(value: Collection<*>) {
72
+ if (value.size < min || value.size > max) {
73
+ throw ValidationException("Number of elements in the collection should be between $min and $max, got ${value.size}")
74
+ }
75
+ }
76
+ }
77
+
78
+ class IntArraySizeValidator(
79
+ private val min: Int,
80
+ private val max: Int
81
+ ) : FieldValidator<IntArray> {
82
+ override fun validate(value: IntArray) {
83
+ if (value.size < min || value.size > max) {
84
+ throw ValidationException("Number of elements in the array should be between $min and $max, got ${value.size}")
85
+ }
86
+ }
87
+ }
88
+
89
+ class DoubleArraySizeValidator(
90
+ private val min: Int,
91
+ private val max: Int
92
+ ) : FieldValidator<DoubleArray> {
93
+ override fun validate(value: DoubleArray) {
94
+ if (value.size < min || value.size > max) {
95
+ throw ValidationException("Number of elements in the array should be between $min and $max, got ${value.size}")
96
+ }
97
+ }
98
+ }
99
+
100
+ class FloatArraySizeValidator(
101
+ private val min: Int,
102
+ private val max: Int
103
+ ) : FieldValidator<FloatArray> {
104
+ override fun validate(value: FloatArray) {
105
+ if (value.size < min || value.size > max) {
106
+ throw ValidationException("Number of elements in the array should be between $min and $max, got ${value.size}")
107
+ }
108
+ }
109
+ }
110
+
111
+ class ArraySizeValidator(
112
+ private val min: Int,
113
+ private val max: Int
114
+ ) : FieldValidator<Array<*>> {
115
+ override fun validate(value: Array<*>) {
116
+ if (value.size < min || value.size > max) {
117
+ throw ValidationException("Number of elements in the array should be between $min and $max, got ${value.size}")
118
+ }
119
+ }
120
+ }
121
+
122
+ class StringSizeValidator(
123
+ private val min: Int,
124
+ private val max: Int
125
+ ) : FieldValidator<String> {
126
+ override fun validate(value: String) {
127
+ if (value.length < min || value.length > max) {
128
+ throw ValidationException("Length of the string should be between $min and $max, got $value (${value.length} characters)")
129
+ }
130
+ }
131
+ }
132
+
133
+ class RegexValidator(private val regex: Regex) : FieldValidator<CharSequence> {
134
+ override fun validate(value: CharSequence) {
135
+ if (!regex.matches(value)) {
136
+ throw ValidationException("Provided string $value didn't match regex $regex")
137
+ }
138
+ }
139
+ }
@@ -1,42 +1,3 @@
1
1
  package expo.modules.kotlin.records
2
2
 
3
- import android.os.Bundle
4
- import com.facebook.react.bridge.Arguments
5
- import com.facebook.react.bridge.ReadableArray
6
- import com.facebook.react.bridge.ReadableMap
7
- import java.lang.IllegalArgumentException
8
- import kotlin.reflect.full.findAnnotation
9
- import kotlin.reflect.full.memberProperties
10
- import kotlin.reflect.jvm.isAccessible
11
-
12
3
  interface Record
13
-
14
- fun Record.toJSMap(): ReadableMap {
15
- val writableMap = Arguments.createMap()
16
- javaClass
17
- .kotlin
18
- .memberProperties.map { property ->
19
- val fieldInformation = property.findAnnotation<Field>() ?: return@map
20
- val jsKey = fieldInformation.key.takeUnless { it == "" } ?: property.name
21
-
22
- property.isAccessible = true
23
-
24
- // TODO(@lukmccall): add more sophisticated conversion method
25
- when (val value = property.get(this)) {
26
- null, is Unit -> writableMap.putNull(jsKey)
27
- is Boolean -> writableMap.putBoolean(jsKey, value)
28
- is Int -> writableMap.putInt(jsKey, value)
29
- is Number -> writableMap.putDouble(jsKey, value.toDouble())
30
- is String -> writableMap.putString(jsKey, value)
31
- is ReadableArray -> writableMap.putArray(jsKey, value)
32
- is ReadableMap -> writableMap.putMap(jsKey, value)
33
- is Record -> writableMap.putMap(jsKey, value.toJSMap())
34
- is Bundle -> writableMap.putMap(jsKey, Arguments.fromBundle(value))
35
- is List<*> -> writableMap.putArray(jsKey, Arguments.fromList(value))
36
- is Array<*> -> writableMap.putArray(jsKey, Arguments.fromArray(value))
37
- else -> throw IllegalArgumentException("Could not convert " + value.javaClass)
38
- }
39
- }
40
-
41
- return writableMap
42
- }
@@ -4,23 +4,41 @@ import com.facebook.react.bridge.Dynamic
4
4
  import expo.modules.kotlin.allocators.ObjectConstructor
5
5
  import expo.modules.kotlin.allocators.ObjectConstructorFactory
6
6
  import expo.modules.kotlin.exception.FieldCastException
7
+ import expo.modules.kotlin.exception.FieldRequiredException
7
8
  import expo.modules.kotlin.exception.RecordCastException
8
9
  import expo.modules.kotlin.exception.exceptionDecorator
9
10
  import expo.modules.kotlin.recycle
10
11
  import expo.modules.kotlin.types.TypeConverter
11
12
  import expo.modules.kotlin.types.TypeConverterProvider
12
13
  import kotlin.reflect.KClass
14
+ import kotlin.reflect.KProperty1
13
15
  import kotlin.reflect.KType
16
+ import kotlin.reflect.full.createInstance
14
17
  import kotlin.reflect.full.findAnnotation
15
18
  import kotlin.reflect.full.memberProperties
16
19
  import kotlin.reflect.jvm.javaField
17
20
 
18
- // TODO(@lukmccall): create all converters during initialization
19
21
  class RecordTypeConverter<T : Record>(
20
22
  private val converterProvider: TypeConverterProvider,
21
23
  val type: KType,
22
24
  ) : TypeConverter<T>(type.isMarkedNullable) {
23
25
  private val objectConstructorFactory = ObjectConstructorFactory()
26
+ private val propertyDescriptors: Map<KProperty1<out Any, *>, PropertyDescriptor> =
27
+ (type.classifier as KClass<*>)
28
+ .memberProperties
29
+ .map { property ->
30
+ val fieldAnnotation = property.findAnnotation<Field>() ?: return@map null
31
+ val typeConverter = converterProvider.obtainTypeConverter(property.returnType)
32
+
33
+ return@map property to PropertyDescriptor(
34
+ typeConverter,
35
+ fieldAnnotation,
36
+ isRequired = property.findAnnotation<Required>() != null,
37
+ validators = getValidators(property)
38
+ )
39
+ }
40
+ .filterNotNull()
41
+ .toMap()
24
42
 
25
43
  override fun convertNonOptional(value: Dynamic): T = exceptionDecorator({ cause -> RecordCastException(type, cause) }) {
26
44
  val jsMap = value.asMap()
@@ -28,23 +46,32 @@ class RecordTypeConverter<T : Record>(
28
46
  val kClass = type.classifier as KClass<*>
29
47
  val instance = getObjectConstructor(kClass.java).construct()
30
48
 
31
- kClass
32
- .memberProperties
33
- .map { property ->
34
- val filedInformation = property.findAnnotation<Field>() ?: return@map
35
- val jsKey = filedInformation.key.takeUnless { it == "" } ?: property.name
49
+ propertyDescriptors
50
+ .forEach { (property, descriptor) ->
51
+ val jsKey = descriptor.fieldAnnotation.key.takeUnless { it.isBlank() } ?: property.name
36
52
 
37
53
  if (!jsMap.hasKey(jsKey)) {
38
- // TODO(@lukmccall): handle required keys
39
- return@map
54
+ if (descriptor.isRequired) {
55
+ throw FieldRequiredException(property)
56
+ }
57
+
58
+ return@forEach
40
59
  }
41
60
 
42
61
  jsMap.getDynamic(jsKey).recycle {
43
62
  val javaField = property.javaField!!
44
63
 
45
- val elementConverter = converterProvider.obtainTypeConverter(property.returnType)
46
64
  val casted = exceptionDecorator({ cause -> FieldCastException(property.name, property.returnType, type, cause) }) {
47
- elementConverter.convert(this)
65
+ descriptor.typeConverter.convert(this)
66
+ }
67
+
68
+ if (casted != null) {
69
+ descriptor
70
+ .validators
71
+ .forEach { validator ->
72
+ @Suppress("UNCHECKED_CAST")
73
+ (validator as FieldValidator<Any>).validate(casted)
74
+ }
48
75
  }
49
76
 
50
77
  javaField.isAccessible = true
@@ -59,4 +86,26 @@ class RecordTypeConverter<T : Record>(
59
86
  private fun <T> getObjectConstructor(clazz: Class<T>): ObjectConstructor<T> {
60
87
  return objectConstructorFactory.get(clazz)
61
88
  }
89
+
90
+ private fun getValidators(property: KProperty1<out Any, *>): List<FieldValidator<*>> {
91
+ return property
92
+ .annotations
93
+ .map findValidators@{ annotation ->
94
+ val binderAnnotation = annotation.annotationClass.findAnnotation<BindUsing>()
95
+ ?: return@findValidators null
96
+ annotation to binderAnnotation
97
+ }
98
+ .filterNotNull()
99
+ .map { (annotation, binderAnnotation) ->
100
+ val binderInstance = binderAnnotation.binder.createInstance() as ValidationBinder
101
+ binderInstance.bind(annotation, property.returnType)
102
+ }
103
+ }
104
+
105
+ private data class PropertyDescriptor(
106
+ val typeConverter: TypeConverter<*>,
107
+ val fieldAnnotation: Field,
108
+ val isRequired: Boolean,
109
+ val validators: List<FieldValidator<*>>
110
+ )
62
111
  }
@@ -0,0 +1,5 @@
1
+ package expo.modules.kotlin.records
2
+
3
+ @Target(AnnotationTarget.PROPERTY)
4
+ @Retention(AnnotationRetention.RUNTIME)
5
+ annotation class Required
@@ -0,0 +1,110 @@
1
+ package expo.modules.kotlin.records
2
+
3
+ import kotlin.reflect.KClass
4
+ import kotlin.reflect.KType
5
+ import kotlin.reflect.full.createType
6
+ import kotlin.reflect.full.isSubclassOf
7
+
8
+ interface ValidationBinder {
9
+ fun bind(annotation: Annotation, fieldType: KType): FieldValidator<*>
10
+ }
11
+
12
+ @Retention(AnnotationRetention.RUNTIME)
13
+ @Target(AnnotationTarget.ANNOTATION_CLASS)
14
+ annotation class BindUsing(val binder: KClass<*>)
15
+
16
+ internal class IntRangeBinder : ValidationBinder {
17
+ override fun bind(annotation: Annotation, fieldType: KType): FieldValidator<*> {
18
+ val rangeAnnotation = annotation as IntRange
19
+ return NumericRangeValidator(
20
+ rangeAnnotation.from,
21
+ rangeAnnotation.to,
22
+ rangeAnnotation.fromInclusive,
23
+ rangeAnnotation.toInclusive
24
+ )
25
+ }
26
+ }
27
+
28
+ internal class LongRangeBinder : ValidationBinder {
29
+ override fun bind(annotation: Annotation, fieldType: KType): FieldValidator<*> {
30
+ val rangeAnnotation = annotation as LongRange
31
+ return NumericRangeValidator(
32
+ rangeAnnotation.from,
33
+ rangeAnnotation.to,
34
+ rangeAnnotation.fromInclusive,
35
+ rangeAnnotation.toInclusive
36
+ )
37
+ }
38
+ }
39
+
40
+ internal class FloatRangeBinder : ValidationBinder {
41
+ override fun bind(annotation: Annotation, fieldType: KType): FieldValidator<*> {
42
+ val rangeAnnotation = annotation as FloatRange
43
+ return NumericRangeValidator(
44
+ rangeAnnotation.from,
45
+ rangeAnnotation.to,
46
+ rangeAnnotation.fromInclusive,
47
+ rangeAnnotation.toInclusive
48
+ )
49
+ }
50
+ }
51
+
52
+ internal class DoubleRangeBinder : ValidationBinder {
53
+ override fun bind(annotation: Annotation, fieldType: KType): FieldValidator<*> {
54
+ val rangeAnnotation = annotation as DoubleRange
55
+ return NumericRangeValidator(
56
+ rangeAnnotation.from,
57
+ rangeAnnotation.to,
58
+ rangeAnnotation.fromInclusive,
59
+ rangeAnnotation.toInclusive
60
+ )
61
+ }
62
+ }
63
+
64
+ internal class IsCollectionNotEmptyBinder : ValidationBinder {
65
+ override fun bind(annotation: Annotation, fieldType: KType): FieldValidator<*> {
66
+ assert(annotation is IsNotEmpty)
67
+
68
+ when (fieldType) {
69
+ IntArray::class.createType() -> return IsNotEmptyIntArrayValidator()
70
+ DoubleArray::class.createType() -> return IsNotEmptyDoubleArrayValidator()
71
+ FloatArray::class.createType() -> return IsNotEmptyFloatArrayValidator()
72
+ }
73
+
74
+ val kClass = fieldType.classifier as KClass<*>
75
+ if (kClass.isSubclassOf(Array::class) || kClass.java.isArray) {
76
+ return IsNotEmptyArrayValidator()
77
+ }
78
+
79
+ return IsNotEmptyCollectionValidator()
80
+ }
81
+ }
82
+
83
+ internal class SizeBinder : ValidationBinder {
84
+ override fun bind(annotation: Annotation, fieldType: KType): FieldValidator<*> {
85
+ val sizeAnnotation = annotation as Size
86
+
87
+ when (fieldType) {
88
+ IntArray::class.createType() -> return IntArraySizeValidator(sizeAnnotation.min, sizeAnnotation.max)
89
+ DoubleArray::class.createType() -> return DoubleArraySizeValidator(sizeAnnotation.min, sizeAnnotation.max)
90
+ FloatArray::class.createType() -> return FloatArraySizeValidator(sizeAnnotation.min, sizeAnnotation.max)
91
+ }
92
+
93
+ val kClass = fieldType.classifier as KClass<*>
94
+
95
+ if (kClass.isSubclassOf(String::class)) {
96
+ return StringSizeValidator(sizeAnnotation.min, sizeAnnotation.max)
97
+ } else if (kClass.isSubclassOf(Array::class) || kClass.java.isArray) {
98
+ return ArraySizeValidator(sizeAnnotation.min, sizeAnnotation.max)
99
+ }
100
+
101
+ return CollectionSizeValidator(sizeAnnotation.min, sizeAnnotation.max)
102
+ }
103
+ }
104
+
105
+ internal class RegexBinder : ValidationBinder {
106
+ override fun bind(annotation: Annotation, fieldType: KType): FieldValidator<*> {
107
+ val regexAnnotation = annotation as RegularExpression
108
+ return RegexValidator(regexAnnotation.regex.toRegex())
109
+ }
110
+ }
@@ -0,0 +1,61 @@
1
+ package expo.modules.kotlin.records
2
+
3
+ @Retention(AnnotationRetention.RUNTIME)
4
+ @Target(AnnotationTarget.PROPERTY, AnnotationTarget.TYPE, AnnotationTarget.VALUE_PARAMETER)
5
+ @BindUsing(IntRangeBinder::class)
6
+ annotation class IntRange(
7
+ val from: Int,
8
+ val to: Int,
9
+ val fromInclusive: Boolean = true,
10
+ val toInclusive: Boolean = true,
11
+ )
12
+
13
+ @Retention(AnnotationRetention.RUNTIME)
14
+ @Target(AnnotationTarget.PROPERTY)
15
+ @BindUsing(LongRangeBinder::class)
16
+ annotation class LongRange(
17
+ val from: Long,
18
+ val to: Long,
19
+ val fromInclusive: Boolean = true,
20
+ val toInclusive: Boolean = true
21
+ )
22
+
23
+ @Retention(AnnotationRetention.RUNTIME)
24
+ @Target(AnnotationTarget.PROPERTY)
25
+ @BindUsing(FloatRangeBinder::class)
26
+ annotation class FloatRange(
27
+ val from: Float,
28
+ val to: Float,
29
+ val fromInclusive: Boolean = true,
30
+ val toInclusive: Boolean = true
31
+ )
32
+
33
+ @Retention(AnnotationRetention.RUNTIME)
34
+ @Target(AnnotationTarget.PROPERTY)
35
+ @BindUsing(DoubleRangeBinder::class)
36
+ annotation class DoubleRange(
37
+ val from: Double,
38
+ val to: Double,
39
+ val fromInclusive: Boolean = true,
40
+ val toInclusive: Boolean = true
41
+ )
42
+
43
+ @Retention(AnnotationRetention.RUNTIME)
44
+ @Target(AnnotationTarget.PROPERTY)
45
+ @BindUsing(IsCollectionNotEmptyBinder::class)
46
+ annotation class IsNotEmpty
47
+
48
+ @Retention(AnnotationRetention.RUNTIME)
49
+ @Target(AnnotationTarget.PROPERTY)
50
+ @BindUsing(SizeBinder::class)
51
+ annotation class Size(
52
+ val min: Int = 0,
53
+ val max: Int = Int.MAX_VALUE
54
+ )
55
+
56
+ @Retention(AnnotationRetention.RUNTIME)
57
+ @Target(AnnotationTarget.PROPERTY)
58
+ @BindUsing(RegexBinder::class)
59
+ annotation class RegularExpression(
60
+ val regex: String
61
+ )
@@ -0,0 +1,35 @@
1
+ package expo.modules.kotlin.types
2
+
3
+ import android.os.Bundle
4
+ import com.facebook.react.bridge.Arguments
5
+ import com.facebook.react.bridge.WritableArray
6
+ import com.facebook.react.bridge.WritableMap
7
+ import expo.modules.kotlin.records.Record
8
+
9
+ object JSTypeConverter {
10
+ interface ContainerProvider {
11
+ fun createMap(): WritableMap
12
+ fun createArray(): WritableArray
13
+ }
14
+
15
+ internal object DefaultContainerProvider : ContainerProvider {
16
+ override fun createMap(): WritableMap = Arguments.createMap()
17
+ override fun createArray(): WritableArray = Arguments.createArray()
18
+ }
19
+
20
+ fun convertToJSValue(value: Any?, containerProvider: ContainerProvider = DefaultContainerProvider): Any? {
21
+ return when (value) {
22
+ null, is Unit -> null
23
+ is Bundle -> value.toJSValue(containerProvider)
24
+ is Iterable<*> -> value.toJSValue(containerProvider)
25
+ is Array<*> -> value.toJSValue(containerProvider)
26
+ is IntArray -> value.toJSValue(containerProvider)
27
+ is FloatArray -> value.toJSValue(containerProvider)
28
+ is DoubleArray -> value.toJSValue(containerProvider)
29
+ is Map<*, *> -> value.toJSValue(containerProvider)
30
+ is Enum<*> -> value.toJSValue()
31
+ is Record -> value.toJSValue(containerProvider)
32
+ else -> value
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,148 @@
1
+ package expo.modules.kotlin.types
2
+
3
+ import android.os.Bundle
4
+ import com.facebook.react.bridge.ReadableArray
5
+ import com.facebook.react.bridge.ReadableMap
6
+ import com.facebook.react.bridge.WritableArray
7
+ import com.facebook.react.bridge.WritableMap
8
+ import expo.modules.kotlin.records.Field
9
+ import expo.modules.kotlin.records.Record
10
+ import kotlin.reflect.KProperty1
11
+ import kotlin.reflect.full.declaredMemberProperties
12
+ import kotlin.reflect.full.findAnnotation
13
+ import kotlin.reflect.full.memberProperties
14
+ import kotlin.reflect.full.primaryConstructor
15
+ import kotlin.reflect.jvm.isAccessible
16
+
17
+ fun Record.toJSValue(containerProvider: JSTypeConverter.ContainerProvider): WritableMap {
18
+ val result = containerProvider.createMap()
19
+
20
+ javaClass
21
+ .kotlin
22
+ .memberProperties.map { property ->
23
+ val fieldInformation = property.findAnnotation<Field>() ?: return@map
24
+ val jsKey = fieldInformation.key.takeUnless { it == "" } ?: property.name
25
+
26
+ property.isAccessible = true
27
+
28
+ val value = property.get(this)
29
+ val convertedValue = JSTypeConverter.convertToJSValue(value, containerProvider)
30
+ result.putGeneric(jsKey, convertedValue)
31
+ }
32
+
33
+ return result
34
+ }
35
+
36
+ fun Bundle.toJSValue(containerProvider: JSTypeConverter.ContainerProvider): WritableMap {
37
+ val result = containerProvider.createMap()
38
+
39
+ for (key in keySet()) {
40
+ val value = get(key)
41
+ val convertedValue = JSTypeConverter.convertToJSValue(value, containerProvider)
42
+ result.putGeneric(key, convertedValue)
43
+ }
44
+
45
+ return result
46
+ }
47
+
48
+ fun <K, V> Map<K, V>.toJSValue(containerProvider: JSTypeConverter.ContainerProvider): WritableMap {
49
+ val result = containerProvider.createMap()
50
+
51
+ for ((key, value) in entries) {
52
+ val convertedValue = JSTypeConverter.convertToJSValue(value, containerProvider)
53
+ result.putGeneric(key.toString(), convertedValue)
54
+ }
55
+
56
+ return result
57
+ }
58
+
59
+ fun <T> Iterable<T>.toJSValue(containerProvider: JSTypeConverter.ContainerProvider): WritableArray {
60
+ val result = containerProvider.createArray()
61
+
62
+ for (value in this) {
63
+ val convertedValue = JSTypeConverter.convertToJSValue(value, containerProvider)
64
+ result.putGeneric(convertedValue)
65
+ }
66
+
67
+ return result
68
+ }
69
+
70
+ fun <T> Array<T>.toJSValue(containerProvider: JSTypeConverter.ContainerProvider): WritableArray {
71
+ val result = containerProvider.createArray()
72
+
73
+ for (value in this) {
74
+ val convertedValue = JSTypeConverter.convertToJSValue(value, containerProvider)
75
+ result.putGeneric(convertedValue)
76
+ }
77
+
78
+ return result
79
+ }
80
+
81
+ fun IntArray.toJSValue(containerProvider: JSTypeConverter.ContainerProvider): WritableArray {
82
+ return containerProvider.createArray().also {
83
+ for (value in this) {
84
+ it.pushInt(value)
85
+ }
86
+ }
87
+ }
88
+
89
+ fun FloatArray.toJSValue(containerProvider: JSTypeConverter.ContainerProvider): WritableArray {
90
+ return containerProvider.createArray().also {
91
+ for (value in this) {
92
+ it.pushDouble(value.toDouble())
93
+ }
94
+ }
95
+ }
96
+
97
+ fun DoubleArray.toJSValue(containerProvider: JSTypeConverter.ContainerProvider): WritableArray {
98
+ return containerProvider.createArray().also {
99
+ for (value in this) {
100
+ it.pushDouble(value)
101
+ }
102
+ }
103
+ }
104
+
105
+ fun Enum<*>.toJSValue(): Any? {
106
+ val primaryConstructor = requireNotNull(this::class.primaryConstructor) {
107
+ "Cannot convert enum without the primary constructor to js value"
108
+ }
109
+
110
+ if (primaryConstructor.parameters.isEmpty()) {
111
+ return this.name
112
+ } else if (primaryConstructor.parameters.size == 1) {
113
+ val parameterName = primaryConstructor.parameters.first().name!!
114
+ @Suppress("UNCHECKED_CAST")
115
+ val parameterProperty = this::class.declaredMemberProperties
116
+ .find { it.name == parameterName } as KProperty1<Enum<*>, *>
117
+
118
+ return parameterProperty.get(this)
119
+ }
120
+
121
+ throw IllegalStateException("Enum '$javaClass' cannot be used as return type (incompatible with JS)")
122
+ }
123
+
124
+ internal fun WritableMap.putGeneric(key: String, value: Any?) {
125
+ when (value) {
126
+ null, is Unit -> putNull(key)
127
+ is ReadableArray -> putArray(key, value)
128
+ is ReadableMap -> putMap(key, value)
129
+ is String -> putString(key, value)
130
+ is Int -> putInt(key, value)
131
+ is Number -> putDouble(key, value.toDouble())
132
+ is Boolean -> putBoolean(key, value)
133
+ else -> throw IllegalArgumentException("Could not put '${value.javaClass}' to WritableMap")
134
+ }
135
+ }
136
+
137
+ internal fun WritableArray.putGeneric(value: Any?) {
138
+ when (value) {
139
+ null, is Unit -> pushNull()
140
+ is ReadableArray -> pushArray(value)
141
+ is ReadableMap -> pushMap(value)
142
+ is String -> pushString(value)
143
+ is Int -> pushInt(value)
144
+ is Number -> pushDouble(value.toDouble())
145
+ is Boolean -> pushBoolean(value)
146
+ else -> throw IllegalArgumentException("Could not put '${value.javaClass}' to WritableArray")
147
+ }
148
+ }
@@ -35,6 +35,7 @@ fun convert(value: Dynamic, type: KType): Any? {
35
35
 
36
36
  object TypeConverterProviderImpl : TypeConverterProvider {
37
37
  private val cachedConverters = createCashedConverters(false) + createCashedConverters(true)
38
+ private val cachedRecordConverters = mutableMapOf<KClass<*>, TypeConverter<*>>()
38
39
 
39
40
  override fun obtainTypeConverter(type: KType): TypeConverter<*> {
40
41
  cachedConverters[type]?.let {
@@ -68,8 +69,15 @@ object TypeConverterProviderImpl : TypeConverterProvider {
68
69
  return EnumTypeConverter(kClass as KClass<Enum<*>>, type.isMarkedNullable)
69
70
  }
70
71
 
72
+ val cachedConverter = cachedRecordConverters[kClass]
73
+ if (cachedConverter != null) {
74
+ return cachedConverter
75
+ }
76
+
71
77
  if (kClass.isSubclassOf(Record::class)) {
72
- return RecordTypeConverter<Record>(this, type)
78
+ val converter = RecordTypeConverter<Record>(this, type)
79
+ cachedRecordConverters[kClass] = converter
80
+ return converter
73
81
  }
74
82
 
75
83
  throw MissingTypeConverter(type)