Skip to content

Commit e5d4fcc

Browse files
authored
Merge pull request #657 from JD557/std-resources
Support standard streams in Resources
2 parents 9e71b03 + 622481b commit e5d4fcc

9 files changed

Lines changed: 270 additions & 120 deletions

File tree

.github/workflows/compile_test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
uses: actions/setup-java@v4
2828
with:
2929
distribution: temurin
30-
java-version: 8
30+
java-version: 17
3131
cache: sbt
3232
- name: Setup SBT
3333
uses: sbt/setup-sbt@v1

backend/js/src/main/scala/eu/joaocosta/minart/backend/JsResource.scala

Lines changed: 97 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -14,90 +14,116 @@ import eu.joaocosta.minart.runtime.Resource
1414
/** Resource loader that fetches resources from the local storage.
1515
* If it fails, it uses a XML HTTP Request.
1616
*/
17-
final case class JsResource(resourcePath: String) extends Resource {
18-
def path = "./" + resourcePath
17+
final case class JsResource(resourcePath: Option[String]) extends Resource {
18+
19+
def path = resourcePath.map(p => s"./$p")
1920

2021
private def loadFromLocalStorage(): Option[String] =
21-
Option(dom.window.localStorage.getItem(resourcePath))
22+
resourcePath.flatMap(p => Option(dom.window.localStorage.getItem(p)))
2223

2324
def unsafeInputStream(): InputStream = {
24-
val data = loadFromLocalStorage() match {
25-
case Some(d) => d
26-
case None =>
27-
val xhr = new XMLHttpRequest()
28-
xhr.open("GET", path, false)
29-
xhr.overrideMimeType("text/plain; charset=x-user-defined")
30-
xhr.send()
31-
if (xhr.status != 200) throw new Exception(s"Couldn't open resource: $resourcePath (${xhr.statusText})")
32-
xhr.responseText
25+
resourcePath match {
26+
case Some(p) =>
27+
val data = loadFromLocalStorage() match {
28+
case Some(d) => d
29+
case None =>
30+
val xhr = new XMLHttpRequest()
31+
xhr.open("GET", s"./$p", false)
32+
xhr.overrideMimeType("text/plain; charset=x-user-defined")
33+
xhr.send()
34+
if (xhr.status != 200) throw new Exception(s"Couldn't open resource: $resourcePath (${xhr.statusText})")
35+
xhr.responseText
36+
}
37+
new ByteArrayInputStream(data.toCharArray.map(_.toByte))
38+
case None =>
39+
// Scala.js doesn't support System.in
40+
InputStream.nullInputStream()
3341
}
34-
new ByteArrayInputStream(data.toCharArray.map(_.toByte))
3542
}
3643

37-
def unsafeOutputStream(): OutputStream = new OutputStream {
38-
val inner = new ByteArrayOutputStream()
39-
override def close(): Unit = {
40-
flush()
41-
inner.close()
42-
}
43-
override def flush(): Unit = {
44-
inner.flush()
45-
dom.window.localStorage.setItem(resourcePath, inner.toByteArray().iterator.map(_.toChar).mkString(""))
44+
def unsafeOutputStream(): OutputStream =
45+
resourcePath match {
46+
case Some(p) =>
47+
new OutputStream {
48+
val inner = new ByteArrayOutputStream()
49+
override def close(): Unit = {
50+
flush()
51+
inner.close()
52+
}
53+
override def flush(): Unit = {
54+
inner.flush()
55+
dom.window.localStorage.setItem(p, inner.toByteArray().iterator.map(_.toChar).mkString(""))
56+
}
57+
override def write(b: Array[Byte]): Unit = inner.write(b)
58+
override def write(b: Array[Byte], off: Int, len: Int): Unit = inner.write(b, off, len)
59+
override def write(b: Int): Unit = inner.write(b)
60+
}
61+
case None => System.out
4662
}
47-
override def write(b: Array[Byte]): Unit = inner.write(b)
48-
override def write(b: Array[Byte], off: Int, len: Int): Unit = inner.write(b, off, len)
49-
override def write(b: Int): Unit = inner.write(b)
50-
}
5163

52-
override def withSource[A](f: Source => A): Try[A] = Try {
53-
val data = loadFromLocalStorage() match {
54-
case Some(d) => d
55-
case None =>
56-
val xhr = new XMLHttpRequest()
57-
xhr.open("GET", path, false)
58-
xhr.send()
59-
if (xhr.status != 200) throw new Exception(s"Couldn't open resource: $resourcePath (${xhr.statusText})")
60-
xhr.responseText
61-
}
62-
f(Source.fromString(data))
64+
override def withSource[A](f: Source => A): Try[A] = resourcePath match {
65+
case Some(p) =>
66+
Try {
67+
val data = loadFromLocalStorage() match {
68+
case Some(d) => d
69+
case None =>
70+
val xhr = new XMLHttpRequest()
71+
xhr.open("GET", s"./$p", false)
72+
xhr.send()
73+
if (xhr.status != 200) throw new Exception(s"Couldn't open resource: $resourcePath (${xhr.statusText})")
74+
xhr.responseText
75+
}
76+
f(Source.fromString(data))
77+
}
78+
case None =>
79+
// Stdin is not supported in Scala.js
80+
Try(f(Source.fromString("")))
6381
}
6482

65-
def withSourceAsync[A](f: Source => A): Future[A] = {
66-
val promise = Promise[A]()
67-
loadFromLocalStorage() match {
68-
case Some(data) =>
69-
promise.complete(Try(f(Source.fromString(data))))
70-
case None =>
71-
val xhr = new XMLHttpRequest()
72-
xhr.open("GET", path)
73-
xhr.onloadend = (_: ProgressEvent) => {
74-
if (xhr.status != 200)
75-
promise.failure(new Exception(s"Couldn't open resource: $resourcePath (${xhr.statusText})"))
76-
else promise.complete(Try(f(Source.fromString(xhr.responseText))))
77-
}
78-
xhr.send()
79-
}
80-
promise.future
83+
def withSourceAsync[A](f: Source => A): Future[A] = resourcePath match {
84+
case Some(p) =>
85+
val promise = Promise[A]()
86+
loadFromLocalStorage() match {
87+
case Some(data) =>
88+
promise.complete(Try(f(Source.fromString(data))))
89+
case None =>
90+
val xhr = new XMLHttpRequest()
91+
xhr.open("GET", s"./$p")
92+
xhr.onloadend = (_: ProgressEvent) => {
93+
if (xhr.status != 200)
94+
promise.failure(new Exception(s"Couldn't open resource: $resourcePath (${xhr.statusText})"))
95+
else promise.complete(Try(f(Source.fromString(xhr.responseText))))
96+
}
97+
xhr.send()
98+
}
99+
promise.future
100+
case None =>
101+
// Stdin is not supported in Scala.js
102+
Future.fromTry(withSource(f))
81103
}
82104

83-
def withInputStreamAsync[A](f: InputStream => A): Future[A] = {
84-
val promise = Promise[A]()
85-
loadFromLocalStorage() match {
86-
case Some(data) =>
87-
val is = new ByteArrayInputStream(data.toCharArray.map(_.toByte))
88-
promise.complete(Try(f(is)))
89-
case None =>
90-
val xhr = new XMLHttpRequest()
91-
xhr.open("GET", path)
92-
xhr.overrideMimeType("text/plain; charset=x-user-defined")
93-
xhr.onloadend = (_: ProgressEvent) => {
94-
if (xhr.status != 200)
95-
promise.failure(new Exception(s"Couldn't open resource: $resourcePath (${xhr.statusText})"))
96-
else promise.complete(Try(f(new ByteArrayInputStream(xhr.responseText.toCharArray.map(_.toByte)))))
97-
}
98-
xhr.send()
99-
}
100-
promise.future
105+
def withInputStreamAsync[A](f: InputStream => A): Future[A] = resourcePath match {
106+
case Some(p) =>
107+
val promise = Promise[A]()
108+
loadFromLocalStorage() match {
109+
case Some(data) =>
110+
val is = new ByteArrayInputStream(data.toCharArray.map(_.toByte))
111+
promise.complete(Try(f(is)))
112+
case None =>
113+
val xhr = new XMLHttpRequest()
114+
xhr.open("GET", s"./$p")
115+
xhr.overrideMimeType("text/plain; charset=x-user-defined")
116+
xhr.onloadend = (_: ProgressEvent) => {
117+
if (xhr.status != 200)
118+
promise.failure(new Exception(s"Couldn't open resource: $resourcePath (${xhr.statusText})"))
119+
else promise.complete(Try(f(new ByteArrayInputStream(xhr.responseText.toCharArray.map(_.toByte)))))
120+
}
121+
xhr.send()
122+
}
123+
promise.future
124+
case None =>
125+
// Stdin is not supported in Scala.js
126+
Future.fromTry(withInputStream(f))
101127
}
102128

103129
}

backend/js/src/main/scala/eu/joaocosta/minart/backend/defaults/package.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@ package object defaults {
1515
given defaultPlatform: DefaultBackend[Any, Platform.JS.type] =
1616
DefaultBackend.fromConstant(Platform.JS)
1717

18-
given defaultResource: DefaultBackend[String, JsResource] =
19-
DefaultBackend.fromFunction[String, JsResource](JsResource.apply)
18+
given defaultResource: DefaultBackend[Option[String], JsResource] =
19+
DefaultBackend.fromFunction[Option[String], JsResource](JsResource.apply)
2020
}

backend/jvm/src/main/scala/eu/joaocosta/minart/backend/JavaResource.scala

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,38 @@ import scala.util.{Failure, Success, Try}
88

99
import eu.joaocosta.minart.runtime.Resource
1010

11-
/** Resource loader by first trying to access the jar's resources.
12-
* If that fails, it tries to fetch the data from a file.
11+
/** Resource loader that fetches the data from a file or stdin/stdout.
12+
* If that fails, it tries to fetch the data from the executable resources.
1313
*/
14-
final case class JavaResource(resourcePath: String) extends Resource {
14+
final case class JavaResource(resourcePath: Option[String]) extends Resource {
1515
given ExecutionContext = ExecutionContext.global
1616

17-
def path = "./" + resourcePath
17+
def path = resourcePath.map(p => s"./$p")
1818

1919
override def exists(): Boolean =
20-
new File(path).exists() || this.getClass().getResource("/" + resourcePath) != null
20+
resourcePath match {
21+
case Some(p) =>
22+
new File(s"./$p").exists() || this.getClass().getResource(s"/$p") != null
23+
case None => true
24+
}
2125

2226
def unsafeInputStream(): InputStream =
23-
Try(new BufferedInputStream(new FileInputStream(path)))
24-
.orElse(
25-
Option(this.getClass().getResourceAsStream("/" + resourcePath))
26-
.fold[Try[InputStream]](Failure(new Exception(s"Couldn't open resource: $resourcePath")))(Success.apply)
27-
)
28-
.get
29-
def unsafeOutputStream(): OutputStream = new FileOutputStream(path)
27+
resourcePath match {
28+
case Some(p) =>
29+
Try(new BufferedInputStream(new FileInputStream(s"./$p")))
30+
.orElse(
31+
Option(this.getClass().getResourceAsStream(s"/$p"))
32+
.fold[Try[InputStream]](Failure(new Exception(s"Couldn't open resource: $resourcePath")))(Success.apply)
33+
)
34+
.get
35+
case None => System.in
36+
}
37+
def unsafeOutputStream(): OutputStream =
38+
resourcePath match {
39+
case Some(p) => new FileOutputStream(s"./$p")
40+
case None => System.out
41+
}
3042

3143
def withSourceAsync[A](f: Source => A): Future[A] = Future(blocking(withSource(f)).get)
3244
def withInputStreamAsync[A](f: InputStream => A): Future[A] = Future(blocking(withInputStream(f)).get)
33-
3445
}

backend/jvm/src/main/scala/eu/joaocosta/minart/backend/defaults/package.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@ package object defaults {
1515
given defaultPlatform: DefaultBackend[Any, Platform.JVM.type] =
1616
DefaultBackend.fromConstant(Platform.JVM)
1717

18-
given defaultResource: DefaultBackend[String, JavaResource] =
19-
DefaultBackend.fromFunction[String, JavaResource](JavaResource.apply)
18+
given defaultResource: DefaultBackend[Option[String], JavaResource] =
19+
DefaultBackend.fromFunction[Option[String], JavaResource](JavaResource.apply)
2020
}

backend/native/src/main/scala/eu/joaocosta/minart/backend/NativeResource.scala

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,44 @@ package eu.joaocosta.minart.backend
22

33
import java.io.{BufferedInputStream, File, FileInputStream, FileOutputStream, InputStream, OutputStream}
44

5-
import scala.concurrent.Future
5+
import scala.concurrent.*
66
import scala.io.Source
77
import scala.util.{Failure, Success, Try}
88

99
import eu.joaocosta.minart.runtime.Resource
1010

11-
/** Resource loader that fetches the data from a file.
11+
/** Resource loader that fetches the data from a file or stdin/stdout.
1212
* If that fails, it tries to fetch the data from the executable resources.
13-
*
14-
* Currently in scala-native limitations, the async methods are actually synchronous.
1513
*/
16-
final case class NativeResource(resourcePath: String) extends Resource {
17-
override def exists(): Boolean =
18-
new File(path).exists() || this.getClass().getResource("/" + resourcePath) != null
14+
final case class NativeResource(resourcePath: Option[String]) extends Resource {
15+
given ExecutionContext = ExecutionContext.global
16+
17+
def path = resourcePath.map(p => s"./$p")
1918

20-
def path = "./" + resourcePath
19+
override def exists(): Boolean =
20+
resourcePath match {
21+
case Some(p) =>
22+
new File(s"./$p").exists() || this.getClass().getResource(s"/$p") != null
23+
case None => true
24+
}
2125

2226
def unsafeInputStream(): InputStream =
23-
Try(new BufferedInputStream(new FileInputStream(path)))
24-
.orElse(
25-
Option(this.getClass().getResourceAsStream("/" + resourcePath))
26-
.fold[Try[InputStream]](Failure(new Exception(s"Couldn't open resource: $resourcePath")))(Success.apply)
27-
)
28-
.get
29-
def unsafeOutputStream(): OutputStream = new FileOutputStream(path)
30-
31-
def withSourceAsync[A](f: Source => A): Future[A] =
32-
Future.fromTry(withSource(f))
33-
def withInputStreamAsync[A](f: InputStream => A): Future[A] =
34-
Future.fromTry(withInputStream(f))
27+
resourcePath match {
28+
case Some(p) =>
29+
Try(new BufferedInputStream(new FileInputStream(s"./$p")))
30+
.orElse(
31+
Option(this.getClass().getResourceAsStream(s"/$p"))
32+
.fold[Try[InputStream]](Failure(new Exception(s"Couldn't open resource: $resourcePath")))(Success.apply)
33+
)
34+
.get
35+
case None => System.in
36+
}
37+
def unsafeOutputStream(): OutputStream =
38+
resourcePath match {
39+
case Some(p) => new FileOutputStream(s"./$p")
40+
case None => System.out
41+
}
42+
43+
def withSourceAsync[A](f: Source => A): Future[A] = Future(blocking(withSource(f)).get)
44+
def withInputStreamAsync[A](f: InputStream => A): Future[A] = Future(blocking(withInputStream(f)).get)
3545
}

backend/native/src/main/scala/eu/joaocosta/minart/backend/defaults/package.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@ package object defaults {
1515
given defaultPlatform: DefaultBackend[Any, Platform.Native.type] =
1616
DefaultBackend.fromConstant(Platform.Native)
1717

18-
given defaultResource: DefaultBackend[String, NativeResource] =
19-
DefaultBackend.fromFunction[String, NativeResource](NativeResource.apply)
18+
given defaultResource: DefaultBackend[Option[String], NativeResource] =
19+
DefaultBackend.fromFunction[Option[String], NativeResource](NativeResource.apply)
2020
}

0 commit comments

Comments
 (0)