add http-client

This commit is contained in:
Sebastian Hugentobler 2022-03-25 19:18:55 +01:00
parent ac2904588c
commit 8bcd555d71
36 changed files with 1978 additions and 49 deletions

View File

@ -5,6 +5,8 @@
<sourceFolder url="file://$MODULE_DIR$/bank/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/http-server/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/socket-server/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/http-client/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/http-lib/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />

356
Cargo.lock generated
View File

@ -19,6 +19,21 @@ dependencies = [
"tokio-util 0.7.0",
]
[[package]]
name = "actix-cors"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "414360eed71ba2d5435b185ba43ecbe281dfab5df3898286d6b7be8074372c92"
dependencies = [
"actix-utils",
"actix-web",
"derive_more",
"futures-util",
"log",
"once_cell",
"smallvec",
]
[[package]]
name = "actix-http"
version = "3.0.4"
@ -228,6 +243,12 @@ version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27"
[[package]]
name = "anymap2"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
[[package]]
name = "atty"
version = "0.2.14"
@ -259,6 +280,15 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]]
name = "bitflags"
version = "1.3.2"
@ -274,6 +304,12 @@ dependencies = [
"generic-array",
]
[[package]]
name = "boolinator"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9"
[[package]]
name = "brotli"
version = "3.3.3"
@ -295,6 +331,12 @@ dependencies = [
"alloc-stdlib",
]
[[package]]
name = "bumpalo"
version = "3.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
[[package]]
name = "bytes"
version = "1.1.0"
@ -325,6 +367,16 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
"cfg-if",
"wasm-bindgen",
]
[[package]]
name = "convert_case"
version = "0.4.0"
@ -496,8 +548,116 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.10.2+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
name = "gloo"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23947965eee55e3e97a5cd142dd4c10631cc349b48cecca0ed230fd296f568cd"
dependencies = [
"gloo-console",
"gloo-dialogs",
"gloo-events",
"gloo-file",
"gloo-render",
"gloo-storage",
"gloo-timers",
"gloo-utils",
]
[[package]]
name = "gloo-console"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3907f786f65bbb4f419e918b0c5674175ef1c231ecda93b2dbd65fd1e8882637"
dependencies = [
"js-sys",
"serde",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-dialogs"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ffb557a2ea2ed283f1334423d303a336fad55fb8572d51ae488f828b1464b40"
dependencies = [
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-events"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "088514ec8ef284891c762c88a66b639b3a730134714692ee31829765c5bc814f"
dependencies = [
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-file"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa5d6084efa4a2b182ef3a8649cb6506cb4843f22cf907c6e0a799944248ae90"
dependencies = [
"gloo-events",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-render"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b4cda6e149df3bb4a3c6a343873903e5bcc2448a9877d61bb8274806ad67f6e"
dependencies = [
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-storage"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5057761927af1b1929d02b1f49cf83553dd347a473ee7c8bb08420f2673ffc"
dependencies = [
"gloo-utils",
"js-sys",
"serde",
"serde_json",
"thiserror",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-timers"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d12a7f4e95cfe710f1d624fb1210b7d961a5fb05c4fd942f4feab06e61f590e"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "gloo-utils"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05c77af6f96a4f9e27c8ac23a88407381a31f4a74c3fb985c85aa79b8d898136"
dependencies = [
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
@ -545,12 +705,39 @@ dependencies = [
"itoa",
]
[[package]]
name = "http-client"
version = "0.1.0"
dependencies = [
"bank",
"gloo-console",
"http-lib",
"js-sys",
"serde",
"serde_json",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"yew",
"yew-agent",
]
[[package]]
name = "http-lib"
version = "0.1.0"
dependencies = [
"bank",
"serde",
]
[[package]]
name = "http-server"
version = "0.1.0"
dependencies = [
"actix-cors",
"actix-web",
"bank",
"http-lib",
"log",
"pretty_env_logger",
"serde",
@ -615,6 +802,15 @@ dependencies = [
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "language-tags"
version = "0.3.2"
@ -817,6 +1013,30 @@ dependencies = [
"log",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.36"
@ -912,6 +1132,12 @@ version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
[[package]]
name = "scoped-tls-hkt"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2e9d7eaddb227e8fbaaa71136ae0e1e913ca159b86c7da82f3e8f0044ad3a63"
[[package]]
name = "scopeguard"
version = "1.1.0"
@ -1226,6 +1452,84 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06"
dependencies = [
"cfg-if",
"serde",
"serde_json",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca"
dependencies = [
"bumpalo",
"lazy_static",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb6ec270a31b1d3c7e266b999739109abce8b6c87e4b31fcfcd788b65267395"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2"
[[package]]
name = "web-sys"
version = "0.3.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winapi"
version = "0.3.9"
@ -1300,6 +1604,58 @@ version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316"
[[package]]
name = "yew"
version = "0.19.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a1ccb53e57d3f7d847338cf5758befa811cabe207df07f543c06f502f9998cd"
dependencies = [
"console_error_panic_hook",
"gloo",
"gloo-utils",
"indexmap",
"js-sys",
"scoped-tls-hkt",
"slab",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"yew-macro",
]
[[package]]
name = "yew-agent"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "616700dc3851945658c44ba4477ede6b77c795462fbbb9b0ad9a8b6273a3ca77"
dependencies = [
"anymap2",
"bincode",
"gloo-console",
"gloo-utils",
"js-sys",
"serde",
"slab",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"yew",
]
[[package]]
name = "yew-macro"
version = "0.19.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fab79082b556d768d6e21811869c761893f0450e1d550a67892b9bce303b7bb"
dependencies = [
"boolinator",
"lazy_static",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zstd"
version = "0.10.0+zstd.1.5.2"

View File

@ -2,6 +2,8 @@
members = [
"bank",
"http-client",
"http-lib",
"http-server",
"socket-server",
]

View File

@ -5,4 +5,4 @@ edition = "2021"
[dependencies]
thiserror = "1.0.30"
uuid = { version = "0.8.2", features = ["v4"] }
uuid = { version = "0.8.2", features = ["v4", "wasm-bindgen"] }

View File

@ -42,8 +42,6 @@ impl PartialEq for Account {
}
}
impl Eq for Account {}
impl Hash for Account {
fn hash<H: Hasher>(&self, state: &mut H) {
self.number.hash(state);

17
http-client/Cargo.toml Normal file
View File

@ -0,0 +1,17 @@
[package]
name = "http-client"
version = "0.1.0"
edition = "2021"
[dependencies]
bank = { path = "../bank" }
http-lib = { path = "../http-lib" }
gloo-console = "0.2.1"
js-sys = "0.3.56"
serde = { version = "1.0.136", features = ["derive"] }
serde_json = "1.0.79"
wasm-bindgen = "0.2.79"
wasm-bindgen-futures = "0.4.29"
web-sys = { version = "0.3.56", features = ["Headers", "HtmlSelectElement", "Request", "RequestInit", "RequestMode", "Response", "Window"] }
yew = "0.19.3"
yew-agent = "0.1.0"

View File

@ -0,0 +1,588 @@
let wasm;
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
cachedTextDecoder.decode();
let cachegetUint8Memory0 = null;
function getUint8Memory0() {
if (cachegetUint8Memory0 === null || cachegetUint8Memory0.buffer !== wasm.memory.buffer) {
cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer);
}
return cachegetUint8Memory0;
}
function getStringFromWasm0(ptr, len) {
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
}
const heap = new Array(32).fill(undefined);
heap.push(undefined, null, true, false);
let heap_next = heap.length;
function addHeapObject(obj) {
if (heap_next === heap.length) heap.push(heap.length + 1);
const idx = heap_next;
heap_next = heap[idx];
heap[idx] = obj;
return idx;
}
function getObject(idx) { return heap[idx]; }
let WASM_VECTOR_LEN = 0;
let cachedTextEncoder = new TextEncoder('utf-8');
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
? function (arg, view) {
return cachedTextEncoder.encodeInto(arg, view);
}
: function (arg, view) {
const buf = cachedTextEncoder.encode(arg);
view.set(buf);
return {
read: arg.length,
written: buf.length
};
});
function passStringToWasm0(arg, malloc, realloc) {
if (realloc === undefined) {
const buf = cachedTextEncoder.encode(arg);
const ptr = malloc(buf.length);
getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf);
WASM_VECTOR_LEN = buf.length;
return ptr;
}
let len = arg.length;
let ptr = malloc(len);
const mem = getUint8Memory0();
let offset = 0;
for (; offset < len; offset++) {
const code = arg.charCodeAt(offset);
if (code > 0x7F) break;
mem[ptr + offset] = code;
}
if (offset !== len) {
if (offset !== 0) {
arg = arg.slice(offset);
}
ptr = realloc(ptr, len, len = offset + arg.length * 3);
const view = getUint8Memory0().subarray(ptr + offset, ptr + len);
const ret = encodeString(arg, view);
offset += ret.written;
}
WASM_VECTOR_LEN = offset;
return ptr;
}
function isLikeNone(x) {
return x === undefined || x === null;
}
let cachegetInt32Memory0 = null;
function getInt32Memory0() {
if (cachegetInt32Memory0 === null || cachegetInt32Memory0.buffer !== wasm.memory.buffer) {
cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer);
}
return cachegetInt32Memory0;
}
let cachegetFloat64Memory0 = null;
function getFloat64Memory0() {
if (cachegetFloat64Memory0 === null || cachegetFloat64Memory0.buffer !== wasm.memory.buffer) {
cachegetFloat64Memory0 = new Float64Array(wasm.memory.buffer);
}
return cachegetFloat64Memory0;
}
function dropObject(idx) {
if (idx < 36) return;
heap[idx] = heap_next;
heap_next = idx;
}
function takeObject(idx) {
const ret = getObject(idx);
dropObject(idx);
return ret;
}
function debugString(val) {
// primitive types
const type = typeof val;
if (type == 'number' || type == 'boolean' || val == null) {
return `${val}`;
}
if (type == 'string') {
return `"${val}"`;
}
if (type == 'symbol') {
const description = val.description;
if (description == null) {
return 'Symbol';
} else {
return `Symbol(${description})`;
}
}
if (type == 'function') {
const name = val.name;
if (typeof name == 'string' && name.length > 0) {
return `Function(${name})`;
} else {
return 'Function';
}
}
// objects
if (Array.isArray(val)) {
const length = val.length;
let debug = '[';
if (length > 0) {
debug += debugString(val[0]);
}
for(let i = 1; i < length; i++) {
debug += ', ' + debugString(val[i]);
}
debug += ']';
return debug;
}
// Test for built-in
const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val));
let className;
if (builtInMatches.length > 1) {
className = builtInMatches[1];
} else {
// Failed to match the standard '[object ClassName]'
return toString.call(val);
}
if (className == 'Object') {
// we're a user defined class or Object
// JSON.stringify avoids problems with cycles, and is generally much
// easier than looping through ownProperties of `val`.
try {
return 'Object(' + JSON.stringify(val) + ')';
} catch (_) {
return 'Object';
}
}
// errors
if (val instanceof Error) {
return `${val.name}: ${val.message}\n${val.stack}`;
}
// TODO we could test for more things here, like `Set`s and `Map`s.
return className;
}
function makeClosure(arg0, arg1, dtor, f) {
const state = { a: arg0, b: arg1, cnt: 1, dtor };
const real = (...args) => {
// First up with a closure we increment the internal reference
// count. This ensures that the Rust closure environment won't
// be deallocated while we're invoking it.
state.cnt++;
try {
return f(state.a, state.b, ...args);
} finally {
if (--state.cnt === 0) {
wasm.__wbindgen_export_2.get(state.dtor)(state.a, state.b);
state.a = 0;
}
}
};
real.original = state;
return real;
}
function __wbg_adapter_22(arg0, arg1, arg2) {
wasm._dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h0b1695f7853f7e5d(arg0, arg1, addHeapObject(arg2));
}
function makeMutClosure(arg0, arg1, dtor, f) {
const state = { a: arg0, b: arg1, cnt: 1, dtor };
const real = (...args) => {
// First up with a closure we increment the internal reference
// count. This ensures that the Rust closure environment won't
// be deallocated while we're invoking it.
state.cnt++;
const a = state.a;
state.a = 0;
try {
return f(a, state.b, ...args);
} finally {
if (--state.cnt === 0) {
wasm.__wbindgen_export_2.get(state.dtor)(a, state.b);
} else {
state.a = a;
}
}
};
real.original = state;
return real;
}
function __wbg_adapter_25(arg0, arg1, arg2) {
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h0235f8df8778306b(arg0, arg1, addHeapObject(arg2));
}
let cachegetUint32Memory0 = null;
function getUint32Memory0() {
if (cachegetUint32Memory0 === null || cachegetUint32Memory0.buffer !== wasm.memory.buffer) {
cachegetUint32Memory0 = new Uint32Array(wasm.memory.buffer);
}
return cachegetUint32Memory0;
}
function getArrayJsValueFromWasm0(ptr, len) {
const mem = getUint32Memory0();
const slice = mem.subarray(ptr / 4, ptr / 4 + len);
const result = [];
for (let i = 0; i < slice.length; i++) {
result.push(takeObject(slice[i]));
}
return result;
}
function handleError(f, args) {
try {
return f.apply(this, args);
} catch (e) {
wasm.__wbindgen_exn_store(addHeapObject(e));
}
}
async function load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
return await WebAssembly.instantiateStreaming(module, imports);
} catch (e) {
if (module.headers.get('Content-Type') != 'application/wasm') {
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
} else {
throw e;
}
}
}
const bytes = await module.arrayBuffer();
return await WebAssembly.instantiate(bytes, imports);
} else {
const instance = await WebAssembly.instantiate(module, imports);
if (instance instanceof WebAssembly.Instance) {
return { instance, module };
} else {
return instance;
}
}
}
async function init(input) {
if (typeof input === 'undefined') {
input = new URL('index-57ce73b23c43b0f5_bg.wasm', import.meta.url);
}
const imports = {};
imports.wbg = {};
imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
var ret = getStringFromWasm0(arg0, arg1);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_string_get = function(arg0, arg1) {
const obj = getObject(arg1);
var ret = typeof(obj) === 'string' ? obj : undefined;
var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
};
imports.wbg.__wbindgen_object_clone_ref = function(arg0) {
var ret = getObject(arg0);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_number_new = function(arg0) {
var ret = arg0;
return addHeapObject(ret);
};
imports.wbg.__wbindgen_number_get = function(arg0, arg1) {
const obj = getObject(arg1);
var ret = typeof(obj) === 'number' ? obj : undefined;
getFloat64Memory0()[arg0 / 8 + 1] = isLikeNone(ret) ? 0 : ret;
getInt32Memory0()[arg0 / 4 + 0] = !isLikeNone(ret);
};
imports.wbg.__wbg_error_09919627ac0992f5 = function(arg0, arg1) {
try {
console.error(getStringFromWasm0(arg0, arg1));
} finally {
wasm.__wbindgen_free(arg0, arg1);
}
};
imports.wbg.__wbg_new_693216e109162396 = function() {
var ret = new Error();
return addHeapObject(ret);
};
imports.wbg.__wbg_stack_0ddaca5d1abfb52f = function(arg0, arg1) {
var ret = getObject(arg1).stack;
var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
};
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
takeObject(arg0);
};
imports.wbg.__wbg_warn_2aa0e7178e1d35f6 = function(arg0, arg1) {
var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice();
wasm.__wbindgen_free(arg0, arg1 * 4);
console.warn(...v0);
};
imports.wbg.__wbg_instanceof_Window_434ce1849eb4e0fc = function(arg0) {
var ret = getObject(arg0) instanceof Window;
return ret;
};
imports.wbg.__wbg_document_5edd43643d1060d9 = function(arg0) {
var ret = getObject(arg0).document;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
};
imports.wbg.__wbg_fetch_427498e0ccea81f4 = function(arg0, arg1) {
var ret = getObject(arg0).fetch(getObject(arg1));
return addHeapObject(ret);
};
imports.wbg.__wbg_body_7538539844356c1c = function(arg0) {
var ret = getObject(arg0).body;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
};
imports.wbg.__wbg_createElement_d017b8d2af99bab9 = function() { return handleError(function (arg0, arg1, arg2) {
var ret = getObject(arg0).createElement(getStringFromWasm0(arg1, arg2));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_createElementNS_fd4a7e49f74039e1 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
var ret = getObject(arg0).createElementNS(arg1 === 0 ? undefined : getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_createTextNode_39a0de25d14bcde5 = function(arg0, arg1, arg2) {
var ret = getObject(arg0).createTextNode(getStringFromWasm0(arg1, arg2));
return addHeapObject(ret);
};
imports.wbg.__wbg_value_d3a30bc2c7caf357 = function(arg0, arg1) {
var ret = getObject(arg1).value;
var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
};
imports.wbg.__wbg_setvalue_6a34bab301f38bf2 = function(arg0, arg1, arg2) {
getObject(arg0).value = getStringFromWasm0(arg1, arg2);
};
imports.wbg.__wbg_target_e560052e31e4567c = function(arg0) {
var ret = getObject(arg0).target;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
};
imports.wbg.__wbg_cancelBubble_17d7988ab2fbe4c9 = function(arg0) {
var ret = getObject(arg0).cancelBubble;
return ret;
};
imports.wbg.__wbg_headers_1a60dec7fbd28a3b = function(arg0) {
var ret = getObject(arg0).headers;
return addHeapObject(ret);
};
imports.wbg.__wbg_newwithstrandinit_c07f0662ece15bc6 = function() { return handleError(function (arg0, arg1, arg2) {
var ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_setchecked_f6ead3490df88a7f = function(arg0, arg1) {
getObject(arg0).checked = arg1 !== 0;
};
imports.wbg.__wbg_value_fc1c354d1a0e9714 = function(arg0, arg1) {
var ret = getObject(arg1).value;
var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
};
imports.wbg.__wbg_setvalue_ce4a23f487065c07 = function(arg0, arg1, arg2) {
getObject(arg0).value = getStringFromWasm0(arg1, arg2);
};
imports.wbg.__wbg_addEventListener_55682f77717d7665 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).addEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3), getObject(arg4));
}, arguments) };
imports.wbg.__wbg_parentElement_96e1e07348340043 = function(arg0) {
var ret = getObject(arg0).parentElement;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
};
imports.wbg.__wbg_lastChild_e2b014abab089e08 = function(arg0) {
var ret = getObject(arg0).lastChild;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
};
imports.wbg.__wbg_setnodeValue_f175b74a390f8fda = function(arg0, arg1, arg2) {
getObject(arg0).nodeValue = arg1 === 0 ? undefined : getStringFromWasm0(arg1, arg2);
};
imports.wbg.__wbg_appendChild_3fe5090c665d3bb4 = function() { return handleError(function (arg0, arg1) {
var ret = getObject(arg0).appendChild(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_insertBefore_4f09909023feac91 = function() { return handleError(function (arg0, arg1, arg2) {
var ret = getObject(arg0).insertBefore(getObject(arg1), getObject(arg2));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_removeChild_f4a83c9698136bbb = function() { return handleError(function (arg0, arg1) {
var ret = getObject(arg0).removeChild(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_instanceof_Element_c9423704dd5d9b1d = function(arg0) {
var ret = getObject(arg0) instanceof Element;
return ret;
};
imports.wbg.__wbg_namespaceURI_e9a971e6c1ce68db = function(arg0, arg1) {
var ret = getObject(arg1).namespaceURI;
var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
};
imports.wbg.__wbg_removeAttribute_1adaecf6b4d35a09 = function() { return handleError(function (arg0, arg1, arg2) {
getObject(arg0).removeAttribute(getStringFromWasm0(arg1, arg2));
}, arguments) };
imports.wbg.__wbg_setAttribute_1776fcc9b98d464e = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).setAttribute(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
}, arguments) };
imports.wbg.__wbg_set_f9448486a94c9aef = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).set(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
}, arguments) };
imports.wbg.__wbg_value_d4cea9e999ffb147 = function(arg0, arg1) {
var ret = getObject(arg1).value;
var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
};
imports.wbg.__wbg_instanceof_Response_ea36d565358a42f7 = function(arg0) {
var ret = getObject(arg0) instanceof Response;
return ret;
};
imports.wbg.__wbg_text_aeba5a5bbfef7f15 = function() { return handleError(function (arg0) {
var ret = getObject(arg0).text();
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbindgen_cb_drop = function(arg0) {
const obj = takeObject(arg0).original;
if (obj.cnt-- == 1) {
obj.a = 0;
return true;
}
var ret = false;
return ret;
};
imports.wbg.__wbg_newnoargs_f579424187aa1717 = function(arg0, arg1) {
var ret = new Function(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
};
imports.wbg.__wbg_call_89558c3e96703ca1 = function() { return handleError(function (arg0, arg1) {
var ret = getObject(arg0).call(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_valueOf_39e0d6bc7e4232b9 = function(arg0) {
var ret = getObject(arg0).valueOf();
return ret;
};
imports.wbg.__wbg_is_3d73f4d91adacc37 = function(arg0, arg1) {
var ret = Object.is(getObject(arg0), getObject(arg1));
return ret;
};
imports.wbg.__wbg_new_d3138911a89329b0 = function() {
var ret = new Object();
return addHeapObject(ret);
};
imports.wbg.__wbg_resolve_4f8f547f26b30b27 = function(arg0) {
var ret = Promise.resolve(getObject(arg0));
return addHeapObject(ret);
};
imports.wbg.__wbg_then_a6860c82b90816ca = function(arg0, arg1) {
var ret = getObject(arg0).then(getObject(arg1));
return addHeapObject(ret);
};
imports.wbg.__wbg_then_58a04e42527f52c6 = function(arg0, arg1, arg2) {
var ret = getObject(arg0).then(getObject(arg1), getObject(arg2));
return addHeapObject(ret);
};
imports.wbg.__wbg_globalThis_d61b1f48a57191ae = function() { return handleError(function () {
var ret = globalThis.globalThis;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_self_e23d74ae45fb17d1 = function() { return handleError(function () {
var ret = self.self;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_window_b4be7f48b24ac56e = function() { return handleError(function () {
var ret = window.window;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_global_e7669da72fd7f239 = function() { return handleError(function () {
var ret = global.global;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbindgen_is_undefined = function(arg0) {
var ret = getObject(arg0) === undefined;
return ret;
};
imports.wbg.__wbg_get_8bbb82393651dd9c = function() { return handleError(function (arg0, arg1) {
var ret = Reflect.get(getObject(arg0), getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_set_c42875065132a932 = function() { return handleError(function (arg0, arg1, arg2) {
var ret = Reflect.set(getObject(arg0), getObject(arg1), getObject(arg2));
return ret;
}, arguments) };
imports.wbg.__wbindgen_debug_string = function(arg0, arg1) {
var ret = debugString(getObject(arg1));
var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
};
imports.wbg.__wbindgen_throw = function(arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
};
imports.wbg.__wbindgen_closure_wrapper4145 = function(arg0, arg1, arg2) {
var ret = makeClosure(arg0, arg1, 178, __wbg_adapter_22);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper7730 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 194, __wbg_adapter_25);
return addHeapObject(ret);
};
if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
input = fetch(input);
}
const { instance, module } = await load(await input, imports);
wasm = instance.exports;
init.__wbindgen_wasm_module = module;
wasm.__wbindgen_start();
return wasm;
}
export default init;

Binary file not shown.

34
http-client/dist/index.html vendored Normal file
View File

@ -0,0 +1,34 @@
<!DOCTYPE html><html lang="en"><head>
<meta charset="utf-8">
<title>Vesys Bank</title>
<link rel="stylesheet" href="/styles-3143ec7e42adb2c6.css">
<link rel="preload" href="/index-57ce73b23c43b0f5_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
<link rel="modulepreload" href="/index-57ce73b23c43b0f5.js"></head>
<body><script type="module">import init from '/index-57ce73b23c43b0f5.js';init('/index-57ce73b23c43b0f5_bg.wasm');</script><script>(function () {
var url = 'ws://' + window.location.host + '/_trunk/ws';
var poll_interval = 5000;
var reload_upon_connect = () => {
window.setTimeout(
() => {
// when we successfully reconnect, we'll force a
// reload (since we presumably lost connection to
// trunk due to it being killed, so it will have
// rebuilt on restart)
var ws = new WebSocket(url);
ws.onopen = () => window.location.reload();
ws.onclose = reload_upon_connect;
},
poll_interval);
};
var ws = new WebSocket(url);
ws.onmessage = (ev) => {
const msg = JSON.parse(ev.data);
if (msg.reload) {
window.location.reload();
}
};
ws.onclose = reload_upon_connect;
})()
</script></body></html>

View File

@ -0,0 +1,123 @@
html, body {
font-style: normal;
font-family: monospace;
padding: 0;
margin: 0;
}
.account {
border-style: solid;
border-color: black;
}
.account__grid {
border-style: none;
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.account__title {
background-color: black;
color: white;
padding: 3px 6px;
}
.account__label {
margin: 0 .5em 0 .5em;
font-weight: bold;
border-bottom-style: solid;
}
.account__amount {
grid-column: 1;
grid-row: 1;
display: flex;
flex-direction: column;
}
.account__accounts {
grid-column: 2 / 4;
grid-row: 1;
display: flex;
flex-direction: column;
}
.account__button {
color: black;
border-radius: 0;
border-color: black;
border-style: solid;
border-width: .2em;
padding: 1em;
margin: .5em;
background: linear-gradient(to top, darkgray, 20%, lightgray);
box-shadow: 3px 3px 3px black;
}
.account__button:hover:enabled {
background: linear-gradient(lightgray, darkgray);
}
.account__button:active:enabled {
transform: translateY(3px);
}
.account__button:disabled {
background: lightgray;
color: darkgray;
}
.account__input {
padding: 1em;
color: black;
margin: .5em;
background-color: #eeeeee;
border-radius: 0;
border-color: black;
border-style: solid;
border-width: .2em;
box-shadow: 3px 3px 3px black;
}
.accounts {
list-style-type: none;
padding: 0.5em;
border-style: solid;
border-color: black;
}
.accounts__item {
border-style: solid;
border-bottom-color: lightgray;
border-width: 0 0 0.1em 0;
}
.accounts__item-selected {
border-bottom-color: darkgray;
background-color: #eeeeee;
font-weight: bold;
}
.error {
background: red;
padding: 1em;
color: white;
}
.content {
padding: 1em;
min-width: 370px;
}
@media (max-width: 605px) {
.account__grid {
display: grid;
grid-template-columns: 1fr;
}
.account__accounts {
grid-column: 1;
grid-row: 2;
}
}

8
http-client/index.html Normal file
View File

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Vesys Bank</title>
<link data-trunk rel="css" type="text/css" href="styles.css">
</head>
</html>

34
http-client/src/api.rs Normal file
View File

@ -0,0 +1,34 @@
use wasm_bindgen::JsValue;
use http_lib::json_account::JsonAccount;
use crate::client;
use crate::client::FetchError;
pub async fn fetch_account_nrs(host: &str) -> Result<Vec<String>, FetchError> {
match client::get_json(&format! {"{}/accounts", host}).await? {
None => Ok(vec![]),
Some(response) => {
let nrs: Vec<String> = serde_json::from_str(&response)?;
Ok(nrs)
}
}
}
pub async fn fetch_account(host: &str, nr: &str) -> Result<JsonAccount, FetchError> {
match client::get_json(&format! {"{}/accounts/{}", host, nr}).await? {
None => Err(FetchError {
err: JsValue::from_str("no such account"),
}),
Some(response) => {
let acc: JsonAccount = serde_json::from_str(&response)?;
Ok(acc)
}
}
}
pub async fn set_balance(host: &str, nr: &str, balance: f64) -> Result<(), FetchError> {
let data = JsValue::from_str(&format!("amount={}", balance));
client::put(&format! {"{}/accounts/{}", host, nr}, data).await?;
Ok(())
}

67
http-client/src/client.rs Normal file
View File

@ -0,0 +1,67 @@
use std::error::Error;
use std::fmt;
use std::fmt::{Debug, Display, Formatter};
use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, RequestMode, Response};
#[derive(Debug, Clone, PartialEq)]
pub struct FetchError {
pub err: JsValue,
}
impl Display for FetchError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
Debug::fmt(&self.err, f)
}
}
impl Error for FetchError {}
impl From<JsValue> for FetchError {
fn from(value: JsValue) -> Self {
Self { err: value }
}
}
impl From<serde_json::Error> for FetchError {
fn from(e: serde_json::Error) -> Self {
Self {
err: JsValue::from(e.to_string()),
}
}
}
async fn send_request(request: Request) -> Result<Response, FetchError> {
let window = web_sys::window().unwrap();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
let resp: Response = resp_value.dyn_into()?;
Ok(resp)
}
pub async fn get_json(url: &str) -> Result<Option<String>, FetchError> {
let mut opts = RequestInit::new();
opts.method("GET");
opts.mode(RequestMode::Cors);
let request = Request::new_with_str_and_init(url, &opts)?;
request.headers().set("Accept", "application/json")?;
let resp = self::send_request(request).await?;
let text = JsFuture::from(resp.text()?).await?;
Ok(text.as_string())
}
pub async fn put(url: &str, data: JsValue) -> Result<(), FetchError> {
let mut opts = RequestInit::new();
opts.method("PUT");
opts.mode(RequestMode::Cors);
opts.body(Some(&data));
let request = Request::new_with_str_and_init(url, &opts)?;
request.headers().set("Content-Type", "application/x-www-form-urlencoded")?;
self::send_request(request).await?;
Ok(())
}

View File

@ -0,0 +1,151 @@
use std::str::FromStr;
use web_sys::{HtmlInputElement, HtmlSelectElement};
use yew::{classes, html, Component, Context, Html, Properties, NodeRef};
use yew_agent::{Dispatched, Dispatcher};
use crate::event_bus::{EventBus, Request};
use crate::events::Event;
pub enum Msg {
AmountChanged,
Deposit,
Withdraw,
Transfer,
}
#[derive(Properties, PartialEq)]
pub struct AccountProps {
pub account_nrs: Vec<String>,
pub balance: f64,
pub nr: String,
pub owner: String,
}
pub struct Account {
amount_ref: NodeRef,
transfer_account_ref: NodeRef,
amount_valid: bool,
event_bus: Dispatcher<EventBus>,
}
impl Account {
fn amount(&self) -> f64 {
if let Some(amount_el) = self.amount_ref.cast::<HtmlInputElement>() {
f64::from_str(&amount_el.value()).unwrap_or_default()
} else { 0_f64 }
}
fn is_amount_valid(&self) -> bool {
self.amount() > 0_f64
}
fn selected_transfer_account(&self) -> Option<String> {
if let Some(transfer_account_el) = self.transfer_account_ref.cast::<HtmlSelectElement>() {
let value: String = transfer_account_el.value();
if value.is_empty() || value == "undefined" { None } else { Some(value) }
} else { None }
}
}
impl Component for Account {
type Message = Msg;
type Properties = AccountProps;
fn create(_: &Context<Self>) -> Self {
Self {
amount_ref: NodeRef::default(),
transfer_account_ref: NodeRef::default(),
amount_valid: false,
event_bus: EventBus::dispatcher(),
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::AmountChanged => {
let is_amount_valid = self.is_amount_valid();
let needs_redraw = is_amount_valid != self.amount_valid;
self.amount_valid = is_amount_valid;
needs_redraw
}
Msg::Deposit => {
let amount = self.amount() + ctx.props().balance;
self.event_bus.send(Request::EventBusMsg(Event::SetBalance(amount, ctx.props().nr.clone())));
false
}
Msg::Withdraw => {
let amount = ctx.props().balance - self.amount();
if amount > 0_f64 {
self.event_bus.send(Request::EventBusMsg(Event::SetBalance(amount, ctx.props().nr.clone())));
} else {
self.event_bus.send(Request::EventBusMsg(Event::ShowError("Balance can not be overdrawn".into())));
}
false
}
Msg::Transfer => {
let amount = ctx.props().balance - self.amount();
if amount > 0_f64 {
self.event_bus.send(Request::EventBusMsg(Event::SetBalance(amount, ctx.props().nr.clone())));
} else {
let transfer_nr = self.selected_transfer_account();
if let Some(transfer_nr) = transfer_nr {
self.event_bus.send(Request::EventBusMsg(Event::Transfer(amount, ctx.props().nr.clone(), transfer_nr)));
}
}
false
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let onchange = ctx
.link()
.callback(|_| Msg::AmountChanged);
let on_deposit = ctx
.link()
.callback(|_| Msg::Deposit);
let on_withdraw = ctx
.link()
.callback(|_| Msg::Withdraw);
let on_transfer = ctx
.link()
.callback(|_| Msg::Transfer);
html! {
<>
<section class={classes!("account")}>
<fieldset class={classes!("account__grid")}>
<legend class={classes!("account__title")}>
{&ctx.props().owner} {""} {&ctx.props().balance}
</legend>
<div class={classes!("account__amount")}>
<label class={classes!("account__label")} for="amount">{"Amount"}</label>
<input {onchange} ref={self.amount_ref.clone()} class={classes!("account__input")} id="amount" type="number" step="0.01" min="0" placeholder="0.0" />
</div>
<div class={classes!("account__accounts")}>
<label class={classes!("account__label")} for="accounts">{"Remote Account"}</label>
<select ref={self.transfer_account_ref.clone()} class={classes!("account__input")} name="accounts" id="account-select">
{ for ctx.props().account_nrs.iter().filter(|e| {
ctx.props().nr != e.to_string()
}).map(|e| html!{ <option value={e.to_string()}>{e}</option> }) }
</select>
</div>
<button onclick={on_deposit} class={classes!("account__button")} disabled={!self.amount_valid}>{"deposit"}</button>
<button onclick={on_withdraw} class={classes!("account__button")} disabled={!self.amount_valid}>{"withdraw"}</button>
<button onclick={on_transfer} class={classes!("account__button")} disabled={!self.amount_valid}>{"transfer"}</button>
</fieldset>
</section>
</>
}
}
}

View File

@ -0,0 +1,73 @@
use crate::event_bus::{EventBus, Request};
use crate::events::Event;
use yew::{classes, html, Classes, Component, Context, Html, Properties};
use yew_agent::{Dispatched, Dispatcher};
pub enum Msg {
SelectAccountNr(String),
}
#[derive(Properties, PartialEq)]
pub struct AccountsProps {
pub account_nrs: Vec<String>,
pub selected_nr: String
}
pub struct Accounts {
event_bus: Dispatcher<EventBus>,
}
impl Component for Accounts {
type Message = Msg;
type Properties = AccountsProps;
fn create(_: &Context<Self>) -> Self {
Self {
event_bus: EventBus::dispatcher(),
}
}
fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::SelectAccountNr(nr) => {
self.event_bus
.send(Request::EventBusMsg(Event::SelectAccountNr(nr)));
false
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<>
<ul class={classes!("accounts")}>
{ for ctx.props().account_nrs.iter().map(|e| self.account_entry(e, &ctx.props().selected_nr, ctx)) }
</ul>
</>
}
}
}
impl Accounts {
fn account_entry(
&self,
nr: &str,
selected_nr: &str,
ctx: &Context<Self>,
) -> Html {
let mut class = Classes::from("accounts__item");
if selected_nr == nr {
class.push("accounts__item-selected");
}
let nr = nr.to_string();
let account_nr = nr.clone();
let onclick = ctx
.link()
.callback(move |_| Msg::SelectAccountNr(nr.clone()));
html! {
<li {onclick} class={class}>{account_nr}</li>
}
}
}

View File

@ -0,0 +1,117 @@
use crate::components::account::Account;
use crate::components::accounts::Accounts;
use crate::event_bus::EventBus;
use crate::events::Event;
use crate::api;
use yew::{classes, html, Component, Context, Html};
use yew_agent::{Bridge, Bridged};
pub struct Main {
_subscriber: Box<dyn Bridge<EventBus>>,
error: Option<String>,
account_nrs: Vec<String>,
selected_balance: f64,
selected_owner: String,
selected_nr: String,
}
impl Main {
fn set_selected_account(&self, ctx: &Context<Self>, nr: String) {
ctx.link().send_future(async move {
match api::fetch_account("http://localhost:8000", &nr).await {
Err(e) => Event::ShowError(e.to_string()),
Ok(account) => Event::SetSelectedAccount(account),
}
});
}
}
impl Component for Main {
type Message = Event;
type Properties = ();
fn create(ctx: &Context<Self>) -> Self {
Self {
error: None,
_subscriber: EventBus::bridge(ctx.link().callback(|x| x)),
account_nrs: vec![],
selected_balance: 0_f64,
selected_nr: "".into(),
selected_owner: "".into()
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Event::GetAccountNrs => {
ctx.link().send_future(async {
match api::fetch_account_nrs("http://localhost:8000").await {
Err(e) => Event::ShowError(e.to_string()),
Ok(nrs) => Event::SetAccountNrs(nrs),
}
});
false
}
Event::SetAccountNrs(nrs) => {
if self.account_nrs.is_empty() && !nrs.is_empty() {
let nr = nrs[0].clone();
self.set_selected_account(ctx, nr);
}
self.account_nrs = nrs;
true
}
Event::ShowError(error) => {
self.error = Some(error);
true
}
Event::SetSelectedAccount(account) => {
self.selected_balance = account.balance;
self.selected_owner = account.owner;
self.selected_nr = account.number;
true
}
Event::SelectAccountNr(nr) => {
self.set_selected_account(ctx, nr);
true
}
Event::SetBalance(balance, nr) => {
ctx.link().send_future(async move {
match api::set_balance("http://localhost:8000", &nr, balance).await {
Err(e) => Event::ShowError(e.to_string()),
Ok(_) => Event::SelectAccountNr(nr),
}
});
false
}
Event::Transfer(amount, from, to) => {
false
}
}
}
fn view(&self, _: &Context<Self>) -> Html {
html! {
<>
if let Some(error_msg) = &self.error {
<div class={classes!("error")}>{ error_msg }</div>
}
<main class={classes!("content")}>
<h1>
{"welcome to your vaults"}
</h1>
<Accounts account_nrs={self.account_nrs.clone()} selected_nr={self.selected_nr.clone()} />
<Account balance={self.selected_balance} owner={self.selected_owner.clone()} nr={self.selected_nr.clone()} account_nrs={self.account_nrs.clone()} />
</main>
</>
}
}
fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
if first_render {
ctx.link().send_message(Event::GetAccountNrs);
}
}
}

View File

@ -0,0 +1,3 @@
pub mod account;
pub mod accounts;
pub mod main;

View File

@ -0,0 +1,48 @@
use crate::events::Event;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use yew_agent::{Agent, AgentLink, Context, HandlerId};
#[derive(Serialize, Deserialize, Debug)]
pub enum Request {
EventBusMsg(Event),
}
pub struct EventBus {
link: AgentLink<EventBus>,
subscribers: HashSet<HandlerId>,
}
impl Agent for EventBus {
type Reach = Context<Self>;
type Message = ();
type Input = Request;
type Output = Event;
fn create(link: AgentLink<Self>) -> Self {
Self {
link,
subscribers: HashSet::new(),
}
}
fn update(&mut self, _msg: Self::Message) {}
fn connected(&mut self, id: HandlerId) {
self.subscribers.insert(id);
}
fn handle_input(&mut self, msg: Self::Input, _id: HandlerId) {
match msg {
Request::EventBusMsg(s) => {
for sub in self.subscribers.iter() {
self.link.respond(*sub, s.clone());
}
}
}
}
fn disconnected(&mut self, id: HandlerId) {
self.subscribers.remove(&id);
}
}

13
http-client/src/events.rs Normal file
View File

@ -0,0 +1,13 @@
use http_lib::json_account::JsonAccount;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum Event {
GetAccountNrs,
SetAccountNrs(Vec<String>),
SelectAccountNr(String),
SetSelectedAccount(JsonAccount),
SetBalance(f64, String),
ShowError(String),
Transfer(f64, String, String),
}

11
http-client/src/main.rs Normal file
View File

@ -0,0 +1,11 @@
use crate::components::main::Main;
mod client;
mod components;
mod event_bus;
mod events;
mod api;
fn main() {
yew::start_app::<Main>();
}

123
http-client/styles.css Normal file
View File

@ -0,0 +1,123 @@
html, body {
font-style: normal;
font-family: monospace;
padding: 0;
margin: 0;
}
.account {
border-style: solid;
border-color: black;
}
.account__grid {
border-style: none;
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.account__title {
background-color: black;
color: white;
padding: 3px 6px;
}
.account__label {
margin: 0 .5em 0 .5em;
font-weight: bold;
border-bottom-style: solid;
}
.account__amount {
grid-column: 1;
grid-row: 1;
display: flex;
flex-direction: column;
}
.account__accounts {
grid-column: 2 / 4;
grid-row: 1;
display: flex;
flex-direction: column;
}
.account__button {
color: black;
border-radius: 0;
border-color: black;
border-style: solid;
border-width: .2em;
padding: 1em;
margin: .5em;
background: linear-gradient(to top, darkgray, 20%, lightgray);
box-shadow: 3px 3px 3px black;
}
.account__button:hover:enabled {
background: linear-gradient(lightgray, darkgray);
}
.account__button:active:enabled {
transform: translateY(3px);
}
.account__button:disabled {
background: lightgray;
color: darkgray;
}
.account__input {
padding: 1em;
color: black;
margin: .5em;
background-color: #eeeeee;
border-radius: 0;
border-color: black;
border-style: solid;
border-width: .2em;
box-shadow: 3px 3px 3px black;
}
.accounts {
list-style-type: none;
padding: 0.5em;
border-style: solid;
border-color: black;
}
.accounts__item {
border-style: solid;
border-bottom-color: lightgray;
border-width: 0 0 0.1em 0;
}
.accounts__item-selected {
border-bottom-color: darkgray;
background-color: #eeeeee;
font-weight: bold;
}
.error {
background: red;
padding: 1em;
color: white;
}
.content {
padding: 1em;
min-width: 370px;
}
@media (max-width: 605px) {
.account__grid {
display: grid;
grid-template-columns: 1fr;
}
.account__accounts {
grid-column: 1;
grid-row: 2;
}
}

8
http-lib/Cargo.toml Normal file
View File

@ -0,0 +1,8 @@
[package]
name = "http-lib"
version = "0.1.0"
edition = "2021"
[dependencies]
bank = { path = "../bank" }
serde = { version = "1.0.136", features = ["derive"] }

View File

@ -0,0 +1,32 @@
use bank::account::Account;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct JsonAccount {
pub number: String,
pub owner: String,
pub balance: f64,
pub is_active: bool,
}
impl From<&Account> for JsonAccount {
fn from(a: &Account) -> Self {
JsonAccount {
number: a.number.clone(),
owner: a.owner.clone(),
balance: a.balance,
is_active: a.is_active,
}
}
}
impl From<JsonAccount> for Account {
fn from(a: JsonAccount) -> Self {
Account {
number: a.number.clone(),
owner: a.owner.clone(),
balance: a.balance,
is_active: a.is_active,
}
}
}

1
http-lib/src/lib.rs Normal file
View File

@ -0,0 +1 @@
pub mod json_account;

View File

@ -5,6 +5,8 @@ edition = "2021"
[dependencies]
bank = { path = "../bank" }
http-lib = { path = "../http-lib" }
actix-cors = "0.6.1"
actix-web = "4.0.1"
thiserror = "1.0.30"
log = "0.4.14"

View File

@ -1,4 +1,4 @@
use actix_web::{post, web, HttpResponse, Responder, Result};
use actix_web::{delete, web, HttpResponse, Responder, Result};
use bank::account::AccountError;
use bank::bank::Bank;
@ -6,7 +6,7 @@ use bank::bank::Bank;
use crate::handlers::error::HttpAccountError;
use crate::AppState;
#[post("/{nr}/close")]
#[delete("/{nr}")]
pub async fn route(info: web::Path<String>, data: web::Data<AppState>) -> Result<impl Responder> {
let nr = info.into_inner();

View File

@ -1,4 +1,4 @@
use actix_web::{post, web, Responder, Result};
use actix_web::{post, web, HttpResponse, Responder, Result};
use serde::{Deserialize, Serialize};
use crate::AppState;
@ -25,5 +25,5 @@ pub async fn route(
let nr = bank.create_account(owner.clone());
info!("created account {}", nr);
Ok(web::Json(AccountCreated { nr }))
Ok(HttpResponse::Created().json(AccountCreated { nr }))
}

View File

@ -1,4 +1,4 @@
use actix_web::{post, web, HttpResponse, Responder, Result};
use actix_web::{patch, web, HttpResponse, Responder, Result};
use serde::Serialize;
use bank::bank::Bank;
@ -12,7 +12,7 @@ struct AccountCreated {
nr: String,
}
#[post("/{nr}/deposit")]
#[patch("/{nr}/deposit")]
pub async fn route(
info: web::Path<String>,
form: web::Form<AmountData>,

View File

@ -36,9 +36,9 @@ impl ResponseError for HttpAccountError {
match self.deref() {
AccountError::NotFound => StatusCode::NOT_FOUND,
AccountError::InvalidAmount => StatusCode::BAD_REQUEST,
AccountError::Inactive => StatusCode::BAD_REQUEST,
AccountError::Inactive => StatusCode::GONE,
AccountError::Overdraw => StatusCode::BAD_REQUEST,
AccountError::AccountNotZero => StatusCode::EXPECTATION_FAILED,
AccountError::AccountNotZero => StatusCode::BAD_REQUEST,
}
}

View File

@ -1,32 +1,13 @@
use std::ops::Deref;
use actix_web::{get, web, Responder, Result};
use serde::Serialize;
use bank::account::{Account, AccountError};
use bank::account::AccountError;
use http_lib::json_account::JsonAccount;
use crate::handlers::error::HttpAccountError;
use crate::AppState;
#[derive(Serialize)]
pub struct JsonAccount {
pub number: String,
pub owner: String,
pub balance: f64,
pub is_active: bool,
}
impl From<&Account> for JsonAccount {
fn from(a: &Account) -> Self {
JsonAccount {
number: a.number.clone(),
owner: a.owner.clone(),
balance: a.balance,
is_active: a.is_active,
}
}
}
#[get("/{nr}")]
pub async fn route(info: web::Path<String>, data: web::Data<AppState>) -> Result<impl Responder> {
let nr = info.into_inner();

View File

@ -0,0 +1,31 @@
use actix_web::{head, web, HttpResponse, Responder, Result};
use bank::account::AccountError;
use serde::Serialize;
use crate::handlers::error::HttpAccountError;
use crate::AppState;
#[derive(Serialize)]
struct AccountCreated {
nr: String,
}
#[head("/{nr}")]
pub async fn route(info: web::Path<String>, data: web::Data<AppState>) -> Result<impl Responder> {
let nr = info.into_inner();
info!("checking account {}...", nr);
let bank = data.bank.read().unwrap();
match bank.accounts.get(&nr) {
None => Err(HttpAccountError(AccountError::NotFound).into()),
Some(account) => {
let account = account.read().unwrap();
if account.is_active {
Ok(HttpResponse::Ok().finish())
} else {
Err(HttpAccountError(AccountError::Inactive).into())
}
}
}
}

View File

@ -6,8 +6,10 @@ pub mod deposit;
pub mod error;
pub mod get_account;
pub mod get_account_nrs;
pub mod is_active;
pub mod pong;
pub mod transfer;
pub mod update_account;
pub mod withdraw;
#[derive(Deserialize, Serialize)]

View File

@ -1,4 +1,4 @@
use actix_web::{post, web, HttpResponse, Responder, Result};
use actix_web::{patch, web, HttpResponse, Responder, Result};
use serde::{Deserialize, Serialize};
use bank::account::AccountError;
@ -13,7 +13,7 @@ pub struct TransferData {
pub(crate) amount: f64,
}
#[post("/transfer")]
#[patch("/transfer")]
pub async fn route(
form: web::Form<TransferData>,
data: web::Data<AppState>,

View File

@ -0,0 +1,39 @@
use actix_web::{put, web, HttpResponse, Responder, Result};
use bank::account::AccountError;
use serde::Serialize;
use bank::bank::Bank;
use crate::handlers::error::HttpAccountError;
use crate::handlers::AmountData;
use crate::AppState;
#[derive(Serialize)]
struct AccountCreated {
nr: String,
}
#[put("/{nr}")]
pub async fn route(
info: web::Path<String>,
form: web::Form<AmountData>,
data: web::Data<AppState>,
) -> Result<impl Responder> {
let nr = info.into_inner();
let amount = form.amount;
if amount <= 0_f64 {
return Err(HttpAccountError(AccountError::InvalidAmount).into());
}
info!("updating account {}...", nr);
let bank = data.bank.read().unwrap();
match Bank::account_action(bank, &nr, |account| {
account.balance = amount;
Ok(amount)
}) {
Err(e) => Err(HttpAccountError(e).into()),
Ok(_) => Ok(HttpResponse::Ok().finish()),
}
}

View File

@ -1,4 +1,4 @@
use actix_web::{post, web, HttpResponse, Responder, Result};
use actix_web::{patch, web, HttpResponse, Responder, Result};
use serde::Serialize;
use bank::bank::Bank;
@ -12,7 +12,7 @@ struct AccountCreated {
nr: String,
}
#[post("/{nr}/withdraw")]
#[patch("/{nr}/withdraw")]
pub async fn route(
info: web::Path<String>,
form: web::Form<AmountData>,

View File

@ -3,6 +3,7 @@ extern crate log;
use std::sync::RwLock;
use actix_cors::Cors;
use actix_web::middleware::TrailingSlash;
use actix_web::web::{Data, ServiceConfig};
use actix_web::{middleware, web, App, HttpServer};
@ -14,8 +15,10 @@ use crate::handlers::create_account;
use crate::handlers::deposit;
use crate::handlers::get_account;
use crate::handlers::get_account_nrs;
use crate::handlers::is_active;
use crate::handlers::pong;
use crate::handlers::transfer;
use crate::handlers::update_account;
use crate::handlers::withdraw;
pub struct AppState {
@ -34,7 +37,9 @@ fn config_app(app_data: Data<AppState>) -> Box<dyn Fn(&mut ServiceConfig)> {
.service(close_account::route)
.service(get_account_nrs::route)
.service(withdraw::route)
.service(transfer::route),
.service(transfer::route)
.service(is_active::route)
.service(update_account::route),
);
})
}
@ -48,11 +53,14 @@ async fn main() -> std::io::Result<()> {
});
HttpServer::new(move || {
let cors = Cors::permissive();
App::new()
.wrap(cors)
.wrap(middleware::NormalizePath::new(TrailingSlash::Trim))
.configure(config_app(app_data.clone()))
})
.bind(("127.0.0.1", 8080))?
.bind(("127.0.0.1", 8000))?
.run()
.await
}
@ -60,6 +68,8 @@ async fn main() -> std::io::Result<()> {
#[cfg(test)]
mod tests {
use actix_web::body::to_bytes;
use actix_web::dev::Service;
use actix_web::http::{Method as HttpMethod, StatusCode};
use actix_web::{test, App};
use serde_json::Value;
@ -92,7 +102,7 @@ mod tests {
.set_form(&payload)
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
assert_eq!(resp.status(), StatusCode::CREATED);
let body_bytes = to_bytes(resp.into_body()).await.unwrap();
let response_json: Value = serde_json::from_slice(&body_bytes).unwrap();
@ -100,15 +110,29 @@ mod tests {
assert!(!nr.is_empty());
let payload = AmountData { amount: 10_f64 };
let req = test::TestRequest::post()
let req = test::TestRequest::default()
.method(HttpMethod::HEAD)
.uri(&format!("/accounts/{}", nr))
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let req = test::TestRequest::default()
.method(HttpMethod::HEAD)
.uri("/accounts/muh")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let payload = AmountData { amount: 5_f64 };
let req = test::TestRequest::patch()
.uri(&format!("/accounts/{}/withdraw", nr))
.set_form(&payload)
.to_request();
let resp = test::call_service(&app, req).await;
assert!(!resp.status().is_success());
let req = test::TestRequest::post()
let req = test::TestRequest::patch()
.uri(&format!("/accounts/{}/deposit", nr))
.set_form(&payload)
.to_request();
@ -121,6 +145,40 @@ mod tests {
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let body_bytes = to_bytes(resp.into_body()).await.unwrap();
let response_json: Value = serde_json::from_slice(&body_bytes).unwrap();
let balance = response_json["balance"].as_f64().unwrap();
assert_eq!(5_f64, balance);
let payload = AmountData { amount: 10_f64 };
let req = test::TestRequest::put()
.uri(&format!("/accounts/{}", nr))
.set_form(&payload)
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let req = test::TestRequest::put()
.uri("/accounts/muh")
.set_form(&payload)
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_client_error());
let payload = AmountData { amount: -10_f64 };
let req = test::TestRequest::put()
.uri(&format!("/accounts/{}", nr))
.set_form(&payload)
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_client_error());
let req = test::TestRequest::get()
.uri(&format!("/accounts/{}", nr))
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let body_bytes = to_bytes(resp.into_body()).await.unwrap();
let response_json: Value = serde_json::from_slice(&body_bytes).unwrap();
let balance = response_json["balance"].as_f64().unwrap();
@ -134,7 +192,7 @@ mod tests {
.set_form(&payload)
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
assert_eq!(resp.status(), StatusCode::CREATED);
let body_bytes = to_bytes(resp.into_body()).await.unwrap();
let response_json: Value = serde_json::from_slice(&body_bytes).unwrap();
@ -145,7 +203,7 @@ mod tests {
to: nr2.to_string(),
amount: 5_f64,
};
let req = test::TestRequest::post()
let req = test::TestRequest::patch()
.uri("/accounts/transfer")
.set_form(&payload)
.to_request();
@ -163,14 +221,14 @@ mod tests {
let balance = response_json["balance"].as_f64().unwrap();
assert_eq!(5_f64, balance);
let req = test::TestRequest::post()
.uri(&format!("/accounts/{}/close", nr))
let req = test::TestRequest::delete()
.uri(&format!("/accounts/{}", nr))
.to_request();
let resp = test::call_service(&app, req).await;
assert!(!resp.status().is_success());
let payload = AmountData { amount: 5_f64 };
let req = test::TestRequest::post()
let req = test::TestRequest::patch()
.uri(&format!("/accounts/{}/withdraw", nr))
.set_form(&payload)
.to_request();
@ -186,12 +244,19 @@ mod tests {
let nrs = response_json.as_array().unwrap();
assert_eq!(2, nrs.len());
let req = test::TestRequest::post()
.uri(&format!("/accounts/{}/close", nr))
let req = test::TestRequest::delete()
.uri(&format!("/accounts/{}", nr))
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let req = test::TestRequest::default()
.method(HttpMethod::HEAD)
.uri(&format!("/accounts/{}", nr))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::GONE);
let req = test::TestRequest::get().uri("/accounts").to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());