Skip to content
This repository was archived by the owner on Jul 10, 2025. It is now read-only.

Commit d2009e3

Browse files
committed
added HIPPOFACTS and tests
Signed-off-by: Matt Butcher <matt.butcher@microsoft.com>
1 parent 57bc81c commit d2009e3

File tree

8 files changed

+261
-14
lines changed

8 files changed

+261
-14
lines changed

HIPPOFACTS

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[bindle]
2+
name = "fileserver"
3+
version = "0.1.0"
4+
description = "Provides static file serving"
5+
6+
[[handler]]
7+
route = "/static/..."
8+
name = "fileserver.gr.wasm"
9+
files = ["README.md", "LICENSE.txt"]

Makefile

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# These are for both `run` (implicit) and `test` (explicit)
22
PATH_INFO ?= /static/fileserver.gr
33
X_MATCHED_ROUTE ?= /static/...
4+
BINDLE_SERVER_URL ?= http://localhost:8080/v1
45

56
.PHONY: run
67
run:
@@ -11,9 +12,18 @@ run:
1112
build:
1213
grain compile fileserver.gr
1314

15+
.PHONY: test-unit
16+
test-unit:
17+
grain tests.gr
18+
1419
.PHONY: test
1520
test:build
21+
test: test-unit
1622
test:
1723
wasmtime --dir . --env PATH_INFO=${PATH_INFO} \
1824
--env X_MATCHED_ROUTE=${X_MATCHED_ROUTE} \
19-
fileserver.gr.wasm
25+
fileserver.gr.wasm > /dev/null
26+
27+
.PHONY: push
28+
push:
29+
hippofactory -s ${BINDLE_SERVER_URL} .

README.md

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,34 @@ $ make test
3131

3232
## Running in Wagi
3333

34+
You have two options for running in Wagi:
35+
36+
1. Use [hippofactory](https://github.com/deislabs/hippofactory) to build and push a bindle, then use `wagi -b $YOUR_BINDLE`
37+
2. Write a `modules.toml` file and use `wagi -c modules.toml`.
38+
39+
### Using `hippofactory`
40+
41+
Edit the `HIPPOFACTS` file to taste.
42+
43+
To use `hippofactory`, you can just run this command from the repo root:
44+
45+
```console
46+
$ hippofactory -s http://localhost:8080/v1 .
47+
pushed: fileserver/0.1.0-technosophos-2021.06.03.17.25.54.484
48+
```
49+
50+
Then run it in Wagi like this:
51+
52+
```console
53+
$ wagi -b fileserver/0.1.0-technosophos-2021.06.03.17.25.54.484 --bindle-server http://localhost:8080/v1
54+
[2021-06-03T23:26:54Z INFO wagi] => Starting server on 127.0.0.1:3000
55+
[2021-06-03T23:26:54Z DEBUG wagi::runtime::bindle] loaded 1 modules from the default group (parcels that do not have conditions.memberOf set)
56+
[2021-06-03T23:26:54Z DEBUG wagi::runtime] module cache miss. Loading module parcel:fileserver/0.1.0-technosophos-2021.06.03.17.25.54.484#110f6f54401b80d9d80dae9257969468a5a70248dba8d96ce74b9bc5bc104fdd from remote.
57+
[2021-06-03T23:26:54Z INFO wagi::runtime] (load_routes) instantiation time for module parcel:fileserver/0.1.0-technosophos-2021.06.03.17.25.54.484#110f6f54401b80d9d80dae9257969468a5a70248dba8d96ce74b9bc5bc104fdd: 107.802106ms
58+
```
59+
60+
### Using `modules.toml`
61+
3462
Here is an example `modules.toml` for [Wagi](https://github.com/deislabs/wagi):
3563

3664
```toml
@@ -42,28 +70,50 @@ volumes = {"/" = "/path/to/fileserver"}
4270

4371
The above configures Wagi to map the path `/static/...` to the `fileserver.gr.wasm` module. Then it serves all of the files in this project.
4472

73+
### Testing the Static Fileserver with `curl`
74+
75+
This step is the same whether you use Bindle or a `modules.toml`.
76+
4577
Assuming you have Wagi running on `http://localhost:3000`, you can then run this command:
4678

4779
```console
48-
$ curl -v localhost:3000/static/fileserver.gr
80+
$ curl -v localhost:3000/static/LICENSE.txt
4981
* Trying 127.0.0.1...
5082
* TCP_NODELAY set
5183
* Connected to localhost (127.0.0.1) port 3000 (#0)
52-
> GET /static/fileserver.gr HTTP/1.1
84+
> GET /static/LICENSE.txt HTTP/1.1
5385
> Host: localhost:3000
5486
> User-Agent: curl/7.64.1
5587
> Accept: */*
56-
>
88+
>
5789
< HTTP/1.1 200 OK
5890
< content-type: text/plain
59-
< content-length: 1522
60-
< date: Thu, 03 Jun 2021 16:44:05 GMT
61-
<
62-
// This is a simple Wagi static file server.
63-
64-
import Env from "./env"
65-
import Map from "map"
66-
// The rest of the source of fileserver.gr
91+
< content-length: 1104
92+
< date: Fri, 04 Jun 2021 00:16:14 GMT
93+
<
94+
The MIT License (MIT)
95+
96+
Copyright (c) Microsoft Corporation. All rights reserved.
97+
98+
Permission is hereby granted, free of charge, to any person obtaining a copy
99+
of this software and associated documentation files (the "Software"), to deal
100+
in the Software without restriction, including without limitation the rights
101+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
102+
copies of the Software, and to permit persons to whom the Software is
103+
furnished to do so, subject to the following conditions:
104+
105+
The above copyright notice and this permission notice shall be included in all
106+
copies or substantial portions of the Software.
107+
108+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
109+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
110+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
111+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
112+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
113+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
114+
SOFTWARE
115+
* Connection #0 to host localhost left intact
116+
* Closing connection 0
67117
```
68118

69119
The fileserver took `/static/filserver.gr`, removed the `/static/` part from the front, and then loaded `fileserver.gr` from the directory mounted in the `modules.toml`. Note that any subdirectories are also served. So `/static/foo/bar` would translate to the path `foo/bar` inside of the WebAssembly module (which in the example above would fully resolve to "/path/to/fileserver/foo/bar").

env.gr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import Option from "option"
77
// Split an environment variable at the first equals sign.
88
// @param item: An environment variable pair, separated by an equals sign (=).
99
// @return (String, String) A tuple key/value pair.
10-
let splitEnvVar = (item) => {
10+
export let splitEnvVar = (item) => {
1111
let offsetOpt = String.indexOf("=", item)
1212

1313
// For now, fail if the env var is malformed.

fileserver.gr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import File from "sys/file"
77
import String from "string"
88

99
let serve = (path) => {
10-
File.fdWrite(File.stderr, "Loading file ")
10+
File.fdWrite(File.stderr, "Fileserver: Loading file ")
1111
File.fdWrite(File.stderr, path)
1212
File.fdWrite(File.stderr, "\n")
1313

mediatype.gr

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import String from "string"
2+
import Array from "array"
3+
import Option from "option"
4+
import Map from "map"
5+
import {lastIndexOf, reverse} from "./stringutil"
6+
7+
let default_mt = "application/octet-stream"
8+
let mut mediatypes = Map.make()
9+
10+
// Text formats
11+
Map.set("txt", "text/plain", mediatypes)
12+
Map.set("md", "text/plain", mediatypes)
13+
Map.set("mdown", "text/plain", mediatypes)
14+
Map.set("htm", "text/html", mediatypes)
15+
Map.set("html", "text/html", mediatypes)
16+
Map.set("xhtml", "application/xhtml+xml", mediatypes)
17+
Map.set("xml", "application/xml", mediatypes)
18+
Map.set("css", "text/css", mediatypes)
19+
Map.set("ics", "text/calendar", mediatypes)
20+
21+
// Serialization formats
22+
Map.set("json", "application/json", mediatypes)
23+
Map.set("jsonld", "application/ld+json", mediatypes)
24+
Map.set("toml", "application/toml", mediatypes)
25+
Map.set("yaml", "application/yaml", mediatypes)
26+
27+
// Applications
28+
// According to MSDN, prefered is text/javascript
29+
Map.set("js", "text/javascript", mediatypes)
30+
Map.set("mjs", "text/javascript", mediatypes)
31+
Map.set("wasm", "application/wasm", mediatypes)
32+
Map.set("csv", "text/csv", mediatypes)
33+
Map.set("sh", "application/x-sh", mediatypes)
34+
35+
// Images
36+
Map.set("apng", "image/apng", mediatypes)
37+
Map.set("avif", "image/avif", mediatypes)
38+
Map.set("png", "image/png", mediatypes)
39+
Map.set("png", "image/png", mediatypes)
40+
Map.set("jpg", "image/jpeg", mediatypes)
41+
Map.set("jpeg", "image/jpeg", mediatypes)
42+
Map.set("pjpeg", "image/jpeg", mediatypes)
43+
Map.set("pjp", "image/jpeg", mediatypes)
44+
Map.set("jfif", "image/jpeg", mediatypes)
45+
Map.set("gif", "image/gif", mediatypes)
46+
Map.set("tif", "image/tiff", mediatypes)
47+
Map.set("tiff", "image/tiff", mediatypes)
48+
Map.set("webp", "image/webp", mediatypes)
49+
Map.set("svg", "image/svg+xml", mediatypes)
50+
Map.set("bmp", "image/bmp", mediatypes)
51+
Map.set("ico", "image/vnd.microsoft.icon", mediatypes)
52+
53+
// Audio/Video
54+
Map.set("aac", "audio/aac", mediatypes)
55+
Map.set("avi", "video/x-msvideo", mediatypes)
56+
Map.set("wav", "audio/wave", mediatypes)
57+
Map.set("webm", "video/webm", mediatypes)
58+
Map.set("mp3", "audio/mpeg", mediatypes)
59+
Map.set("mp4", "video/mp4", mediatypes)
60+
Map.set("mpeg", "video/mpeg", mediatypes)
61+
Map.set("oga", "audio/ogg", mediatypes)
62+
Map.set("ogv", "video/ogg", mediatypes)
63+
Map.set("ogx", "application/ogg", mediatypes)
64+
Map.set("ts", "video/mp2t", mediatypes)
65+
66+
// Compressed
67+
Map.set("bz2", "application/x-bzip2", mediatypes)
68+
Map.set("tbz", "application/x-bzip2", mediatypes)
69+
Map.set("tbz2", "application/x-bzip2", mediatypes)
70+
Map.set("gz", "application/gzip", mediatypes)
71+
Map.set("rar", "application/vnd.rar", mediatypes)
72+
Map.set("tar", "text/x-tar", mediatypes)
73+
Map.set("tgz", "application/gzip", mediatypes)
74+
Map.set("jar", "application/java-archive", mediatypes)
75+
Map.set("mpkg", "application/vnd.apple.installer+xml", mediatypes)
76+
Map.set("zip", "application/zip", mediatypes)
77+
Map.set("7z", "application/x-7z-compressed", mediatypes)
78+
79+
// Binary
80+
Map.set("azw", "application/vnd.amazon.ebook", mediatypes)
81+
Map.set("bin", "application/octet-stream", mediatypes)
82+
Map.set("doc", "application/msword", mediatypes)
83+
Map.set("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", mediatypes)
84+
Map.set("epub", "application/epub+zip", mediatypes)
85+
Map.set("odp", "application/vnd.oasis.opendocument.presentation", mediatypes)
86+
Map.set("ods", "application/vnd.oasis.opendocument.spreadsheet", mediatypes)
87+
Map.set("odt", "application/vnd.oasis.opendocument.text", mediatypes)
88+
Map.set("pdf", "application/pdf", mediatypes)
89+
Map.set("ppt", "application/vnd.ms-powerpoint", mediatypes)
90+
Map.set("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation", mediatypes)
91+
Map.set("rtf", "application/rtf", mediatypes)
92+
Map.set("vsd", "application/vnd.visio", mediatypes)
93+
Map.set("xls", "application/vnd.ms-excel", mediatypes)
94+
Map.set("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", mediatypes)
95+
96+
// Fonts
97+
Map.set("eot", "application/vnd.ms-fontobject", mediatypes)
98+
Map.set("otf", "font/otf", mediatypes)
99+
Map.set("ttf", "font/ttf", mediatypes)
100+
Map.set("woff", "font/woff", mediatypes)
101+
Map.set("woff2", "font/woff2", mediatypes)
102+
103+
// Guess the media type of this file
104+
// @param filename: The name of the file
105+
export let guess = (filename: String) => {
106+
match (lastIndexOf(".", filename)) {
107+
Some(extOffset) => {
108+
let ext = String.slice(extOffset + 1, String.length(filename), filename)
109+
Option.unwrapWithDefault(default_mt, Map.get(ext, mediatypes))
110+
},
111+
None => default_mt
112+
}
113+
}

stringutil.gr

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import String from "string"
2+
import Array from "array"
3+
4+
// Return a String that is the reverse of the given String.
5+
export let reverse = (str: String) => {
6+
let chars = String.explode(str)
7+
let clen = Array.length(chars)
8+
let rev = Array.init(clen, (index) => {
9+
let last = clen - index - 1
10+
chars[last]
11+
})
12+
String.implode(rev)
13+
}
14+
15+
// Get the index of the last appearance of needle in the haystack.
16+
// @param needle: The string to search for
17+
// @param haystack: The string to be searched
18+
export let lastIndexOf = (needle: String, haystack: String) => {
19+
let rev = reverse(haystack)
20+
let i = String.indexOf(needle, rev)
21+
match (i) {
22+
Some(offset) => Some(String.length(haystack) - 1 - offset),
23+
None => None,
24+
}
25+
}

tests.gr

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import Env from "./env"
2+
import Util from "./stringutil"
3+
import String from "string"
4+
import Process from "sys/process"
5+
import File from "sys/file"
6+
7+
let mut totalErr = 0
8+
9+
let check = (a, b, msg: String) => {
10+
match (a == b) {
11+
true => Ok(String.concat("✅ PASS\t\t", msg)),
12+
_ => {
13+
totalErr += 1
14+
Err(String.concat("⛔️ FAIL\t\t", msg))
15+
}
16+
}
17+
}
18+
19+
let expect = (a, b, msg: String) => {
20+
match (check(a, b, msg)) {
21+
Ok(yay) => print(yay),
22+
Err(e) => print(e)
23+
}
24+
}
25+
26+
let report = () => {
27+
if (totalErr > 0) {
28+
File.fdWrite(File.stderr, "❌ Total failed tests: ")
29+
File.fdWrite(File.stderr, toString(totalErr))
30+
File.fdWrite(File.stderr, "❌\n")
31+
Process.exit(1)
32+
}
33+
}
34+
35+
expect(("a", "b"), Env.splitEnvVar("a=b"), "Env.splitEnvVar should parse")
36+
expect("gfedcba", Util.reverse("abcdefg"), "Util.reverse should reverse string")
37+
expect(Some(5), Util.lastIndexOf("..", "aaaa.."), "UtillastIndexOf should find Some")
38+
expect(None, Util.lastIndexOf("??", "aaaa.."), "Util.lastIndexOf should find None")
39+
40+
report()

0 commit comments

Comments
 (0)