spinel_kit 0.1.1 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +1 -0
- data/lib/spinel_kit/hex.rb +63 -0
- data/lib/spinel_kit/url.rb +164 -0
- data/lib/spinel_kit/version.rb +1 -1
- data/lib/spinel_kit.rb +6 -0
- data/sig/spinel_kit/hex.rbs +8 -0
- data/sig/spinel_kit/url.rbs +9 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a45aac9b6f91813e7d837ea8878d4dcee1731b5582f4a42e5fae4f73e9f764ce
|
|
4
|
+
data.tar.gz: f201acc7e5c64ba8f5666e4c9c454440c7d1cc6835a30dd8adfb07442b1b42e8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5a6f770a93f9975455637ed2fb2ffd8874dcc69d1e6fdf8547b7120c13ea341f2154a0427f2bf317851d0cb86072232649f6e1b011354c8ab68760f120a4bc2e
|
|
7
|
+
data.tar.gz: 45d722737fce1cce7d606765af43f405fa6f34c96fa8a0167c8794262dee1ffebaa034600d865399577aaf9e826491059f338a8e688502bfd52b00c1071157b3
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to SpinelKit are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.2.0] - 2026-06-08
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **`SpinelKit::Hex`** (`lib/spinel_kit/hex.rb`) — hex digit/byte codec:
|
|
9
|
+
`nibble` (hex char → 0..15), `nibble_char` (0..15 → uppercase hex char),
|
|
10
|
+
`byte2` (byte → two lowercase hex chars), `to_int` (leading hex digits →
|
|
11
|
+
int). Consolidates a `hex_nibble` that was **byte-identical** across
|
|
12
|
+
`Tep::Url`, `Tep::Llm`, and `SpinelKit::Json`'s decoder.
|
|
13
|
+
- **`SpinelKit::Url`** (`lib/spinel_kit/url.rb`) — the `CGI`/`URI`-component
|
|
14
|
+
surface Spinel can't get from stdlib: `escape`/`unescape` (RFC 3986
|
|
15
|
+
percent-codec), `parse_query` (form-urlencoded → Hash), `split_url`
|
|
16
|
+
(scheme/host/port/path/query). Ported from `Tep::Url`; uses `SpinelKit::Hex`
|
|
17
|
+
and is self-contained (inline typed-hash seeding + substring search).
|
|
18
|
+
|
|
19
|
+
`SpinelKit::Json` keeps its own private `hex2`/`hex_nibble` so a JSON-only
|
|
20
|
+
consumer never compiles `Hex` (Spinel has no tree-shaking) — the small
|
|
21
|
+
duplication is the surface-isolation cost.
|
|
22
|
+
|
|
5
23
|
## [0.1.1] - 2026-06-08
|
|
6
24
|
|
|
7
25
|
### Fixed
|
data/README.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# SpinelKit
|
|
2
2
|
|
|
3
|
+
[](https://github.com/OriPekelman/spinelkit/actions/workflows/ci.yml)
|
|
3
4
|
[](https://rubygems.org/gems/spinel_kit)
|
|
4
5
|
[](LICENSE)
|
|
5
6
|

|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# SpinelKit::Hex -- hex digit/byte encode + decode: the pieces every Spinel
|
|
2
|
+
# project re-rolls. The decode nibble appeared BYTE-IDENTICAL in Tep::Url,
|
|
3
|
+
# inside SpinelKit::Json's string decoder, and (as a multi-digit variant) in
|
|
4
|
+
# Tep::Llm's chunked-transfer size parser. Pure string/byte ops, Spinel-safe.
|
|
5
|
+
#
|
|
6
|
+
# NOTE on the Json overlap: SpinelKit::Json keeps its OWN private `hex2`/
|
|
7
|
+
# `hex_nibble` so a JSON-only consumer never compiles this file (Spinel has no
|
|
8
|
+
# tree-shaking — see json.rb). Hex is the shared surface for everyone else,
|
|
9
|
+
# e.g. SpinelKit::Url.
|
|
10
|
+
module SpinelKit
|
|
11
|
+
class Hex
|
|
12
|
+
# Hex digit char -> int 0..15, or -1 if not a hex digit (upper or lower).
|
|
13
|
+
def self.nibble(c)
|
|
14
|
+
if c >= "0" && c <= "9"
|
|
15
|
+
return c.getbyte(0) - "0".getbyte(0)
|
|
16
|
+
end
|
|
17
|
+
if c >= "a" && c <= "f"
|
|
18
|
+
return c.getbyte(0) - "a".getbyte(0) + 10
|
|
19
|
+
end
|
|
20
|
+
if c >= "A" && c <= "F"
|
|
21
|
+
return c.getbyte(0) - "A".getbyte(0) + 10
|
|
22
|
+
end
|
|
23
|
+
-1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Int nibble 0..15 -> single UPPERCASE hex char ("0".."9","A".."F").
|
|
27
|
+
# (RFC 3986 percent-encoding uses uppercase.)
|
|
28
|
+
def self.nibble_char(n)
|
|
29
|
+
if n < 10
|
|
30
|
+
return ("0".getbyte(0) + n).chr
|
|
31
|
+
end
|
|
32
|
+
("A".getbyte(0) + n - 10).chr
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Int byte 0..255 -> two-char LOWERCASE hex (15 -> "0f"). (JSON \u00XX
|
|
36
|
+
# and similar use lowercase.)
|
|
37
|
+
def self.byte2(n)
|
|
38
|
+
hex = "0123456789abcdef"
|
|
39
|
+
out = ""
|
|
40
|
+
out = out + hex[(n / 16) % 16, 1]
|
|
41
|
+
out = out + hex[n % 16, 1]
|
|
42
|
+
out
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Parse the leading hex digits of `s` -> int ("1a3" -> 419). Stops at the
|
|
46
|
+
# first non-hex char; returns 0 if there is no leading hex digit. Useful
|
|
47
|
+
# for chunked-transfer sizes and the like.
|
|
48
|
+
def self.to_int(s)
|
|
49
|
+
n = 0
|
|
50
|
+
i = 0
|
|
51
|
+
len = s.length
|
|
52
|
+
while i < len
|
|
53
|
+
v = Hex.nibble(s[i])
|
|
54
|
+
if v < 0
|
|
55
|
+
return n
|
|
56
|
+
end
|
|
57
|
+
n = n * 16 + v
|
|
58
|
+
i += 1
|
|
59
|
+
end
|
|
60
|
+
n
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
require_relative "hex"
|
|
2
|
+
|
|
3
|
+
# SpinelKit::Url -- percent-encode/decode (the CGI / URI-component surface
|
|
4
|
+
# Spinel can't get from stdlib) plus a form-query parser and a small URL
|
|
5
|
+
# splitter. Ported from Tep::Url; the hex digits now come from SpinelKit::Hex.
|
|
6
|
+
#
|
|
7
|
+
# Self-contained: the empty str=>str hashes are seeded inline (the
|
|
8
|
+
# `{"" => ""}`-then-delete idiom that pins Spinel's value type), and substring
|
|
9
|
+
# search is a private `find_idx` (the `< 0` callsites can't narrow against
|
|
10
|
+
# String#index's int|nil under Spinel's current model). All pure string ops.
|
|
11
|
+
module SpinelKit
|
|
12
|
+
class Url
|
|
13
|
+
# "%41+b" -> "A b" (form-decode: `+` is space, `%XX` is a byte).
|
|
14
|
+
def self.unescape(s)
|
|
15
|
+
out = ""
|
|
16
|
+
i = 0
|
|
17
|
+
n = s.length
|
|
18
|
+
while i < n
|
|
19
|
+
c = s[i]
|
|
20
|
+
if c == "+"
|
|
21
|
+
out = out + " "
|
|
22
|
+
i += 1
|
|
23
|
+
elsif c == "%" && i + 2 < n
|
|
24
|
+
hi = Hex.nibble(s[i + 1])
|
|
25
|
+
lo = Hex.nibble(s[i + 2])
|
|
26
|
+
if hi >= 0 && lo >= 0
|
|
27
|
+
out = out + ((hi * 16 + lo).chr)
|
|
28
|
+
i += 3
|
|
29
|
+
else
|
|
30
|
+
out = out + c
|
|
31
|
+
i += 1
|
|
32
|
+
end
|
|
33
|
+
else
|
|
34
|
+
out = out + c
|
|
35
|
+
i += 1
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
out
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Percent-encode everything outside the RFC 3986 unreserved set
|
|
42
|
+
# (ALPHA / DIGIT / `-._~`); the rest becomes `%XX` with UPPERCASE hex.
|
|
43
|
+
# (Space -> `%20`, not `+` -- this is the URI-component, not form, encoder.)
|
|
44
|
+
#
|
|
45
|
+
# Byte-oriented: under Spinel `String#[]` indexes BYTES, so a multi-byte
|
|
46
|
+
# UTF-8 char is encoded byte-by-byte (correct %XX of each byte). Under CRuby,
|
|
47
|
+
# pass a binary string for the same behaviour (else `[]` splits on chars).
|
|
48
|
+
def self.escape(s)
|
|
49
|
+
out = ""
|
|
50
|
+
i = 0
|
|
51
|
+
while i < s.length
|
|
52
|
+
c = s[i]
|
|
53
|
+
if (c >= "a" && c <= "z") || (c >= "A" && c <= "Z") ||
|
|
54
|
+
(c >= "0" && c <= "9") || c == "-" || c == "." ||
|
|
55
|
+
c == "_" || c == "~"
|
|
56
|
+
out = out + c
|
|
57
|
+
else
|
|
58
|
+
b = c.getbyte(0)
|
|
59
|
+
out = out + "%" + Hex.nibble_char(b / 16) + Hex.nibble_char(b % 16)
|
|
60
|
+
end
|
|
61
|
+
i += 1
|
|
62
|
+
end
|
|
63
|
+
out
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# "a=1&b=2&c" -> {"a"=>"1","b"=>"2","c"=>""}. Keys + values are
|
|
67
|
+
# form-decoded (`unescape`).
|
|
68
|
+
def self.parse_query(s)
|
|
69
|
+
h = {"" => ""}
|
|
70
|
+
h.delete("")
|
|
71
|
+
if s.length == 0
|
|
72
|
+
return h
|
|
73
|
+
end
|
|
74
|
+
pairs = s.split("&")
|
|
75
|
+
pairs.each do |pair|
|
|
76
|
+
if pair.length > 0
|
|
77
|
+
eq = Url.find_idx(pair, "=", 0)
|
|
78
|
+
if eq < 0
|
|
79
|
+
h[Url.unescape(pair)] = ""
|
|
80
|
+
else
|
|
81
|
+
k = pair[0, eq]
|
|
82
|
+
v = pair[eq + 1, pair.length - eq - 1]
|
|
83
|
+
h[Url.unescape(k)] = Url.unescape(v)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
h
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Split `http(s)://host[:port]/path?query` into a str=>str hash keyed
|
|
91
|
+
# scheme / host / port / path / query. Without a scheme the input is
|
|
92
|
+
# treated as a path (host stays empty). Default ports follow the scheme
|
|
93
|
+
# (80 / 443); `query` is the raw substring after `?` (not decoded).
|
|
94
|
+
#
|
|
95
|
+
# One body on purpose: Spinel widens a Hash-typed value when a helper
|
|
96
|
+
# mutates it and the caller keeps reading, so `out` stays StrStrHash only
|
|
97
|
+
# if nothing factors the mutation out (find_idx returns an int, no mutate).
|
|
98
|
+
def self.split_url(u)
|
|
99
|
+
out = {"" => ""}
|
|
100
|
+
out.delete("")
|
|
101
|
+
out["scheme"] = ""
|
|
102
|
+
out["host"] = ""
|
|
103
|
+
out["port"] = ""
|
|
104
|
+
out["path"] = "/"
|
|
105
|
+
out["query"] = ""
|
|
106
|
+
|
|
107
|
+
rest = u
|
|
108
|
+
if rest.length >= 7 && rest[0, 7] == "http://"
|
|
109
|
+
out["scheme"] = "http"
|
|
110
|
+
out["port"] = "80"
|
|
111
|
+
rest = rest[7, rest.length - 7]
|
|
112
|
+
elsif rest.length >= 8 && rest[0, 8] == "https://"
|
|
113
|
+
out["scheme"] = "https"
|
|
114
|
+
out["port"] = "443"
|
|
115
|
+
rest = rest[8, rest.length - 8]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
if out["scheme"].length > 0
|
|
119
|
+
slash = Url.find_idx(rest, "/", 0)
|
|
120
|
+
hostport = rest
|
|
121
|
+
tail = "/"
|
|
122
|
+
if slash >= 0
|
|
123
|
+
hostport = rest[0, slash]
|
|
124
|
+
tail = rest[slash, rest.length - slash]
|
|
125
|
+
end
|
|
126
|
+
colon = Url.find_idx(hostport, ":", 0)
|
|
127
|
+
if colon >= 0
|
|
128
|
+
out["host"] = hostport[0, colon]
|
|
129
|
+
out["port"] = hostport[colon + 1, hostport.length - colon - 1]
|
|
130
|
+
else
|
|
131
|
+
out["host"] = hostport
|
|
132
|
+
end
|
|
133
|
+
rest = tail
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
qi = Url.find_idx(rest, "?", 0)
|
|
137
|
+
if qi >= 0
|
|
138
|
+
out["path"] = rest[0, qi]
|
|
139
|
+
out["query"] = rest[qi + 1, rest.length - qi - 1]
|
|
140
|
+
else
|
|
141
|
+
out["path"] = rest
|
|
142
|
+
end
|
|
143
|
+
if out["path"].length == 0
|
|
144
|
+
out["path"] = "/"
|
|
145
|
+
end
|
|
146
|
+
out
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# First index of `needle` in `s` at/after `start`, or -1. Internal
|
|
150
|
+
# (Spinel-safe substring search; see the module comment).
|
|
151
|
+
def self.find_idx(s, needle, start)
|
|
152
|
+
nlen = needle.length
|
|
153
|
+
slen = s.length
|
|
154
|
+
pos = start
|
|
155
|
+
while pos <= slen - nlen
|
|
156
|
+
if s[pos, nlen] == needle
|
|
157
|
+
return pos
|
|
158
|
+
end
|
|
159
|
+
pos += 1
|
|
160
|
+
end
|
|
161
|
+
-1
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
data/lib/spinel_kit/version.rb
CHANGED
data/lib/spinel_kit.rb
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
# SpinelKit::Json -- JSON encoders (json.rb) + flat-key decoders
|
|
12
12
|
# (json_decoder.rb).
|
|
13
13
|
# SpinelKit::Json::Builder -- incremental ordered-object builder.
|
|
14
|
+
# SpinelKit::Hex -- hex digit/byte encode + decode.
|
|
15
|
+
# SpinelKit::Url -- percent-encode/decode + form-query + URL split.
|
|
14
16
|
# SpinelKit::Git -- git provenance from .git/HEAD (was Toy::Git).
|
|
15
17
|
# SpinelKit::Log -- minimal levelled logger (was Tep::Logger).
|
|
16
18
|
#
|
|
@@ -23,6 +25,8 @@
|
|
|
23
25
|
# require "spinel_kit/json" # encoders
|
|
24
26
|
# require "spinel_kit/json_decoder" # decoders (require alongside json if you decode)
|
|
25
27
|
# require "spinel_kit/json_builder" # builder
|
|
28
|
+
# require "spinel_kit/hex" # hex digit/byte codec
|
|
29
|
+
# require "spinel_kit/url" # percent-codec + query (pulls in hex)
|
|
26
30
|
# require "spinel_kit/git"
|
|
27
31
|
# require "spinel_kit/log"
|
|
28
32
|
#
|
|
@@ -32,6 +36,8 @@ require_relative "spinel_kit/version"
|
|
|
32
36
|
require_relative "spinel_kit/json"
|
|
33
37
|
require_relative "spinel_kit/json_decoder"
|
|
34
38
|
require_relative "spinel_kit/json_builder"
|
|
39
|
+
require_relative "spinel_kit/hex"
|
|
40
|
+
require_relative "spinel_kit/url"
|
|
35
41
|
require_relative "spinel_kit/git"
|
|
36
42
|
require_relative "spinel_kit/log"
|
|
37
43
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
module SpinelKit
|
|
2
|
+
class Hex
|
|
3
|
+
def self.nibble: (String c) -> Integer # hex digit -> 0..15, or -1
|
|
4
|
+
def self.nibble_char: (Integer n) -> String # 0..15 -> "0".."9","A".."F"
|
|
5
|
+
def self.byte2: (Integer n) -> String # 0..255 -> two lowercase hex chars
|
|
6
|
+
def self.to_int: (String s) -> Integer # leading hex digits -> int
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module SpinelKit
|
|
2
|
+
class Url
|
|
3
|
+
def self.unescape: (String s) -> String
|
|
4
|
+
def self.escape: (String s) -> String
|
|
5
|
+
def self.parse_query: (String s) -> Hash[String, String]
|
|
6
|
+
def self.split_url: (String u) -> Hash[String, String]
|
|
7
|
+
def self.find_idx: (String s, String needle, Integer start) -> Integer
|
|
8
|
+
end
|
|
9
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spinel_kit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ori Pekelman
|
|
@@ -33,16 +33,20 @@ files:
|
|
|
33
33
|
- docs/spinelgems-issues.md
|
|
34
34
|
- lib/spinel_kit.rb
|
|
35
35
|
- lib/spinel_kit/git.rb
|
|
36
|
+
- lib/spinel_kit/hex.rb
|
|
36
37
|
- lib/spinel_kit/json.rb
|
|
37
38
|
- lib/spinel_kit/json_builder.rb
|
|
38
39
|
- lib/spinel_kit/json_decoder.rb
|
|
39
40
|
- lib/spinel_kit/log.rb
|
|
41
|
+
- lib/spinel_kit/url.rb
|
|
40
42
|
- lib/spinel_kit/version.rb
|
|
41
43
|
- sig/spinel_kit/git.rbs
|
|
44
|
+
- sig/spinel_kit/hex.rbs
|
|
42
45
|
- sig/spinel_kit/json.rbs
|
|
43
46
|
- sig/spinel_kit/json_builder.rbs
|
|
44
47
|
- sig/spinel_kit/json_decoder.rbs
|
|
45
48
|
- sig/spinel_kit/log.rbs
|
|
49
|
+
- sig/spinel_kit/url.rbs
|
|
46
50
|
- sig/spinel_kit/version.rbs
|
|
47
51
|
- spinel-ext.json
|
|
48
52
|
homepage: https://github.com/OriPekelman/spinelkit
|