Files
jabba/src/main/java/com/reliancy/io/JsonDecoder.java
2026-05-01 13:38:37 -05:00

267 lines
7.3 KiB
Java

package com.reliancy.io;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Lightweight JSON decoder that produces common Java object trees.
*/
public final class JsonDecoder {
private final CharSequence input;
private int index;
private JsonDecoder(CharSequence input) {
this.input = input == null ? "" : input;
}
public static Object decode(CharSequence input) {
JsonDecoder parser = new JsonDecoder(input);
Object value = parser.readValue();
parser.skipWhitespace();
if (parser.hasMore()) {
throw parser.error("Unexpected trailing content");
}
return value;
}
public static Map<String, Object> decodeObject(CharSequence input) {
Object value = decode(input);
if (value instanceof Map<?, ?> map) {
@SuppressWarnings("unchecked")
Map<String, Object> typed = (Map<String, Object>) map;
return typed;
}
throw new IllegalArgumentException("JSON payload is not an object");
}
public static List<Object> decodeArray(CharSequence input) {
Object value = decode(input);
if (value instanceof List<?> list) {
@SuppressWarnings("unchecked")
List<Object> typed = (List<Object>) list;
return typed;
}
throw new IllegalArgumentException("JSON payload is not an array");
}
private Object readValue() {
skipWhitespace();
if (!hasMore()) {
throw error("Unexpected end of input");
}
char ch = current();
switch (ch) {
case '{':
return readObject();
case '[':
return readArray();
case '"':
return readString();
case 't':
expectLiteral("true");
return Boolean.TRUE;
case 'f':
expectLiteral("false");
return Boolean.FALSE;
case 'n':
expectLiteral("null");
return null;
default:
if (ch == '-' || Character.isDigit(ch)) {
return readNumber();
}
throw error("Unexpected token");
}
}
private Map<String, Object> readObject() {
expect('{');
LinkedHashMap<String, Object> result = new LinkedHashMap<>();
skipWhitespace();
if (peek('}')) {
index++;
return result;
}
while (true) {
skipWhitespace();
String key = readString();
skipWhitespace();
expect(':');
Object value = readValue();
result.put(key, value);
skipWhitespace();
if (peek('}')) {
index++;
return result;
}
expect(',');
}
}
private List<Object> readArray() {
expect('[');
ArrayList<Object> result = new ArrayList<>();
skipWhitespace();
if (peek(']')) {
index++;
return result;
}
while (true) {
result.add(readValue());
skipWhitespace();
if (peek(']')) {
index++;
return result;
}
expect(',');
}
}
private String readString() {
expect('"');
StringBuilder buf = new StringBuilder();
while (hasMore()) {
char ch = input.charAt(index++);
if (ch == '"') {
return buf.toString();
}
if (ch != '\\') {
buf.append(ch);
continue;
}
if (!hasMore()) {
throw error("Unterminated escape sequence");
}
char esc = input.charAt(index++);
switch (esc) {
case '"':
case '\\':
case '/':
buf.append(esc);
break;
case 'b':
buf.append('\b');
break;
case 'f':
buf.append('\f');
break;
case 'n':
buf.append('\n');
break;
case 'r':
buf.append('\r');
break;
case 't':
buf.append('\t');
break;
case 'u':
buf.append(readUnicodeEscape());
break;
default:
throw error("Unsupported escape sequence");
}
}
throw error("Unterminated string");
}
private Number readNumber() {
int start = index;
if (peek('-')) {
index++;
}
readDigits();
boolean fractional = false;
if (peek('.')) {
fractional = true;
index++;
readDigits();
}
if (peek('e') || peek('E')) {
fractional = true;
index++;
if (peek('+') || peek('-')) {
index++;
}
readDigits();
}
String token = input.subSequence(start, index).toString();
if (!Handy.isNumeric(token)) {
throw error("Invalid number");
}
if (fractional) {
return Double.valueOf(token);
}
try {
return Integer.valueOf(token);
} catch (NumberFormatException ignore) {
return Long.valueOf(token);
}
}
private char readUnicodeEscape() {
if (index + 4 > input.length()) {
throw error("Invalid unicode escape");
}
int value = 0;
for (int i = 0; i < 4; i++) {
char ch = input.charAt(index++);
int digit = Character.digit(ch, 16);
if (digit < 0) {
throw error("Invalid unicode escape");
}
value = (value << 4) + digit;
}
return (char) value;
}
private void readDigits() {
int start = index;
while (hasMore() && Character.isDigit(current())) {
index++;
}
if (start == index) {
throw error("Expected digit");
}
}
private void expectLiteral(String literal) {
for (int i = 0; i < literal.length(); i++) {
if (!hasMore() || input.charAt(index++) != literal.charAt(i)) {
throw error("Expected literal " + literal);
}
}
}
private void expect(char expected) {
skipWhitespace();
if (!peek(expected)) {
throw error("Expected '" + expected + "'");
}
index++;
}
private void skipWhitespace() {
while (hasMore() && Character.isWhitespace(current())) {
index++;
}
}
private boolean hasMore() {
return index < input.length();
}
private boolean peek(char ch) {
return hasMore() && current() == ch;
}
private char current() {
return input.charAt(index);
}
private IllegalArgumentException error(String message) {
return new IllegalArgumentException(message + " at offset " + index);
}
}