Module: Spinel::Json

Defined in:
lib/fresco/runtime/runtime.rb

Overview

Hand-rolled JSON encoder. Scalars and Arrays dispatch through the polymorphic ‘encode`; Hashes are handled by `encode_hash`, which is pinned to `Hash<Symbol, *>` (`SymPolyHash`) via a default-arg type hint and a single-call-site discipline.

Spinel limitation: ‘.each` lowering on a polymorphic-typed (`sp_RbVal`) hash only emits array-tag branches — a Hash there silently iterates zero times. The analyzer also widens `encode_hash`’s parameter to ‘sp_RbVal` if there’s any call site where the argument is polymorphic. So ‘encode_hash` is called only from `Response.json` (where `body` is a concrete SymPolyHash), and `encode`’s ‘value.is_a?(Hash)` branch recurses through it via an implicit `(sp_SymPolyHash *)` cast that Spinel inserts at the call site. Nested hashes within Arrays / Hash values therefore only encode their first level correctly when they happen to be SymPolyHash; deeper nesting through a polymorphic-typed value will need either a Spinel codegen change or per-shape encoders.

Class Method Summary collapse

Class Method Details

.encode(value) ⇒ Object



857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
# File 'lib/fresco/runtime/runtime.rb', line 857

def self.encode(value)
  return "null"  if value.nil?
  return "true"  if value == true
  return "false" if value == false
  return value.to_s if value.is_a?(Integer)
  return value.to_s if value.is_a?(Float)
  return encode_string(value)        if value.is_a?(String)
  return encode_string(value.to_s)   if value.is_a?(Symbol)
  if value.is_a?(Array)
    out = "["
    i = 0
    while i < value.length
      out += "," if i > 0
      out += encode(value[i])
      i += 1
    end
    return out + "]"
  end
  # Polymorphic Hash recursion path. Under CRuby this iterates
  # correctly; under Spinel the `.each` lowering on a RbVal-typed
  # hash only emits array-tag branches and silently produces
  # `"{}"`. The two implementations diverge here on purpose —
  # CRuby (bin/dev) reads payloads as written, while Spinel-built
  # binaries should pre-encode nested hashes with `encode_hash`
  # and concatenate the outer JSON manually if they need it.
  return encode_recursive_hash(value) if value.is_a?(Hash)
  encode_string(value.to_s)
end

.encode_hash(h = { __t: nil }) ⇒ Object

Hash branch lives in its own function so the ‘h = { __t: nil }` default pins `h` to `SymPolyHash` for Spinel’s analyzer. Inline iteration on a polymorphic ‘value` inside `encode` would lower to array-only branches; passing through a typed parameter forces a real hash walk. The default `{ __t: nil }` is never visible —the function is only ever called with an actual hash from the `value.is_a?(Hash)` branch in `encode`.



911
912
913
914
915
916
917
918
919
920
921
# File 'lib/fresco/runtime/runtime.rb', line 911

def self.encode_hash(h = { __t: nil })
  out = "{"
  first = true
  h.each do |k, v|
    out += "," unless first
    first = false
    encoded_value = encode(v)
    out += encode_string(k.to_s) + ":" + encoded_value
  end
  out + "}"
end

.encode_recursive_hash(h = { __t: nil }) ⇒ Object

CRuby-correct, Spinel-degraded sibling of ‘encode_hash`. Lives behind a different name so `encode_hash` stays single-caller and pinned to `SymPolyHash`; this one is the polymorphic recursion target from `encode` and Spinel widens its parameter to RbVal, breaking the iteration. Kept for CRuby parity so `bin/dev` and unit tests still behave as expected.



892
893
894
895
896
897
898
899
900
901
902
# File 'lib/fresco/runtime/runtime.rb', line 892

def self.encode_recursive_hash(h = { __t: nil })
  out = "{"
  first = true
  h.each do |k, v|
    out += "," unless first
    first = false
    encoded_value = encode(v)
    out += encode_string(k.to_s) + ":" + encoded_value
  end
  out + "}"
end

.encode_string(s) ⇒ Object

Char-by-char concat (plan quirk: stick to the existing parsers’ shape rather than ‘gsub` chains). gsub-with-string-replacement interprets backslash backreferences under CRuby — encoding the JSON `\` escape requires four-deep escaping that doesn’t carry to Spinel. The walk sidesteps all of that and stays monomorphic. Control chars below 0x20 other than the named escapes pass through unescaped; an MVP encoder for app-built payloads doesn’t need the full u00XX table.



931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
# File 'lib/fresco/runtime/runtime.rb', line 931

def self.encode_string(s)
  out = "\""
  i = 0
  n = s.length
  while i < n
    c = s[i]
    if c == "\\"
      out += "\\\\"
    elsif c == "\""
      out += "\\\""
    elsif c == "\n"
      out += "\\n"
    elsif c == "\r"
      out += "\\r"
    elsif c == "\t"
      out += "\\t"
    elsif c == "\b"
      out += "\\b"
    elsif c == "\f"
      out += "\\f"
    else
      out += c
    end
    i += 1
  end
  out + "\""
end