@@ -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