MinimalJson.java
package io.github.baokhang83.mnemo.warden.agent.resize;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* A minimal JSON reader: enough to navigate the Kubernetes API's response shape ({@code
* status.containerStatuses[].resources}) and nothing more — no writer, no streaming, no
* generic (de)serialization.
*
* <p>Exists in preference to a JSON library because {@link PodResizeClient} only ever needs to
* read a handful of string leaves out of a known response shape; a dependency-free ~150-line
* reader keeps that need in proportion, matching the agent's zero-runtime-dependency
* architecture (the same reasoning that ruled out Fabric8 for the HTTP layer itself).
*
* <p>Parses into plain JDK types: {@link Map}<String,Object> for objects, {@link
* List}<Object> for arrays, {@link String}, {@link Double}, {@link Boolean}, or {@code
* null} for scalars — callers navigate with ordinary casts, exactly like consuming any
* other parsed-tree JSON API.
*
* <p>Every character read goes through {@link #peek()}/{@link #next()}, which check bounds and
* throw {@link IllegalArgumentException} on truncated input — not raw {@code
* String.charAt}, which would let an unchecked {@code StringIndexOutOfBoundsException} escape
* instead (caught by a test feeding truncated JSON — an object literal missing its
* closing brace).
*
* <p>Public, not package-private: {@code io.github.baokhang83.mnemo.warden.agent.intent}
* (W-306) reads the same API server response shape (a pod's annotations and container status)
* and reuses this rather than duplicating a second minimal JSON reader in the same module.
*/
public final class MinimalJson {
private final String source;
private int pos;
private MinimalJson(String source) {
this.source = source;
}
public static Object parse(String json) {
MinimalJson parser = new MinimalJson(json);
parser.skipWhitespace();
Object value = parser.parseValue();
parser.skipWhitespace();
if (parser.pos != json.length()) {
throw new IllegalArgumentException("trailing content after JSON value at index " + parser.pos);
}
return value;
}
private Object parseValue() {
char c = peek();
return switch (c) {
case '{' -> parseObject();
case '[' -> parseArray();
case '"' -> parseString();
case 't', 'f' -> parseBoolean();
case 'n' -> parseNull();
default -> parseNumber();
};
}
private Map<String, Object> parseObject() {
Map<String, Object> result = new LinkedHashMap<>();
expect('{');
skipWhitespace();
if (peek() == '}') {
pos++;
return result;
}
while (true) {
skipWhitespace();
String key = parseString();
skipWhitespace();
expect(':');
skipWhitespace();
result.put(key, parseValue());
skipWhitespace();
char c = next();
if (c == '}') {
return result;
}
if (c != ',') {
throw new IllegalArgumentException("expected ',' or '}' at index " + (pos - 1));
}
}
}
private List<Object> parseArray() {
List<Object> result = new ArrayList<>();
expect('[');
skipWhitespace();
if (peek() == ']') {
pos++;
return result;
}
while (true) {
skipWhitespace();
result.add(parseValue());
skipWhitespace();
char c = next();
if (c == ']') {
return result;
}
if (c != ',') {
throw new IllegalArgumentException("expected ',' or ']' at index " + (pos - 1));
}
}
}
private String parseString() {
expect('"');
StringBuilder result = new StringBuilder();
while (true) {
char c = next();
if (c == '"') {
return result.toString();
}
if (c != '\\') {
result.append(c);
continue;
}
char escaped = next();
switch (escaped) {
case '"' -> result.append('"');
case '\\' -> result.append('\\');
case '/' -> result.append('/');
case 'b' -> result.append('\b');
case 'f' -> result.append('\f');
case 'n' -> result.append('\n');
case 'r' -> result.append('\r');
case 't' -> result.append('\t');
case 'u' -> {
if (pos + 4 > source.length()) {
throw new IllegalArgumentException("truncated \\u escape at index " + (pos - 2));
}
result.append((char) Integer.parseInt(source.substring(pos, pos + 4), 16));
pos += 4;
}
default -> throw new IllegalArgumentException("invalid escape '\\" + escaped + "' at index " + (pos - 2));
}
}
}
private Boolean parseBoolean() {
if (source.startsWith("true", pos)) {
pos += 4;
return Boolean.TRUE;
}
if (source.startsWith("false", pos)) {
pos += 5;
return Boolean.FALSE;
}
throw new IllegalArgumentException("invalid literal at index " + pos);
}
private Object parseNull() {
if (source.startsWith("null", pos)) {
pos += 4;
return null;
}
throw new IllegalArgumentException("invalid literal at index " + pos);
}
private Double parseNumber() {
int start = pos;
while (pos < source.length() && "-+.eE0123456789".indexOf(source.charAt(pos)) >= 0) {
pos++;
}
if (pos == start) {
throw new IllegalArgumentException("expected a value at index " + pos);
}
return Double.parseDouble(source.substring(start, pos));
}
private char peek() {
if (pos >= source.length()) {
throw new IllegalArgumentException("unexpected end of input at index " + pos);
}
return source.charAt(pos);
}
private char next() {
char c = peek();
pos++;
return c;
}
private void expect(char c) {
if (peek() != c) {
throw new IllegalArgumentException("expected '" + c + "' at index " + pos);
}
pos++;
}
private void skipWhitespace() {
while (pos < source.length() && Character.isWhitespace(source.charAt(pos))) {
pos++;
}
}
}