diff --git a/examples/extract_lynglang_version.lyng b/examples/extract_lynglang_version.lyng new file mode 100644 index 0000000..0192550 --- /dev/null +++ b/examples/extract_lynglang_version.lyng @@ -0,0 +1,23 @@ +#!/env/bin lyng + +import lyng.io.http + +// Step 1: download the main lynglang.com page. +val home = Http.get("https://lynglang.com").text() + +// Step 2: find the version-script reference in the page HTML. +val jsRef = "src=\"([^\"]*lyng-version\\.js)\"".re.find(home) +require(jsRef != null, "lyng-version.js reference not found on the homepage") + +// Step 3: extract the referenced script path from the first regex capture. +val versionJsPath = jsRef[1] + +// Step 4: download the script that exposes `window.LYNG_VERSION`. +val versionJs = Http.get("https://lynglang.com/" + versionJsPath).text() + +// Step 5: pull the actual version string from the JavaScript source. +val versionMatch = "LYNG_VERSION\\s*=\\s*\"([^\"]+)\"".re.find(versionJs) +require(versionMatch != null, "LYNG_VERSION assignment not found") + +// Step 6: print the discovered version for the user. +println("Lynglang.com version: " + ((versionMatch as RegexMatch)[1])) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 35836bf..7241781 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,6 +14,7 @@ firebaseCrashlyticsBuildtools = "3.0.3" okioVersion = "3.10.2" compiler = "3.2.0-alpha11" ktor = "3.3.1" +slf4j = "2.0.17" [libraries] clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" } @@ -40,6 +41,7 @@ ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" } ktor-client-winhttp = { module = "io.ktor:ktor-client-winhttp", version.ref = "ktor" } ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" } ktor-network = { module = "io.ktor:ktor-network", version.ref = "ktor" } +slf4j-nop = { module = "org.slf4j:slf4j-nop", version.ref = "slf4j" } [plugins] androidLibrary = { id = "com.android.library", version.ref = "agp" } diff --git a/lyng/build.gradle.kts b/lyng/build.gradle.kts index 6afc78d..424f822 100644 --- a/lyng/build.gradle.kts +++ b/lyng/build.gradle.kts @@ -80,6 +80,11 @@ kotlin { val nativeMain by creating { dependsOn(commonMain) } + val jvmMain by getting { + dependencies { + implementation(libs.slf4j.nop) + } + } val jvmTest by getting { dependencies { implementation(kotlin("test")) diff --git a/lyng/src/commonMain/kotlin/Common.kt b/lyng/src/commonMain/kotlin/Common.kt index 08c392a..9fbfb93 100644 --- a/lyng/src/commonMain/kotlin/Common.kt +++ b/lyng/src/commonMain/kotlin/Common.kt @@ -34,9 +34,15 @@ import net.sergeych.lyng.ScriptError import net.sergeych.lyng.Source import net.sergeych.lyng.io.console.createConsoleModule import net.sergeych.lyng.io.fs.createFs +import net.sergeych.lyng.io.http.createHttpModule +import net.sergeych.lyng.io.net.createNetModule +import net.sergeych.lyng.io.ws.createWsModule import net.sergeych.lyng.obj.* import net.sergeych.lyngio.console.security.PermitAllConsoleAccessPolicy import net.sergeych.lyngio.fs.security.PermitAllAccessPolicy +import net.sergeych.lyngio.http.security.PermitAllHttpAccessPolicy +import net.sergeych.lyngio.net.security.PermitAllNetAccessPolicy +import net.sergeych.lyngio.ws.security.PermitAllWsAccessPolicy import net.sergeych.mp_tools.globalDefer import okio.FileSystem import okio.Path.Companion.toPath @@ -74,6 +80,11 @@ val baseScopeDefer = globalDefer { // Install console access by default for interactive CLI scripts. // Scripts still need to `import lyng.io.console` to use it. createConsoleModule(PermitAllConsoleAccessPolicy, this) + // Install network-oriented lyngio modules for CLI scripts. + // Scripts still need to import the modules they use explicitly. + createHttpModule(PermitAllHttpAccessPolicy, this) + createWsModule(PermitAllWsAccessPolicy, this) + createNetModule(PermitAllNetAccessPolicy, this) } } diff --git a/lyng/src/jvmTest/kotlin/net/sergeych/lyng_cli/CliNetworkJvmTest.kt b/lyng/src/jvmTest/kotlin/net/sergeych/lyng_cli/CliNetworkJvmTest.kt new file mode 100644 index 0000000..973212f --- /dev/null +++ b/lyng/src/jvmTest/kotlin/net/sergeych/lyng_cli/CliNetworkJvmTest.kt @@ -0,0 +1,141 @@ +/* + * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package net.sergeych.lyng_cli + +import com.sun.net.httpserver.HttpExchange +import com.sun.net.httpserver.HttpServer +import net.sergeych.jvmExitImpl +import net.sergeych.runMain +import org.junit.After +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.io.ByteArrayOutputStream +import java.io.PrintStream +import java.net.InetSocketAddress + +class CliNetworkJvmTest { + private val originalOut: PrintStream = System.out + private val originalErr: PrintStream = System.err + + private class TestExit(val code: Int) : RuntimeException() + + @Before + fun setUp() { + jvmExitImpl = { code -> throw TestExit(code) } + } + + @After + fun tearDown() { + System.setOut(originalOut) + System.setErr(originalErr) + jvmExitImpl = { code -> kotlin.system.exitProcess(code) } + } + + private data class CliResult(val out: String, val err: String, val exitCode: Int?) + + private fun runCli(vararg args: String): CliResult { + val outBuf = ByteArrayOutputStream() + val errBuf = ByteArrayOutputStream() + System.setOut(PrintStream(outBuf, true, Charsets.UTF_8)) + System.setErr(PrintStream(errBuf, true, Charsets.UTF_8)) + + var exitCode: Int? = null + try { + runMain(arrayOf(*args)) + } catch (e: TestExit) { + exitCode = e.code + } finally { + System.out.flush() + System.err.flush() + } + return CliResult(outBuf.toString("UTF-8"), errBuf.toString("UTF-8"), exitCode) + } + + @Test + fun cliHasAllNetworkingModulesInstalled() { + val server = newServer() + try { + val script = """ + import lyng.io.http + import lyng.io.ws + import lyng.io.net + + assert(Http.isSupported()) + println("ws=" + Ws.isSupported()) + println("net=" + Net.isSupported()) + + val home = Http.get("http://127.0.0.1:${server.address.port}/").text() + val jsRef = "src=\"([^\"]*lyng-version\\.js)\"".re.find(home) + require(jsRef != null, "lyng-version.js reference not found") + + val versionJsPath = (jsRef as RegexMatch)[1] + val versionJs = Http.get("http://127.0.0.1:${server.address.port}/" + versionJsPath).text() + val versionMatch = "LYNG_VERSION\\s*=\\s*\"([^\"]+)\"".re.find(versionJs) + require(versionMatch != null, "LYNG_VERSION assignment not found") + + println("version=" + ((versionMatch as RegexMatch)[1])) + """.trimIndent() + + val result = runCli("-x", script) + assertNull(result.exitCode) + assertTrue(result.err, result.err.isBlank()) + assertTrue(result.out, result.out.contains("ws=")) + assertTrue(result.out, result.out.contains("net=")) + assertTrue(result.out, result.out.contains("version=9.9.9-test")) + } finally { + server.stop(0) + } + } + + private fun newServer(): HttpServer { + val server = HttpServer.create(InetSocketAddress("127.0.0.1", 0), 0) + server.createContext("/") { exchange -> + when (exchange.requestURI.path) { + "/" -> writeResponse( + exchange, + 200, + """ + + + + + + + + + + """.trimIndent() + ) + + "/lyng-version.js" -> writeResponse(exchange, 200, """window.LYNG_VERSION = "9.9.9-test";""") + else -> writeResponse(exchange, 404, "missing") + } + } + server.start() + return server + } + + private fun writeResponse(exchange: HttpExchange, status: Int, body: String) { + val bytes = body.toByteArray() + exchange.sendResponseHeaders(status, bytes.size.toLong()) + exchange.responseBody.use { out -> + out.write(bytes) + } + } +}