ruby_wasm_ic_rack 0.1.1
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +115 -0
- data/lib/ruby_wasm_ic_rack/version.rb +5 -0
- data/lib/ruby_wasm_ic_rack.rb +120 -0
- metadata +45 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5723c6f1d158956c8dede6d1ddc98b5b5f048ec51931df4d1eadd8531d59aea4
|
|
4
|
+
data.tar.gz: '0119fd70d66b909e5185bd0b17f6570b60550ff99de4d8338a496382882825bc'
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d02430595e7709b5d26efedab2b78f0693e9c6788479942eafd53ef2d2ee1215f86cd50893f9e0a0582dbbe5dbc5f9e4f945d32113da497f604862fd212a94f7
|
|
7
|
+
data.tar.gz: 4528b16357279759bb9195f15dc668923e8826c2edadc68b9cbe198eda50ddea7c49b437f9e13243ed3e5cd76ef219678ff7d1d3cd149447c555a59e92f48911
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 the ruby_wasm_builder authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# ruby_wasm_ic_rack
|
|
2
|
+
|
|
3
|
+
Serve a [Rack](https://github.com/rack/rack) app from a Ruby canister on the
|
|
4
|
+
Internet Computer, through the IC HTTP gateway. For canisters packed by
|
|
5
|
+
[ruby_wasm_builder](https://gitlab.com/phrug/ruby_wasm_builder) from 0.7.0.
|
|
6
|
+
|
|
7
|
+
Pure Ruby, no dependencies. It works the same on the `minimal` and `full`
|
|
8
|
+
Builds: it names no standard library, no C extension and no `Encoding`
|
|
9
|
+
constant.
|
|
10
|
+
|
|
11
|
+
## Carrying it
|
|
12
|
+
|
|
13
|
+
A canister has no `bundle install`. Carry the gem's files in your Application,
|
|
14
|
+
as you carry Rack:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
gem fetch ruby_wasm_ic_rack && gem unpack ruby_wasm_ic_rack-*.gem
|
|
18
|
+
cp -r ruby_wasm_ic_rack-*/lib my_app/vendor/ruby_wasm_ic_rack
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
# app.rb
|
|
23
|
+
$LOAD_PATH.unshift(File.join(__dir__, "vendor/ruby_wasm_ic_rack"))
|
|
24
|
+
require "ruby_wasm_ic_rack"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
To move to a newer version, unpack it and copy its `lib/` again.
|
|
28
|
+
|
|
29
|
+
## Declaring and serving the gateway's methods
|
|
30
|
+
|
|
31
|
+
From ruby_wasm_builder 0.8.0, one word in `ic-interface.rb` declares the
|
|
32
|
+
gateway's types and both methods:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
# ic-interface.rb
|
|
36
|
+
http_gateway
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
On 0.7.0, write them out (this is what the word declares):
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
type :HeaderField, Tuple[Text, Text]
|
|
43
|
+
type :HttpRequest, record(method: Text, url: Text, headers: Vec[:HeaderField], body: Blob,
|
|
44
|
+
certificate_version: Opt[Nat16])
|
|
45
|
+
type :HttpUpdateRequest, record(method: Text, url: Text, headers: Vec[:HeaderField], body: Blob)
|
|
46
|
+
type :HttpResponse, record(status_code: Nat16, headers: Vec[:HeaderField], body: Blob, upgrade: Opt[Bool])
|
|
47
|
+
|
|
48
|
+
query :http_request, takes: [:HttpRequest], returns: :HttpResponse
|
|
49
|
+
update :http_request_update, takes: [:HttpUpdateRequest], returns: :HttpResponse
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Then hand both to your app:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
# app.rb
|
|
56
|
+
APP = ->(env) { [200, { "content-type" => "text/plain" }, ["hello from #{env['PATH_INFO']}"]] }
|
|
57
|
+
|
|
58
|
+
def http_request(req) = RubyWasmIcRack.query(APP, req)
|
|
59
|
+
def http_request_update(req) = RubyWasmIcRack.update(APP, req)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## What it does
|
|
63
|
+
|
|
64
|
+
- **The env:** `REQUEST_METHOD`, `PATH_INFO`, `QUERY_STRING`, `SERVER_NAME`
|
|
65
|
+
(from `host`), `SERVER_PORT` 443, `rack.url_scheme` `https`, every header as
|
|
66
|
+
`HTTP_*` (`CONTENT_TYPE` and `CONTENT_LENGTH` as Rack names them), a repeated
|
|
67
|
+
header joined with `, `, and `rack.input` over the body.
|
|
68
|
+
- **The response:** a header whose value is an Array, or lines joined by
|
|
69
|
+
`"\n"` (Rack 3), goes out as one header each, so two `set-cookie` headers
|
|
70
|
+
arrive as two. The body is joined and closed.
|
|
71
|
+
- **Upgrades.** The gateway calls `http_request` as a query, and a query keeps
|
|
72
|
+
nothing it writes. So `RubyWasmIcRack.query` answers only `GET`, `HEAD` and `OPTIONS`
|
|
73
|
+
and asks the gateway to upgrade everything else, which then calls
|
|
74
|
+
`http_request_update` through consensus. A `GET` that writes says so with the
|
|
75
|
+
response header `x-ic-upgrade` (any value); the query upgrades it, and the
|
|
76
|
+
header is never sent on.
|
|
77
|
+
- **`rack.input`** is a small reader of its own (`read`, `gets`, `each`,
|
|
78
|
+
`rewind`). For a `StringIO`, as Rack's own servers give, redefine
|
|
79
|
+
`RubyWasmIcRack.input`:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
module RubyWasmIcRack
|
|
83
|
+
def self.input(bytes) = StringIO.new(bytes.b)
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## On the `minimal` Build
|
|
88
|
+
|
|
89
|
+
Rack and Roda need four libraries `minimal` lacks: `Encoding::UTF_8` and its
|
|
90
|
+
two siblings, `date` (only because `time` requires it), `strscan` and
|
|
91
|
+
`stringio`. The [`ic_minimal`](../ic_minimal) gem carries pure-Ruby stand-ins
|
|
92
|
+
for them, for a `minimal` canister only. `ruby_wasm_ic_rack` itself needs none of them.
|
|
93
|
+
|
|
94
|
+
Carry only the files of Rack your routes load, and check every carried file's
|
|
95
|
+
magic comment: `minimal` knows only `utf-8`, `us-ascii` and `ascii-8bit` by
|
|
96
|
+
those names, and Rack 3.1's `rack/utils.rb` says `# encoding: binary`, which
|
|
97
|
+
traps the install. Change that word to `ascii-8bit`.
|
|
98
|
+
|
|
99
|
+
## Not yet
|
|
100
|
+
|
|
101
|
+
- **Certified responses.** Until the canister certifies its replies, the
|
|
102
|
+
gateway refuses a plain query's reply on the certified domain
|
|
103
|
+
(`<id>.icp0.io`) and serves it on `<id>.raw.icp0.io`. An upgraded request is
|
|
104
|
+
served on either. So a public page is served raw, or by upgrading every
|
|
105
|
+
`GET` (one update per page view).
|
|
106
|
+
- File uploads (Tempfile) and streaming responses.
|
|
107
|
+
|
|
108
|
+
## Development
|
|
109
|
+
|
|
110
|
+
The gem lives in the ruby_wasm_builder repository, `gems/ruby_wasm_ic_rack/`. Its specs
|
|
111
|
+
are in that repository's `spec/gems/`, and run with the builder's suite
|
|
112
|
+
(`bundle exec rspec`), including the 0.7.0 `rack_adapter` and `roda_on_minimal`
|
|
113
|
+
Scripts played with this gem's files in place of the Challenges' own copies.
|
|
114
|
+
|
|
115
|
+
MIT licensed.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# ruby_wasm_ic_rack: serves a Rack app (anything answering call(env) with
|
|
2
|
+
# [status, headers, body]) from the IC HTTP gateway's two methods, in a Ruby
|
|
3
|
+
# canister packed by ruby_wasm_builder. Pure Ruby with no dependency, the same
|
|
4
|
+
# on the `minimal` and `full` Builds: it names no standard library, no C
|
|
5
|
+
# extension and no Encoding constant. rack.input is a small reader of its own
|
|
6
|
+
# unless the Application redefines RubyWasmIcRack.input (a StringIO, say).
|
|
7
|
+
#
|
|
8
|
+
# Taken from the builder's 0.7.0 Challenges (rack_adapter, roda_on_minimal),
|
|
9
|
+
# which keep their own copies; this gem moves on its own versions.
|
|
10
|
+
#
|
|
11
|
+
# A query can't keep a write, so http_request answers only the safe methods
|
|
12
|
+
# (GET, HEAD, OPTIONS) and asks the gateway to upgrade everything else to
|
|
13
|
+
# http_request_update. An app whose GET writes says so with the response
|
|
14
|
+
# header `x-ic-upgrade`, and the query upgrades that too; the header is never
|
|
15
|
+
# sent on.
|
|
16
|
+
require_relative "ruby_wasm_ic_rack/version"
|
|
17
|
+
|
|
18
|
+
module RubyWasmIcRack
|
|
19
|
+
SAFE = %w[GET HEAD OPTIONS].freeze
|
|
20
|
+
UPGRADE = "x-ic-upgrade"
|
|
21
|
+
|
|
22
|
+
# rack.input over the body's bytes: what Rack 3 asks of an input stream.
|
|
23
|
+
class Input
|
|
24
|
+
def initialize(bytes)
|
|
25
|
+
@bytes = bytes.b
|
|
26
|
+
@pos = 0
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def read(length = nil, buffer = nil)
|
|
30
|
+
rest = @bytes.byteslice(@pos, @bytes.bytesize - @pos) || "".b
|
|
31
|
+
chunk = length ? rest.byteslice(0, length) : rest
|
|
32
|
+
@pos += chunk.bytesize
|
|
33
|
+
return nil if length && length.positive? && chunk.empty?
|
|
34
|
+
|
|
35
|
+
buffer ? buffer.replace(chunk) : chunk
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def gets
|
|
39
|
+
return nil if @pos >= @bytes.bytesize
|
|
40
|
+
|
|
41
|
+
stop = @bytes.index("\n", @pos)
|
|
42
|
+
line = @bytes.byteslice(@pos, (stop ? stop + 1 : @bytes.bytesize) - @pos)
|
|
43
|
+
@pos += line.bytesize
|
|
44
|
+
line
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def each
|
|
48
|
+
while (line = gets)
|
|
49
|
+
yield line
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def rewind = @pos = 0
|
|
54
|
+
def close = nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
module_function
|
|
58
|
+
|
|
59
|
+
# rack.input over the body. An Application may redefine it (roda_on_minimal
|
|
60
|
+
# gives a StringIO).
|
|
61
|
+
def input(bytes) = Input.new(bytes)
|
|
62
|
+
|
|
63
|
+
def env(req)
|
|
64
|
+
path, query = req[:url].split("?", 2)
|
|
65
|
+
host = header(req, "host").to_s
|
|
66
|
+
env = {
|
|
67
|
+
"REQUEST_METHOD" => req[:method],
|
|
68
|
+
"SCRIPT_NAME" => "",
|
|
69
|
+
"PATH_INFO" => path.to_s.empty? ? "/" : path,
|
|
70
|
+
"QUERY_STRING" => query.to_s,
|
|
71
|
+
"SERVER_NAME" => host.split(":").first.to_s,
|
|
72
|
+
"SERVER_PORT" => "443",
|
|
73
|
+
"SERVER_PROTOCOL" => "HTTP/1.1",
|
|
74
|
+
"rack.url_scheme" => "https",
|
|
75
|
+
"rack.input" => input(req[:body]),
|
|
76
|
+
"rack.errors" => $stderr
|
|
77
|
+
}
|
|
78
|
+
req[:headers].each do |name, value|
|
|
79
|
+
key = name.upcase.tr("-", "_")
|
|
80
|
+
key = "HTTP_#{key}" unless %w[CONTENT_TYPE CONTENT_LENGTH].include?(key)
|
|
81
|
+
env[key] = env.key?(key) ? "#{env[key]}, #{value}" : value
|
|
82
|
+
end
|
|
83
|
+
env
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def header(req, name) = req[:headers].find { |n, _| n.downcase == name }&.last
|
|
87
|
+
|
|
88
|
+
# The gateway's response record from a Rack response. A header's value may
|
|
89
|
+
# be an Array, or lines joined by "\n" (Rack 3): one header each.
|
|
90
|
+
def response(status, headers, body)
|
|
91
|
+
pairs = headers.flat_map do |name, value|
|
|
92
|
+
Array(value).flat_map { |v| v.to_s.split("\n") }.map { |v| [name.to_s.downcase, v] }
|
|
93
|
+
end
|
|
94
|
+
bytes = +"".b
|
|
95
|
+
body.each { |part| bytes << part.b }
|
|
96
|
+
{ status_code: Integer(status), headers: pairs, body: bytes, upgrade: nil }
|
|
97
|
+
ensure
|
|
98
|
+
body.close if body.respond_to?(:close)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def upgrade = { status_code: 200, headers: [], body: "".b, upgrade: true }
|
|
102
|
+
|
|
103
|
+
# http_request: the query.
|
|
104
|
+
def query(app, req)
|
|
105
|
+
return upgrade unless SAFE.include?(req[:method])
|
|
106
|
+
|
|
107
|
+
status, headers, body = app.call(env(req))
|
|
108
|
+
if headers.keys.any? { |k| k.to_s.downcase == UPGRADE }
|
|
109
|
+
body.close if body.respond_to?(:close)
|
|
110
|
+
return upgrade
|
|
111
|
+
end
|
|
112
|
+
response(status, headers, body)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# http_request_update: the update, after an upgrade.
|
|
116
|
+
def update(app, req)
|
|
117
|
+
status, headers, body = app.call(env(req))
|
|
118
|
+
response(status, headers.reject { |k, _| k.to_s.downcase == UPGRADE }, body)
|
|
119
|
+
end
|
|
120
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ruby_wasm_ic_rack
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- graial
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Use this gem to run rack and roda on the ruby.wasm build for Internet
|
|
13
|
+
Computer.
|
|
14
|
+
email:
|
|
15
|
+
- graial@paglalayag.net
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- LICENSE
|
|
21
|
+
- README.md
|
|
22
|
+
- lib/ruby_wasm_ic_rack.rb
|
|
23
|
+
- lib/ruby_wasm_ic_rack/version.rb
|
|
24
|
+
homepage: https://github.com/graial/ruby_wasm_ic_rack
|
|
25
|
+
licenses:
|
|
26
|
+
- MIT
|
|
27
|
+
metadata: {}
|
|
28
|
+
rdoc_options: []
|
|
29
|
+
require_paths:
|
|
30
|
+
- lib
|
|
31
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
32
|
+
requirements:
|
|
33
|
+
- - ">="
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: 3.3.3
|
|
36
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
41
|
+
requirements: []
|
|
42
|
+
rubygems_version: 3.6.7
|
|
43
|
+
specification_version: 4
|
|
44
|
+
summary: Run rack and roda on the ruby.wasm build for IC!
|
|
45
|
+
test_files: []
|