diff --git a/.idea/bank-server.iml b/.idea/bank-server.iml index 8258077..8f65552 100644 --- a/.idea/bank-server.iml +++ b/.idea/bank-server.iml @@ -5,6 +5,8 @@ + + diff --git a/Cargo.lock b/Cargo.lock index 8c5a544..cb0d8a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index fb768ba..c4d6de9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,8 @@ members = [ "bank", + "http-client", + "http-lib", "http-server", "socket-server", ] \ No newline at end of file diff --git a/bank/Cargo.toml b/bank/Cargo.toml index 8b673a0..e3c07fa 100644 --- a/bank/Cargo.toml +++ b/bank/Cargo.toml @@ -5,4 +5,4 @@ edition = "2021" [dependencies] thiserror = "1.0.30" -uuid = { version = "0.8.2", features = ["v4"] } \ No newline at end of file +uuid = { version = "0.8.2", features = ["v4", "wasm-bindgen"] } \ No newline at end of file diff --git a/bank/src/account.rs b/bank/src/account.rs index 4e027f6..80dce03 100644 --- a/bank/src/account.rs +++ b/bank/src/account.rs @@ -42,8 +42,6 @@ impl PartialEq for Account { } } -impl Eq for Account {} - impl Hash for Account { fn hash(&self, state: &mut H) { self.number.hash(state); diff --git a/http-client/Cargo.toml b/http-client/Cargo.toml new file mode 100644 index 0000000..6f93c15 --- /dev/null +++ b/http-client/Cargo.toml @@ -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" diff --git a/http-client/dist/index-57ce73b23c43b0f5.js b/http-client/dist/index-57ce73b23c43b0f5.js new file mode 100644 index 0000000..5f494cf --- /dev/null +++ b/http-client/dist/index-57ce73b23c43b0f5.js @@ -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; + diff --git a/http-client/dist/index-57ce73b23c43b0f5_bg.wasm b/http-client/dist/index-57ce73b23c43b0f5_bg.wasm new file mode 100644 index 0000000..a4d154c Binary files /dev/null and b/http-client/dist/index-57ce73b23c43b0f5_bg.wasm differ diff --git a/http-client/dist/index.html b/http-client/dist/index.html new file mode 100644 index 0000000..a3e4f11 --- /dev/null +++ b/http-client/dist/index.html @@ -0,0 +1,34 @@ + + + Vesys Bank + + + + + \ No newline at end of file diff --git a/http-client/dist/styles-3143ec7e42adb2c6.css b/http-client/dist/styles-3143ec7e42adb2c6.css new file mode 100644 index 0000000..8213a6a --- /dev/null +++ b/http-client/dist/styles-3143ec7e42adb2c6.css @@ -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; + } +} diff --git a/http-client/index.html b/http-client/index.html new file mode 100644 index 0000000..0eb7c67 --- /dev/null +++ b/http-client/index.html @@ -0,0 +1,8 @@ + + + + + Vesys Bank + + + \ No newline at end of file diff --git a/http-client/src/api.rs b/http-client/src/api.rs new file mode 100644 index 0000000..e6347d5 --- /dev/null +++ b/http-client/src/api.rs @@ -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, FetchError> { + match client::get_json(&format! {"{}/accounts", host}).await? { + None => Ok(vec![]), + Some(response) => { + let nrs: Vec = serde_json::from_str(&response)?; + Ok(nrs) + } + } +} + +pub async fn fetch_account(host: &str, nr: &str) -> Result { + 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(()) +} diff --git a/http-client/src/client.rs b/http-client/src/client.rs new file mode 100644 index 0000000..e35b20b --- /dev/null +++ b/http-client/src/client.rs @@ -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 for FetchError { + fn from(value: JsValue) -> Self { + Self { err: value } + } +} + +impl From for FetchError { + fn from(e: serde_json::Error) -> Self { + Self { + err: JsValue::from(e.to_string()), + } + } +} + +async fn send_request(request: Request) -> Result { + 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, 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(()) +} diff --git a/http-client/src/components/account.rs b/http-client/src/components/account.rs new file mode 100644 index 0000000..0e8640d --- /dev/null +++ b/http-client/src/components/account.rs @@ -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, + 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, +} + +impl Account { + fn amount(&self) -> f64 { + if let Some(amount_el) = self.amount_ref.cast::() { + 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 { + if let Some(transfer_account_el) = self.transfer_account_ref.cast::() { + 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 { + amount_ref: NodeRef::default(), + transfer_account_ref: NodeRef::default(), + amount_valid: false, + event_bus: EventBus::dispatcher(), + } + } + + fn update(&mut self, ctx: &Context, 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) -> 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! { + <> +
+
+ + {&ctx.props().owner} {" — "} {&ctx.props().balance} + + +
+ + +
+ +
+ + +
+ + + + +
+
+ + } + } +} diff --git a/http-client/src/components/accounts.rs b/http-client/src/components/accounts.rs new file mode 100644 index 0000000..3269c70 --- /dev/null +++ b/http-client/src/components/accounts.rs @@ -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, + pub selected_nr: String +} + +pub struct Accounts { + event_bus: Dispatcher, +} + +impl Component for Accounts { + type Message = Msg; + type Properties = AccountsProps; + + fn create(_: &Context) -> Self { + Self { + event_bus: EventBus::dispatcher(), + } + } + + fn update(&mut self, _: &Context, msg: Self::Message) -> bool { + match msg { + Msg::SelectAccountNr(nr) => { + self.event_bus + .send(Request::EventBusMsg(Event::SelectAccountNr(nr))); + false + } + } + } + + fn view(&self, ctx: &Context) -> Html { + html! { + <> +
    + { for ctx.props().account_nrs.iter().map(|e| self.account_entry(e, &ctx.props().selected_nr, ctx)) } +
+ + } + } +} + +impl Accounts { + fn account_entry( + &self, + nr: &str, + selected_nr: &str, + ctx: &Context, + ) -> 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! { +
  • {account_nr}
  • + } + } +} diff --git a/http-client/src/components/main.rs b/http-client/src/components/main.rs new file mode 100644 index 0000000..a5dafa9 --- /dev/null +++ b/http-client/src/components/main.rs @@ -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>, + error: Option, + account_nrs: Vec, + selected_balance: f64, + selected_owner: String, + selected_nr: String, +} + +impl Main { + fn set_selected_account(&self, ctx: &Context, 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 { + 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, 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) -> Html { + html! { + <> + if let Some(error_msg) = &self.error { +
    { error_msg }
    + } +
    +

    + {"welcome to your vaults"} +

    + + +
    + + } + } + + fn rendered(&mut self, ctx: &Context, first_render: bool) { + if first_render { + ctx.link().send_message(Event::GetAccountNrs); + } + } +} diff --git a/http-client/src/components/mod.rs b/http-client/src/components/mod.rs new file mode 100644 index 0000000..6e5312d --- /dev/null +++ b/http-client/src/components/mod.rs @@ -0,0 +1,3 @@ +pub mod account; +pub mod accounts; +pub mod main; diff --git a/http-client/src/event_bus.rs b/http-client/src/event_bus.rs new file mode 100644 index 0000000..d239959 --- /dev/null +++ b/http-client/src/event_bus.rs @@ -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, + subscribers: HashSet, +} + +impl Agent for EventBus { + type Reach = Context; + type Message = (); + type Input = Request; + type Output = Event; + + fn create(link: AgentLink) -> 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); + } +} diff --git a/http-client/src/events.rs b/http-client/src/events.rs new file mode 100644 index 0000000..9de1362 --- /dev/null +++ b/http-client/src/events.rs @@ -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), + SelectAccountNr(String), + SetSelectedAccount(JsonAccount), + SetBalance(f64, String), + ShowError(String), + Transfer(f64, String, String), +} diff --git a/http-client/src/main.rs b/http-client/src/main.rs new file mode 100644 index 0000000..1e99dbe --- /dev/null +++ b/http-client/src/main.rs @@ -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::
    (); +} diff --git a/http-client/styles.css b/http-client/styles.css new file mode 100644 index 0000000..8213a6a --- /dev/null +++ b/http-client/styles.css @@ -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; + } +} diff --git a/http-lib/Cargo.toml b/http-lib/Cargo.toml new file mode 100644 index 0000000..e78a0c4 --- /dev/null +++ b/http-lib/Cargo.toml @@ -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"] } \ No newline at end of file diff --git a/http-lib/src/json_account.rs b/http-lib/src/json_account.rs new file mode 100644 index 0000000..08355ba --- /dev/null +++ b/http-lib/src/json_account.rs @@ -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 for Account { + fn from(a: JsonAccount) -> Self { + Account { + number: a.number.clone(), + owner: a.owner.clone(), + balance: a.balance, + is_active: a.is_active, + } + } +} diff --git a/http-lib/src/lib.rs b/http-lib/src/lib.rs new file mode 100644 index 0000000..38c6eda --- /dev/null +++ b/http-lib/src/lib.rs @@ -0,0 +1 @@ +pub mod json_account; diff --git a/http-server/Cargo.toml b/http-server/Cargo.toml index 61bc6b5..4e2d72c 100644 --- a/http-server/Cargo.toml +++ b/http-server/Cargo.toml @@ -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" diff --git a/http-server/src/handlers/close_account.rs b/http-server/src/handlers/close_account.rs index d89c948..fd7a1fd 100644 --- a/http-server/src/handlers/close_account.rs +++ b/http-server/src/handlers/close_account.rs @@ -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, data: web::Data) -> Result { let nr = info.into_inner(); diff --git a/http-server/src/handlers/create_account.rs b/http-server/src/handlers/create_account.rs index 75fba20..636d84a 100644 --- a/http-server/src/handlers/create_account.rs +++ b/http-server/src/handlers/create_account.rs @@ -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 })) } diff --git a/http-server/src/handlers/deposit.rs b/http-server/src/handlers/deposit.rs index 6f01823..8bbdd74 100644 --- a/http-server/src/handlers/deposit.rs +++ b/http-server/src/handlers/deposit.rs @@ -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, form: web::Form, diff --git a/http-server/src/handlers/error.rs b/http-server/src/handlers/error.rs index 4e45d6c..9ed7d90 100644 --- a/http-server/src/handlers/error.rs +++ b/http-server/src/handlers/error.rs @@ -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, } } diff --git a/http-server/src/handlers/get_account.rs b/http-server/src/handlers/get_account.rs index fb1e5d9..7e2a4b0 100644 --- a/http-server/src/handlers/get_account.rs +++ b/http-server/src/handlers/get_account.rs @@ -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, data: web::Data) -> Result { let nr = info.into_inner(); diff --git a/http-server/src/handlers/is_active.rs b/http-server/src/handlers/is_active.rs new file mode 100644 index 0000000..6db7b4d --- /dev/null +++ b/http-server/src/handlers/is_active.rs @@ -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, data: web::Data) -> Result { + 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()) + } + } + } +} diff --git a/http-server/src/handlers/mod.rs b/http-server/src/handlers/mod.rs index d93c68f..ea0208c 100644 --- a/http-server/src/handlers/mod.rs +++ b/http-server/src/handlers/mod.rs @@ -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)] diff --git a/http-server/src/handlers/transfer.rs b/http-server/src/handlers/transfer.rs index 16cfa6c..99c21c4 100644 --- a/http-server/src/handlers/transfer.rs +++ b/http-server/src/handlers/transfer.rs @@ -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, data: web::Data, diff --git a/http-server/src/handlers/update_account.rs b/http-server/src/handlers/update_account.rs new file mode 100644 index 0000000..a0cbaf6 --- /dev/null +++ b/http-server/src/handlers/update_account.rs @@ -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, + form: web::Form, + data: web::Data, +) -> Result { + 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()), + } +} diff --git a/http-server/src/handlers/withdraw.rs b/http-server/src/handlers/withdraw.rs index 526243f..043d534 100644 --- a/http-server/src/handlers/withdraw.rs +++ b/http-server/src/handlers/withdraw.rs @@ -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, form: web::Form, diff --git a/http-server/src/main.rs b/http-server/src/main.rs index 7bb0d19..e8063b9 100644 --- a/http-server/src/main.rs +++ b/http-server/src/main.rs @@ -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) -> Box { .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());