Skip to content

Commit ef37134

Browse files
committed
Add the JSON parser
1 parent 5f677a3 commit ef37134

File tree

3 files changed

+301
-0
lines changed

3 files changed

+301
-0
lines changed

src/main/kotlin/parser/JSONParser.kt

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package net.quickwrite.parser
2+
3+
import net.quickwrite.lexer.JSONLexeme
4+
import net.quickwrite.lexer.JSONLexemeType.*
5+
import net.quickwrite.lexer.StringJSONLexer
6+
import java.math.BigDecimal
7+
import java.util.Stack
8+
9+
private enum class State {
10+
START,
11+
OBJECT,
12+
ARRAY,
13+
}
14+
15+
fun jsonParse(input: String): Any? {
16+
val lexer = StringJSONLexer(input)
17+
18+
var state = State.START
19+
20+
var token: JSONLexeme
21+
22+
var stack = Stack<Any?>()
23+
24+
while (true) {
25+
token = lexer.getNext()
26+
27+
if (token.type == EOF) {
28+
break
29+
}
30+
31+
when(state) {
32+
State.START -> {
33+
when(token.type) {
34+
NULL -> stack.push(null)
35+
TRUE -> stack.push(true)
36+
FALSE -> stack.push(false)
37+
STRING -> stack.push(token.content)
38+
NUMBER -> stack.push(BigDecimal(token.content!!))
39+
CURLY_OPEN -> {
40+
state = State.OBJECT
41+
stack.push(HashMap<String, Any?>())
42+
continue
43+
}
44+
SQUARE_OPEN -> {
45+
state = State.ARRAY
46+
stack.push(ArrayList<Any?>())
47+
continue
48+
}
49+
EOF -> error("Unreachable")
50+
else -> throw ParserException("Invalid Token") // TODO: Better error reporting
51+
}
52+
break
53+
}
54+
State.OBJECT -> {
55+
if(token.type == CURLY_CLOSE) {
56+
val jsonObject = stack.pop()
57+
58+
if(stack.empty()){
59+
stack.push(jsonObject)
60+
break
61+
}
62+
63+
// If this is an object
64+
if (stack.peek() is String) {
65+
val name = stack.pop() as String
66+
67+
(stack.peek() as HashMap<String, Any?>)[name] = jsonObject
68+
continue
69+
}
70+
71+
// Should now be an array
72+
(stack.peek() as ArrayList<Any?>).add(jsonObject)
73+
74+
state = State.ARRAY
75+
continue
76+
}
77+
78+
val map = stack.peek() as HashMap<String, Any?>
79+
80+
if(map.isNotEmpty()) {
81+
if (token.type != COMMA) {
82+
throw ParserException("Expected a ',' or a '}', but got ${token.type}") // TODO: Better error reporting
83+
}
84+
85+
token = lexer.getNext()
86+
}
87+
88+
if (token.type != STRING) {
89+
throw ParserException("Expected an entry or a '}', but got ${token.type}") // TODO: Better error reporting
90+
}
91+
92+
val identifier = token.content!!
93+
94+
token = lexer.getNext()
95+
96+
if (token.type != COLON) {
97+
throw ParserException("Expeced ':' (COLON), but got ${token.type}") // TODO: Better error reporting
98+
}
99+
100+
token = lexer.getNext()
101+
102+
when(token.type) {
103+
NULL -> map[identifier] = null
104+
TRUE -> map[identifier] = true
105+
FALSE -> map[identifier] = false
106+
STRING -> map[identifier] = token.content
107+
NUMBER -> map[identifier] = BigDecimal(token.content!!)
108+
CURLY_OPEN -> {
109+
stack.push(identifier)
110+
state = State.OBJECT
111+
stack.push(HashMap<String, Any?>())
112+
continue
113+
}
114+
SQUARE_OPEN -> {
115+
stack.push(identifier)
116+
state = State.ARRAY
117+
stack.push(ArrayList<Any?>())
118+
continue
119+
}
120+
EOF -> break
121+
else -> throw ParserException("Invalid Token") // TODO: Better error reporting
122+
}
123+
}
124+
State.ARRAY -> {
125+
if (token.type == SQUARE_CLOSE) {
126+
val array = stack.pop()
127+
128+
if(stack.empty()){
129+
stack.push(array)
130+
break
131+
}
132+
133+
// If this is an object
134+
if (stack.peek() is String) {
135+
val name = stack.pop() as String
136+
137+
(stack.peek() as HashMap<String, Any?>)[name] = array
138+
139+
state = State.OBJECT
140+
continue
141+
}
142+
143+
// Should now be an array
144+
(stack.peek() as ArrayList<Any?>).add(array)
145+
continue
146+
}
147+
148+
val array = stack.peek() as ArrayList<Any?>
149+
150+
if (array.isNotEmpty()) {
151+
if (token.type != COMMA) {
152+
throw ParserException("Expected a ',' or a ']', but got ${token.type}") // TODO: Better error reporting
153+
}
154+
155+
token = lexer.getNext()
156+
}
157+
158+
when(token.type) {
159+
NULL -> array.add(null)
160+
TRUE -> array.add(true)
161+
FALSE -> array.add(false)
162+
STRING -> array.add(token.content)
163+
NUMBER -> array.add(BigDecimal(token.content!!))
164+
CURLY_OPEN -> {
165+
state = State.OBJECT
166+
stack.push(HashMap<String, Any?>())
167+
continue
168+
}
169+
SQUARE_OPEN -> {
170+
state = State.ARRAY
171+
stack.push(ArrayList<Any?>())
172+
continue
173+
}
174+
EOF -> break
175+
else -> throw ParserException("Invalid Token") // TODO: Better error reporting
176+
}
177+
}
178+
}
179+
}
180+
181+
if (stack.empty()) {
182+
throw ParserException("JSON cannot be empty")
183+
}
184+
185+
// Too much JSON
186+
if (lexer.getNext().type != EOF) {
187+
throw ParserException("Invalid JSON") // TODO: Better error handling
188+
}
189+
190+
val result = stack.pop()
191+
192+
// Too little JSON
193+
if (!stack.empty()) {
194+
throw ParserException("Invalid JSON") // TODO: Better error handling
195+
}
196+
197+
return result
198+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package net.quickwrite.parser
2+
3+
class ParserException(content: String) : Exception(content)
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package parser
2+
3+
import net.quickwrite.parser.jsonParse
4+
import org.junit.jupiter.api.Test
5+
6+
import org.junit.jupiter.api.Assertions.*
7+
import java.math.BigDecimal
8+
9+
class JSONParserKtTest {
10+
11+
@Test
12+
fun `null literal test`() {
13+
val input = "null"
14+
15+
assertEquals(null, jsonParse(input))
16+
}
17+
18+
@Test
19+
fun `true literal test`() {
20+
val input = "true"
21+
22+
assertEquals(true, jsonParse(input))
23+
}
24+
25+
@Test
26+
fun `false literal test`() {
27+
val input = "false"
28+
29+
assertEquals(false, jsonParse(input))
30+
}
31+
32+
@Test
33+
fun `number test`() {
34+
val inputs = arrayOf("1", "42", "42e2", "0.5", "1.42e2")
35+
36+
inputs.forEach {
37+
assertEquals(BigDecimal(it), jsonParse(it))
38+
}
39+
}
40+
41+
@Test
42+
fun `object test`() {
43+
val input = """
44+
{
45+
"hello": "World",
46+
"foo": "bar",
47+
"number": 42
48+
}
49+
""".trimIndent()
50+
51+
val result = HashMap<String, Any?>()
52+
result["hello"] = "World"
53+
result["foo"] = "bar"
54+
result["number"] = 42
55+
56+
assertEquals(result.toString(), jsonParse(input).toString()) // toString as the equals method is not completely implemented
57+
}
58+
59+
@Test
60+
fun `array test`() {
61+
val input = """
62+
[1, 2, 3, "foo", "bar", null]
63+
""".trimIndent()
64+
65+
val result = ArrayList<Any?>()
66+
result.add(1)
67+
result.add(2)
68+
result.add(3)
69+
result.add("foo")
70+
result.add("bar")
71+
result.add(null)
72+
73+
assertEquals(result.toString(), jsonParse(input).toString())
74+
}
75+
76+
@Test
77+
fun `nested test`() {
78+
val input = """
79+
{
80+
"test": [1, {
81+
"yay": []
82+
}
83+
]
84+
}
85+
""".trimIndent()
86+
87+
val result = HashMap<String, Any?>();
88+
89+
val array = ArrayList<Any?>()
90+
array.add(1)
91+
92+
val map = HashMap<String, Any?>()
93+
map["yay"] = ArrayList<Any?>()
94+
array.add(map)
95+
96+
result["test"] = array
97+
98+
assertEquals(result.toString(), jsonParse(input).toString())
99+
}
100+
}

0 commit comments

Comments
 (0)