Skip to content
Ori Pekelman edited this page May 11, 2026 · 1 revision

Tep::Json

JSON encoder + flat-key decoder for JSON-over-HTTP. Hand-written because the canonical json gem leans on classes and method_missing patterns spinel doesn't lower.

Scope

  • Encoder: primitives + Tep's typed hashes / arrays.
  • Decoder: flat-key reads (Tep::Json.get_str(body, "name")). No nested traversal, no full AST. For nested JSON, use the dot notation in the key ("user.name").
  • No streaming / SAX. Bodies are typically API responses up to a few KB.

Encode

Tep::Json.escape("hello \"world\"")        # \" inside the string
Tep::Json.quote("hello")                   # "hello"  (wrapped + escaped)

Tep::Json.encode_pair_str("name", "alice") # "name":"alice"
Tep::Json.encode_pair_int("age", 42)       # "age":42

For full hashes/arrays:

h = Tep.str_hash
h["name"] = "alice"
h["city"] = "tlv"
Tep::Json.from_str_hash(h)
# => {"name":"alice","city":"tlv"}

h2 = {"" => 0}; h2.delete("")
h2["a"] = 1; h2["b"] = 2
Tep::Json.from_int_hash(h2)
# => {"a":1,"b":2}

arr = [""]; arr.delete_at(0)
arr.push("x"); arr.push("y")
Tep::Json.from_str_array(arr)
# => ["x","y"]

arr2 = [0]; arr2.delete_at(0)
arr2.push(1); arr2.push(2)
Tep::Json.from_int_array(arr2)
# => [1,2]

The empty-then-delete-at-0 idiom is the type-seed pattern spinel needs to pin the element type of an otherwise-empty array/hash.

Decode

body = '{"user": {"name": "alice"}, "age": 42, "active": true}'

Tep::Json.get_str(body, "user.name")     # "alice"
Tep::Json.get_int(body, "age")           # 42
Tep::Json.has_key?(body, "active")       # true
Tep::Json.has_key?(body, "deleted")      # false
  • get_str returns "" if the key is missing.
  • get_int returns 0 if the key is missing or non-numeric.
  • Dot notation walks nested objects: user.name reads body["user"]["name"].
  • Arrays aren't supported in the decoder (yet). For an array response, treat it as opaque body bytes and parse downstream.

Cookbook

POST a JSON body to an upstream

h = Tep.str_hash
h["name"] = params[:name]
h["email"] = params[:email]
req = Tep::Json.from_str_hash(h)

hdr = Tep.str_hash
hdr["Content-Type"] = "application/json"
res = Tep::Http.send_req("POST", "http://api.local/users", req, hdr)

Read a JSON response

res = Tep::Http.get("http://api.local/users/42")
if res.status == 200
  name = Tep::Json.get_str(res.body, "name")
  age  = Tep::Json.get_int(res.body, "age")
  name + " (" + age.to_s + ")"
end

Return JSON from a handler

get '/users/:id' do
  content_type 'application/json'

  h = Tep.str_hash
  h["id"] = params[:id]
  h["name"] = "alice"
  Tep::Json.from_str_hash(h)
end

Pitfalls

  • No floats. Numbers round-trip as INTEGER only. If you need 1.5, encode it as a string and parse on the other side.
  • No arrays in the decoder. A response body [1,2,3] won't decode through get_int; treat array responses as opaque strings and parse by hand (split(",") on the trimmed body, etc.).
  • Whitespace tolerance. The decoder skips ASCII whitespace between tokens but is otherwise strict. Tabs and unicode-space characters aren't handled — fine for real JSON, surprising if you're feeding it hand-formatted data.
  • The encoder doesn't escape U+2028 / U+2029. These are valid in JSON but invalid in JavaScript string literals. If your output goes straight into a <script> tag, post-process.

Clone this wiki locally