Skip to content

Commit 5f0919d

Browse files
committed
fix scala parsers
1 parent fd6e904 commit 5f0919d

3 files changed

Lines changed: 174 additions & 92 deletions

File tree

src/main/resources/io/viash/languages/scala/ViashParseJson.scala

Lines changed: 75 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -20,66 +20,82 @@ import java.io.File
2020

2121
object ViashJsonParser {
2222

23-
def parseJson(jsonPath: Option[String] = None): Map[String, Any] = {
24-
val path = jsonPath.getOrElse {
25-
sys.env.getOrElse("VIASH_WORK_PARAMS",
23+
def parseJson(path: Option[String] = None): Map[String, Any] = {
24+
val jsonPath = path.getOrElse {
25+
sys.env.getOrElse("VIASH_WORK_PARAMS",
2626
throw new RuntimeException("VIASH_WORK_PARAMS environment variable not set"))
2727
}
2828

29-
val file = new File(path)
29+
val file = new File(jsonPath)
3030
if (!file.exists()) {
31-
throw new RuntimeException(s"Parameters file not found: $path")
31+
throw new RuntimeException(s"Parameters file not found: $jsonPath")
3232
}
3333

3434
val jsonText = Source.fromFile(file).mkString
35-
parseJsonString(jsonText).asInstanceOf[Map[String, Any]]
35+
parse(jsonText).asInstanceOf[Map[String, Any]]
3636
}
3737

38-
private def parseJsonString(json: String): Any = {
38+
private def parse(json: String): Any = {
3939
var pos = 0
4040

4141
def skipWhitespace(): Unit = {
4242
while (pos < json.length && json(pos).isWhitespace) pos += 1
4343
}
4444

45-
def parseValue(): Any = {
45+
def peek: Char = {
4646
skipWhitespace()
47-
if (pos >= json.length) throw new RuntimeException("Unexpected end of JSON")
48-
49-
json(pos) match {
47+
if (pos < json.length) json(pos) else throw new RuntimeException("Unexpected end of JSON")
48+
}
49+
50+
def consume(expected: Char): Unit = {
51+
skipWhitespace()
52+
if (pos >= json.length || json(pos) != expected) {
53+
throw new RuntimeException(s"Expected '$expected' at position $pos")
54+
}
55+
pos += 1
56+
}
57+
58+
def parseValue(): Any = {
59+
peek match {
5060
case '"' => parseString()
5161
case '{' => parseObject()
5262
case '[' => parseArray()
5363
case 't' | 'f' => parseBoolean()
5464
case 'n' => parseNull()
5565
case c if c == '-' || c.isDigit => parseNumber()
56-
case c => throw new RuntimeException(s"Unexpected character: $c")
66+
case c => throw new RuntimeException(s"Unexpected character '$c' at position $pos")
5767
}
5868
}
5969

6070
def parseString(): String = {
61-
pos += 1
71+
consume('"')
6272
val sb = new StringBuilder
6373
while (pos < json.length && json(pos) != '"') {
6474
if (json(pos) == '\\') {
6575
pos += 1
66-
if (pos >= json.length) throw new RuntimeException("Unterminated string")
76+
if (pos >= json.length) throw new RuntimeException("Unterminated string escape")
6777
json(pos) match {
6878
case 'n' => sb.append('\n')
6979
case 't' => sb.append('\t')
7080
case 'r' => sb.append('\r')
81+
case 'b' => sb.append('\b')
82+
case 'f' => sb.append('\f')
7183
case '\\' => sb.append('\\')
7284
case '"' => sb.append('"')
7385
case '/' => sb.append('/')
86+
case 'u' =>
87+
if (pos + 4 >= json.length) throw new RuntimeException("Invalid unicode escape")
88+
val hex = json.substring(pos + 1, pos + 5)
89+
sb.append(Integer.parseInt(hex, 16).toChar)
90+
pos += 4
7491
case c => sb.append(c)
7592
}
7693
} else {
7794
sb.append(json(pos))
7895
}
7996
pos += 1
8097
}
81-
if (pos >= json.length) throw new RuntimeException("Unterminated string")
82-
pos += 1
98+
consume('"')
8399
sb.toString
84100
}
85101

@@ -88,114 +104,89 @@ object ViashJsonParser {
88104
if (json(pos) == '-') pos += 1
89105
while (pos < json.length && json(pos).isDigit) pos += 1
90106

91-
var isDouble = false
92-
if (pos < json.length && json(pos) == '.') {
93-
isDouble = true
107+
val hasDecimal = pos < json.length && json(pos) == '.'
108+
if (hasDecimal) {
94109
pos += 1
95110
while (pos < json.length && json(pos).isDigit) pos += 1
96111
}
97112

98-
if (pos < json.length && (json(pos) == 'e' || json(pos) == 'E')) {
99-
isDouble = true
113+
val hasExponent = pos < json.length && (json(pos) == 'e' || json(pos) == 'E')
114+
if (hasExponent) {
100115
pos += 1
101116
if (pos < json.length && (json(pos) == '+' || json(pos) == '-')) pos += 1
102117
while (pos < json.length && json(pos).isDigit) pos += 1
103118
}
104119

105120
val numStr = json.substring(start, pos)
106-
if (isDouble) numStr.toDouble else numStr.toInt
121+
if (hasDecimal || hasExponent) {
122+
numStr.toDouble
123+
} else {
124+
val n = BigInt(numStr)
125+
if (n.isValidInt) n.toInt
126+
else if (n.isValidLong) n.toLong
127+
else n.toDouble
128+
}
107129
}
108130

109131
def parseBoolean(): Boolean = {
110-
if (pos + 4 <= json.length && json.substring(pos, pos + 4) == "true") {
132+
if (json.substring(pos).startsWith("true")) {
111133
pos += 4
112134
true
113-
} else if (pos + 5 <= json.length && json.substring(pos, pos + 5) == "false") {
135+
} else if (json.substring(pos).startsWith("false")) {
114136
pos += 5
115137
false
116138
} else {
117-
throw new RuntimeException("Invalid boolean")
139+
throw new RuntimeException(s"Invalid boolean at position $pos")
118140
}
119141
}
120142

121143
def parseNull(): Null = {
122-
if (pos + 4 <= json.length && json.substring(pos, pos + 4) == "null") {
144+
if (json.substring(pos).startsWith("null")) {
123145
pos += 4
124146
null
125147
} else {
126-
throw new RuntimeException("Invalid null")
148+
throw new RuntimeException(s"Invalid null at position $pos")
127149
}
128150
}
129151

130152
def parseArray(): List[Any] = {
131-
pos += 1
132-
skipWhitespace()
133-
134-
if (pos < json.length && json(pos) == ']') {
135-
pos += 1
136-
return List.empty
153+
consume('[')
154+
if (peek == ']') {
155+
consume(']')
156+
return Nil
137157
}
138158

139-
val result = scala.collection.mutable.ListBuffer[Any]()
140-
while (true) {
141-
result += parseValue()
142-
skipWhitespace()
143-
144-
if (pos >= json.length) throw new RuntimeException("Unterminated array")
145-
146-
if (json(pos) == ']') {
147-
pos += 1
148-
return result.toList
149-
} else if (json(pos) == ',') {
150-
pos += 1
151-
skipWhitespace()
152-
} else {
153-
throw new RuntimeException(s"Expected ',' or ']'")
154-
}
159+
val items = scala.collection.mutable.ListBuffer[Any]()
160+
items += parseValue()
161+
while (peek == ',') {
162+
consume(',')
163+
items += parseValue()
155164
}
156-
List.empty
165+
consume(']')
166+
items.toList
157167
}
158168

159169
def parseObject(): Map[String, Any] = {
160-
pos += 1
161-
skipWhitespace()
162-
163-
if (pos < json.length && json(pos) == '}') {
164-
pos += 1
170+
consume('{')
171+
if (peek == '}') {
172+
consume('}')
165173
return Map.empty
166174
}
167175

168-
val result = scala.collection.mutable.Map[String, Any]()
169-
while (true) {
170-
skipWhitespace()
171-
172-
if (pos >= json.length || json(pos) != '"') {
173-
throw new RuntimeException("Expected string key")
174-
}
176+
val entries = scala.collection.mutable.Map[String, Any]()
177+
def parseEntry(): Unit = {
175178
val key = parseString()
176-
177-
skipWhitespace()
178-
if (pos >= json.length || json(pos) != ':') {
179-
throw new RuntimeException("Expected ':'")
180-
}
181-
pos += 1
182-
183-
val value = parseValue()
184-
result(key) = value
185-
186-
skipWhitespace()
187-
if (pos >= json.length) throw new RuntimeException("Unterminated object")
188-
189-
if (json(pos) == '}') {
190-
pos += 1
191-
return result.toMap
192-
} else if (json(pos) == ',') {
193-
pos += 1
194-
} else {
195-
throw new RuntimeException(s"Expected ',' or '}'")
196-
}
179+
consume(':')
180+
entries(key) = parseValue()
181+
}
182+
183+
parseEntry()
184+
while (peek == ',') {
185+
consume(',')
186+
parseEntry()
197187
}
198-
Map.empty
188+
consume('}')
189+
entries.toMap
199190
}
200191

201192
parseValue()

src/main/scala/io/viash/languages/Scala.scala

Lines changed: 98 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
package io.viash.languages
1919

2020
import io.viash.helpers.Resources
21-
import io.viash.config.arguments.Argument
21+
import io.viash.config.arguments._
2222
import io.viash.config.Config
2323
import io.viash.config.resources.ScriptInjectionMods
2424

@@ -38,13 +38,104 @@ object Scala extends Language {
3838
.mkString("\n")
3939

4040
val paramsCode = if (argsMetaAndDeps.nonEmpty) {
41-
// Parse JSON once and extract all sections
42-
val parseOnce = "// Parse JSON parameters once and extract all sections\nval _viashJsonData = ViashJsonParser.parseJson()\n"
43-
val extractSections = argsMetaAndDeps.map { case (dest, _) =>
44-
s"val $dest = _viashJsonData.getOrElse(\"$dest\", Map.empty[String, Any])"
45-
}.mkString("\n")
41+
// Parse JSON once
42+
val parseOnce = "// Parse JSON parameters\nval _viashJsonData = ViashJsonParser.parseJson()\n\n"
4643

47-
parseOnce + extractSections
44+
// Generate case class and instance for each section (par, meta, dep)
45+
val sections = argsMetaAndDeps.map { case (dest, params) =>
46+
val className = s"Viash${dest.capitalize}"
47+
48+
// Generate case class field types
49+
val classTypes = params.map { par =>
50+
val classType = par match {
51+
case a: BooleanArgumentBase if a.multiple => "List[Boolean]"
52+
case a: IntegerArgument if a.multiple => "List[Int]"
53+
case a: LongArgument if a.multiple => "List[Long]"
54+
case a: DoubleArgument if a.multiple => "List[Double]"
55+
case a: FileArgument if a.multiple => "List[String]"
56+
case a: StringArgument if a.multiple => "List[String]"
57+
// Optional types for non-required, non-flag arguments
58+
case a: BooleanArgumentBase if !a.required && a.flagValue.isEmpty => "Option[Boolean]"
59+
case a: IntegerArgument if !a.required => "Option[Int]"
60+
case a: LongArgument if !a.required => "Option[Long]"
61+
case a: DoubleArgument if !a.required => "Option[Double]"
62+
case a: FileArgument if !a.required => "Option[String]"
63+
case a: StringArgument if !a.required => "Option[String]"
64+
// Required types
65+
case _: BooleanArgumentBase => "Boolean"
66+
case _: IntegerArgument => "Int"
67+
case _: LongArgument => "Long"
68+
case _: DoubleArgument => "Double"
69+
case _: FileArgument => "String"
70+
case _: StringArgument => "String"
71+
}
72+
s" ${par.plainName}: $classType"
73+
}
74+
75+
// Generate JSON extraction code for each parameter
76+
val extractors = params.map { par =>
77+
val jsonKey = par.plainName
78+
val extractor = par match {
79+
// Multiple values - extract as List (handle null as empty list)
80+
case a: BooleanArgumentBase if a.multiple =>
81+
s"""_${dest}Json.get("$jsonKey").flatMap(v => Option(v)).map(_.asInstanceOf[List[Any]].map(_.toString.toBoolean)).getOrElse(Nil)"""
82+
case a: IntegerArgument if a.multiple =>
83+
s"""_${dest}Json.get("$jsonKey").flatMap(v => Option(v)).map(_.asInstanceOf[List[Any]].map(v => v match { case i: Int => i; case d: Double => d.toInt; case s => s.toString.toInt })).getOrElse(Nil)"""
84+
case a: LongArgument if a.multiple =>
85+
s"""_${dest}Json.get("$jsonKey").flatMap(v => Option(v)).map(_.asInstanceOf[List[Any]].map(v => v match { case i: Int => i.toLong; case l: Long => l; case d: Double => d.toLong; case s => s.toString.toLong })).getOrElse(Nil)"""
86+
case a: DoubleArgument if a.multiple =>
87+
s"""_${dest}Json.get("$jsonKey").flatMap(v => Option(v)).map(_.asInstanceOf[List[Any]].map(v => v match { case d: Double => d; case i: Int => i.toDouble; case s => s.toString.toDouble })).getOrElse(Nil)"""
88+
case a: FileArgument if a.multiple =>
89+
s"""_${dest}Json.get("$jsonKey").flatMap(v => Option(v)).map(_.asInstanceOf[List[Any]].map(_.toString)).getOrElse(Nil)"""
90+
case a: StringArgument if a.multiple =>
91+
s"""_${dest}Json.get("$jsonKey").flatMap(v => Option(v)).map(_.asInstanceOf[List[Any]].map(_.toString)).getOrElse(Nil)"""
92+
93+
// Optional values
94+
case a: BooleanArgumentBase if !a.required && a.flagValue.isEmpty =>
95+
s"""_${dest}Json.get("$jsonKey").flatMap(v => Option(v)).map(_.toString.toBoolean)"""
96+
case a: IntegerArgument if !a.required =>
97+
s"""_${dest}Json.get("$jsonKey").flatMap(v => Option(v)).map(v => v match { case i: Int => i; case d: Double => d.toInt; case s => s.toString.toInt })"""
98+
case a: LongArgument if !a.required =>
99+
s"""_${dest}Json.get("$jsonKey").flatMap(v => Option(v)).map(v => v match { case i: Int => i.toLong; case l: Long => l; case d: Double => d.toLong; case s => s.toString.toLong })"""
100+
case a: DoubleArgument if !a.required =>
101+
s"""_${dest}Json.get("$jsonKey").flatMap(v => Option(v)).map(v => v match { case d: Double => d; case i: Int => i.toDouble; case s => s.toString.toDouble })"""
102+
case a: FileArgument if !a.required =>
103+
s"""_${dest}Json.get("$jsonKey").flatMap(v => Option(v)).map(_.toString)"""
104+
case a: StringArgument if !a.required =>
105+
s"""_${dest}Json.get("$jsonKey").flatMap(v => Option(v)).map(_.toString)"""
106+
107+
// Required values (handle null as default value)
108+
case _: BooleanArgumentBase =>
109+
s"""_${dest}Json.get("$jsonKey").flatMap(v => Option(v)).map(_.toString.toBoolean).getOrElse(false)"""
110+
case _: IntegerArgument =>
111+
s"""_${dest}Json.get("$jsonKey").flatMap(v => Option(v)).map(v => v match { case i: Int => i; case d: Double => d.toInt; case s => s.toString.toInt }).getOrElse(0)"""
112+
case _: LongArgument =>
113+
s"""_${dest}Json.get("$jsonKey").flatMap(v => Option(v)).map(v => v match { case i: Int => i.toLong; case l: Long => l; case d: Double => d.toLong; case s => s.toString.toLong }).getOrElse(0L)"""
114+
case _: DoubleArgument =>
115+
s"""_${dest}Json.get("$jsonKey").flatMap(v => Option(v)).map(v => v match { case d: Double => d; case i: Int => i.toDouble; case s => s.toString.toDouble }).getOrElse(0.0)"""
116+
case _: FileArgument =>
117+
s"""_${dest}Json.get("$jsonKey").flatMap(v => Option(v)).map(_.toString).getOrElse("")"""
118+
case _: StringArgument =>
119+
s"""_${dest}Json.get("$jsonKey").flatMap(v => Option(v)).map(_.toString).getOrElse("")"""
120+
}
121+
s" $extractor"
122+
}
123+
124+
// Generate the case class definition
125+
val caseClassDef = s"""case class $className(
126+
${classTypes.mkString(",\n")}
127+
)"""
128+
129+
// Generate the JSON extraction and instance creation
130+
val extraction = s"""val _${dest}Json = _viashJsonData.getOrElse("$dest", Map.empty[String, Any]).asInstanceOf[Map[String, Any]]
131+
val $dest = $className(
132+
${extractors.mkString(",\n")}
133+
)"""
134+
135+
caseClassDef + "\n" + extraction
136+
}
137+
138+
parseOnce + sections.mkString("\n\n")
48139
} else {
49140
""
50141
}

src/test/resources/test_languages/scala/script.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ try {
4949

5050
val input = Source.fromFile(par.input).getLines().toArray
5151
outputFun(s"head of input: |${input(0)}|")
52-
val resource1 = Source.fromFile("resource1.txt").getLines().toArray
52+
val resource1 = Source.fromFile(s"${meta.resources_dir}/resource1.txt").getLines().toArray
5353
outputFun(s"head of resource1: |${resource1(0)}|")
5454

5555
} finally {

0 commit comments

Comments
 (0)