Class: Tep::Json

Inherits:
Object
  • Object
show all
Defined in:
lib/tep/json.rb

Class Method Summary collapse

Class Method Details

.byte_to_chr(n) ⇒ Object

Build a single-byte string from an integer 0..255. Spinel doesn’t expose ‘n.chr` for arbitrary bytes uniformly; the table covers the ASCII printable range and falls back to “?” for anything else (the JSON encoder side never produces non-ASCII via u, so the fallback is reachable only for malformed input).



474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
# File 'lib/tep/json.rb', line 474

def self.byte_to_chr(n)
  printable = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
  if n >= 32 && n < 127
    return printable[n - 32, 1]
  end
  if n == 9
    return "\t"
  end
  if n == 10
    return "\n"
  end
  if n == 13
    return "\r"
  end
  "?"
end

.encode_pair_int(k, v) ⇒ Object

Same shape, integer value side. ‘v` is rendered via `.to_s` so JSON-numeric output without quoting.



117
118
119
# File 'lib/tep/json.rb', line 117

def self.encode_pair_int(k, v)
  Json.quote(k) + ":" + v.to_s
end

.encode_pair_str(k, v) ⇒ Object

Encode a single key/value pair as ‘“k”:“v”` (escaped both sides). Building block for ad-hoc object literals where the caller wants control over key ordering or layout:

"{" + Tep::Json.encode_pair_str("name", name) + "," +
      Tep::Json.encode_pair_int("age", age) + "}"

When you have a real Hash, prefer ‘from_str_hash` / `from_int_hash` – those iterate via `each |k, v|` directly.



111
112
113
# File 'lib/tep/json.rb', line 111

def self.encode_pair_str(k, v)
  Json.quote(k) + ":" + Json.quote(v)
end

.escape(s) ⇒ Object

Escape a string for inclusion inside a JSON string literal (does NOT add the surrounding quotes – use ‘quote(s)` for that). Handles “, , and the JSON-required control-char escapes (b, f, n, r, t); other control bytes go through u00XX. Forward slash is left unescaped (legal either way; the unescaped form is more readable and shorter).



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/tep/json.rb', line 55

def self.escape(s)
  out = ""
  i = 0
  n = s.length
  while i < n
    c = s[i]
    if c == "\""
      out = out + "\\\""
    elsif c == "\\"
      out = out + "\\\\"
    elsif c == "\n"
      out = out + "\\n"
    elsif c == "\r"
      out = out + "\\r"
    elsif c == "\t"
      out = out + "\\t"
    elsif c == "\b"
      out = out + "\\b"
    elsif c == "\f"
      out = out + "\\f"
    elsif c < " "
      # Other control byte -- emit \u00XX. c.getbyte(0) is the
      # raw byte value, mapped to two hex digits.
      b = c.getbyte(0)
      out = out + "\\u00" + Json.hex2(b)
    else
      out = out + c
    end
    i += 1
  end
  out
end

.find_value_start(s, target_key) ⇒ Object

Walk the top-level object looking for the entry whose key matches ‘target_key`; return the position of the value’s first non-ws character. Returns -1 if not found.



525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
# File 'lib/tep/json.rb', line 525

def self.find_value_start(s, target_key)
  pos = Json.skip_ws(s, 0)
  if pos >= s.length || s[pos] != "{"
    return -1
  end
  pos += 1
  while pos < s.length
    pos = Json.skip_ws(s, pos)
    if pos >= s.length
      return -1
    end
    if s[pos] == "}"
      return -1
    end
    # Read a key.
    if s[pos] != "\""
      return -1
    end
    key_start = pos
    pos = Json.skip_str(s, pos)
    if pos < 0
      return -1
    end
    # Decode the key for comparison (handles \" inside keys).
    key = Json.parse_str_value(s, key_start)
    # Skip ws, ":".
    pos = Json.skip_ws(s, pos)
    if pos >= s.length || s[pos] != ":"
      return -1
    end
    pos += 1
    pos = Json.skip_ws(s, pos)
    if key == target_key
      return pos
    end
    # Skip the value, then the comma (if any).
    pos = Json.skip_value(s, pos)
    pos = Json.skip_ws(s, pos)
    if pos < s.length && s[pos] == ","
      pos += 1
    elsif pos < s.length && s[pos] == "}"
      return -1
    end
  end
  -1
end

.from_int_array(a) ⇒ Object

Encode an int array as a JSON array of numbers.



164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/tep/json.rb', line 164

def self.from_int_array(a)
  out = "["
  i = 0
  while i < a.length
    if i > 0
      out = out + ","
    end
    out = out + a[i].to_s
    i += 1
  end
  out + "]"
end

.from_int_hash(h) ⇒ Object

Same shape with integer values. JSON-numeric, no quoting.



136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/tep/json.rb', line 136

def self.from_int_hash(h)
  out = "{"
  first = true
  h.each do |k, v|
    if !first
      out = out + ","
    end
    first = false
    out = out + Json.quote(k) + ":" + v.to_s
  end
  out + "}"
end

.from_str_array(a) ⇒ Object

Encode a string array as a JSON array of quoted strings.



150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/tep/json.rb', line 150

def self.from_str_array(a)
  out = "["
  i = 0
  while i < a.length
    if i > 0
      out = out + ","
    end
    out = out + Json.quote(a[i])
    i += 1
  end
  out + "]"
end

.from_str_hash(h) ⇒ Object

Encode a Hash<String,String> as a JSON object.



122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/tep/json.rb', line 122

def self.from_str_hash(h)
  out = "{"
  first = true
  h.each do |k, v|
    if !first
      out = out + ","
    end
    first = false
    out = out + Json.quote(k) + ":" + Json.quote(v)
  end
  out + "}"
end

.get_float(s, key) ⇒ Object

Decode a JSON number value at ‘key` -> Float. Accepts both integer-literal (`42`) and float-literal (`3.14`, `-0.5`, `1e2`) JSON-number syntax; the integer form returns N.0. Missing key or malformed value returns 0.0 (consistent with the other getters’ missing-key defaults).

Implementation: delegates the value-span walking to skip_value (already handles all JSON-number syntax + structural-char boundaries), then String#to_f on the substring. Inlined rather than factored into a parse_float_value helper because spinel’s type inference mis-widens ‘s` to int through the indirection (“cannot resolve call to ’length’ on int” + the downstream skip_ws/skip_value pointer-vs-int conversion errors).



220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/tep/json.rb', line 220

def self.get_float(s, key)
  pos = Json.find_value_start(s, key)
  if pos < 0
    return 0.0
  end
  pos = Json.skip_ws(s, pos)
  if pos >= s.length
    return 0.0
  end
  end_pos = Json.skip_value(s, pos)
  if end_pos <= pos
    return 0.0
  end
  s[pos, end_pos - pos].to_f
end

.get_int(s, key) ⇒ Object



199
200
201
202
203
204
205
# File 'lib/tep/json.rb', line 199

def self.get_int(s, key)
  pos = Json.find_value_start(s, key)
  if pos < 0
    return 0
  end
  Json.parse_int_value(s, pos)
end

.get_int_array(s, key) ⇒ Object

Decode a flat JSON array of integers at ‘key` -> Array. The `prompt` of /v1/completions is a token-id array (`[464, 6193, …]`). A missing or non-array value yields [] (the tep typed-empty-array idiom); non-int elements are skipped.



244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/tep/json.rb', line 244

def self.get_int_array(s, key)
  out = [0]
  out.delete_at(0)
  pos = Json.find_value_start(s, key)
  if pos < 0
    return out
  end
  pos = Json.skip_ws(s, pos)
  if pos >= s.length || s[pos] != "["
    return out
  end
  pos += 1
  while pos < s.length
    pos = Json.skip_ws(s, pos)
    if pos >= s.length
      return out
    end
    c = s[pos]
    if c == "]"
      return out
    elsif c == ","
      pos += 1
    elsif (c >= "0" && c <= "9") || c == "-"
      out.push(Json.parse_int_value(s, pos))
      # Advance past the number parse_int_value just consumed
      # (optional '-' then digits).
      if s[pos] == "-"
        pos += 1
      end
      while pos < s.length && s[pos] >= "0" && s[pos] <= "9"
        pos += 1
      end
    else
      # Non-int element (string / object / etc.): skip it.
      pos = Json.skip_value(s, pos)
    end
  end
  out
end

.get_str(s, key) ⇒ Object

—- Decoders (flat-key, top-level only) —-

‘get_str(s, key)` finds the entry for `key` in the top-level object literal `s` and returns its value as a string. Returns “” when `key` is absent or the value isn’t a string. Same shape for ‘get_int`. `has_key?(s, key)` returns a boolean independent of value type.

The parser is a hand-rolled state machine that walks one ‘{ “k”: <value>, … }` pair at a time, skipping over any value (including nested objects / arrays) it doesn’t need. Strings inside values are honoured for escape sequences so that ‘"` doesn’t terminate the string and corrupt the walk.



191
192
193
194
195
196
197
# File 'lib/tep/json.rb', line 191

def self.get_str(s, key)
  pos = Json.find_value_start(s, key)
  if pos < 0
    return ""
  end
  Json.parse_str_value(s, pos)
end

.has_key?(s, key) ⇒ Boolean

Returns:

  • (Boolean)


236
237
238
# File 'lib/tep/json.rb', line 236

def self.has_key?(s, key)
  Json.find_value_start(s, key) >= 0
end

.hex2(n) ⇒ Object

Two-digit lowercase hex of a byte (0..255).



89
90
91
92
93
94
95
# File 'lib/tep/json.rb', line 89

def self.hex2(n)
  hex = "0123456789abcdef"
  out = ""
  out = out + hex[(n / 16) % 16, 1]
  out = out + hex[n % 16, 1]
  out
end

.hex_nibble(c) ⇒ Object



455
456
457
458
459
460
461
462
463
464
465
466
# File 'lib/tep/json.rb', line 455

def self.hex_nibble(c)
  if c >= "0" && c <= "9"
    return c.getbyte(0) - "0".getbyte(0)
  end
  if c >= "a" && c <= "f"
    return c.getbyte(0) - "a".getbyte(0) + 10
  end
  if c >= "A" && c <= "F"
    return c.getbyte(0) - "A".getbyte(0) + 10
  end
  -1
end

.parse_int_value(s, pos) ⇒ Object

Read an integer at ‘pos`. Accepts an optional leading `-`. Returns 0 on no-digit / non-numeric input (caller can use `has_key?` first if 0-vs-absent matters).



494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
# File 'lib/tep/json.rb', line 494

def self.parse_int_value(s, pos)
  pos = Json.skip_ws(s, pos)
  if pos >= s.length
    return 0
  end
  neg = false
  if s[pos] == "-"
    neg = true
    pos += 1
  end
  n = 0
  saw_digit = false
  while pos < s.length
    c = s[pos]
    if c >= "0" && c <= "9"
      n = n * 10 + (c.getbyte(0) - "0".getbyte(0))
      saw_digit = true
      pos += 1
    else
      break
    end
  end
  if !saw_digit
    return 0
  end
  neg ? -n : n
end

.parse_str_value(s, pos) ⇒ Object

Read a JSON-quoted string at ‘pos` and return its decoded contents (no surrounding quotes). Decodes the same escape sequences that `escape` produces. Returns “” on malformed input.



388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
# File 'lib/tep/json.rb', line 388

def self.parse_str_value(s, pos)
  pos = Json.skip_ws(s, pos)
  if pos >= s.length || s[pos] != "\""
    return ""
  end
  pos += 1
  out = ""
  while pos < s.length
    c = s[pos]
    if c == "\""
      return out
    end
    if c == "\\"
      if pos + 1 >= s.length
        return out
      end
      esc = s[pos + 1]
      if esc == "\""
        out = out + "\""
      elsif esc == "\\"
        out = out + "\\"
      elsif esc == "/"
        out = out + "/"
      elsif esc == "n"
        out = out + "\n"
      elsif esc == "r"
        out = out + "\r"
      elsif esc == "t"
        out = out + "\t"
      elsif esc == "b"
        out = out + "\b"
      elsif esc == "f"
        out = out + "\f"
      elsif esc == "u"
        # \u00XX -> map the two-digit hex back to a byte. Wider
        # codepoints (Ā+ or surrogate pairs) aren't
        # decoded; the byte we emit is the low byte of the
        # codepoint, which round-trips ASCII at minimum.
        if pos + 5 < s.length
          h1 = Json.hex_nibble(s[pos + 4])
          h2 = Json.hex_nibble(s[pos + 5])
          if h1 >= 0 && h2 >= 0
            # rebuild the byte and push it -- spinel strings
            # are byte-blobs, so this works for ASCII; for
            # non-ASCII the original encoder would have used a
            # passthrough byte anyway.
            b = h1 * 16 + h2
            out = out + Json.byte_to_chr(b)
            pos += 6
            next
          end
        end
        out = out + "?"
        pos += 2
        next
      else
        out = out + esc
      end
      pos += 2
    else
      out = out + c
      pos += 1
    end
  end
  out
end

.quote(s) ⇒ Object

Wrap a string in JSON quotes, escaping its body.



98
99
100
# File 'lib/tep/json.rb', line 98

def self.quote(s)
  "\"" + Json.escape(s) + "\""
end

.skip_container(s, pos) ⇒ Object

Walk a balanced { … } or [ … ] starting at ‘pos`. Honours string literals so that `/ `` inside a value-string don’t confuse the brace counter. Returns position one past the matching closer.



357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
# File 'lib/tep/json.rb', line 357

def self.skip_container(s, pos)
  open_c = s[pos]
  close_c = open_c == "{" ? "}" : "]"
  depth = 1
  pos += 1
  while pos < s.length && depth > 0
    c = s[pos]
    if c == "\""
      # whole nested string -- skip past it
      npos = Json.skip_str(s, pos)
      if npos < 0
        return s.length
      end
      pos = npos
    elsif c == open_c
      depth += 1
      pos += 1
    elsif c == close_c
      depth -= 1
      pos += 1
    else
      pos += 1
    end
  end
  pos
end

.skip_str(s, pos) ⇒ Object

Walk a JSON-quoted string starting at ‘pos` (which must point at the opening `“`). Returns the position one past the closing `”`. Returns -1 on malformed input.



302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/tep/json.rb', line 302

def self.skip_str(s, pos)
  if pos >= s.length || s[pos] != "\""
    return -1
  end
  pos += 1
  while pos < s.length
    c = s[pos]
    if c == "\\"
      # Skip the escape and the escaped character. \uXXXX
      # spans 6 chars total but skipping 2 still keeps us
      # inside the string for the rest of the walk -- the
      # remaining 4 hex digits look like ordinary string
      # bytes and won't terminate the literal.
      pos += 2
    elsif c == "\""
      return pos + 1
    else
      pos += 1
    end
  end
  -1
end

.skip_value(s, pos) ⇒ Object

Walk a JSON value starting at ‘pos` (which must point at the first non-ws char of the value). Returns the position one past the value (or the input length on truncation).



328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/tep/json.rb', line 328

def self.skip_value(s, pos)
  pos = Json.skip_ws(s, pos)
  if pos >= s.length
    return pos
  end
  c = s[pos]
  if c == "\""
    return Json.skip_str(s, pos)
  end
  if c == "{" || c == "["
    return Json.skip_container(s, pos)
  end
  # number / true / false / null -- read until the next
  # structural / whitespace char.
  while pos < s.length
    c = s[pos]
    if c == "," || c == "}" || c == "]" ||
       c == " " || c == "\t" || c == "\n" || c == "\r"
      return pos
    end
    pos += 1
  end
  pos
end

.skip_ws(s, pos) ⇒ Object

Skip whitespace starting at ‘pos`, return the new position.



287
288
289
290
291
292
293
294
295
296
297
# File 'lib/tep/json.rb', line 287

def self.skip_ws(s, pos)
  while pos < s.length
    c = s[pos]
    if c == " " || c == "\t" || c == "\n" || c == "\r"
      pos += 1
    else
      return pos
    end
  end
  pos
end