spinel_kit 0.2.0 → 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: a45aac9b6f91813e7d837ea8878d4dcee1731b5582f4a42e5fae4f73e9f764ce
4
- data.tar.gz: f201acc7e5c64ba8f5666e4c9c454440c7d1cc6835a30dd8adfb07442b1b42e8
3
+ metadata.gz: fca4df06c4d57130a5053954a6429beac7d6ce0d6f6d1ee26194753aea11732c
4
+ data.tar.gz: 496c1116e14068768cb482723d1be6c6daebfc26f45d9972da3ab36d572eae69
5
5
  SHA512:
6
- metadata.gz: 5a6f770a93f9975455637ed2fb2ffd8874dcc69d1e6fdf8547b7120c13ea341f2154a0427f2bf317851d0cb86072232649f6e1b011354c8ab68760f120a4bc2e
7
- data.tar.gz: 45d722737fce1cce7d606765af43f405fa6f34c96fa8a0167c8794262dee1ffebaa034600d865399577aaf9e826491059f338a8e688502bfd52b00c1071157b3
6
+ metadata.gz: 1d15a4186fd29c584767e64c7f015b6424ce72bae04d39b283529f48478923075662bc2ecdc5e0ac03a73c51bcc344a9140cc775123a33c02e2dc76f9860cf73
7
+ data.tar.gz: 4d0ee98f92b44fa1e1345efdac6378626c31ba11c42cf0a6be12ab065f9e9854799eec16edb363da670b01eacd8421623631d2259c296f66f8b9892cd6f2eaa5
data/CHANGELOG.md CHANGED
@@ -2,6 +2,39 @@
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
+
5
38
  ## [0.2.0] - 2026-06-08
6
39
 
7
40
  ### Added
data/README.md CHANGED
@@ -6,18 +6,38 @@
6
6
  ![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.2-CC342D)
7
7
  ![Pure Ruby](https://img.shields.io/badge/native%20ext-none-brightgreen)
8
8
 
9
- **The Spinel stdlib-surface gem.** A pure-Ruby, Spinel-safe toolkit holding
10
- the generic "stdlib substitute" shims that every Spinel-compiled project would
11
- otherwise hand-roll.
12
-
13
- [Spinel](https://github.com/matz/spinel) is a Ruby→native AOT compiler. It
14
- cannot lower large chunks of the CRuby standard library — the `json` gem's
15
- C-extension fast path and its metaprogrammed pure-Ruby fallback, stdlib
16
- `Logger`, C-extension git bindings, and more. So every Spinel project re-derives
17
- the same shims. [toy](https://github.com/OriPekelman/toy) and
18
- [tep](https://github.com/OriPekelman/tep) grew theirs independently, and the
19
- JSON escape/quote/hex code came out *byte-identical*. SpinelKit is that code,
20
- consolidated once.
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.
17
+
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.
21
41
 
22
42
  ## Installation
23
43
 
@@ -38,20 +58,20 @@ cleanly into a Spinel build via [bundler-spinel](https://github.com/OriPekelman/
38
58
 
39
59
  The first thing we did was audit the
40
60
  [spinelgems](https://github.com/OriPekelman/spinelgems) compatibility catalog
41
- (verdict ladder: `verified > loaded > clean > risky > rejected`) for an existing
42
- gem to reuse — that would have been the biggest win. **There wasn't one.** These
43
- 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:
44
64
 
45
65
  | Surface | Catalog finding | Decision |
46
66
  |---------|-----------------|----------|
47
- | JSON | `json` **rejected** (C-ext + metaprogrammed fallback); `oj` **risky** (C-ext) | implement |
48
67
  | Log | `logger` **rejected** (unresolved calls) | implement |
49
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 |
50
70
  | Path | `hike` **verified** at an older rev, only **loaded** now; overkill for basename/join | deferred |
51
71
  | Bytes | `unicode_utils`/`utf8-cleaner` **clean** only; toy's need is tokenizer-specific | deferred |
52
72
 
53
- See [`docs/gem-audit-first.md`](docs/gem-audit-first.md) for the full audit and
54
- 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).
55
75
 
56
76
  ## What's in it
57
77
 
@@ -61,31 +81,8 @@ require "spinel_kit" # everything (CRuby / convenience)
61
81
 
62
82
  For a **Spinel-compiled** consumer, require only the surface you use — Spinel
63
83
  has no tree-shaking, so every loaded method is compiled (and an uncalled one
64
- can degrade). The Json surface is split into three files for exactly this
65
- reason: `spinel_kit/json` (encoders), `spinel_kit/json_decoder` (decoders), and
66
- `spinel_kit/json_builder` (the builder). e.g. tep requires `json` + `json_decoder`;
67
- toy requires `json_builder`.
68
-
69
- - **`SpinelKit::Json`** — a JSON-over-HTTP codec: encoders
70
- (`escape`/`quote`/`encode_pair_*`/`from_*`, in `spinel_kit/json`) and flat-key
71
- decoders (`get_str`/`get_int`/`get_float`/`get_int_array`/`has_key?`, in
72
- `spinel_kit/json_decoder`).
73
-
74
- ```ruby
75
- SpinelKit::Json.get_int('{"age":33}', "age") # => 33
76
- SpinelKit::Json.from_int_hash({"a" => 1, "b" => 2}) # => {"a":1,"b":2}
77
- ```
78
-
79
- - **`SpinelKit::Json::Builder`** — an incremental ordered-object builder
80
- (`add_str`/`add_num`/`add_bool`/`add_raw`/`add_obj`/`dump`), in its own file
81
- so a builder-only consumer never compiles the codec, and vice versa.
82
-
83
- ```ruby
84
- j = SpinelKit::Json::Builder.new
85
- j.add_str("kind", "run_start")
86
- j.add_num("t", 1715000000)
87
- j.dump # => {"kind":"run_start","t":1715000000}
88
- ```
84
+ can degrade): `spinel_kit/hex`, `spinel_kit/url` (pulls in hex),
85
+ `spinel_kit/git`, `spinel_kit/log`.
89
86
 
90
87
  - **`SpinelKit::Git`** — git provenance from `.git/HEAD`.
91
88
 
@@ -95,6 +92,15 @@ toy requires `json_builder`.
95
92
  g.branch # => "main"
96
93
  ```
97
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
+
98
104
  - **`SpinelKit::Log`** — a minimal levelled logger (CRuby `Logger` doesn't
99
105
  compile under Spinel).
100
106
 
@@ -104,6 +110,9 @@ toy requires `json_builder`.
104
110
  log.info("server up")
105
111
  ```
106
112
 
113
+ - **`SpinelKit::Hex`** — hex digit/byte encode + decode (`nibble`,
114
+ `nibble_char`, `byte2`, `to_int`); the shared surface `Url` builds on.
115
+
107
116
  ## Design constraints (read before editing)
108
117
 
109
118
  SpinelKit is **pure Ruby, no native extension** (`spinel-ext.json` is `[]`) and
@@ -111,24 +120,44 @@ has **no runtime dependencies**, so it vendors cleanly via `bundler-spinel`. The
111
120
  surface uses plain, standard names; the `j_`/`tj_`/`gi_` prefixes the donor
112
121
  copies carried were a workaround for a Spinel whole-program-inference bug that
113
122
  has since been fixed upstream (verified with toy's `gate-poly-degrade` on the
114
- current compiler). One numeric caveat remains for `add_num` — see
115
- [`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.
116
142
 
117
143
  ## Status
118
144
 
119
- Pre-alpha (`0.1.0`). The shims are implemented, CRuby-verified, and the surface
120
- is the single canonical one (the donor prefixes and duplicated escapers are
121
- gone the Spinel inference bug that motivated them is fixed). Consumer adoption
122
- is the next phase: because we author every repo in this set, tep and toy
123
- **migrate to `SpinelKit::*` directly and delete their donor modules** — we
124
- standardize and clean rather than leave compatibility aliases behind. See
125
- [`docs/adoption.md`](docs/adoption.md). Tracking issue:
126
- [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).
127
155
 
128
156
  ## Development
129
157
 
130
158
  ```sh
131
- 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
132
161
  rake rbs:validate # syntax-check the advisory RBS in sig/
133
162
  gem build spinel_kit.gemspec
134
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 `[]`.
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"
@@ -1,12 +1,8 @@
1
1
  # SpinelKit::Hex -- hex digit/byte encode + decode: the pieces every Spinel
2
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.
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.
10
6
  module SpinelKit
11
7
  class Hex
12
8
  # Hex digit char -> int 0..15, or -1 if not a hex digit (upper or lower).
@@ -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
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.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ori Pekelman
@@ -10,14 +10,14 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: |-
13
- SpinelKit consolidates the pure-Ruby "stdlib substitute" shims that every
14
- Spinel-compiled project would otherwise hand-roll. Spinel (the Ruby->native
15
- AOT compiler) cannot lower the json gem's C-ext fast path / metaprogrammed
16
- fallback, stdlib Logger, or C-ext git bindings -- and the spinelgems
17
- compatibility catalog confirms no verified gem exists to reuse. So this gem
18
- ships a JSON encoder+decoder+builder, .git/HEAD provenance, and a minimal
19
- levelled logger. Pure Ruby, no native extension, no runtime dependencies --
20
- it vendors cleanly via bundler-spinel. Pre-alpha.
13
+ SpinelKit consolidates the pure-Ruby shims that every Spinel-compiled
14
+ project would otherwise hand-roll and that Spinel (the Ruby->native AOT
15
+ compiler) does not provide: .git/HEAD provenance, URL percent-codec +
16
+ query parsing, a minimal levelled logger, and a hex digit/byte codec.
17
+ (JSON left in 0.3.0: Spinel now bundles `json` as a require-gated stdlib
18
+ package, so consumers use JSON.parse/JSON.generate directly.) Pure Ruby,
19
+ no native extension, no runtime dependencies -- it vendors cleanly via
20
+ bundler-spinel. Pre-alpha.
21
21
  email:
22
22
  - ori@pekelman.com
23
23
  executables: []
@@ -31,24 +31,19 @@ files:
31
31
  - docs/gem-audit-first.md
32
32
  - docs/spinel-discipline.md
33
33
  - docs/spinelgems-issues.md
34
- - lib/spinel_kit.rb
35
- - lib/spinel_kit/git.rb
36
- - lib/spinel_kit/hex.rb
37
- - lib/spinel_kit/json.rb
38
- - lib/spinel_kit/json_builder.rb
39
- - lib/spinel_kit/json_decoder.rb
40
- - lib/spinel_kit/log.rb
41
- - lib/spinel_kit/url.rb
42
- - lib/spinel_kit/version.rb
43
34
  - sig/spinel_kit/git.rbs
44
35
  - sig/spinel_kit/hex.rbs
45
- - sig/spinel_kit/json.rbs
46
- - sig/spinel_kit/json_builder.rbs
47
- - sig/spinel_kit/json_decoder.rbs
48
36
  - sig/spinel_kit/log.rbs
49
37
  - sig/spinel_kit/url.rbs
50
38
  - sig/spinel_kit/version.rbs
39
+ - spin.toml
51
40
  - spinel-ext.json
41
+ - spinel_kit.rb
42
+ - spinel_kit/git.rb
43
+ - spinel_kit/hex.rb
44
+ - spinel_kit/log.rb
45
+ - spinel_kit/url.rb
46
+ - spinel_kit/version.rb
52
47
  homepage: https://github.com/OriPekelman/spinelkit
53
48
  licenses:
54
49
  - MIT
@@ -60,7 +55,7 @@ metadata:
60
55
  rubygems_mfa_required: 'true'
61
56
  rdoc_options: []
62
57
  require_paths:
63
- - lib
58
+ - "."
64
59
  required_ruby_version: !ruby/object:Gem::Requirement
65
60
  requirements:
66
61
  - - ">="
@@ -74,6 +69,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
74
69
  requirements: []
75
70
  rubygems_version: 3.6.9
76
71
  specification_version: 4
77
- summary: 'The Spinel stdlib-surface gem: pure-Ruby JSON/Git/Log shims for AOT-compiled
78
- apps'
72
+ summary: Pure-Ruby Git/Url/Log/Hex shims for Spinel AOT-compiled apps
79
73
  test_files: []
@@ -1,149 +0,0 @@
1
- # SpinelKit::Json -- Spinel-safe JSON ENCODERS (stateless).
2
- #
3
- # This file holds the encode half of the codec; the decode half lives in
4
- # spinel_kit/json_decoder.rb (also `SpinelKit::Json`), and the incremental
5
- # object builder in spinel_kit/json_builder.rb (`SpinelKit::Json::Builder`).
6
- # The three are split because Spinel has no tree-shaking: every loaded method
7
- # is compiled, and a set of uncalled methods can degrade each other's params
8
- # (e.g. the dead decoder walkers collectively widening `escape`'s string arg
9
- # to int, which silently miscompiled string keys to ""). Keeping
10
- # encode/decode/build in separate files means a consumer compiles only the
11
- # surface it calls, and each surface is independently warning-clean. Require
12
- # only what you use:
13
- #
14
- # require "spinel_kit/json" # encoders (this file)
15
- # require "spinel_kit/json_decoder" # decoders
16
- # require "spinel_kit/json_builder" # builder
17
- #
18
- # WHY HAND-ROLLED. Spinel cannot lower the stdlib `json` gem (C-ext fast path
19
- # + metaprogrammed pure fallback); `oj`/`yajl`/`multi_json` are C extensions.
20
- # The spinelgems catalog confirms no verified pure-Ruby JSON gem exists. This
21
- # is tep's encoder, standardized (the `j_`/`tj_` prefixes that worked around a
22
- # now-fixed Spinel inference bug are gone -- see docs/spinel-discipline.md).
23
- #
24
- # Compose objects in user code by concatenation:
25
- #
26
- # "{" + SpinelKit::Json.encode_pair_str("name", name) + "," +
27
- # SpinelKit::Json.encode_pair_int("age", age) + "}"
28
- module SpinelKit
29
- class Json
30
- # Escape a string for inclusion inside a JSON string literal (does NOT
31
- # add the surrounding quotes -- use `quote(s)` for that). Handles ", \,
32
- # and the JSON-required control-char escapes (\b, \f, \n, \r, \t);
33
- # other control bytes go through \u00XX. Forward slash is left
34
- # unescaped (legal either way; unescaped is shorter/readable).
35
- def self.escape(s)
36
- out = ""
37
- i = 0
38
- n = s.length
39
- while i < n
40
- c = s[i]
41
- if c == "\""
42
- out = out + "\\\""
43
- elsif c == "\\"
44
- out = out + "\\\\"
45
- elsif c == "\n"
46
- out = out + "\\n"
47
- elsif c == "\r"
48
- out = out + "\\r"
49
- elsif c == "\t"
50
- out = out + "\\t"
51
- elsif c == "\b"
52
- out = out + "\\b"
53
- elsif c == "\f"
54
- out = out + "\\f"
55
- elsif c < " "
56
- # Other control byte -- emit \u00XX. c.getbyte(0) is the raw
57
- # byte value, mapped to two hex digits.
58
- b = c.getbyte(0)
59
- out = out + "\\u00" + Json.hex2(b)
60
- else
61
- out = out + c
62
- end
63
- i += 1
64
- end
65
- out
66
- end
67
-
68
- # Two-digit lowercase hex of a byte (0..255).
69
- def self.hex2(n)
70
- hex = "0123456789abcdef"
71
- out = ""
72
- out = out + hex[(n / 16) % 16, 1]
73
- out = out + hex[n % 16, 1]
74
- out
75
- end
76
-
77
- # Wrap a string in JSON quotes, escaping its body.
78
- def self.quote(s)
79
- "\"" + Json.escape(s) + "\""
80
- end
81
-
82
- # Encode a single key/value pair as `"k":"v"` (escaped both sides).
83
- def self.encode_pair_str(k, v)
84
- Json.quote(k) + ":" + Json.quote(v)
85
- end
86
-
87
- # Same shape, integer value side. `v` is rendered via `.to_s` so
88
- # JSON-numeric output without quoting.
89
- def self.encode_pair_int(k, v)
90
- Json.quote(k) + ":" + v.to_s
91
- end
92
-
93
- # Encode a Hash<String,String> as a JSON object.
94
- def self.from_str_hash(h)
95
- out = "{"
96
- first = true
97
- h.each do |k, v|
98
- if !first
99
- out = out + ","
100
- end
101
- first = false
102
- out = out + Json.quote(k) + ":" + Json.quote(v)
103
- end
104
- out + "}"
105
- end
106
-
107
- # Same shape with integer values. JSON-numeric, no quoting.
108
- def self.from_int_hash(h)
109
- out = "{"
110
- first = true
111
- h.each do |k, v|
112
- if !first
113
- out = out + ","
114
- end
115
- first = false
116
- out = out + Json.quote(k) + ":" + v.to_s
117
- end
118
- out + "}"
119
- end
120
-
121
- # Encode a string array as a JSON array of quoted strings.
122
- def self.from_str_array(a)
123
- out = "["
124
- i = 0
125
- while i < a.length
126
- if i > 0
127
- out = out + ","
128
- end
129
- out = out + Json.quote(a[i])
130
- i += 1
131
- end
132
- out + "]"
133
- end
134
-
135
- # Encode an int array as a JSON array of numbers.
136
- def self.from_int_array(a)
137
- out = "["
138
- i = 0
139
- while i < a.length
140
- if i > 0
141
- out = out + ","
142
- end
143
- out = out + a[i].to_s
144
- i += 1
145
- end
146
- out + "]"
147
- end
148
- end
149
- end
@@ -1,142 +0,0 @@
1
- # SpinelKit::Json::Builder -- an incremental, ordered JSON-object builder.
2
- #
3
- # WHY A SEPARATE FILE/CLASS. This is toy's hand-rolled builder; the codec
4
- # (SpinelKit::Json, encode/decode) is tep's. They are split so a consumer
5
- # compiles only the surface it uses: Spinel has no tree-shaking, so an
6
- # uncalled method is still compiled and degrades its params, emitting benign
7
- # but gate-tripping `emitting 0` warnings. A builder-only consumer requires
8
- # THIS file and never pays for the decoder walkers; a codec-only consumer
9
- # requires spinel_kit/json and never pays for the builder.
10
- #
11
- # To stay self-contained, Builder carries its OWN escape/quote/hex2 rather
12
- # than calling SpinelKit::Json.* (which would drag the whole codec, decoders
13
- # included, into a builder-only compile). These are byte-identical to the
14
- # codec's; the small duplication buys surface isolation. The same-named
15
- # methods across the two classes are safe -- the Spinel name-collision bug
16
- # that once made that dangerous is fixed (see docs/spinel-discipline.md).
17
- #
18
- # USAGE (mutating appenders, NOT method-chaining -- chaining is a Spinel
19
- # poly-degradation risk):
20
- #
21
- # j = SpinelKit::Json::Builder.new
22
- # j.add_str("kind", "run_start")
23
- # j.add_num("t", now) # int OR float via .to_s
24
- # host = SpinelKit::Json::Builder.new
25
- # host.add_str("name", host_name)
26
- # j.add_obj("host", host) # nest a sub-builder
27
- # j.add_raw("lr", "0.001") # already-encoded JSON literal
28
- # ev = j.dump # "{...}"
29
- #
30
- # NUMERIC NOTE: if a single compiled program passes BOTH Integer and Float to
31
- # `add_num`'s `value`, Spinel unifies the param to Float and an Integer
32
- # renders as "N.0". For hardcoded numeric literals where byte-exact output
33
- # matters, use `add_raw` with an already-encoded string instead of relying on
34
- # `value.to_s`.
35
- module SpinelKit
36
- class Json
37
- class Builder
38
- def initialize
39
- @buf = "{"
40
- @first = true
41
- end
42
-
43
- # Append `"key":"escaped-value"`.
44
- def add_str(key, value)
45
- comma
46
- @buf = @buf + Builder.quote(key) + ":" + Builder.quote(value)
47
- end
48
-
49
- # Append `"key":<number>` -- `value.to_s` covers Integer ("5") and
50
- # Float ("1.5"). For hardcoded literals prefer add_raw.
51
- def add_num(key, value)
52
- comma
53
- @buf = @buf + Builder.quote(key) + ":" + value.to_s
54
- end
55
-
56
- # Append `"key":true|false`.
57
- def add_bool(key, value)
58
- comma
59
- @buf = @buf + Builder.quote(key) + ":" + (value ? "true" : "false")
60
- end
61
-
62
- # Append `"key":<already-encoded JSON>` -- for arrays / numeric literals.
63
- def add_raw(key, raw)
64
- comma
65
- @buf = @buf + Builder.quote(key) + ":" + raw
66
- end
67
-
68
- # Append `"key":<nested object>` from another Builder.
69
- def add_obj(key, child)
70
- comma
71
- @buf = @buf + Builder.quote(key) + ":" + child.dump
72
- end
73
-
74
- # Close the object and return the JSON string.
75
- def dump
76
- @buf + "}"
77
- end
78
-
79
- # Emit a separator before every entry except the first.
80
- def comma
81
- if @first
82
- @first = false
83
- else
84
- @buf = @buf + ","
85
- end
86
- end
87
-
88
- # ---- self-contained string escaping (byte-identical to
89
- # SpinelKit::Json.escape/quote/hex2; kept local so a builder-only
90
- # compile never pulls in the codec) ----
91
-
92
- # Wrap a string in JSON quotes, escaping its body.
93
- def self.quote(s)
94
- "\"" + Builder.escape(s) + "\""
95
- end
96
-
97
- # Escape a string for inclusion inside a JSON string literal (no
98
- # surrounding quotes). Handles ", \, and the JSON control-char escapes
99
- # (\b \f \n \r \t); other control bytes go through \u00XX. ASCII-clean
100
- # input passes through unchanged.
101
- def self.escape(s)
102
- out = ""
103
- i = 0
104
- n = s.length
105
- while i < n
106
- c = s[i]
107
- if c == "\""
108
- out = out + "\\\""
109
- elsif c == "\\"
110
- out = out + "\\\\"
111
- elsif c == "\n"
112
- out = out + "\\n"
113
- elsif c == "\r"
114
- out = out + "\\r"
115
- elsif c == "\t"
116
- out = out + "\\t"
117
- elsif c == "\b"
118
- out = out + "\\b"
119
- elsif c == "\f"
120
- out = out + "\\f"
121
- elsif c < " "
122
- b = c.getbyte(0)
123
- out = out + "\\u00" + Builder.hex2(b)
124
- else
125
- out = out + c
126
- end
127
- i += 1
128
- end
129
- out
130
- end
131
-
132
- # Two-digit lowercase hex of a byte (0..255).
133
- def self.hex2(n)
134
- hex = "0123456789abcdef"
135
- out = ""
136
- out = out + hex[(n / 16) % 16, 1]
137
- out = out + hex[n % 16, 1]
138
- out
139
- end
140
- end
141
- end
142
- end
@@ -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.2.0"
6
- end
data/lib/spinel_kit.rb DELETED
@@ -1,45 +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::Hex -- hex digit/byte encode + decode.
15
- # SpinelKit::Url -- percent-encode/decode + form-query + URL split.
16
- # SpinelKit::Git -- git provenance from .git/HEAD (was Toy::Git).
17
- # SpinelKit::Log -- minimal levelled logger (was Tep::Logger).
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/json" # encoders
26
- # require "spinel_kit/json_decoder" # decoders (require alongside json if you decode)
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)
30
- # require "spinel_kit/git"
31
- # require "spinel_kit/log"
32
- #
33
- # No native extension (spinel-ext.json is []), no runtime dependencies. See
34
- # docs/adoption.md and docs/spinel-discipline.md.
35
- require_relative "spinel_kit/version"
36
- require_relative "spinel_kit/json"
37
- require_relative "spinel_kit/json_decoder"
38
- require_relative "spinel_kit/json_builder"
39
- require_relative "spinel_kit/hex"
40
- require_relative "spinel_kit/url"
41
- require_relative "spinel_kit/git"
42
- require_relative "spinel_kit/log"
43
-
44
- module SpinelKit
45
- 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
File without changes