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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a199ba3cb14b31a8bb59a0f05df22e0bb982a1c4862b315236feb05278e653c7
4
- data.tar.gz: bad3e3859f7647ae5f80387a5b97a5abdfd34b3154d85761a3d02084e0b7c346
3
+ metadata.gz: fca4df06c4d57130a5053954a6429beac7d6ce0d6f6d1ee26194753aea11732c
4
+ data.tar.gz: 496c1116e14068768cb482723d1be6c6daebfc26f45d9972da3ab36d572eae69
5
5
  SHA512:
6
- metadata.gz: affc333ba8f7aad5487cef5d38ec5e5c18fb0c73b3fade57489fa25bd9a3f14f5fbf66b56d6083cb5081555a0c36706b45d9d7166f206133b7eba8301eee5876
7
- data.tar.gz: 29e1deb16e19017e12c94c40ad56af0bdde7361b166c7f51b727139fd98758496361006ea77934254041a70626d9d8db0d116a1080f454a2d12d24a04a7ed67b
6
+ metadata.gz: 1d15a4186fd29c584767e64c7f015b6424ce72bae04d39b283529f48478923075662bc2ecdc5e0ac03a73c51bcc344a9140cc775123a33c02e2dc76f9860cf73
7
+ data.tar.gz: 4d0ee98f92b44fa1e1345efdac6378626c31ba11c42cf0a6be12ab065f9e9854799eec16edb363da670b01eacd8421623631d2259c296f66f8b9892cd6f2eaa5
data/CHANGELOG.md CHANGED
@@ -2,6 +2,57 @@
2
2
 
3
3
  All notable changes to SpinelKit are documented here.
4
4
 
5
+ ## [0.3.0] - 2026-07-09
6
+
7
+ ### Changed
8
+ - **Converted to a spin package** (spin is Spinel's package system;
9
+ [#3](https://github.com/OriPekelman/spinelkit/issues/3)). The package root
10
+ is now the require root: `lib/spinel_kit{,.rb}` moved to the repo top level,
11
+ `spin.toml` carries the identity (`name = "spinel_kit"`), and the gem
12
+ mirrors the layout with `require_paths = ["."]` — one tree serves both
13
+ consumers. Tests split by oracle: `test/*.rb` are compiled `spin test`
14
+ snapshot programs with committed `.expected` files (verified byte-identical
15
+ under CRuby and under spinel master `f9a81b1d`, 4/4); the CRuby minitest
16
+ parity suite moved to `test/parity/`.
17
+
18
+ ### Removed
19
+ - **`SpinelKit::Json` and `SpinelKit::Json::Builder`** (`lib/spinel_kit/json.rb`,
20
+ `json_decoder.rb`, `json_builder.rb`, their `sig/` RBS and the parity test).
21
+ Spinel now bundles `json` as a require-gated stdlib package — a typed native
22
+ binding (`native_func :parse/:generate/:dump`) into the runtime's
23
+ `sp_json.c` — so the compiler provides the stdlib surface SpinelKit was
24
+ shimming ([#3](https://github.com/OriPekelman/spinelkit/issues/3)).
25
+ Verified on spinel master `f9a81b1d` with a tep-shaped consumer under
26
+ `SPINEL_REQUIRE_GATE=1`: byte-identical CRuby parity and **no
27
+ cross-degrade**; values from `JSON.parse` must be coerced at the boundary
28
+ (`.to_s`/`.to_i`/`.to_f`/`.map { |x| x.to_i }`) to stay fully typed —
29
+ without coercion, direct sinks widen to `untyped` (slow path, still
30
+ correct output).
31
+
32
+ **Migration:** `require "json"`; `JSON.parse(s)["k"].to_i` replaces
33
+ `SpinelKit::Json.get_int(s, "k")` (same shape for `get_str`/`get_float`/
34
+ `get_int_array`); `h.key?("k")` replaces `has_key?(s, "k")`;
35
+ `JSON.generate(hash)` replaces `encode_pair_*`/`from_*` and the Builder
36
+ (Ruby hashes are insertion-ordered; `native_obj_reflect` covers Structs).
37
+
38
+ ## [0.2.0] - 2026-06-08
39
+
40
+ ### Added
41
+ - **`SpinelKit::Hex`** (`lib/spinel_kit/hex.rb`) — hex digit/byte codec:
42
+ `nibble` (hex char → 0..15), `nibble_char` (0..15 → uppercase hex char),
43
+ `byte2` (byte → two lowercase hex chars), `to_int` (leading hex digits →
44
+ int). Consolidates a `hex_nibble` that was **byte-identical** across
45
+ `Tep::Url`, `Tep::Llm`, and `SpinelKit::Json`'s decoder.
46
+ - **`SpinelKit::Url`** (`lib/spinel_kit/url.rb`) — the `CGI`/`URI`-component
47
+ surface Spinel can't get from stdlib: `escape`/`unescape` (RFC 3986
48
+ percent-codec), `parse_query` (form-urlencoded → Hash), `split_url`
49
+ (scheme/host/port/path/query). Ported from `Tep::Url`; uses `SpinelKit::Hex`
50
+ and is self-contained (inline typed-hash seeding + substring search).
51
+
52
+ `SpinelKit::Json` keeps its own private `hex2`/`hex_nibble` so a JSON-only
53
+ consumer never compiles `Hex` (Spinel has no tree-shaking) — the small
54
+ duplication is the surface-isolation cost.
55
+
5
56
  ## [0.1.1] - 2026-06-08
6
57
 
7
58
  ### Fixed
data/README.md CHANGED
@@ -1,22 +1,43 @@
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)
6
7
  ![Pure Ruby](https://img.shields.io/badge/native%20ext-none-brightgreen)
7
8
 
8
- **The Spinel stdlib-surface gem.** A pure-Ruby, Spinel-safe toolkit holding
9
- the generic "stdlib substitute" shims that every Spinel-compiled project would
10
- otherwise hand-roll.
9
+ **The non-stdlib toolkit for Spinel-compiled apps.** A pure-Ruby, Spinel-safe
10
+ library holding the shims [Spinel](https://github.com/matz/spinel) (the
11
+ Ruby→native AOT compiler) does not provide and that every Spinel project
12
+ would otherwise hand-roll: **git provenance, URL percent-codec + query
13
+ parsing, a minimal levelled logger, and a hex codec**.
14
+ [toy](https://github.com/OriPekelman/toy) and
15
+ [tep](https://github.com/OriPekelman/tep) grew these independently — parts
16
+ came out *byte-identical* — and SpinelKit is that code, consolidated once.
11
17
 
12
- [Spinel](https://github.com/matz/spinel) is a Ruby→native AOT compiler. It
13
- cannot lower large chunks of the CRuby standard library — the `json` gem's
14
- C-extension fast path and its metaprogrammed pure-Ruby fallback, stdlib
15
- `Logger`, C-extension git bindings, and more. So every Spinel project re-derives
16
- the same shims. [toy](https://github.com/OriPekelman/toy) and
17
- [tep](https://github.com/OriPekelman/tep) grew theirs independently, and the
18
- JSON escape/quote/hex code came out *byte-identical*. SpinelKit is that code,
19
- consolidated once.
18
+ ## Where JSON went
19
+
20
+ SpinelKit began as "the stdlib-surface gem," and its founding surface was a
21
+ JSON codec. Spinel now **bundles `json` as a require-gated stdlib package** —
22
+ a typed native binding straight into the runtime's `sp_json.c` so the
23
+ compiler provides the stdlib surface itself and `SpinelKit::Json` was retired
24
+ in 0.3.0 ([#3](https://github.com/OriPekelman/spinelkit/issues/3)). Migration:
25
+
26
+ ```ruby
27
+ require "json" # bare stdlib require, no manifest entry
28
+
29
+ h = JSON.parse(body)
30
+ model = h["model"].to_s # coerce at the parse boundary and
31
+ max_tokens = h["max_tokens"].to_i # everything downstream stays fully
32
+ temp = h["temperature"].to_f # specialized under whole-program
33
+ ids = h["prompt"].map { |x| x.to_i } # inference
34
+
35
+ JSON.generate({ "ok" => true, "n" => max_tokens }) # replaces the Builder
36
+ ```
37
+
38
+ Coerce parsed values once at the boundary (`.to_s`/`.to_i`/`.to_f`/mapped
39
+ `.to_i`) — verified against spinel master: byte-identical CRuby parity, no
40
+ cross-degrade, and full re-typification with the coercions in place.
20
41
 
21
42
  ## Installation
22
43
 
@@ -37,20 +58,20 @@ cleanly into a Spinel build via [bundler-spinel](https://github.com/OriPekelman/
37
58
 
38
59
  The first thing we did was audit the
39
60
  [spinelgems](https://github.com/OriPekelman/spinelgems) compatibility catalog
40
- (verdict ladder: `verified > loaded > clean > risky > rejected`) for an existing
41
- gem to reuse — that would have been the biggest win. **There wasn't one.** These
42
- shims *are* the ecosystem's gaps:
61
+ (verdict ladder: `verified > loaded > clean > risky > rejected`) for existing
62
+ gems to reuse — that would have been the biggest win. **There weren't any.**
63
+ These shims *are* the ecosystem's gaps:
43
64
 
44
65
  | Surface | Catalog finding | Decision |
45
66
  |---------|-----------------|----------|
46
- | JSON | `json` **rejected** (C-ext + metaprogrammed fallback); `oj` **risky** (C-ext) | implement |
47
67
  | Log | `logger` **rejected** (unresolved calls) | implement |
48
68
  | Git | `rugged` **rejected** (C); `gitkite`/`git_manager` only **clean**, unmet `needs:` | implement (read `.git/HEAD`) |
69
+ | Url/Hex | `CGI`/`URI` stdlib not lowered; no verified gem | implement |
49
70
  | Path | `hike` **verified** at an older rev, only **loaded** now; overkill for basename/join | deferred |
50
71
  | Bytes | `unicode_utils`/`utf8-cleaner` **clean** only; toy's need is tokenizer-specific | deferred |
51
72
 
52
- See [`docs/gem-audit-first.md`](docs/gem-audit-first.md) for the full audit and
53
- the verification-request issues we filed on spinelgems.
73
+ See [`docs/gem-audit-first.md`](docs/gem-audit-first.md) for the full audit
74
+ (which also covers the since-retired JSON surface).
54
75
 
55
76
  ## What's in it
56
77
 
@@ -60,31 +81,8 @@ require "spinel_kit" # everything (CRuby / convenience)
60
81
 
61
82
  For a **Spinel-compiled** consumer, require only the surface you use — Spinel
62
83
  has no tree-shaking, so every loaded method is compiled (and an uncalled one
63
- can degrade). The Json surface is split into three files for exactly this
64
- reason: `spinel_kit/json` (encoders), `spinel_kit/json_decoder` (decoders), and
65
- `spinel_kit/json_builder` (the builder). e.g. tep requires `json` + `json_decoder`;
66
- toy requires `json_builder`.
67
-
68
- - **`SpinelKit::Json`** — a JSON-over-HTTP codec: encoders
69
- (`escape`/`quote`/`encode_pair_*`/`from_*`, in `spinel_kit/json`) and flat-key
70
- decoders (`get_str`/`get_int`/`get_float`/`get_int_array`/`has_key?`, in
71
- `spinel_kit/json_decoder`).
72
-
73
- ```ruby
74
- SpinelKit::Json.get_int('{"age":33}', "age") # => 33
75
- SpinelKit::Json.from_int_hash({"a" => 1, "b" => 2}) # => {"a":1,"b":2}
76
- ```
77
-
78
- - **`SpinelKit::Json::Builder`** — an incremental ordered-object builder
79
- (`add_str`/`add_num`/`add_bool`/`add_raw`/`add_obj`/`dump`), in its own file
80
- so a builder-only consumer never compiles the codec, and vice versa.
81
-
82
- ```ruby
83
- j = SpinelKit::Json::Builder.new
84
- j.add_str("kind", "run_start")
85
- j.add_num("t", 1715000000)
86
- j.dump # => {"kind":"run_start","t":1715000000}
87
- ```
84
+ can degrade): `spinel_kit/hex`, `spinel_kit/url` (pulls in hex),
85
+ `spinel_kit/git`, `spinel_kit/log`.
88
86
 
89
87
  - **`SpinelKit::Git`** — git provenance from `.git/HEAD`.
90
88
 
@@ -94,6 +92,15 @@ toy requires `json_builder`.
94
92
  g.branch # => "main"
95
93
  ```
96
94
 
95
+ - **`SpinelKit::Url`** — the `CGI`/`URI`-component surface:
96
+ `escape`/`unescape` (RFC 3986 percent-codec), `parse_query`
97
+ (form-urlencoded → Hash), `split_url` (scheme/host/port/path/query).
98
+
99
+ ```ruby
100
+ SpinelKit::Url.escape("a b&c") # => "a%20b%26c"
101
+ SpinelKit::Url.parse_query("x=1&y=z") # => {"x"=>"1", "y"=>"z"}
102
+ ```
103
+
97
104
  - **`SpinelKit::Log`** — a minimal levelled logger (CRuby `Logger` doesn't
98
105
  compile under Spinel).
99
106
 
@@ -103,6 +110,9 @@ toy requires `json_builder`.
103
110
  log.info("server up")
104
111
  ```
105
112
 
113
+ - **`SpinelKit::Hex`** — hex digit/byte encode + decode (`nibble`,
114
+ `nibble_char`, `byte2`, `to_int`); the shared surface `Url` builds on.
115
+
106
116
  ## Design constraints (read before editing)
107
117
 
108
118
  SpinelKit is **pure Ruby, no native extension** (`spinel-ext.json` is `[]`) and
@@ -110,24 +120,44 @@ has **no runtime dependencies**, so it vendors cleanly via `bundler-spinel`. The
110
120
  surface uses plain, standard names; the `j_`/`tj_`/`gi_` prefixes the donor
111
121
  copies carried were a workaround for a Spinel whole-program-inference bug that
112
122
  has since been fixed upstream (verified with toy's `gate-poly-degrade` on the
113
- current compiler). One numeric caveat remains for `add_num` — see
114
- [`docs/spinel-discipline.md`](docs/spinel-discipline.md).
123
+ current compiler). See [`docs/spinel-discipline.md`](docs/spinel-discipline.md).
124
+
125
+ ## Package formats: spin package + gem, one tree
126
+
127
+ This repo **is a [spin](https://github.com/matz/spin-index) package** (spin is
128
+ Spinel's package system): the package root is the require root
129
+ (`spinel_kit.rb` + `spinel_kit/` at top level), `spin.toml` carries the
130
+ identity, and the top-level `test/*.rb` programs are compiled snapshot tests
131
+ (`spin test` — each diffs against its committed `.expected`, byte-identical
132
+ under CRuby too). Spinel projects take it with:
133
+
134
+ ```sh
135
+ spin add spinel_kit # index form, once seeded — or:
136
+ spin add spinel_kit --git https://github.com/OriPekelman/spinelkit
137
+ ```
138
+
139
+ The same tree still ships as the `spinel_kit` **gem** for CRuby-side
140
+ consumers (`require_paths` is the package root), and vendors via
141
+ `bundler-spinel` as before.
115
142
 
116
143
  ## Status
117
144
 
118
- Pre-alpha (`0.1.0`). The shims are implemented, CRuby-verified, and the surface
119
- is the single canonical one (the donor prefixes and duplicated escapers are
120
- gone the Spinel inference bug that motivated them is fixed). Consumer adoption
121
- is the next phase: because we author every repo in this set, tep and toy
122
- **migrate to `SpinelKit::*` directly and delete their donor modules** — we
123
- standardize and clean rather than leave compatibility aliases behind. See
124
- [`docs/adoption.md`](docs/adoption.md). Tracking issue:
125
- [OriPekelman/toy#44](https://github.com/OriPekelman/toy/issues/44).
145
+ Pre-alpha (`0.3.0` the JSON retirement + spin package conversion). tep and
146
+ toy migrated to `SpinelKit::*` directly and deleted their donor modules (see
147
+ [`docs/adoption.md`](docs/adoption.md)); their JSON call sites migrate to the
148
+ bundled stdlib `json` next
149
+ ([tep#217](https://github.com/OriPekelman/tep/issues/217),
150
+ [tep#213](https://github.com/OriPekelman/tep/issues/213)), and their
151
+ dependency declarations move to `spin.toml`
152
+ ([tep#234](https://github.com/OriPekelman/tep/issues/234),
153
+ [toy#107](https://github.com/OriPekelman/toy/issues/107)). Repositioning
154
+ tracked in [#3](https://github.com/OriPekelman/spinelkit/issues/3).
126
155
 
127
156
  ## Development
128
157
 
129
158
  ```sh
130
- rake test # CRuby-side parity tests (never compiled)
159
+ rake test # CRuby-side parity tests, test/parity/ (never compiled)
160
+ spin test # compiled snapshot tests, test/*.rb vs .expected
131
161
  rake rbs:validate # syntax-check the advisory RBS in sig/
132
162
  gem build spinel_kit.gemspec
133
163
  ```
data/docs/adoption.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Adoption — standardize and clean (not shim-and-alias)
2
2
 
3
+ > **Historical note (2026-07-08).** This doc describes the 0.1–0.2 era
4
+ > migration of tep and toy onto `SpinelKit::*`, including the Json surface.
5
+ > `SpinelKit::Json`/`Builder` were **retired in 0.3.0** — Spinel now bundles
6
+ > `json` as a stdlib package, and the Json call sites below migrate again, to
7
+ > bare `require "json"` (see
8
+ > [#3](https://github.com/OriPekelman/spinelkit/issues/3) and the CHANGELOG
9
+ > migration note). The Git/Url/Log/Hex parts remain accurate.
10
+
3
11
  Bootstrap landed the gem and its three shims. Adoption by tep and toy is the
4
12
  next phase. **Because we author every repo in this set — toy, tep, spinelgems,
5
13
  and Spinel itself — the goal is to standardize and clean, not to bolt
@@ -2,7 +2,7 @@
2
2
 
3
3
  SpinelKit ships code that gets compiled into *other* programs by Spinel's
4
4
  whole-program AOT compiler. That changes the rules for how it must be written.
5
- This doc is the contract for anyone editing `lib/spinel_kit/*.rb`.
5
+ This doc is the contract for anyone editing `spinel_kit/*.rb`.
6
6
 
7
7
  ## 1. The name-keyed inference bug — FIXED, prefixes dropped
8
8
 
@@ -35,28 +35,24 @@ names (`escape`/`quote`/`add_str`/`sha`/`branch`/plain `value`).
35
35
  - **Split the surface so consumers don't compile dead code.** Spinel has no
36
36
  tree-shaking: every loaded method is compiled, and a *set* of uncalled
37
37
  methods can degrade each other's (and nearby live methods') param types. We
38
- saw this concretely — with the encoders and decoders in one class, an
39
- encode-only program left the 9 decoder walkers dead; their `s` params
40
- collapsed to `int` and dragged `escape`'s `s` to `int` too, silently
41
- emitting `""` keys from `from_*_hash`. The fix was structural: encoders
42
- (`json.rb`), decoders (`json_decoder.rb`), and builder (`json_builder.rb`)
43
- live in separate files, so a consumer loads only a coherent surface it
44
- actually exercises. Verified clean (0 emit-0, correct output) for the real
45
- consumer shapes builder-only (toy) and encode+decode (tep) — on rev
46
- `57af7f9`. A consumer that loads a surface but leaves part of it uncalled can
47
- still trip the gate; that's expected, and the gate is what flags it.
48
- - **`escape`/`quote`/`hex2` are single canonical methods** — the builder
49
- carries its own byte-identical copies (so a builder-only compile pulls in no
50
- codec); there is no `tj_*` prefix any more.
51
- - **Keep `get_float` inlined** (it does NOT delegate to a `parse_float_value`
52
- helper). This is unrelated to the name bug — it's a value-walk *indirection*
53
- issue where Spinel mis-widened the string arg `s` to int through the helper
54
- call. Until that's separately confirmed fixed, leave it inlined.
38
+ saw this concretely in the retired Json surface — with the encoders and
39
+ decoders in one class, an encode-only program left the 9 decoder walkers
40
+ dead; their `s` params collapsed to `int` and dragged `escape`'s `s` to
41
+ `int` too, silently emitting `""` keys. The fix was structural: one
42
+ coherent surface per file (today: `hex.rb`, `url.rb`, `git.rb`, `log.rb`),
43
+ so a consumer loads only what it actually exercises. A consumer that loads
44
+ a surface but leaves part of it uncalled can still trip the gate; that's
45
+ expected, and the gate is what flags it.
55
46
  - **Never override `to_s`** — it merges across the whole program.
56
47
  - The stale cautionary comments in toy's `toy_json.rb`/`toy_git.rb` and the
57
48
  landmine memory note should be annotated "fixed upstream by `ac7720e`/
58
49
  `23ba632`, verified on `57af7f9`" when those repos are next touched.
59
50
 
51
+ (Historical, retired with the Json surface in 0.3.0: the three-file Json
52
+ split, the builder's byte-identical private escapers, and the keep-`get_float`
53
+ -inlined rule. The last known instance of the value-walk indirection issue was
54
+ verified fixed on master `b60fbd7`.)
55
+
60
56
  ## 2. The poly-degrade gate (how consumers verify SpinelKit is safe)
61
57
 
62
58
  Each consumer has a poly-degrade scan that compiles a canonical entrypoint and
@@ -101,6 +97,7 @@ destined for Spinel. SpinelKit's own decoders follow this rule.
101
97
  runs, but file output is the portable path.
102
98
  - `Time.now` exposes integer seconds only (no rich `strftime`) — `Log` formats
103
99
  with `Time.now.to_i`.
104
- - `Integer#chr` is not uniform for arbitrary bytes — `Json.byte_to_chr` uses a
105
- printable-ASCII table with a `"?"` fallback.
100
+ - `Integer#chr` is not uniform for arbitrary bytes — the retired Json surface
101
+ used a printable-ASCII table with a `"?"` fallback; apply the same pattern
102
+ if a surface ever needs byte→char again.
106
103
  - No C extensions — `spinel-ext.json` is `[]`.
@@ -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
data/spin.toml ADDED
@@ -0,0 +1,6 @@
1
+ # spin package manifest — SpinelKit as an ordinary spin package.
2
+ # The package NAME is the require string (`require "spinel_kit/git"` etc.);
3
+ # the repo keeps its historical name. See issue #3 for the repositioning.
4
+ [package]
5
+ name = "spinel_kit"
6
+ version = "0.3.0"
data/spinel_kit/hex.rb ADDED
@@ -0,0 +1,59 @@
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
+ # in the retired SpinelKit::Json string decoder, and (as a multi-digit
4
+ # variant) in Tep::Llm's chunked-transfer size parser. Pure string/byte ops,
5
+ # Spinel-safe. Hex is the shared surface, e.g. for SpinelKit::Url.
6
+ module SpinelKit
7
+ class Hex
8
+ # Hex digit char -> int 0..15, or -1 if not a hex digit (upper or lower).
9
+ def self.nibble(c)
10
+ if c >= "0" && c <= "9"
11
+ return c.getbyte(0) - "0".getbyte(0)
12
+ end
13
+ if c >= "a" && c <= "f"
14
+ return c.getbyte(0) - "a".getbyte(0) + 10
15
+ end
16
+ if c >= "A" && c <= "F"
17
+ return c.getbyte(0) - "A".getbyte(0) + 10
18
+ end
19
+ -1
20
+ end
21
+
22
+ # Int nibble 0..15 -> single UPPERCASE hex char ("0".."9","A".."F").
23
+ # (RFC 3986 percent-encoding uses uppercase.)
24
+ def self.nibble_char(n)
25
+ if n < 10
26
+ return ("0".getbyte(0) + n).chr
27
+ end
28
+ ("A".getbyte(0) + n - 10).chr
29
+ end
30
+
31
+ # Int byte 0..255 -> two-char LOWERCASE hex (15 -> "0f"). (JSON \u00XX
32
+ # and similar use lowercase.)
33
+ def self.byte2(n)
34
+ hex = "0123456789abcdef"
35
+ out = ""
36
+ out = out + hex[(n / 16) % 16, 1]
37
+ out = out + hex[n % 16, 1]
38
+ out
39
+ end
40
+
41
+ # Parse the leading hex digits of `s` -> int ("1a3" -> 419). Stops at the
42
+ # first non-hex char; returns 0 if there is no leading hex digit. Useful
43
+ # for chunked-transfer sizes and the like.
44
+ def self.to_int(s)
45
+ n = 0
46
+ i = 0
47
+ len = s.length
48
+ while i < len
49
+ v = Hex.nibble(s[i])
50
+ if v < 0
51
+ return n
52
+ end
53
+ n = n * 16 + v
54
+ i += 1
55
+ end
56
+ n
57
+ end
58
+ end
59
+ end
data/spinel_kit/url.rb ADDED
@@ -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
@@ -0,0 +1,6 @@
1
+ # SpinelKit version. Kept in its own file so the gemspec can read it
2
+ # without loading the rest of the library (the modules pull in no deps,
3
+ # but this matches the toy/tep convention exactly).
4
+ module SpinelKit
5
+ VERSION = "0.3.0"
6
+ end
data/spinel_kit.rb ADDED
@@ -0,0 +1,39 @@
1
+ # SpinelKit -- the non-stdlib toolkit for Spinel-compiled apps.
2
+ #
3
+ # A pure-Ruby, Spinel-safe toolkit holding the shims Spinel (the Ruby->native
4
+ # AOT compiler) does not provide and that every Spinel-compiled project would
5
+ # otherwise hand-roll:
6
+ #
7
+ # SpinelKit::Hex -- hex digit/byte encode + decode.
8
+ # SpinelKit::Url -- percent-encode/decode + form-query + URL split.
9
+ # SpinelKit::Git -- git provenance from .git/HEAD (was Toy::Git).
10
+ # SpinelKit::Log -- minimal levelled logger (was Tep::Logger).
11
+ #
12
+ # JSON is NOT here any more. Spinel bundles `json` as a require-gated stdlib
13
+ # package (a typed native binding into sp_json.c), so consumers write a bare
14
+ # `require "json"` and use JSON.parse / JSON.generate, coercing parsed values
15
+ # at the boundary (`.to_s` / `.to_i` / `.to_f` / `.map { |x| x.to_i }`) to
16
+ # keep inference fully typed. The old SpinelKit::Json codec + Builder were
17
+ # retired in 0.3.0 -- see CHANGELOG.md and issue #3.
18
+ #
19
+ # MINIMAL-SURFACE REQUIRING. This umbrella requires everything for
20
+ # convenience (and for CRuby use). But because Spinel compiles every loaded
21
+ # method with no tree-shaking, a Spinel-compiled consumer should require ONLY
22
+ # the file(s) it uses, to avoid compiling -- and degrading -- code it never
23
+ # calls:
24
+ #
25
+ # require "spinel_kit/hex" # hex digit/byte codec
26
+ # require "spinel_kit/url" # percent-codec + query (pulls in hex)
27
+ # require "spinel_kit/git"
28
+ # require "spinel_kit/log"
29
+ #
30
+ # No native extension (spinel-ext.json is []), no runtime dependencies. See
31
+ # docs/adoption.md and docs/spinel-discipline.md.
32
+ require_relative "spinel_kit/version"
33
+ require_relative "spinel_kit/hex"
34
+ require_relative "spinel_kit/url"
35
+ require_relative "spinel_kit/git"
36
+ require_relative "spinel_kit/log"
37
+
38
+ module SpinelKit
39
+ end