spinel_kit 0.1.1 → 0.3.0

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.
@@ -1,394 +0,0 @@
1
- # SpinelKit::Json -- Spinel-safe JSON DECODERS (flat-key, top-level only).
2
- #
3
- # The decode half of the codec; encoders are in spinel_kit/json.rb. Split out
4
- # so an encode-only consumer never compiles these walkers (their dead-code
5
- # degradation otherwise widens the encoders' string args to int -- see the
6
- # header of json.rb and docs/spinel-discipline.md).
7
- #
8
- # `get_str(s, key)` finds the entry for `key` in the top-level object literal
9
- # `s` and returns its value as a string. Returns "" when `key` is absent or
10
- # the value isn't a string. Same shape for `get_int`. `has_key?(s, key)`
11
- # returns a boolean independent of value type. The parser is a hand-rolled
12
- # state machine that walks one `{ "k": <value>, ... }` pair at a time,
13
- # skipping over any value (including nested objects / arrays) it doesn't need.
14
- # Strings inside values are honoured for escape sequences so that `\"` doesn't
15
- # terminate the string and corrupt the walk. Decodes the escape sequences
16
- # `SpinelKit::Json.escape` produces.
17
- module SpinelKit
18
- class Json
19
- def self.get_str(s, key)
20
- pos = Json.find_value_start(s, key)
21
- if pos < 0
22
- return ""
23
- end
24
- Json.parse_str_value(s, pos)
25
- end
26
-
27
- def self.get_int(s, key)
28
- pos = Json.find_value_start(s, key)
29
- if pos < 0
30
- return 0
31
- end
32
- Json.parse_int_value(s, pos)
33
- end
34
-
35
- # Decode a JSON number value at `key` -> Float. Accepts both
36
- # integer-literal (`42`) and float-literal (`3.14`, `-0.5`, `1e2`)
37
- # JSON-number syntax; the integer form returns N.0. Missing key or
38
- # malformed value returns 0.0 (consistent with the other getters'
39
- # missing-key defaults).
40
- #
41
- # Implementation: delegates the value-span walking to skip_value (already
42
- # handles all JSON-number syntax + structural-char boundaries), then
43
- # String#to_f on the substring. Inlined rather than factored into a
44
- # parse_float_value helper because spinel's type inference mis-widens `s`
45
- # to int through the indirection. NOTE: that is a value-walk indirection
46
- # concern, NOT the name-collision bug (which was fixed) -- keep it inlined.
47
- def self.get_float(s, key)
48
- pos = Json.find_value_start(s, key)
49
- if pos < 0
50
- return 0.0
51
- end
52
- pos = Json.skip_ws(s, pos)
53
- if pos >= s.length
54
- return 0.0
55
- end
56
- end_pos = Json.skip_value(s, pos)
57
- if end_pos <= pos
58
- return 0.0
59
- end
60
- s[pos, end_pos - pos].to_f
61
- end
62
-
63
- def self.has_key?(s, key)
64
- Json.find_value_start(s, key) >= 0
65
- end
66
-
67
- # Decode a flat JSON array of integers at `key` -> Array[Integer].
68
- # A missing or non-array value yields [] (the typed-empty-array idiom);
69
- # non-int elements are skipped.
70
- def self.get_int_array(s, key)
71
- out = [0]
72
- out.delete_at(0)
73
- pos = Json.find_value_start(s, key)
74
- if pos < 0
75
- return out
76
- end
77
- pos = Json.skip_ws(s, pos)
78
- if pos >= s.length || s[pos] != "["
79
- return out
80
- end
81
- pos += 1
82
- while pos < s.length
83
- pos = Json.skip_ws(s, pos)
84
- if pos >= s.length
85
- return out
86
- end
87
- c = s[pos]
88
- if c == "]"
89
- return out
90
- elsif c == ","
91
- pos += 1
92
- elsif (c >= "0" && c <= "9") || c == "-"
93
- out.push(Json.parse_int_value(s, pos))
94
- # Advance past the number parse_int_value just consumed
95
- # (optional '-' then digits).
96
- if s[pos] == "-"
97
- pos += 1
98
- end
99
- while pos < s.length && s[pos] >= "0" && s[pos] <= "9"
100
- pos += 1
101
- end
102
- else
103
- # Non-int element (string / object / etc.): skip it.
104
- pos = Json.skip_value(s, pos)
105
- end
106
- end
107
- out
108
- end
109
-
110
- # ---- Internal helpers ----
111
-
112
- # Skip whitespace starting at `pos`, return the new position.
113
- def self.skip_ws(s, pos)
114
- while pos < s.length
115
- c = s[pos]
116
- if c == " " || c == "\t" || c == "\n" || c == "\r"
117
- pos += 1
118
- else
119
- return pos
120
- end
121
- end
122
- pos
123
- end
124
-
125
- # Walk a JSON-quoted string starting at `pos` (which must point at the
126
- # opening `"`). Returns the position one past the closing `"`. Returns
127
- # -1 on malformed input.
128
- def self.skip_str(s, pos)
129
- if pos >= s.length || s[pos] != "\""
130
- return -1
131
- end
132
- pos += 1
133
- while pos < s.length
134
- c = s[pos]
135
- if c == "\\"
136
- # Skip the escape and the escaped character. \uXXXX spans 6
137
- # chars total but skipping 2 still keeps us inside the string
138
- # for the rest of the walk -- the remaining 4 hex digits look
139
- # like ordinary string bytes and won't terminate the literal.
140
- pos += 2
141
- elsif c == "\""
142
- return pos + 1
143
- else
144
- pos += 1
145
- end
146
- end
147
- -1
148
- end
149
-
150
- # Walk a JSON value starting at `pos` (which must point at the first
151
- # non-ws char of the value). Returns the position one past the value
152
- # (or the input length on truncation).
153
- def self.skip_value(s, pos)
154
- pos = Json.skip_ws(s, pos)
155
- if pos >= s.length
156
- return pos
157
- end
158
- c = s[pos]
159
- if c == "\""
160
- return Json.skip_str(s, pos)
161
- end
162
- if c == "{" || c == "["
163
- return Json.skip_container(s, pos)
164
- end
165
- # number / true / false / null -- read until the next structural /
166
- # whitespace char.
167
- while pos < s.length
168
- c = s[pos]
169
- if c == "," || c == "}" || c == "]" ||
170
- c == " " || c == "\t" || c == "\n" || c == "\r"
171
- return pos
172
- end
173
- pos += 1
174
- end
175
- pos
176
- end
177
-
178
- # Walk a balanced { ... } or [ ... ] starting at `pos`. Honours string
179
- # literals so that `{` / `}` inside a value-string don't confuse the
180
- # brace counter. Returns position one past the matching closer.
181
- def self.skip_container(s, pos)
182
- open_c = s[pos]
183
- close_c = open_c == "{" ? "}" : "]"
184
- depth = 1
185
- pos += 1
186
- while pos < s.length && depth > 0
187
- c = s[pos]
188
- if c == "\""
189
- # whole nested string -- skip past it
190
- npos = Json.skip_str(s, pos)
191
- if npos < 0
192
- return s.length
193
- end
194
- pos = npos
195
- elsif c == open_c
196
- depth += 1
197
- pos += 1
198
- elsif c == close_c
199
- depth -= 1
200
- pos += 1
201
- else
202
- pos += 1
203
- end
204
- end
205
- pos
206
- end
207
-
208
- # Read a JSON-quoted string at `pos` and return its decoded contents
209
- # (no surrounding quotes). Decodes the same escape sequences that
210
- # `escape` produces. Returns "" on malformed input.
211
- def self.parse_str_value(s, pos)
212
- pos = Json.skip_ws(s, pos)
213
- if pos >= s.length || s[pos] != "\""
214
- return ""
215
- end
216
- pos += 1
217
- out = ""
218
- while pos < s.length
219
- c = s[pos]
220
- if c == "\""
221
- return out
222
- end
223
- if c == "\\"
224
- if pos + 1 >= s.length
225
- return out
226
- end
227
- esc = s[pos + 1]
228
- if esc == "\""
229
- out = out + "\""
230
- elsif esc == "\\"
231
- out = out + "\\"
232
- elsif esc == "/"
233
- out = out + "/"
234
- elsif esc == "n"
235
- out = out + "\n"
236
- elsif esc == "r"
237
- out = out + "\r"
238
- elsif esc == "t"
239
- out = out + "\t"
240
- elsif esc == "b"
241
- out = out + "\b"
242
- elsif esc == "f"
243
- out = out + "\f"
244
- elsif esc == "u"
245
- # \u00XX -> map the two-digit hex back to a byte. Wider
246
- # codepoints (U+0100+ or surrogate pairs) aren't decoded; the
247
- # byte we emit is the low byte of the codepoint, which
248
- # round-trips ASCII at minimum.
249
- if pos + 5 < s.length
250
- h1 = Json.hex_nibble(s[pos + 4])
251
- h2 = Json.hex_nibble(s[pos + 5])
252
- if h1 >= 0 && h2 >= 0
253
- # rebuild the byte and push it -- spinel strings are
254
- # byte-blobs, so this works for ASCII; for non-ASCII the
255
- # original encoder would have used a passthrough byte
256
- # anyway.
257
- b = h1 * 16 + h2
258
- out = out + Json.byte_to_chr(b)
259
- pos += 6
260
- next
261
- end
262
- end
263
- out = out + "?"
264
- pos += 2
265
- next
266
- else
267
- out = out + esc
268
- end
269
- pos += 2
270
- else
271
- out = out + c
272
- pos += 1
273
- end
274
- end
275
- out
276
- end
277
-
278
- def self.hex_nibble(c)
279
- if c >= "0" && c <= "9"
280
- return c.getbyte(0) - "0".getbyte(0)
281
- end
282
- if c >= "a" && c <= "f"
283
- return c.getbyte(0) - "a".getbyte(0) + 10
284
- end
285
- if c >= "A" && c <= "F"
286
- return c.getbyte(0) - "A".getbyte(0) + 10
287
- end
288
- -1
289
- end
290
-
291
- # Build a single-byte string from an integer 0..255. Spinel doesn't
292
- # expose `n.chr` for arbitrary bytes uniformly; the table covers the
293
- # ASCII printable range and falls back to "?" for anything else (the
294
- # JSON encoder side never produces non-ASCII via \u, so the fallback
295
- # is reachable only for malformed input).
296
- def self.byte_to_chr(n)
297
- printable = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
298
- if n >= 32 && n < 127
299
- return printable[n - 32, 1]
300
- end
301
- if n == 9
302
- return "\t"
303
- end
304
- if n == 10
305
- return "\n"
306
- end
307
- if n == 13
308
- return "\r"
309
- end
310
- "?"
311
- end
312
-
313
- # Read an integer at `pos`. Accepts an optional leading `-`. Returns 0
314
- # on no-digit / non-numeric input (caller can use `has_key?` first if
315
- # 0-vs-absent matters).
316
- def self.parse_int_value(s, pos)
317
- pos = Json.skip_ws(s, pos)
318
- if pos >= s.length
319
- return 0
320
- end
321
- neg = false
322
- if s[pos] == "-"
323
- neg = true
324
- pos += 1
325
- end
326
- n = 0
327
- saw_digit = false
328
- while pos < s.length
329
- c = s[pos]
330
- if c >= "0" && c <= "9"
331
- n = n * 10 + (c.getbyte(0) - "0".getbyte(0))
332
- saw_digit = true
333
- pos += 1
334
- else
335
- break
336
- end
337
- end
338
- if !saw_digit
339
- return 0
340
- end
341
- neg ? -n : n
342
- end
343
-
344
- # Walk the top-level object looking for the entry whose key matches
345
- # `target_key`; return the position of the value's first non-ws
346
- # character. Returns -1 if not found.
347
- def self.find_value_start(s, target_key)
348
- pos = Json.skip_ws(s, 0)
349
- if pos >= s.length || s[pos] != "{"
350
- return -1
351
- end
352
- pos += 1
353
- while pos < s.length
354
- pos = Json.skip_ws(s, pos)
355
- if pos >= s.length
356
- return -1
357
- end
358
- if s[pos] == "}"
359
- return -1
360
- end
361
- # Read a key.
362
- if s[pos] != "\""
363
- return -1
364
- end
365
- key_start = pos
366
- pos = Json.skip_str(s, pos)
367
- if pos < 0
368
- return -1
369
- end
370
- # Decode the key for comparison (handles \" inside keys).
371
- key = Json.parse_str_value(s, key_start)
372
- # Skip ws, ":".
373
- pos = Json.skip_ws(s, pos)
374
- if pos >= s.length || s[pos] != ":"
375
- return -1
376
- end
377
- pos += 1
378
- pos = Json.skip_ws(s, pos)
379
- if key == target_key
380
- return pos
381
- end
382
- # Skip the value, then the comma (if any).
383
- pos = Json.skip_value(s, pos)
384
- pos = Json.skip_ws(s, pos)
385
- if pos < s.length && s[pos] == ","
386
- pos += 1
387
- elsif pos < s.length && s[pos] == "}"
388
- return -1
389
- end
390
- end
391
- -1
392
- end
393
- end
394
- end
@@ -1,6 +0,0 @@
1
- # SpinelKit version. Kept in its own file so the gemspec can read it
2
- # without loading the rest of the library (the json/git/log modules pull
3
- # in no deps, but this matches the toy/tep convention exactly).
4
- module SpinelKit
5
- VERSION = "0.1.1"
6
- end
data/lib/spinel_kit.rb DELETED
@@ -1,39 +0,0 @@
1
- # SpinelKit -- the Spinel stdlib-surface gem.
2
- #
3
- # A pure-Ruby, Spinel-safe toolkit holding the generic "stdlib substitute"
4
- # shims that every Spinel-compiled project would otherwise hand-roll. Spinel
5
- # (the Ruby->native AOT compiler) cannot lower large chunks of CRuby stdlib
6
- # -- the `json` gem's C-ext fast path and metaprogrammed pure-Ruby fallback,
7
- # stdlib `Logger`, C-ext git bindings -- and the spinelgems compatibility
8
- # catalog confirms there is no verified gem to reuse for any of them. So this
9
- # gem consolidates the shims toy and tep each grew independently:
10
- #
11
- # SpinelKit::Json -- JSON encoders (json.rb) + flat-key decoders
12
- # (json_decoder.rb).
13
- # SpinelKit::Json::Builder -- incremental ordered-object builder.
14
- # SpinelKit::Git -- git provenance from .git/HEAD (was Toy::Git).
15
- # SpinelKit::Log -- minimal levelled logger (was Tep::Logger).
16
- #
17
- # MINIMAL-SURFACE REQUIRING. This umbrella requires everything for
18
- # convenience (and for CRuby use). But because Spinel compiles every loaded
19
- # method with no tree-shaking, a Spinel-compiled consumer should require ONLY
20
- # the file(s) it uses, to avoid compiling -- and degrading -- code it never
21
- # calls:
22
- #
23
- # require "spinel_kit/json" # encoders
24
- # require "spinel_kit/json_decoder" # decoders (require alongside json if you decode)
25
- # require "spinel_kit/json_builder" # builder
26
- # require "spinel_kit/git"
27
- # require "spinel_kit/log"
28
- #
29
- # No native extension (spinel-ext.json is []), no runtime dependencies. See
30
- # docs/adoption.md and docs/spinel-discipline.md.
31
- require_relative "spinel_kit/version"
32
- require_relative "spinel_kit/json"
33
- require_relative "spinel_kit/json_decoder"
34
- require_relative "spinel_kit/json_builder"
35
- require_relative "spinel_kit/git"
36
- require_relative "spinel_kit/log"
37
-
38
- module SpinelKit
39
- end
@@ -1,15 +0,0 @@
1
- # Advisory RBS for SpinelKit::Json encoders (lib/spinel_kit/json.rb).
2
- # Decoders are in json_decoder.rbs; the builder in json_builder.rbs.
3
- module SpinelKit
4
- class Json
5
- def self.escape: (String s) -> String
6
- def self.hex2: (Integer n) -> String
7
- def self.quote: (String s) -> String
8
- def self.encode_pair_str: (String k, String v) -> String
9
- def self.encode_pair_int: (String k, Integer v) -> String
10
- def self.from_str_hash: (Hash[String, String] h) -> String
11
- def self.from_int_hash: (Hash[String, Integer] h) -> String
12
- def self.from_str_array: (Array[String] a) -> String
13
- def self.from_int_array: (Array[Integer] a) -> String
14
- end
15
- end
@@ -1,19 +0,0 @@
1
- module SpinelKit
2
- class Json
3
- class Builder
4
- def initialize: () -> void
5
- def add_str: (String key, String value) -> String
6
- def add_num: (String key, untyped value) -> String
7
- def add_bool: (String key, bool value) -> String
8
- def add_raw: (String key, String raw) -> String
9
- def add_obj: (String key, SpinelKit::Json::Builder child) -> String
10
- def dump: () -> String
11
- def comma: () -> void
12
-
13
- # self-contained escapers (byte-identical to SpinelKit::Json.*)
14
- def self.quote: (String s) -> String
15
- def self.escape: (String s) -> String
16
- def self.hex2: (Integer n) -> String
17
- end
18
- end
19
- end
@@ -1,21 +0,0 @@
1
- # Advisory RBS for SpinelKit::Json decoders (lib/spinel_kit/json_decoder.rb).
2
- module SpinelKit
3
- class Json
4
- def self.get_str: (String s, String key) -> String
5
- def self.get_int: (String s, String key) -> Integer
6
- def self.get_float: (String s, String key) -> Float
7
- def self.get_int_array: (String s, String key) -> Array[Integer]
8
- def self.has_key?: (String s, String key) -> bool
9
-
10
- # internal walkers
11
- def self.skip_ws: (String s, Integer pos) -> Integer
12
- def self.skip_str: (String s, Integer pos) -> Integer
13
- def self.skip_value: (String s, Integer pos) -> Integer
14
- def self.skip_container: (String s, Integer pos) -> Integer
15
- def self.parse_str_value: (String s, Integer pos) -> String
16
- def self.hex_nibble: (String c) -> Integer
17
- def self.byte_to_chr: (Integer n) -> String
18
- def self.parse_int_value: (String s, Integer pos) -> Integer
19
- def self.find_value_start: (String s, String target_key) -> Integer
20
- end
21
- end
File without changes
File without changes