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 +4 -4
- data/CHANGELOG.md +51 -0
- data/README.md +83 -53
- data/docs/adoption.md +8 -0
- data/docs/spinel-discipline.md +17 -20
- data/sig/spinel_kit/hex.rbs +8 -0
- data/sig/spinel_kit/url.rbs +9 -0
- data/spin.toml +6 -0
- data/spinel_kit/hex.rb +59 -0
- data/spinel_kit/url.rb +164 -0
- data/spinel_kit/version.rb +6 -0
- data/spinel_kit.rb +39 -0
- metadata +20 -22
- data/lib/spinel_kit/json.rb +0 -149
- data/lib/spinel_kit/json_builder.rb +0 -142
- data/lib/spinel_kit/json_decoder.rb +0 -394
- data/lib/spinel_kit/version.rb +0 -6
- data/lib/spinel_kit.rb +0 -39
- data/sig/spinel_kit/json.rbs +0 -15
- data/sig/spinel_kit/json_builder.rbs +0 -19
- data/sig/spinel_kit/json_decoder.rbs +0 -21
- /data/{lib/spinel_kit → spinel_kit}/git.rb +0 -0
- /data/{lib/spinel_kit → spinel_kit}/log.rb +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fca4df06c4d57130a5053954a6429beac7d6ce0d6f6d1ee26194753aea11732c
|
|
4
|
+
data.tar.gz: 496c1116e14068768cb482723d1be6c6daebfc26f45d9972da3ab36d572eae69
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
+
[](https://github.com/OriPekelman/spinelkit/actions/workflows/ci.yml)
|
|
3
4
|
[](https://rubygems.org/gems/spinel_kit)
|
|
4
5
|
[](LICENSE)
|
|
5
6
|

|
|
6
7
|

|
|
7
8
|
|
|
8
|
-
**The Spinel
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
the
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
41
|
-
|
|
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
|
|
53
|
-
|
|
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)
|
|
64
|
-
|
|
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).
|
|
114
|
-
|
|
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.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
[
|
|
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
|
data/docs/spinel-discipline.md
CHANGED
|
@@ -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 `
|
|
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
|
|
39
|
-
encode-only program left the 9 decoder walkers
|
|
40
|
-
collapsed to `int` and dragged `escape`'s `s` to
|
|
41
|
-
emitting `""` keys
|
|
42
|
-
(`
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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 —
|
|
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
|
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
|