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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a199ba3cb14b31a8bb59a0f05df22e0bb982a1c4862b315236feb05278e653c7
4
- data.tar.gz: bad3e3859f7647ae5f80387a5b97a5abdfd34b3154d85761a3d02084e0b7c346
3
+ metadata.gz: a45aac9b6f91813e7d837ea8878d4dcee1731b5582f4a42e5fae4f73e9f764ce
4
+ data.tar.gz: f201acc7e5c64ba8f5666e4c9c454440c7d1cc6835a30dd8adfb07442b1b42e8
5
5
  SHA512:
6
- metadata.gz: affc333ba8f7aad5487cef5d38ec5e5c18fb0c73b3fade57489fa25bd9a3f14f5fbf66b56d6083cb5081555a0c36706b45d9d7166f206133b7eba8301eee5876
7
- data.tar.gz: 29e1deb16e19017e12c94c40ad56af0bdde7361b166c7f51b727139fd98758496361006ea77934254041a70626d9d8db0d116a1080f454a2d12d24a04a7ed67b
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
+ [![CI](https://github.com/OriPekelman/spinelkit/actions/workflows/ci.yml/badge.svg)](https://github.com/OriPekelman/spinelkit/actions/workflows/ci.yml)
3
4
  [![Gem Version](https://img.shields.io/gem/v/spinel_kit)](https://rubygems.org/gems/spinel_kit)
4
5
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
5
6
  ![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.2-CC342D)
@@ -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
@@ -2,5 +2,5 @@
2
2
  # without loading the rest of the library (the json/git/log modules pull
3
3
  # in no deps, but this matches the toy/tep convention exactly).
4
4
  module SpinelKit
5
- VERSION = "0.1.1"
5
+ VERSION = "0.2.0"
6
6
  end
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.1.1
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