Skip to content

Commit deb634e

Browse files
fix: implement zero-dependency JSON parser, replace broken placeholder
1 parent fa75353 commit deb634e

1 file changed

Lines changed: 159 additions & 10 deletions

File tree

src/main/java/io/vexis/sdk/Vexis.java

Lines changed: 159 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -290,26 +290,47 @@ boolean canRequest() {
290290
String state() { return state.get(); }
291291
}
292292

293-
// ── Minimal JSON (no external deps) ─────────────────────
293+
// ── Minimal JSON (zero dependencies) ────────────────────
294294

295295
static final class SimpleJson {
296+
297+
/**
298+
* Minimal recursive-descent JSON parser. Handles the subset needed for
299+
* VEXIS API responses: objects, arrays, strings, numbers, booleans, null.
300+
* Zero external dependencies. For high-throughput production use, add
301+
* Jackson or Gson to the classpath and swap this implementation.
302+
*/
296303
@SuppressWarnings("unchecked")
297304
static Map<String, Object> parse(String json) {
298-
// Minimal JSON parser — for production use, replace with Jackson/Gson
299-
// This handles the subset needed for VEXIS API responses
300-
try {
301-
return (Map<String, Object>) new com.sun.net.httpserver.Headers(); // placeholder
302-
} catch (Exception e) {
303-
// Fallback: use built-in nashorn or simple parsing
305+
if (json == null || json.isBlank()) {
306+
return Map.of();
304307
}
305-
// In practice, enterprises will have Jackson on classpath
306-
throw new UnsupportedOperationException("Add com.fasterxml.jackson.databind to classpath, or use VexisJackson adapter");
308+
var parser = new JsonParser(json.trim());
309+
Object result = parser.parseValue();
310+
if (result instanceof Map<?, ?> map) {
311+
return (Map<String, Object>) map;
312+
}
313+
return Map.of("value", result);
307314
}
308315

309316
static String buildVerifyBody(VerifyRequest req) {
310317
var sb = new StringBuilder("{");
311318
sb.append("\"prompt\":").append(escapeJson(req.prompt()));
312319
if (req.extractedText() != null) sb.append(",\"extracted_text\":").append(escapeJson(req.extractedText()));
320+
if (req.metadata() != null && !req.metadata().isEmpty()) {
321+
sb.append(",\"metadata\":{");
322+
var it = req.metadata().entrySet().iterator();
323+
while (it.hasNext()) {
324+
var e = it.next();
325+
sb.append(escapeJson(e.getKey())).append(":");
326+
if (e.getValue() instanceof String s) sb.append(escapeJson(s));
327+
else if (e.getValue() instanceof Number n) sb.append(n);
328+
else if (e.getValue() instanceof Boolean b) sb.append(b);
329+
else sb.append(escapeJson(String.valueOf(e.getValue())));
330+
if (it.hasNext()) sb.append(",");
331+
}
332+
sb.append("}");
333+
}
313334
if (req.attachments() != null && !req.attachments().isEmpty()) {
314335
sb.append(",\"attachments\":[");
315336
for (int i = 0; i < req.attachments().size(); i++) {
@@ -330,5 +351,133 @@ private static String escapeJson(String s) {
330351
if (s == null) return "null";
331352
return "\"" + s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t") + "\"";
332353
}
354+
355+
// ── Recursive-descent JSON parser ───────────────────
356+
357+
private static final class JsonParser {
358+
private final String input;
359+
private int pos;
360+
361+
JsonParser(String input) { this.input = input; this.pos = 0; }
362+
363+
Object parseValue() {
364+
skipWhitespace();
365+
if (pos >= input.length()) return null;
366+
char c = input.charAt(pos);
367+
return switch (c) {
368+
case '{' -> parseObject();
369+
case '[' -> parseArray();
370+
case '"' -> parseString();
371+
case 't', 'f' -> parseBoolean();
372+
case 'n' -> parseNull();
373+
default -> parseNumber();
374+
};
375+
}
376+
377+
@SuppressWarnings("unchecked")
378+
Map<String, Object> parseObject() {
379+
expect('{');
380+
var map = new LinkedHashMap<String, Object>();
381+
skipWhitespace();
382+
if (pos < input.length() && input.charAt(pos) == '}') { pos++; return map; }
383+
while (pos < input.length()) {
384+
skipWhitespace();
385+
String key = parseString();
386+
skipWhitespace();
387+
expect(':');
388+
Object value = parseValue();
389+
map.put(key, value);
390+
skipWhitespace();
391+
if (pos < input.length() && input.charAt(pos) == ',') { pos++; continue; }
392+
break;
393+
}
394+
skipWhitespace();
395+
if (pos < input.length() && input.charAt(pos) == '}') pos++;
396+
return map;
397+
}
398+
399+
List<Object> parseArray() {
400+
expect('[');
401+
var list = new ArrayList<>();
402+
skipWhitespace();
403+
if (pos < input.length() && input.charAt(pos) == ']') { pos++; return list; }
404+
while (pos < input.length()) {
405+
list.add(parseValue());
406+
skipWhitespace();
407+
if (pos < input.length() && input.charAt(pos) == ',') { pos++; continue; }
408+
break;
409+
}
410+
skipWhitespace();
411+
if (pos < input.length() && input.charAt(pos) == ']') pos++;
412+
return list;
413+
}
414+
415+
String parseString() {
416+
expect('"');
417+
var sb = new StringBuilder();
418+
while (pos < input.length()) {
419+
char c = input.charAt(pos++);
420+
if (c == '"') return sb.toString();
421+
if (c == '\\' && pos < input.length()) {
422+
char esc = input.charAt(pos++);
423+
switch (esc) {
424+
case '"' -> sb.append('"');
425+
case '\\' -> sb.append('\\');
426+
case '/' -> sb.append('/');
427+
case 'n' -> sb.append('\n');
428+
case 'r' -> sb.append('\r');
429+
case 't' -> sb.append('\t');
430+
case 'b' -> sb.append('\b');
431+
case 'f' -> sb.append('\f');
432+
case 'u' -> {
433+
if (pos + 4 <= input.length()) {
434+
sb.append((char) Integer.parseInt(input.substring(pos, pos + 4), 16));
435+
pos += 4;
436+
}
437+
}
438+
default -> { sb.append('\\'); sb.append(esc); }
439+
}
440+
} else {
441+
sb.append(c);
442+
}
443+
}
444+
return sb.toString();
445+
}
446+
447+
Number parseNumber() {
448+
int start = pos;
449+
if (pos < input.length() && input.charAt(pos) == '-') pos++;
450+
while (pos < input.length() && Character.isDigit(input.charAt(pos))) pos++;
451+
boolean isFloat = false;
452+
if (pos < input.length() && input.charAt(pos) == '.') { isFloat = true; pos++; while (pos < input.length() && Character.isDigit(input.charAt(pos))) pos++; }
453+
if (pos < input.length() && (input.charAt(pos) == 'e' || input.charAt(pos) == 'E')) { isFloat = true; pos++; if (pos < input.length() && (input.charAt(pos) == '+' || input.charAt(pos) == '-')) pos++; while (pos < input.length() && Character.isDigit(input.charAt(pos))) pos++; }
454+
String numStr = input.substring(start, pos);
455+
if (isFloat) return Double.parseDouble(numStr);
456+
long val = Long.parseLong(numStr);
457+
if (val >= Integer.MIN_VALUE && val <= Integer.MAX_VALUE) return (int) val;
458+
return val;
459+
}
460+
461+
Boolean parseBoolean() {
462+
if (input.startsWith("true", pos)) { pos += 4; return Boolean.TRUE; }
463+
if (input.startsWith("false", pos)) { pos += 5; return Boolean.FALSE; }
464+
throw new IllegalStateException("Expected boolean at position " + pos);
465+
}
466+
467+
Object parseNull() {
468+
if (input.startsWith("null", pos)) { pos += 4; return null; }
469+
throw new IllegalStateException("Expected null at position " + pos);
470+
}
471+
472+
void expect(char c) {
473+
skipWhitespace();
474+
if (pos < input.length() && input.charAt(pos) == c) { pos++; return; }
475+
throw new IllegalStateException("Expected '" + c + "' at position " + pos + " but got '" + (pos < input.length() ? input.charAt(pos) : "EOF") + "'");
476+
}
477+
478+
void skipWhitespace() {
479+
while (pos < input.length() && Character.isWhitespace(input.charAt(pos))) pos++;
480+
}
481+
}
333482
}
334-
}
483+
}

0 commit comments

Comments
 (0)