Skip to content

Commit 7d5a4c5

Browse files
committed
add geolocation weather example
+ requires api token for weather site + can provide an ip addr to test with SPIN_VARIABLE_TEST_IP_ADDR=<some ip> SPIN_VARIABLE_WAQI_API_TOKEN=<some token> spin up Signed-off-by: Michelle Dhanani <[email protected]>
1 parent 93ec341 commit 7d5a4c5

File tree

10 files changed

+3988
-0
lines changed

10 files changed

+3988
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
dist
3+
target
4+
.spin/
5+
build/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
KNITWIT_SOURCE=./config/knitwit.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"version": 1,
3+
"project": {
4+
"worlds": [
5+
"spin-http"
6+
]
7+
},
8+
"packages": {
9+
"@fermyon/spin-sdk": {
10+
"witPath": "../../bin/wit",
11+
"world": "spin-imports"
12+
}
13+
}
14+
}

samples/geolocation-weather-application/package-lock.json

+3,732
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "geolocation-weather-application",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"build": "knitwit --out-dir build/wit/knitwit --out-world combined && npx webpack --mode=production && npx mkdirp dist && npx j2w -i build/bundle.js -d build/wit/knitwit -n combined -o dist/geolocation-weather-application.wasm",
8+
"test": "echo \"Error: no test specified\" && exit 1"
9+
},
10+
"keywords": [],
11+
"author": "",
12+
"license": "ISC",
13+
"devDependencies": {
14+
"mkdirp": "^3.0.1",
15+
"ts-loader": "^9.4.1",
16+
"typescript": "^4.8.4",
17+
"webpack": "^5.74.0",
18+
"webpack-cli": "^4.10.0",
19+
"@fermyon/knitwit": "0.3.0"
20+
},
21+
"dependencies": {
22+
"@fermyon/spin-sdk": "^3.0.0",
23+
"itty-router": "^5.0.18"
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
spin_manifest_version = 2
2+
3+
[application]
4+
authors = ["John Doe <[email protected]>"]
5+
description = ""
6+
name = "geolocation-weather-application"
7+
version = "0.1.0"
8+
9+
[variables]
10+
waqi_api_token = { required = true }
11+
test_ip_addr = { default = "63.65.232.194" }
12+
13+
[[trigger.http]]
14+
route = "/..."
15+
component = "geolocation-weather-application"
16+
17+
[component.geolocation-weather-application]
18+
allowed_outbound_hosts = ["https://api.waqi.info", "https://ip-api.io"]
19+
source = "dist/geolocation-weather-application.wasm"
20+
exclude_files = ["**/node_modules"]
21+
[component.geolocation-weather-application.build]
22+
command = ["npm install", "npm run build"]
23+
watch = ["src/**/*.ts"]
24+
[component.geolocation-weather-application.variables]
25+
waqi_api_token = "{{ waqi_api_token }}"
26+
test_ip_addr = "{{ test_ip_addr }}"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const getClientAddressFromRequest = (req: Request): string | null => {
2+
const clientAddress = req.headers.get("spin-client-addr");
3+
if (clientAddress) {
4+
return clientAddress;
5+
}
6+
return req.headers.get("true-client-ip");
7+
};
8+
9+
const cleanupIpAddress = (input: string): string => {
10+
const ipv4Regex = /^(\\d{1,3}\\.){3}\d{1,3}:/;
11+
const ipv4RegexWithPort = /^(\d{1,3}\.){3}\d{1,3}:\d+$/;
12+
const ipv6Regex = /^([a-fA-F0-9:]+)$/; // not perfect TODO
13+
const ipv6WithPortRegex = /^\[([a-fA-F0-9:]+)\]:\d+$/;
14+
15+
if (ipv4RegexWithPort.test(input)) {
16+
return input.split(':')[0];
17+
} else if (ipv6WithPortRegex.test(input)) {
18+
const match = RegExp(ipv6WithPortRegex).exec(input);
19+
return match ? match[1] : input;
20+
} else if (ipv4Regex.test(input) || ipv6Regex.test(input)) {
21+
return input;
22+
} else {
23+
console.log("Invalid IP address", input);
24+
return input;
25+
}
26+
};
27+
28+
export {
29+
getClientAddressFromRequest,
30+
cleanupIpAddress
31+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// For AutoRouter documentation refer to https://itty.dev/itty-router/routers/autorouter
2+
import { AutoRouter } from 'itty-router';
3+
import { getClientAddressFromRequest, cleanupIpAddress } from "./helpers";
4+
import { Variables } from "@fermyon/spin-sdk";
5+
6+
7+
let router = AutoRouter();
8+
9+
// Route ordering matters, the first route that matches will be used
10+
// Any route that does not return will be treated as a middleware
11+
// Any unmatched route will return a 404
12+
router
13+
.get("/", getWeather);
14+
15+
//@ts-ignore
16+
addEventListener('fetch', async (event: FetchEvent) => {
17+
event.respondWith(router.fetch(event.request));
18+
});
19+
20+
async function getWeather(request: Request): Promise<Response> {
21+
console.log("Request received", request.headers.get("spin-client-addr"));
22+
23+
const clientAddress = getClientAddressFromRequest(request);
24+
if (!clientAddress) {
25+
return new Response("Could not determine client ip address", { status: 500 });
26+
}
27+
28+
let ip = cleanupIpAddress(clientAddress);
29+
console.log(`Client IP: ${ip}`);
30+
ip = ip === "127.0.0.1" ? Variables.get("test_ip_addr") ?? ip : ip;
31+
32+
let longitude, latitude;
33+
try {
34+
[latitude, longitude] = await getGeolocation(ip);
35+
} catch (error) {
36+
if (ip == "127.0.0.1") {
37+
return new Response("Unable to get geolocation data for localhost, try using test_ip_addr variable", { status: 500 });
38+
}
39+
return new Response("Failed to get geolocation data", { status: 500 });
40+
}
41+
42+
43+
let endpoint = "https://api.waqi.info/feed/geo:";
44+
let token = Variables.get("waqi_api_token"); //Use a token from https://aqicn.org/api/
45+
let html_style = `body{padding:6em; font-family: sans-serif;} h1{color:#f6821f}`;
46+
47+
let html_content = "<h1>Weather 🌦</h1>";
48+
49+
endpoint += `${latitude};${longitude}/?token=${token}`;
50+
const init = {
51+
headers: {
52+
"content-type": "application/json;charset=UTF-8",
53+
},
54+
};
55+
56+
const response = await fetch(endpoint, init);
57+
console.log("response", response.status);
58+
if (response.status !== 200) {
59+
return new Response("Failed to get weather info", { status: 500 });
60+
}
61+
const content = await response.json();
62+
63+
html_content += `<p>This is a demo using geolocation data. </p>`;
64+
html_content += `You are located at: ${latitude},${longitude}.</p>`;
65+
html_content += `<p>Based off sensor data from <a href="${content.data.city.url}">${content.data.city.name}</a>:</p>`;
66+
html_content += `<p>The temperature is: ${content.data.iaqi.t?.v}°C.</p>`;
67+
html_content += `<p>The AQI level is: ${content.data.aqi}.</p>`;
68+
html_content += `<p>The N02 level is: ${content.data.iaqi.no2?.v}.</p>`;
69+
html_content += `<p>The O3 level is: ${content.data.iaqi.o3?.v}.</p>`;
70+
71+
let html = `
72+
<!DOCTYPE html>
73+
<head>
74+
<title>Geolocation: Weather</title>
75+
</head>
76+
<body>
77+
<style>${html_style}</style>
78+
<div id="container">
79+
${html_content}
80+
</div>
81+
</body>`;
82+
83+
return new Response(html, {
84+
headers: {
85+
"content-type": "text/html;charset=UTF-8",
86+
},
87+
});
88+
}
89+
90+
async function getGeolocation(ip: string): Promise<[number, number]> {
91+
console.log("Fetching geolocation data for", ip);
92+
const response = await fetch(`https://ip-api.io/json/${ip}`);
93+
if (!response.ok) {
94+
throw new Error(`Failed to fetch geolocation data: ${response.status}`);
95+
}
96+
const data = await response.json();
97+
return [data.latitude, data.longitude];
98+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"compilerOptions": {
3+
"outDir": "./dist/",
4+
"noImplicitAny": true,
5+
"module": "es6",
6+
"target": "es2020",
7+
"jsx": "react",
8+
"skipLibCheck": true,
9+
"lib": [
10+
"ES2020",
11+
"WebWorker"
12+
],
13+
"allowJs": true,
14+
"strict": true,
15+
"noImplicitReturns": true,
16+
"moduleResolution": "node"
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
const path = require('path');
2+
const SpinSdkPlugin = require("@fermyon/spin-sdk/plugins/webpack")
3+
4+
module.exports = {
5+
entry: './src/index.ts',
6+
experiments: {
7+
outputModule: true,
8+
},
9+
module: {
10+
rules: [
11+
{
12+
test: /\.tsx?$/,
13+
use: 'ts-loader',
14+
exclude: /node_modules/,
15+
},
16+
],
17+
},
18+
resolve: {
19+
extensions: ['.tsx', '.ts', '.js'],
20+
},
21+
output: {
22+
path: path.resolve(__dirname, './build'),
23+
filename: 'bundle.js',
24+
module: true,
25+
library: {
26+
type: "module",
27+
}
28+
},
29+
plugins: [
30+
new SpinSdkPlugin()
31+
],
32+
optimization: {
33+
minimize: false
34+
},
35+
performance: {
36+
hints: false,
37+
}
38+
};

0 commit comments

Comments
 (0)