From a1af6fdd56a1d18fc53bfcb35b25a1baf067b272 Mon Sep 17 00:00:00 2001 From: Andrey Dorofeev Date: Tue, 15 Jun 2021 15:33:02 +0700 Subject: [PATCH 1/3] added ios implementation for JavaScriptEngine --- .../moko/javascript/JavaScriptEngine.kt | 99 +++++++++++++++++++ .../ios-app/ios-app.xcodeproj/project.pbxproj | 3 + .../src/Resources/Base.lproj/Main.storyboard | 75 ++++++++++++-- sample/ios-app/src/TestViewController.swift | 15 +++ .../com/icerockdev/library/Calculator.kt | 28 +++++- 5 files changed, 209 insertions(+), 11 deletions(-) create mode 100644 javascript/src/iosMain/kotlin/dev/icerock/moko/javascript/JavaScriptEngine.kt diff --git a/javascript/src/iosMain/kotlin/dev/icerock/moko/javascript/JavaScriptEngine.kt b/javascript/src/iosMain/kotlin/dev/icerock/moko/javascript/JavaScriptEngine.kt new file mode 100644 index 0000000..92c9a01 --- /dev/null +++ b/javascript/src/iosMain/kotlin/dev/icerock/moko/javascript/JavaScriptEngine.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.javascript + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.encodeToJsonElement +import platform.Foundation.NSArray +import platform.Foundation.NSDictionary +import platform.Foundation.NSJSONSerialization +import platform.Foundation.NSString +import platform.Foundation.NSUTF8StringEncoding +import platform.Foundation.create +import platform.Foundation.dataUsingEncoding +import platform.JavaScriptCore.JSContext +import platform.JavaScriptCore.JSValue +import platform.JavaScriptCore.setObject + +actual class JavaScriptEngine actual constructor() { + actual fun evaluate(context: Map, script: String): JsType { + + val jsContext = JSContext() + + jsContext.exceptionHandler = { exceptionContext, exception -> + val message = "\"context = $exceptionContext, exception = $exception\"" + throw JavaScriptEvaluationException(cause = null, message = message) + } + + context.forEach { + jsContext.setObject( + `object` = it.value.getValue(), + forKeyedSubscript = NSString.create(string = it.key) + ) + } + + val result = jsContext.evaluateScript(script) + + return result?.toMokoJSType() ?: JsType.Null + } + + actual fun close() { + // Nothing to do here + } +} + +private fun JsonObject.toNSDictionary(): NSDictionary { + val data = NSString.create(string = this.toString()).dataUsingEncoding(NSUTF8StringEncoding) + ?: return NSDictionary() + return (NSJSONSerialization.JSONObjectWithData( + data = data, + options = 0, + error = null + ) as? NSDictionary) ?: NSDictionary() +} + +private fun JsonArray.toNSArray(): NSArray { + val data = NSString.create(string = this.toString()).dataUsingEncoding(NSUTF8StringEncoding) + ?: return NSArray() + return (NSJSONSerialization.JSONObjectWithData( + data = data, + options = 0, + error = null + ) as? NSArray) ?: NSArray() +} + +private fun JsonElement.getValue(): Any? { + return (this as? JsonObject)?.toNSDictionary() + ?: (this as? JsonArray)?.toNSArray() + ?: (this as? JsonPrimitive)?.content +} + +private fun JsType.getValue(): Any? { + return when (this) { + is JsType.Bool -> value + is JsType.Str -> value + is JsType.IntNum -> value + is JsType.DoubleNum -> value + is JsType.Json -> value.getValue() + is JsType.Null -> null + } +} + +private fun JSValue.toMokoJSType(): JsType { + return when { + isBoolean -> JsType.Bool(toBool()) + isString -> JsType.Str(toString_().orEmpty()) + isNumber -> JsType.DoubleNum(toDouble()) + isObject -> JsType.Json(Json.encodeToJsonElement(toDictionary())) + isArray -> JsType.Json(Json.encodeToJsonElement(toArray())) + isUndefined -> JsType.Null + isNull -> JsType.Null + else -> JsType.Null + } +} diff --git a/sample/ios-app/ios-app.xcodeproj/project.pbxproj b/sample/ios-app/ios-app.xcodeproj/project.pbxproj index fee4d55..f6169bb 100644 --- a/sample/ios-app/ios-app.xcodeproj/project.pbxproj +++ b/sample/ios-app/ios-app.xcodeproj/project.pbxproj @@ -145,6 +145,7 @@ hasScannedForEncodings = 0; knownRegions = ( English, + Base, ); mainGroup = 287627F61F319065007FA12B; productRefGroup = 287628001F319065007FA12B /* Products */; @@ -270,6 +271,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 4VU932NX78; INFOPLIST_FILE = src/Info.plist; + ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.icerock.moko.sample.javascript; PRODUCT_NAME = mokoSampleJavascript; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -288,6 +290,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 4VU932NX78; INFOPLIST_FILE = src/Info.plist; + ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.icerock.moko.sample.javascript; PRODUCT_NAME = mokoSampleJavascript; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/sample/ios-app/src/Resources/Base.lproj/Main.storyboard b/sample/ios-app/src/Resources/Base.lproj/Main.storyboard index 594bc2b..ca8b011 100644 --- a/sample/ios-app/src/Resources/Base.lproj/Main.storyboard +++ b/sample/ios-app/src/Resources/Base.lproj/Main.storyboard @@ -1,11 +1,8 @@ - - - - + + - - + @@ -14,7 +11,7 @@ - + @@ -28,7 +25,7 @@ - + @@ -36,8 +33,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sample/ios-app/src/TestViewController.swift b/sample/ios-app/src/TestViewController.swift index 23a9c17..895ec43 100644 --- a/sample/ios-app/src/TestViewController.swift +++ b/sample/ios-app/src/TestViewController.swift @@ -7,7 +7,22 @@ import MultiPlatformLibrary class TestViewController: UIViewController { + @IBOutlet private var firstValueTextField: UITextField! + @IBOutlet private var secondValueTextField: UITextField! + @IBOutlet private var resultLabel: UILabel! + override func viewDidLoad() { super.viewDidLoad() } + + @IBAction private func run() { + let result = Calculator().run(a: firstValueTextField.text ?? "", b: secondValueTextField.text ?? "") + if let value = (result as? JsType.Str)?.value { + resultLabel.text = value + } + + if let value = (result as? JsType.DoubleNum)?.value { + resultLabel.text = "\(value)" + } + } } diff --git a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/Calculator.kt b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/Calculator.kt index 4489719..32427f7 100644 --- a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/Calculator.kt +++ b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/Calculator.kt @@ -4,10 +4,32 @@ package com.icerockdev.library -import dev.icerock.moko.javascript.multiply +import dev.icerock.moko.javascript.JavaScriptEngine +import dev.icerock.moko.javascript.JsType class Calculator { - fun run() { - println(multiply(a = 2, b = 3)) + fun run(a: String, b: String): JsType { + val engine = JavaScriptEngine() + val testScript = "a+b" + + val intA = a.toIntOrNull() + val intB = b.toIntOrNull() + + val context = if (intA != null && intB != null) { + mapOf( + "a" to JsType.IntNum(intA), + "b" to JsType.IntNum(intB) + ) + } else { + mapOf( + "a" to JsType.Str(a), + "b" to JsType.Str(b) + ) + } + + return engine.evaluate( + context = context, + script = testScript + ) } } From 9d42299df86cad62a7d63ace54aee7839807ee06 Mon Sep 17 00:00:00 2001 From: Andrey Dorofeev Date: Tue, 15 Jun 2021 17:04:43 +0700 Subject: [PATCH 2/3] added test for javascript evaluate --- buildSrc/src/main/kotlin/Deps.kt | 4 +- .../moko/javascript/JavaScriptEngineTest.kt | 44 +++++++++++++++++++ .../dev/icerock/moko/javascript/SampleTest.kt | 15 ------- sample/mpp-library/build.gradle.kts | 1 + 4 files changed, 47 insertions(+), 17 deletions(-) create mode 100644 javascript/src/commonTest/kotlin/dev/icerock/moko/javascript/JavaScriptEngineTest.kt delete mode 100644 javascript/src/commonTest/kotlin/dev/icerock/moko/javascript/SampleTest.kt diff --git a/buildSrc/src/main/kotlin/Deps.kt b/buildSrc/src/main/kotlin/Deps.kt index bd13d79..a5501a1 100644 --- a/buildSrc/src/main/kotlin/Deps.kt +++ b/buildSrc/src/main/kotlin/Deps.kt @@ -13,7 +13,7 @@ object Deps { private const val coroutinesVersion = "1.4.2" private const val kotlinxSerializationVersion = "1.1.0" - private const val mokoTestVersion = "0.2.0" + private const val mokoTestVersion = "0.3.0" const val mokoJavascriptVersion = "0.1.0" object Android { @@ -37,7 +37,7 @@ object Deps { const val kotlinSerialization = "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion" - const val mokoTest = "dev.icerock.moko:test:$mokoTestVersion" + const val mokoTest = "dev.icerock.moko:test-core:$mokoTestVersion" const val mokoJavascript = "dev.icerock.moko:javascript:$mokoJavascriptVersion" } } diff --git a/javascript/src/commonTest/kotlin/dev/icerock/moko/javascript/JavaScriptEngineTest.kt b/javascript/src/commonTest/kotlin/dev/icerock/moko/javascript/JavaScriptEngineTest.kt new file mode 100644 index 0000000..5a622d1 --- /dev/null +++ b/javascript/src/commonTest/kotlin/dev/icerock/moko/javascript/JavaScriptEngineTest.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.javascript + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.encodeToJsonElement + +class JavaScriptEngineTest { + @Test + fun `test json context`() { + val form = mapOf("selector_1" to "first_value", "selector_2" to "second_value") + val profile = mapOf("email" to "test@test.com") + val formJson = Json.encodeToJsonElement(form) + val profileJson = Json.encodeToJsonElement(profile) + val context: Map = mapOf("form" to JsType.Json(formJson), "profile" to JsType.Json(profileJson)) + + val jsEngine = JavaScriptEngine() + + assertEquals(JsType.Bool(true), jsEngine.evaluate(context = context, script = "form.selector_1 == \"first_value\"")) + + assertEquals(JsType.Bool(true), jsEngine.evaluate(context = context, script = "profile.email != null")) + + assertEquals(JsType.Str("test@test.com"), jsEngine.evaluate(context = context, script = "profile.email")) + + assertEquals(JsType.Null, jsEngine.evaluate(context = context, script = "profile.first_name")) + } + + @Test + fun `test plus script`() { + val list = listOf(5, 15) + val listJson = Json.encodeToJsonElement(list) + val context: Map = mapOf("list" to JsType.Json(listJson), "number" to JsType.IntNum(4), "doubleString" to JsType.Str(" Hello ")) + + val jsEngine = JavaScriptEngine() + + assertEquals(JsType.DoubleNum(19.0), jsEngine.evaluate(context = context, script = "list[1]+number")) + + assertEquals(JsType.Str(" Hello 5"), jsEngine.evaluate(context = context, script = "doubleString+list[0]")) + } +} diff --git a/javascript/src/commonTest/kotlin/dev/icerock/moko/javascript/SampleTest.kt b/javascript/src/commonTest/kotlin/dev/icerock/moko/javascript/SampleTest.kt deleted file mode 100644 index 0309b38..0000000 --- a/javascript/src/commonTest/kotlin/dev/icerock/moko/javascript/SampleTest.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. - */ - -package dev.icerock.moko.javascript - -import kotlin.test.Test -import kotlin.test.assertEquals - -class SampleTest { - @Test - fun `equality test`() { - assertEquals(expected = 4, actual = multiply(a = 2, b = 2)) - } -} diff --git a/sample/mpp-library/build.gradle.kts b/sample/mpp-library/build.gradle.kts index 4c3ed48..04ed8b3 100644 --- a/sample/mpp-library/build.gradle.kts +++ b/sample/mpp-library/build.gradle.kts @@ -9,6 +9,7 @@ plugins { dependencies { commonMainApi(Deps.Libs.MultiPlatform.coroutines) + commonMainApi(Deps.Libs.MultiPlatform.kotlinSerialization) commonMainApi(Deps.Libs.MultiPlatform.mokoJavascript) } From 295684e9b06f2c61b0983203b5e0c1eeaa6e1578 Mon Sep 17 00:00:00 2001 From: Andrey Dorofeev Date: Tue, 15 Jun 2021 18:05:27 +0700 Subject: [PATCH 3/3] move tests to iosTest --- .../kotlin/dev/icerock/moko/javascript/JavaScriptEngineTest.kt | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename javascript/src/{commonTest => iosTest}/kotlin/dev/icerock/moko/javascript/JavaScriptEngineTest.kt (100%) diff --git a/javascript/src/commonTest/kotlin/dev/icerock/moko/javascript/JavaScriptEngineTest.kt b/javascript/src/iosTest/kotlin/dev/icerock/moko/javascript/JavaScriptEngineTest.kt similarity index 100% rename from javascript/src/commonTest/kotlin/dev/icerock/moko/javascript/JavaScriptEngineTest.kt rename to javascript/src/iosTest/kotlin/dev/icerock/moko/javascript/JavaScriptEngineTest.kt