tep 0.11.3 → 0.11.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Makefile +31 -1
- data/README.md +4 -4
- data/SINATRA_COMPAT.md +20 -20
- data/bin/tep +8 -8
- data/examples/api_gateway/app.rb +1 -1
- data/examples/blog/app.rb +17 -17
- data/examples/chat/app.rb +12 -12
- data/examples/chatbot/README.md +2 -2
- data/examples/chatbot/app.rb +24 -24
- data/examples/llm_gateway/app.rb +4 -4
- data/lib/spinel_kit/hex.rb +65 -0
- data/lib/spinel_kit/json.rb +151 -0
- data/lib/spinel_kit/json_decoder.rb +396 -0
- data/lib/{tep/logger.rb → spinel_kit/log.rb} +25 -21
- data/lib/spinel_kit/url.rb +166 -0
- data/lib/tep/auth_bearer_token.rb +6 -6
- data/lib/tep/auth_oauth2.rb +4 -4
- data/lib/tep/events.rb +37 -37
- data/lib/tep/http.rb +3 -3
- data/lib/tep/job.rb +2 -2
- data/lib/tep/jwt.rb +4 -4
- data/lib/tep/live_view.rb +4 -4
- data/lib/tep/llm.rb +13 -45
- data/lib/tep/mcp.rb +12 -12
- data/lib/tep/multipart.rb +1 -1
- data/lib/tep/openai_server.rb +102 -94
- data/lib/tep/parser.rb +2 -2
- data/lib/tep/presence.rb +11 -11
- data/lib/tep/proxy.rb +7 -7
- data/lib/tep/request.rb +1 -1
- data/lib/tep/response.rb +1 -1
- data/lib/tep/router.rb +1 -1
- data/lib/tep/session.rb +2 -2
- data/lib/tep/version.rb +1 -1
- data/lib/tep.rb +30 -29
- data/test/helper.rb +95 -8
- data/test/run_parallel.rb +44 -7
- data/test/test_auth.rb +17 -17
- data/test/test_auth_oauth2.rb +5 -5
- data/test/test_http_pool.rb +4 -4
- data/test/test_http_pool_send.rb +3 -3
- data/test/test_json.rb +12 -12
- data/test/test_jwt.rb +4 -4
- data/test/test_live_view.rb +3 -3
- data/test/test_llm.rb +12 -9
- data/test/test_llm_gateway.rb +2 -2
- data/test/test_logger.rb +2 -2
- data/test/test_openai_server.rb +10 -1
- data/test/test_password.rb +3 -3
- data/test/test_real_world.rb +6 -1
- data/test/test_shutdown.rb +40 -0
- metadata +9 -8
- data/lib/tep/json.rb +0 -572
- data/lib/tep/url.rb +0 -161
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# VENDORED from OriPekelman/spinelkit @ 09e8558 -- DO NOT EDIT HERE.
|
|
2
|
+
# Edit upstream and re-sync with `make vendor-spinelkit`.
|
|
3
|
+
# SpinelKit::Json -- Spinel-safe JSON ENCODERS (stateless).
|
|
4
|
+
#
|
|
5
|
+
# This file holds the encode half of the codec; the decode half lives in
|
|
6
|
+
# spinel_kit/json_decoder.rb (also `SpinelKit::Json`), and the incremental
|
|
7
|
+
# object builder in spinel_kit/json_builder.rb (`SpinelKit::Json::Builder`).
|
|
8
|
+
# The three are split because Spinel has no tree-shaking: every loaded method
|
|
9
|
+
# is compiled, and a set of uncalled methods can degrade each other's params
|
|
10
|
+
# (e.g. the dead decoder walkers collectively widening `escape`'s string arg
|
|
11
|
+
# to int, which silently miscompiled string keys to ""). Keeping
|
|
12
|
+
# encode/decode/build in separate files means a consumer compiles only the
|
|
13
|
+
# surface it calls, and each surface is independently warning-clean. Require
|
|
14
|
+
# only what you use:
|
|
15
|
+
#
|
|
16
|
+
# require "spinel_kit/json" # encoders (this file)
|
|
17
|
+
# require "spinel_kit/json_decoder" # decoders
|
|
18
|
+
# require "spinel_kit/json_builder" # builder
|
|
19
|
+
#
|
|
20
|
+
# WHY HAND-ROLLED. Spinel cannot lower the stdlib `json` gem (C-ext fast path
|
|
21
|
+
# + metaprogrammed pure fallback); `oj`/`yajl`/`multi_json` are C extensions.
|
|
22
|
+
# The spinelgems catalog confirms no verified pure-Ruby JSON gem exists. This
|
|
23
|
+
# is tep's encoder, standardized (the `j_`/`tj_` prefixes that worked around a
|
|
24
|
+
# now-fixed Spinel inference bug are gone -- see docs/spinel-discipline.md).
|
|
25
|
+
#
|
|
26
|
+
# Compose objects in user code by concatenation:
|
|
27
|
+
#
|
|
28
|
+
# "{" + SpinelKit::Json.encode_pair_str("name", name) + "," +
|
|
29
|
+
# SpinelKit::Json.encode_pair_int("age", age) + "}"
|
|
30
|
+
module SpinelKit
|
|
31
|
+
class Json
|
|
32
|
+
# Escape a string for inclusion inside a JSON string literal (does NOT
|
|
33
|
+
# add the surrounding quotes -- use `quote(s)` for that). Handles ", \,
|
|
34
|
+
# and the JSON-required control-char escapes (\b, \f, \n, \r, \t);
|
|
35
|
+
# other control bytes go through \u00XX. Forward slash is left
|
|
36
|
+
# unescaped (legal either way; unescaped is shorter/readable).
|
|
37
|
+
def self.escape(s)
|
|
38
|
+
out = ""
|
|
39
|
+
i = 0
|
|
40
|
+
n = s.length
|
|
41
|
+
while i < n
|
|
42
|
+
c = s[i]
|
|
43
|
+
if c == "\""
|
|
44
|
+
out = out + "\\\""
|
|
45
|
+
elsif c == "\\"
|
|
46
|
+
out = out + "\\\\"
|
|
47
|
+
elsif c == "\n"
|
|
48
|
+
out = out + "\\n"
|
|
49
|
+
elsif c == "\r"
|
|
50
|
+
out = out + "\\r"
|
|
51
|
+
elsif c == "\t"
|
|
52
|
+
out = out + "\\t"
|
|
53
|
+
elsif c == "\b"
|
|
54
|
+
out = out + "\\b"
|
|
55
|
+
elsif c == "\f"
|
|
56
|
+
out = out + "\\f"
|
|
57
|
+
elsif c < " "
|
|
58
|
+
# Other control byte -- emit \u00XX. c.getbyte(0) is the raw
|
|
59
|
+
# byte value, mapped to two hex digits.
|
|
60
|
+
b = c.getbyte(0)
|
|
61
|
+
out = out + "\\u00" + Json.hex2(b)
|
|
62
|
+
else
|
|
63
|
+
out = out + c
|
|
64
|
+
end
|
|
65
|
+
i += 1
|
|
66
|
+
end
|
|
67
|
+
out
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Two-digit lowercase hex of a byte (0..255).
|
|
71
|
+
def self.hex2(n)
|
|
72
|
+
hex = "0123456789abcdef"
|
|
73
|
+
out = ""
|
|
74
|
+
out = out + hex[(n / 16) % 16, 1]
|
|
75
|
+
out = out + hex[n % 16, 1]
|
|
76
|
+
out
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Wrap a string in JSON quotes, escaping its body.
|
|
80
|
+
def self.quote(s)
|
|
81
|
+
"\"" + Json.escape(s) + "\""
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Encode a single key/value pair as `"k":"v"` (escaped both sides).
|
|
85
|
+
def self.encode_pair_str(k, v)
|
|
86
|
+
Json.quote(k) + ":" + Json.quote(v)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Same shape, integer value side. `v` is rendered via `.to_s` so
|
|
90
|
+
# JSON-numeric output without quoting.
|
|
91
|
+
def self.encode_pair_int(k, v)
|
|
92
|
+
Json.quote(k) + ":" + v.to_s
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Encode a Hash<String,String> as a JSON object.
|
|
96
|
+
def self.from_str_hash(h)
|
|
97
|
+
out = "{"
|
|
98
|
+
first = true
|
|
99
|
+
h.each do |k, v|
|
|
100
|
+
if !first
|
|
101
|
+
out = out + ","
|
|
102
|
+
end
|
|
103
|
+
first = false
|
|
104
|
+
out = out + Json.quote(k) + ":" + Json.quote(v)
|
|
105
|
+
end
|
|
106
|
+
out + "}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Same shape with integer values. JSON-numeric, no quoting.
|
|
110
|
+
def self.from_int_hash(h)
|
|
111
|
+
out = "{"
|
|
112
|
+
first = true
|
|
113
|
+
h.each do |k, v|
|
|
114
|
+
if !first
|
|
115
|
+
out = out + ","
|
|
116
|
+
end
|
|
117
|
+
first = false
|
|
118
|
+
out = out + Json.quote(k) + ":" + v.to_s
|
|
119
|
+
end
|
|
120
|
+
out + "}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Encode a string array as a JSON array of quoted strings.
|
|
124
|
+
def self.from_str_array(a)
|
|
125
|
+
out = "["
|
|
126
|
+
i = 0
|
|
127
|
+
while i < a.length
|
|
128
|
+
if i > 0
|
|
129
|
+
out = out + ","
|
|
130
|
+
end
|
|
131
|
+
out = out + Json.quote(a[i])
|
|
132
|
+
i += 1
|
|
133
|
+
end
|
|
134
|
+
out + "]"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Encode an int array as a JSON array of numbers.
|
|
138
|
+
def self.from_int_array(a)
|
|
139
|
+
out = "["
|
|
140
|
+
i = 0
|
|
141
|
+
while i < a.length
|
|
142
|
+
if i > 0
|
|
143
|
+
out = out + ","
|
|
144
|
+
end
|
|
145
|
+
out = out + a[i].to_s
|
|
146
|
+
i += 1
|
|
147
|
+
end
|
|
148
|
+
out + "]"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
# VENDORED from OriPekelman/spinelkit @ 09e8558 -- DO NOT EDIT HERE.
|
|
2
|
+
# Edit upstream and re-sync with `make vendor-spinelkit`.
|
|
3
|
+
# SpinelKit::Json -- Spinel-safe JSON DECODERS (flat-key, top-level only).
|
|
4
|
+
#
|
|
5
|
+
# The decode half of the codec; encoders are in spinel_kit/json.rb. Split out
|
|
6
|
+
# so an encode-only consumer never compiles these walkers (their dead-code
|
|
7
|
+
# degradation otherwise widens the encoders' string args to int -- see the
|
|
8
|
+
# header of json.rb and docs/spinel-discipline.md).
|
|
9
|
+
#
|
|
10
|
+
# `get_str(s, key)` finds the entry for `key` in the top-level object literal
|
|
11
|
+
# `s` and returns its value as a string. Returns "" when `key` is absent or
|
|
12
|
+
# the value isn't a string. Same shape for `get_int`. `has_key?(s, key)`
|
|
13
|
+
# returns a boolean independent of value type. The parser is a hand-rolled
|
|
14
|
+
# state machine that walks one `{ "k": <value>, ... }` pair at a time,
|
|
15
|
+
# skipping over any value (including nested objects / arrays) it doesn't need.
|
|
16
|
+
# Strings inside values are honoured for escape sequences so that `\"` doesn't
|
|
17
|
+
# terminate the string and corrupt the walk. Decodes the escape sequences
|
|
18
|
+
# `SpinelKit::Json.escape` produces.
|
|
19
|
+
module SpinelKit
|
|
20
|
+
class Json
|
|
21
|
+
def self.get_str(s, key)
|
|
22
|
+
pos = Json.find_value_start(s, key)
|
|
23
|
+
if pos < 0
|
|
24
|
+
return ""
|
|
25
|
+
end
|
|
26
|
+
Json.parse_str_value(s, pos)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.get_int(s, key)
|
|
30
|
+
pos = Json.find_value_start(s, key)
|
|
31
|
+
if pos < 0
|
|
32
|
+
return 0
|
|
33
|
+
end
|
|
34
|
+
Json.parse_int_value(s, pos)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Decode a JSON number value at `key` -> Float. Accepts both
|
|
38
|
+
# integer-literal (`42`) and float-literal (`3.14`, `-0.5`, `1e2`)
|
|
39
|
+
# JSON-number syntax; the integer form returns N.0. Missing key or
|
|
40
|
+
# malformed value returns 0.0 (consistent with the other getters'
|
|
41
|
+
# missing-key defaults).
|
|
42
|
+
#
|
|
43
|
+
# Implementation: delegates the value-span walking to skip_value (already
|
|
44
|
+
# handles all JSON-number syntax + structural-char boundaries), then
|
|
45
|
+
# String#to_f on the substring. Inlined rather than factored into a
|
|
46
|
+
# parse_float_value helper because spinel's type inference mis-widens `s`
|
|
47
|
+
# to int through the indirection. NOTE: that is a value-walk indirection
|
|
48
|
+
# concern, NOT the name-collision bug (which was fixed) -- keep it inlined.
|
|
49
|
+
def self.get_float(s, key)
|
|
50
|
+
pos = Json.find_value_start(s, key)
|
|
51
|
+
if pos < 0
|
|
52
|
+
return 0.0
|
|
53
|
+
end
|
|
54
|
+
pos = Json.skip_ws(s, pos)
|
|
55
|
+
if pos >= s.length
|
|
56
|
+
return 0.0
|
|
57
|
+
end
|
|
58
|
+
end_pos = Json.skip_value(s, pos)
|
|
59
|
+
if end_pos <= pos
|
|
60
|
+
return 0.0
|
|
61
|
+
end
|
|
62
|
+
s[pos, end_pos - pos].to_f
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.has_key?(s, key)
|
|
66
|
+
Json.find_value_start(s, key) >= 0
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Decode a flat JSON array of integers at `key` -> Array[Integer].
|
|
70
|
+
# A missing or non-array value yields [] (the typed-empty-array idiom);
|
|
71
|
+
# non-int elements are skipped.
|
|
72
|
+
def self.get_int_array(s, key)
|
|
73
|
+
out = [0]
|
|
74
|
+
out.delete_at(0)
|
|
75
|
+
pos = Json.find_value_start(s, key)
|
|
76
|
+
if pos < 0
|
|
77
|
+
return out
|
|
78
|
+
end
|
|
79
|
+
pos = Json.skip_ws(s, pos)
|
|
80
|
+
if pos >= s.length || s[pos] != "["
|
|
81
|
+
return out
|
|
82
|
+
end
|
|
83
|
+
pos += 1
|
|
84
|
+
while pos < s.length
|
|
85
|
+
pos = Json.skip_ws(s, pos)
|
|
86
|
+
if pos >= s.length
|
|
87
|
+
return out
|
|
88
|
+
end
|
|
89
|
+
c = s[pos]
|
|
90
|
+
if c == "]"
|
|
91
|
+
return out
|
|
92
|
+
elsif c == ","
|
|
93
|
+
pos += 1
|
|
94
|
+
elsif (c >= "0" && c <= "9") || c == "-"
|
|
95
|
+
out.push(Json.parse_int_value(s, pos))
|
|
96
|
+
# Advance past the number parse_int_value just consumed
|
|
97
|
+
# (optional '-' then digits).
|
|
98
|
+
if s[pos] == "-"
|
|
99
|
+
pos += 1
|
|
100
|
+
end
|
|
101
|
+
while pos < s.length && s[pos] >= "0" && s[pos] <= "9"
|
|
102
|
+
pos += 1
|
|
103
|
+
end
|
|
104
|
+
else
|
|
105
|
+
# Non-int element (string / object / etc.): skip it.
|
|
106
|
+
pos = Json.skip_value(s, pos)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
out
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# ---- Internal helpers ----
|
|
113
|
+
|
|
114
|
+
# Skip whitespace starting at `pos`, return the new position.
|
|
115
|
+
def self.skip_ws(s, pos)
|
|
116
|
+
while pos < s.length
|
|
117
|
+
c = s[pos]
|
|
118
|
+
if c == " " || c == "\t" || c == "\n" || c == "\r"
|
|
119
|
+
pos += 1
|
|
120
|
+
else
|
|
121
|
+
return pos
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
pos
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Walk a JSON-quoted string starting at `pos` (which must point at the
|
|
128
|
+
# opening `"`). Returns the position one past the closing `"`. Returns
|
|
129
|
+
# -1 on malformed input.
|
|
130
|
+
def self.skip_str(s, pos)
|
|
131
|
+
if pos >= s.length || s[pos] != "\""
|
|
132
|
+
return -1
|
|
133
|
+
end
|
|
134
|
+
pos += 1
|
|
135
|
+
while pos < s.length
|
|
136
|
+
c = s[pos]
|
|
137
|
+
if c == "\\"
|
|
138
|
+
# Skip the escape and the escaped character. \uXXXX spans 6
|
|
139
|
+
# chars total but skipping 2 still keeps us inside the string
|
|
140
|
+
# for the rest of the walk -- the remaining 4 hex digits look
|
|
141
|
+
# like ordinary string bytes and won't terminate the literal.
|
|
142
|
+
pos += 2
|
|
143
|
+
elsif c == "\""
|
|
144
|
+
return pos + 1
|
|
145
|
+
else
|
|
146
|
+
pos += 1
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
-1
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Walk a JSON value starting at `pos` (which must point at the first
|
|
153
|
+
# non-ws char of the value). Returns the position one past the value
|
|
154
|
+
# (or the input length on truncation).
|
|
155
|
+
def self.skip_value(s, pos)
|
|
156
|
+
pos = Json.skip_ws(s, pos)
|
|
157
|
+
if pos >= s.length
|
|
158
|
+
return pos
|
|
159
|
+
end
|
|
160
|
+
c = s[pos]
|
|
161
|
+
if c == "\""
|
|
162
|
+
return Json.skip_str(s, pos)
|
|
163
|
+
end
|
|
164
|
+
if c == "{" || c == "["
|
|
165
|
+
return Json.skip_container(s, pos)
|
|
166
|
+
end
|
|
167
|
+
# number / true / false / null -- read until the next structural /
|
|
168
|
+
# whitespace char.
|
|
169
|
+
while pos < s.length
|
|
170
|
+
c = s[pos]
|
|
171
|
+
if c == "," || c == "}" || c == "]" ||
|
|
172
|
+
c == " " || c == "\t" || c == "\n" || c == "\r"
|
|
173
|
+
return pos
|
|
174
|
+
end
|
|
175
|
+
pos += 1
|
|
176
|
+
end
|
|
177
|
+
pos
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Walk a balanced { ... } or [ ... ] starting at `pos`. Honours string
|
|
181
|
+
# literals so that `{` / `}` inside a value-string don't confuse the
|
|
182
|
+
# brace counter. Returns position one past the matching closer.
|
|
183
|
+
def self.skip_container(s, pos)
|
|
184
|
+
open_c = s[pos]
|
|
185
|
+
close_c = open_c == "{" ? "}" : "]"
|
|
186
|
+
depth = 1
|
|
187
|
+
pos += 1
|
|
188
|
+
while pos < s.length && depth > 0
|
|
189
|
+
c = s[pos]
|
|
190
|
+
if c == "\""
|
|
191
|
+
# whole nested string -- skip past it
|
|
192
|
+
npos = Json.skip_str(s, pos)
|
|
193
|
+
if npos < 0
|
|
194
|
+
return s.length
|
|
195
|
+
end
|
|
196
|
+
pos = npos
|
|
197
|
+
elsif c == open_c
|
|
198
|
+
depth += 1
|
|
199
|
+
pos += 1
|
|
200
|
+
elsif c == close_c
|
|
201
|
+
depth -= 1
|
|
202
|
+
pos += 1
|
|
203
|
+
else
|
|
204
|
+
pos += 1
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
pos
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Read a JSON-quoted string at `pos` and return its decoded contents
|
|
211
|
+
# (no surrounding quotes). Decodes the same escape sequences that
|
|
212
|
+
# `escape` produces. Returns "" on malformed input.
|
|
213
|
+
def self.parse_str_value(s, pos)
|
|
214
|
+
pos = Json.skip_ws(s, pos)
|
|
215
|
+
if pos >= s.length || s[pos] != "\""
|
|
216
|
+
return ""
|
|
217
|
+
end
|
|
218
|
+
pos += 1
|
|
219
|
+
out = ""
|
|
220
|
+
while pos < s.length
|
|
221
|
+
c = s[pos]
|
|
222
|
+
if c == "\""
|
|
223
|
+
return out
|
|
224
|
+
end
|
|
225
|
+
if c == "\\"
|
|
226
|
+
if pos + 1 >= s.length
|
|
227
|
+
return out
|
|
228
|
+
end
|
|
229
|
+
esc = s[pos + 1]
|
|
230
|
+
if esc == "\""
|
|
231
|
+
out = out + "\""
|
|
232
|
+
elsif esc == "\\"
|
|
233
|
+
out = out + "\\"
|
|
234
|
+
elsif esc == "/"
|
|
235
|
+
out = out + "/"
|
|
236
|
+
elsif esc == "n"
|
|
237
|
+
out = out + "\n"
|
|
238
|
+
elsif esc == "r"
|
|
239
|
+
out = out + "\r"
|
|
240
|
+
elsif esc == "t"
|
|
241
|
+
out = out + "\t"
|
|
242
|
+
elsif esc == "b"
|
|
243
|
+
out = out + "\b"
|
|
244
|
+
elsif esc == "f"
|
|
245
|
+
out = out + "\f"
|
|
246
|
+
elsif esc == "u"
|
|
247
|
+
# \u00XX -> map the two-digit hex back to a byte. Wider
|
|
248
|
+
# codepoints (U+0100+ or surrogate pairs) aren't decoded; the
|
|
249
|
+
# byte we emit is the low byte of the codepoint, which
|
|
250
|
+
# round-trips ASCII at minimum.
|
|
251
|
+
if pos + 5 < s.length
|
|
252
|
+
h1 = Json.hex_nibble(s[pos + 4])
|
|
253
|
+
h2 = Json.hex_nibble(s[pos + 5])
|
|
254
|
+
if h1 >= 0 && h2 >= 0
|
|
255
|
+
# rebuild the byte and push it -- spinel strings are
|
|
256
|
+
# byte-blobs, so this works for ASCII; for non-ASCII the
|
|
257
|
+
# original encoder would have used a passthrough byte
|
|
258
|
+
# anyway.
|
|
259
|
+
b = h1 * 16 + h2
|
|
260
|
+
out = out + Json.byte_to_chr(b)
|
|
261
|
+
pos += 6
|
|
262
|
+
next
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
out = out + "?"
|
|
266
|
+
pos += 2
|
|
267
|
+
next
|
|
268
|
+
else
|
|
269
|
+
out = out + esc
|
|
270
|
+
end
|
|
271
|
+
pos += 2
|
|
272
|
+
else
|
|
273
|
+
out = out + c
|
|
274
|
+
pos += 1
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
out
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def self.hex_nibble(c)
|
|
281
|
+
if c >= "0" && c <= "9"
|
|
282
|
+
return c.getbyte(0) - "0".getbyte(0)
|
|
283
|
+
end
|
|
284
|
+
if c >= "a" && c <= "f"
|
|
285
|
+
return c.getbyte(0) - "a".getbyte(0) + 10
|
|
286
|
+
end
|
|
287
|
+
if c >= "A" && c <= "F"
|
|
288
|
+
return c.getbyte(0) - "A".getbyte(0) + 10
|
|
289
|
+
end
|
|
290
|
+
-1
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Build a single-byte string from an integer 0..255. Spinel doesn't
|
|
294
|
+
# expose `n.chr` for arbitrary bytes uniformly; the table covers the
|
|
295
|
+
# ASCII printable range and falls back to "?" for anything else (the
|
|
296
|
+
# JSON encoder side never produces non-ASCII via \u, so the fallback
|
|
297
|
+
# is reachable only for malformed input).
|
|
298
|
+
def self.byte_to_chr(n)
|
|
299
|
+
printable = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
|
|
300
|
+
if n >= 32 && n < 127
|
|
301
|
+
return printable[n - 32, 1]
|
|
302
|
+
end
|
|
303
|
+
if n == 9
|
|
304
|
+
return "\t"
|
|
305
|
+
end
|
|
306
|
+
if n == 10
|
|
307
|
+
return "\n"
|
|
308
|
+
end
|
|
309
|
+
if n == 13
|
|
310
|
+
return "\r"
|
|
311
|
+
end
|
|
312
|
+
"?"
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Read an integer at `pos`. Accepts an optional leading `-`. Returns 0
|
|
316
|
+
# on no-digit / non-numeric input (caller can use `has_key?` first if
|
|
317
|
+
# 0-vs-absent matters).
|
|
318
|
+
def self.parse_int_value(s, pos)
|
|
319
|
+
pos = Json.skip_ws(s, pos)
|
|
320
|
+
if pos >= s.length
|
|
321
|
+
return 0
|
|
322
|
+
end
|
|
323
|
+
neg = false
|
|
324
|
+
if s[pos] == "-"
|
|
325
|
+
neg = true
|
|
326
|
+
pos += 1
|
|
327
|
+
end
|
|
328
|
+
n = 0
|
|
329
|
+
saw_digit = false
|
|
330
|
+
while pos < s.length
|
|
331
|
+
c = s[pos]
|
|
332
|
+
if c >= "0" && c <= "9"
|
|
333
|
+
n = n * 10 + (c.getbyte(0) - "0".getbyte(0))
|
|
334
|
+
saw_digit = true
|
|
335
|
+
pos += 1
|
|
336
|
+
else
|
|
337
|
+
break
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
if !saw_digit
|
|
341
|
+
return 0
|
|
342
|
+
end
|
|
343
|
+
neg ? -n : n
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Walk the top-level object looking for the entry whose key matches
|
|
347
|
+
# `target_key`; return the position of the value's first non-ws
|
|
348
|
+
# character. Returns -1 if not found.
|
|
349
|
+
def self.find_value_start(s, target_key)
|
|
350
|
+
pos = Json.skip_ws(s, 0)
|
|
351
|
+
if pos >= s.length || s[pos] != "{"
|
|
352
|
+
return -1
|
|
353
|
+
end
|
|
354
|
+
pos += 1
|
|
355
|
+
while pos < s.length
|
|
356
|
+
pos = Json.skip_ws(s, pos)
|
|
357
|
+
if pos >= s.length
|
|
358
|
+
return -1
|
|
359
|
+
end
|
|
360
|
+
if s[pos] == "}"
|
|
361
|
+
return -1
|
|
362
|
+
end
|
|
363
|
+
# Read a key.
|
|
364
|
+
if s[pos] != "\""
|
|
365
|
+
return -1
|
|
366
|
+
end
|
|
367
|
+
key_start = pos
|
|
368
|
+
pos = Json.skip_str(s, pos)
|
|
369
|
+
if pos < 0
|
|
370
|
+
return -1
|
|
371
|
+
end
|
|
372
|
+
# Decode the key for comparison (handles \" inside keys).
|
|
373
|
+
key = Json.parse_str_value(s, key_start)
|
|
374
|
+
# Skip ws, ":".
|
|
375
|
+
pos = Json.skip_ws(s, pos)
|
|
376
|
+
if pos >= s.length || s[pos] != ":"
|
|
377
|
+
return -1
|
|
378
|
+
end
|
|
379
|
+
pos += 1
|
|
380
|
+
pos = Json.skip_ws(s, pos)
|
|
381
|
+
if key == target_key
|
|
382
|
+
return pos
|
|
383
|
+
end
|
|
384
|
+
# Skip the value, then the comma (if any).
|
|
385
|
+
pos = Json.skip_value(s, pos)
|
|
386
|
+
pos = Json.skip_ws(s, pos)
|
|
387
|
+
if pos < s.length && s[pos] == ","
|
|
388
|
+
pos += 1
|
|
389
|
+
elsif pos < s.length && s[pos] == "}"
|
|
390
|
+
return -1
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
-1
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
end
|
|
@@ -1,28 +1,33 @@
|
|
|
1
|
-
#
|
|
1
|
+
# VENDORED from OriPekelman/spinelkit @ 09e8558 -- DO NOT EDIT HERE.
|
|
2
|
+
# Edit upstream and re-sync with `make vendor-spinelkit`.
|
|
3
|
+
# SpinelKit::Log -- minimal levelled logger for spinel-AOT'd apps.
|
|
2
4
|
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# WHY THIS EXISTS. CRuby's stdlib `Logger` is metaprogrammed (the severity
|
|
6
|
+
# dispatch loop, the formatter API, the device-rotation logic) and the
|
|
7
|
+
# spinelgems catalog rejects it (unresolved calls). Most app code that wants
|
|
8
|
+
# logging really wants three things: a level guard, a formatted line, and a
|
|
9
|
+
# destination. Ported verbatim from Tep::Logger; toy gains it for free.
|
|
8
10
|
#
|
|
9
11
|
# Surface
|
|
10
12
|
# -------
|
|
11
|
-
#
|
|
12
|
-
# logger = Tep::Logger.new
|
|
13
|
+
# logger = SpinelKit::Log.new
|
|
13
14
|
# logger.set_level("info") # one of: debug / info / warn / error
|
|
14
15
|
# logger.info("server up on " + port.to_s)
|
|
15
16
|
# logger.error("db connect failed")
|
|
16
17
|
#
|
|
17
18
|
# # File output: appends to the path. Leave unset for stderr.
|
|
18
|
-
# logger.to_file("/var/log/
|
|
19
|
+
# logger.to_file("/var/log/app.log")
|
|
20
|
+
#
|
|
21
|
+
# Each line is `[<unix_seconds>] [<level>] <message>`. The integer-seconds
|
|
22
|
+
# timestamp is what spinel exposes from `Time.now`; wider strftime support
|
|
23
|
+
# would need a C-shim (defer until callers ask for it).
|
|
19
24
|
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
module
|
|
25
|
-
class
|
|
25
|
+
# SPINEL NAMING DISCIPLINE: the method/param names below are the donor's
|
|
26
|
+
# proven-green spellings; the class-side `level_value` keeps the comparison
|
|
27
|
+
# a pure function so spinel pins its arg type to :str cleanly via a consumer
|
|
28
|
+
# type-seed. See docs/spinel-discipline.md.
|
|
29
|
+
module SpinelKit
|
|
30
|
+
class Log
|
|
26
31
|
attr_accessor :min_level, :file_path
|
|
27
32
|
|
|
28
33
|
def initialize
|
|
@@ -54,12 +59,11 @@ module Tep
|
|
|
54
59
|
end
|
|
55
60
|
|
|
56
61
|
def should_log?(level)
|
|
57
|
-
|
|
62
|
+
Log.level_value(level) >= Log.level_value(@min_level)
|
|
58
63
|
end
|
|
59
64
|
|
|
60
|
-
# Class-side helper so the comparison stays a pure function and
|
|
61
|
-
#
|
|
62
|
-
# tep.rb.
|
|
65
|
+
# Class-side helper so the comparison stays a pure function and spinel
|
|
66
|
+
# pins its arg type to :str cleanly via a consumer-side type-seed.
|
|
63
67
|
def self.level_value(name)
|
|
64
68
|
if name == "debug"
|
|
65
69
|
return 0
|
|
@@ -73,8 +77,8 @@ module Tep
|
|
|
73
77
|
if name == "error"
|
|
74
78
|
return 3
|
|
75
79
|
end
|
|
76
|
-
# Unknown level -- treat as info so misspelled labels don't
|
|
77
|
-
#
|
|
80
|
+
# Unknown level -- treat as info so misspelled labels don't vanish
|
|
81
|
+
# silently.
|
|
78
82
|
1
|
|
79
83
|
end
|
|
80
84
|
|