From 866d2dc2555c1e3cd0a4afdf93ab9a3632bc6c18 Mon Sep 17 00:00:00 2001 From: Ivan Yuriev Date: Thu, 12 Dec 2024 03:32:23 +0300 Subject: [PATCH] full implementation! needs heavy testing and benchmarking --- .gitignore | 3 + README.md | 2 +- gleam.toml | 8 ++- manifest.toml | 17 ++++++ src/config.gleam | 39 +++++------- src/forest.gleam | 117 +++++++++++++++++++++++++++++++++++ src/json_serde.gleam | 8 ++- src/query.gleam | 79 +++++++++++++++++------- src/query_error.gleam | 3 + src/revault.gleam | 6 -- src/tree_events.gleam | 20 ++++++ src/treevault.gleam | 125 ++++++++++++++++++++++++++++++++++++++ test/benchmark.gleam | 9 ++- test/config_test.gleam | 12 ---- test/revault_test.gleam | 68 --------------------- test/server_test.gleam | 38 ++++++++++++ test/treevault_test.gleam | 81 ++++++++++++++++++++++++ test/vault.json | 7 ++- 18 files changed, 500 insertions(+), 142 deletions(-) create mode 100644 src/forest.gleam create mode 100644 src/query_error.gleam delete mode 100644 src/revault.gleam create mode 100644 src/tree_events.gleam create mode 100644 src/treevault.gleam delete mode 100644 test/config_test.gleam delete mode 100644 test/revault_test.gleam create mode 100644 test/server_test.gleam create mode 100644 test/treevault_test.gleam diff --git a/.gitignore b/.gitignore index 599be4e..bfb160d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ *.ez /build erl_crash.dump + +/snapshots +config.json \ No newline at end of file diff --git a/README.md b/README.md index b7063e2..a388127 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# revault +# treevault diff --git a/gleam.toml b/gleam.toml index da8b0e5..badd29e 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,4 +1,4 @@ -name = "revault" +name = "treevault" version = "1.0.0" # Fill out these fields if you intend to generate HTML documentation or publish @@ -20,7 +20,13 @@ simplifile = ">= 2.2.0 and < 3.0.0" decode = ">= 0.5.0 and < 1.0.0" glisten = ">= 7.0.0 and < 8.0.0" gleam_erlang = ">= 0.33.0 and < 1.0.0" +spinner = ">= 1.3.0 and < 2.0.0" +gleam_community_ansi = ">= 1.4.1 and < 2.0.0" + [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" birl = ">= 1.7.1 and < 2.0.0" +gleam_httpc = ">= 4.0.0 and < 5.0.0" +mug = ">= 1.2.0 and < 2.0.0" +gleescript = ">= 1.4.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml index fcf6897..baee2a7 100644 --- a/manifest.toml +++ b/manifest.toml @@ -2,28 +2,45 @@ # You typically do not need to edit this file packages = [ + { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, { name = "decode", version = "0.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "decode", source = "hex", outer_checksum = "05E14DC95A550BA51B8774485B04894B87A898C588B9B1C920104B110AED218B" }, { name = "filepath", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "67A6D15FB39EEB69DD31F8C145BB5A421790581BD6AA14B33D64D5A55DBD6587" }, + { name = "gleam_community_ansi", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "4CD513FC62523053E62ED7BAC2F36136EC17D6A8942728250A9A00A15E340E4B" }, + { name = "gleam_community_colour", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "386CB9B01B33371538672EEA8A6375A0A0ADEF41F17C86DDCB81C92AD00DA610" }, { name = "gleam_erlang", version = "0.33.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "A1D26B80F01901B59AABEE3475DD4C18D27D58FA5C897D922FCB9B099749C064" }, + { name = "gleam_http", version = "3.7.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8A70D2F70BB7CFEB5DF048A2183FFBA91AF6D4CF5798504841744A16999E33D2" }, + { name = "gleam_httpc", version = "4.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "76FEEC99473E568EBA34336A37CF3D54629ACE77712950DC9BB097B5FD664664" }, { name = "gleam_json", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "0A57FB5666E695FD2BEE74C0428A98B0FC11A395D2C7B4CDF5E22C5DD32C74C6" }, { name = "gleam_otp", version = "0.15.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "E9ED3DF7E7285DA0C440F46AE8236ADC8475E8CCBEE4899BF09A8468DA3F9187" }, { name = "gleam_stdlib", version = "0.45.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "206FCE1A76974AECFC55AEBCD0217D59EDE4E408C016E2CFCCC8FF51278F186E" }, + { name = "glearray", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "B99767A9BC63EF9CC8809F66C7276042E5EFEACAA5B25188B552D3691B91AC6D" }, + { name = "gleescript", version = "1.4.0", build_tools = ["gleam"], requirements = ["argv", "filepath", "gleam_erlang", "gleam_stdlib", "simplifile", "snag", "tom"], otp_app = "gleescript", source = "hex", outer_checksum = "8CDDD29F91064E69950A91A40061785F10275ADB70A0520075591F61A724C455" }, { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, { name = "glisten", version = "7.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "028C0882EAC7ABEDEFBE92CE4D1FEDADE95FA81B1B1AB099C4F91C133BEF2C42" }, { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, + { name = "mug", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "mug", source = "hex", outer_checksum = "C5F62A3FD753B823CE296ED1B223D4B2FF06E91170A7DE35A283D70BB74B700E" }, { name = "ranger", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "B8F3AFF23A3A5B5D9526B8D18E7C43A7DFD3902B151B97EC65397FE29192B695" }, + { name = "repeatedly", version = "2.1.2", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "93AE1938DDE0DC0F7034F32C1BF0D4E89ACEBA82198A1FE21F604E849DA5F589" }, { name = "simplifile", version = "2.2.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0DFABEF7DC7A9E2FF4BB27B108034E60C81BEBFCB7AB816B9E7E18ED4503ACD8" }, + { name = "snag", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "08E9EB87C413457DB1DD66CD704C6878DACC9C93B418600F63873D0CD224E756" }, + { name = "spinner", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "B824C4CFDA6AC912D14365BF365F2A52C4DA63EF2D768D2A1C46D9BF7AF669E7" }, { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" }, + { name = "tom", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "228E667239504B57AD05EC3C332C930391592F6C974D0EFECF32FFD0F3629A27" }, ] [requirements] birl = { version = ">= 1.7.1 and < 2.0.0" } decode = { version = ">= 0.5.0 and < 1.0.0" } +gleam_community_ansi = { version = ">= 1.4.1 and < 2.0.0" } gleam_erlang = { version = ">= 0.33.0 and < 1.0.0" } +gleam_httpc = { version = ">= 4.0.0 and < 5.0.0" } gleam_json = { version = ">= 2.1.0 and < 3.0.0" } gleam_otp = { version = ">= 0.15.0 and < 1.0.0" } gleam_stdlib = { version = ">= 0.45.0 and < 2.0.0" } +gleescript = { version = ">= 1.4.0 and < 2.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" } glisten = { version = ">= 7.0.0 and < 8.0.0" } +mug = { version = ">= 1.2.0 and < 2.0.0" } simplifile = { version = ">= 2.2.0 and < 3.0.0" } +spinner = { version = ">= 1.3.0 and < 2.0.0" } diff --git a/src/config.gleam b/src/config.gleam index 13ef0a2..e9874e3 100644 --- a/src/config.gleam +++ b/src/config.gleam @@ -1,16 +1,16 @@ +import gleam/dynamic import gleam/io -import gleam/result +import gleam/json + import gleam/string -import json_serde import simplifile -import tree pub type Config { - Config(snapshots_path: String, port: Int) + Config(snapshots_path: String, port: Int, forest: List(String)) } fn default() -> Config { - Config(snapshots_path: "snapshots", port: 8080) + Config(snapshots_path: "snapshots", port: 8080, forest: ["root"]) } pub fn load(path) -> Config { @@ -22,27 +22,18 @@ pub fn load(path) -> Config { default() } Ok(data) -> { - let config_tree = json_serde.deserialize(data) + let config_tree = + dynamic.decode3( + Config, + dynamic.field("snapshots_path", dynamic.string), + dynamic.field("port", dynamic.int), + dynamic.field("forest", dynamic.list(dynamic.string)), + ) + |> json.decode(data, _) + case config_tree { Error(_) -> handle_malformed() - Ok(tree) -> { - let snapshots_path = tree |> tree.get(["snapshots_path"]) - let port = tree |> tree.get(["port"]) - - let res = result.all([snapshots_path, port]) - case res { - Error(_) -> handle_malformed() - - Ok(values) -> - case values { - [ - tree.Leaf(tree.String(snapshots_path)), - tree.Leaf(tree.Int(port)), - ] -> Config(snapshots_path, port) - _ -> handle_malformed() - } - } - } + Ok(config) -> config } } } diff --git a/src/forest.gleam b/src/forest.gleam new file mode 100644 index 0000000..1b974d6 --- /dev/null +++ b/src/forest.gleam @@ -0,0 +1,117 @@ +import gleam/dict.{type Dict} +import gleam/erlang/process.{type Subject} +import gleam/list +import gleam/otp/actor + +import gleam/result.{try} +import json_serde +import query +import simplifile +import tree +import tree_events.{ + type TreeEvents, GetEvent, RecursiveGetEvent, SetEvent, Shutdown, +} + +pub type Message { + Message +} + +pub type LoadError { + ActorError + FileError + JsonError +} + +pub type Forest { + Forest(trees: Dict(String, #(String, Subject(TreeEvents)))) +} + +pub fn load(snapshots_path, names) { + let _ = simplifile.create_directory(snapshots_path) + let paths = + names + |> list.map(fn(filename) { + #(filename, snapshots_path <> "/" <> filename <> ".json") + }) + + let trees = + paths + |> list.map(fn(root) { + let _ = simplifile.create_file(root.1) + use json <- try( + simplifile.read(root.1) |> result.map_error(fn(_) { FileError }), + ) + case json == "" { + False -> + case json |> json_serde.deserialize() { + Error(_) -> Error(JsonError) + Ok(tree) -> Ok(#(root.0, root.1, tree)) + } + True -> Ok(#(root.0, root.1, tree.new())) + } + }) + + case trees |> result.all { + Error(err) -> Error(err) + Ok(trees) -> { + let actor_startup = + trees + |> list.map(fn(tree) { + let #(name, path, tree) = tree + + #(name, path, actor.start(tree, handle_message)) + }) + + actor_startup + |> list.map(fn(tuple) { + let assert Ok(actor) = tuple.2 + #(tuple.0, #(tuple.1, actor)) + }) + |> dict.from_list + |> Forest + |> Ok + } + } +} + +fn handle_message(event: TreeEvents, tree) { + case event { + GetEvent(path, client) -> { + let res = query.get(tree, path, False) + actor.send( + client, + res |> result.map(fn(tree) { tree |> json_serde.serialize }), + ) + actor.continue(tree) + } + RecursiveGetEvent(path, client) -> { + let res = query.get(tree, path, False) + actor.send( + client, + res |> result.map(fn(tree) { tree |> json_serde.serialize }), + ) + actor.continue(tree) + } + SetEvent(path, data, client) -> { + let res = query.set(tree, path, data) + case res { + Error(_) -> { + actor.send(client, res |> result.map(fn(_) { "Ok" })) + actor.continue(tree) + } + Ok(tree) -> { + actor.send(client, res |> result.map(fn(_) { "Ok" })) + actor.continue(tree) + } + } + } + Shutdown(filepath) -> { + let json = tree |> json_serde.serialize() + case simplifile.write(filepath, json) { + Error(err) -> + actor.Stop(process.Abnormal(err |> simplifile.describe_error)) + Ok(_) -> actor.Stop(process.Normal) + } + } + } +} diff --git a/src/json_serde.gleam b/src/json_serde.gleam index 8b6e205..b1d96f9 100644 --- a/src/json_serde.gleam +++ b/src/json_serde.gleam @@ -93,8 +93,8 @@ fn tree_decoder(data, root) { } } -fn leaf_decoder(data) { - let decoded = case dynamic.classify(data) { +pub fn decode_leafdata(data) { + case dynamic.classify(data) { "Int" -> { use value <- result.try(dynamic.int(data)) Ok(tree.Int(value)) @@ -113,7 +113,9 @@ fn leaf_decoder(data) { } _ -> Ok(tree.Null) } +} - use data <- result.try(decoded) +fn leaf_decoder(data) { + use data <- result.try(decode_leafdata(data)) Ok(tree.Leaf(data)) } diff --git a/src/query.gleam b/src/query.gleam index 078928f..80fe3b9 100644 --- a/src/query.gleam +++ b/src/query.gleam @@ -1,38 +1,75 @@ +import gleam/dynamic +import gleam/json import gleam/string +import json_serde +import query_error.{type QueryError, QueryError} import tree +import tree_events -pub type QueryError { - QueryError(String) +/// Parses a string query into corresponding TreeEvent +/// +/// Query formats: +/// +/// - get | rget (path) +/// - set (path) (data) +pub fn parse(query: String) { + let args = query |> string.split(" ") + case args { + ["get", path] -> { + case path |> parse_path { + Error(_) -> Error(QueryError("Wrong query format.")) + Ok(#(head, tail)) -> + #(head, fn(actor) { tree_events.GetEvent(tail, actor) }) + |> Ok + } + } + ["rget", path] -> { + case path |> parse_path { + Error(_) -> Error(QueryError("Wrong query format.")) + Ok(#(head, tail)) -> + #(head, fn(actor) { tree_events.RecursiveGetEvent(tail, actor) }) + |> Ok + } + } + ["set", path, data] -> { + case path |> parse_path { + Error(_) -> Error(QueryError("Wrong query format.")) + Ok(#(head, tail)) -> { + let assert Ok(data) = data |> json.decode(dynamic.dynamic) + let data = data |> json_serde.decode_leafdata + case data { + Error(_) -> Error(QueryError("Wrong data format.")) + Ok(data) -> + #(head, fn(actor) { tree_events.SetEvent(tail, data, actor) }) + |> Ok + } + } + } + } + _ -> Error(QueryError("Wrong query format.")) + } } pub fn get(tree, path, recursive) -> Result(tree.Tree, QueryError) { - let path = path |> parse - case path { - Error(err) -> Error(err) - Ok(path) -> { - let res = case recursive { - False -> tree.get(tree, path) - _ -> tree.rget(tree, path) - } - case res { - Error(_) -> Error(QueryError("There is no such node.")) - Ok(tree) -> Ok(tree) - } + { + let res = case recursive { + False -> tree.get(tree, path) + _ -> tree.rget(tree, path) + } + case res { + Error(_) -> Error(QueryError("There is no such node.")) + Ok(tree) -> Ok(tree) } } } pub fn set(tree, path, data) -> Result(tree.Tree, QueryError) { - let path = path |> parse - case path { - Error(err) -> Error(err) - Ok(path) -> Ok(tree.set(tree, path, data)) - } + Ok(tree.set(tree, path, data)) } -pub fn parse(path) -> Result(List(String), QueryError) { +pub fn parse_path(path) { case path |> string.split(".") { - [head, ..tail] if head == "" -> Ok(tail) + [head, ..tail] -> Ok(#(head, tail)) _ -> Error(QueryError("Bad path.")) } } diff --git a/src/query_error.gleam b/src/query_error.gleam new file mode 100644 index 0000000..ccbc617 --- /dev/null +++ b/src/query_error.gleam @@ -0,0 +1,3 @@ +pub type QueryError { + QueryError(String) +} diff --git a/src/revault.gleam b/src/revault.gleam deleted file mode 100644 index 9215f03..0000000 --- a/src/revault.gleam +++ /dev/null @@ -1,6 +0,0 @@ -import config -import gleam/io - -pub fn main() { - config.load("./config.json") |> io.debug -} diff --git a/src/tree_events.gleam b/src/tree_events.gleam new file mode 100644 index 0000000..20b32da --- /dev/null +++ b/src/tree_events.gleam @@ -0,0 +1,20 @@ +import gleam/erlang/process.{type Subject} +import query_error.{type QueryError} +import tree + +pub type TreeEvents { + GetEvent(path: List(String), reply_with: Subject(Result(String, QueryError))) + + RecursiveGetEvent( + path: List(String), + reply_with: Subject(Result(String, QueryError)), + ) + + SetEvent( + path: List(String), + data: tree.LeafData, + reply_with: Subject(Result(String, QueryError)), + ) + + Shutdown(filepath: String) +} diff --git a/src/treevault.gleam b/src/treevault.gleam new file mode 100644 index 0000000..771a276 --- /dev/null +++ b/src/treevault.gleam @@ -0,0 +1,125 @@ +import config +import forest +import gleam/bit_array +import gleam/bytes_tree +import gleam/dict +import gleam/erlang +import gleam/erlang/process +import gleam/io +import gleam/list +import gleam/option +import gleam/otp/actor +import gleam/string +import gleam_community/ansi +import glisten +import spinner +import tree_events + +import query +import query_error + +pub fn main() { + let config = config.load("./config.json") |> io.debug + let forest = case forest.load(config.snapshots_path, config.forest) { + Error(err) -> { + let reason = err |> string.inspect + panic as reason + } + Ok(forest) -> forest + } + + let assert Ok(_) = + glisten.handler(fn(_conn) { #(Nil, option.None) }, fn(msg, state, conn) { + let assert glisten.Packet(msg) = msg + let _ = case msg |> bit_array.to_string { + Error(_) -> { + glisten.send(conn, "Not a UTF-8 request." |> bytes_tree.from_string) + } + + Ok(data) -> { + let _ = case data |> query.parse { + Error(err) -> + glisten.send( + conn, + err |> string.inspect |> bytes_tree.from_string, + ) + Ok(#(root, evt)) -> { + let tree = forest.trees |> dict.get(root) + case tree { + Error(_) -> + glisten.send(conn, "No such tree." |> bytes_tree.from_string) + Ok(tree) -> { + let res = tree.1 |> process.call_forever(evt) + let _ = case res { + Error(query_error.QueryError(reason)) -> + glisten.send(conn, reason |> bytes_tree.from_string) + Ok(res) -> { + glisten.send(conn, res |> bytes_tree.from_string) + } + } + } + } + } + } + } + } + + actor.continue(state) + }) + |> glisten.serve(config.port) + + io.println( + "Hello from Treevault! 🌳\n + To exit gracefully, use \"exit\". + To list available commands, use \"help\". + ", + ) + + read_next(forest) +} + +fn read_next(forest) { + let command = erlang.get_line("> ") + case command { + Error(err) -> { + err |> string.inspect |> io.println + read_next(forest) + } + + Ok(term) -> { + case term { + "exit\n" -> { + graceful(forest) + let spinner = + spinner.new("Shutting down...") + |> spinner.with_colour(ansi.pink) + |> spinner.start + process.sleep(5000) + "Bye" |> io.println + spinner |> spinner.stop + } + "help\n" -> { + "Available commands: " |> io.println + "exit" |> io.println + read_next(forest) + } + _ -> { + "Unknown command" |> io.println + read_next(forest) + } + } + } + } +} + +fn graceful(forest: forest.Forest) { + forest.trees + |> dict.to_list + |> list.map(fn(tree) { + let ps = tree.1.1 + let path = tree.1.0 + + ps |> process.send(tree_events.Shutdown(path)) + { "Exiting: " <> tree.0 } |> io.println + }) +} diff --git a/test/benchmark.gleam b/test/benchmark.gleam index 9b6fa4b..92dab5d 100644 --- a/test/benchmark.gleam +++ b/test/benchmark.gleam @@ -39,7 +39,7 @@ fn fill(tree, i) { 1_000_000 -> tree _ -> { let assert Ok(tree) = - tree |> query.set("." <> int.to_string(i), tree.Int(i)) + tree |> query.set([".", int.to_string(i)], tree.Int(i)) tree |> fill(i + 1) } } @@ -51,7 +51,7 @@ fn get_100k(tree, i) { _ -> { let rint = int.random(1_000_000) let assert Ok(_) = - tree |> query.get("." <> rint |> int.to_string(), False) + tree |> query.get([".", rint |> int.to_string()], False) get_100k(tree, i + 1) } } @@ -104,7 +104,6 @@ fn get_nodes(tree, paths) { } fn path(i: Int) { - { i |> int.to_string() |> string.to_graphemes() } - |> list.fold("", fn(path, s) { path <> s <> "." }) - |> string.reverse() + { i |> int.to_string() |> string.to_graphemes } + |> list.reverse() } diff --git a/test/config_test.gleam b/test/config_test.gleam deleted file mode 100644 index 216e634..0000000 --- a/test/config_test.gleam +++ /dev/null @@ -1,12 +0,0 @@ -import config -import gleeunit -import gleeunit/should - -pub fn main() { - gleeunit.main() -} - -pub fn config_test() { - let config = config.load("./test/vault.json") - should.equal(config.Config("./snapshots", 12_345), config) -} diff --git a/test/revault_test.gleam b/test/revault_test.gleam deleted file mode 100644 index 14e3662..0000000 --- a/test/revault_test.gleam +++ /dev/null @@ -1,68 +0,0 @@ -import gleam/dict -import gleeunit -import gleeunit/should -import json_serde -import query -import tree - -pub fn main() { - gleeunit.main() -} - -pub fn path_parse_test() { - ".a.b.c" |> query.parse() |> should.equal(Ok(["a", "b", "c"])) -} - -pub fn kv_test() { - let tree = tree.new() - let assert Ok(tree) = query.set(tree, ".a", tree.String("foo")) - let assert Ok(tree) = query.set(tree, ".b", tree.Int(42)) - let assert Ok(tree) = query.set(tree, ".c", tree.Float(12.34)) - let assert Ok(tree) = query.set(tree, ".d", tree.Bool(False)) - let assert Ok(tree) = query.set(tree, ".e", tree.Null) - - query.get(tree, ".a", False) - |> should.equal(tree.Leaf(tree.String("foo")) |> Ok()) - query.get(tree, ".b", False) - |> should.equal(tree.Leaf(tree.Int(42)) |> Ok()) - query.get(tree, ".c", False) - |> should.equal(tree.Leaf(tree.Float(12.34)) |> Ok()) - query.get(tree, ".d", False) - |> should.equal(tree.Leaf(tree.Bool(False)) |> Ok()) - query.get(tree, ".e", False) - |> should.equal(tree.Leaf(tree.Null) |> Ok()) -} - -pub fn tree_test() { - let tree = tree.new() - let assert Ok(tree) = query.set(tree, ".a.b.c.d", tree.String("foo")) - let assert Ok(tree) = query.set(tree, ".a.b2.c", tree.String("bar")) - let assert Ok(tree) = query.set(tree, ".a.b", tree.String("new foo")) - let assert Ok(tree) = query.set(tree, ".a2.b3", tree.Int(42)) - let assert Ok(tree) = query.set(tree, ".a2.b3", tree.Int(43)) - - query.get(tree, ".a.b", False) - |> should.equal(tree.Leaf(tree.String("new foo")) |> Ok()) - - query.get(tree, ".a.b2", True) - |> should.equal( - tree.Node(dict.new() |> dict.insert("c", tree.Leaf(tree.String("bar")))) - |> Ok(), - ) - query.get(tree, ".a2.b3", False) - |> should.equal(tree.Leaf(tree.Int(43)) |> Ok()) -} - -pub fn json_test() { - let tree = tree.new() - let assert Ok(tree) = query.set(tree, ".a.b.c.d", tree.String("foo")) - let assert Ok(tree) = query.set(tree, ".a.b2.c", tree.String("bar")) - let assert Ok(tree) = query.set(tree, ".a.b", tree.String("new foo")) - let assert Ok(tree) = query.set(tree, ".a2.b3", tree.Int(42)) - let assert Ok(tree) = query.set(tree, ".a2.b3", tree.Int(43)) - - let json = tree |> json_serde.serialize - let assert Ok(decoded) = json |> json_serde.deserialize() - - should.equal(tree, decoded) -} diff --git a/test/server_test.gleam b/test/server_test.gleam new file mode 100644 index 0000000..df196d7 --- /dev/null +++ b/test/server_test.gleam @@ -0,0 +1,38 @@ +import gleam/erlang/process +import gleam/otp/task +import gleeunit/should +import mug +import treevault + +pub fn main() { + server_test() +} + +pub fn server_test() { + let _ = task.async(treevault.main) + process.sleep(2000) + + let assert Ok(socket) = + mug.new("localhost", port: 8080) + |> mug.timeout(milliseconds: 500) + |> mug.connect() + + let assert Ok(Nil) = mug.send(socket, <<"set root.a 2":utf8>>) + let assert Ok(packet) = mug.receive(socket, timeout_milliseconds: 100) + packet |> should.equal(<<"Ok":utf8>>) + + let assert Ok(Nil) = mug.send(socket, <<"get root.a":utf8>>) + let assert Ok(packet) = mug.receive(socket, timeout_milliseconds: 100) + packet |> should.equal(<<"2":utf8>>) + + let assert Ok(Nil) = mug.send(socket, <<"foo root.a":utf8>>) + let assert Ok(packet) = mug.receive(socket, timeout_milliseconds: 100) + packet |> should.equal(<<"QueryError(\"Wrong query format.\")":utf8>>) + + let assert Ok(Nil) = mug.send(socket, <<"set root.b \"foo\"":utf8>>) + let assert Ok(_) = mug.receive(socket, timeout_milliseconds: 100) + let assert Ok(Nil) = mug.send(socket, <<"rget root":utf8>>) + let assert Ok(packet) = mug.receive(socket, timeout_milliseconds: 100) + + packet |> should.equal(<<"{\"a\":2,\"b\":\"foo\"}":utf8>>) +} diff --git a/test/treevault_test.gleam b/test/treevault_test.gleam new file mode 100644 index 0000000..8ad980b --- /dev/null +++ b/test/treevault_test.gleam @@ -0,0 +1,81 @@ +import config +import gleam/dict +import gleeunit +import gleeunit/should +import json_serde +import query +import tree + +pub fn main() { + gleeunit.main() +} + +pub fn path_parse_test() { + "root.a.b.c" + |> query.parse_path() + |> should.equal(Ok(#("root", ["a", "b", "c"]))) +} + +pub fn kv_test() { + let tree = tree.new() + let assert Ok(tree) = query.set(tree, ["a"], tree.String("foo")) + let assert Ok(tree) = query.set(tree, ["b"], tree.Int(42)) + let assert Ok(tree) = query.set(tree, ["c"], tree.Float(12.34)) + let assert Ok(tree) = query.set(tree, ["d"], tree.Bool(False)) + let assert Ok(tree) = query.set(tree, ["e"], tree.Null) + + query.get(tree, ["a"], False) + |> should.equal(tree.Leaf(tree.String("foo")) |> Ok()) + query.get(tree, ["b"], False) + |> should.equal(tree.Leaf(tree.Int(42)) |> Ok()) + query.get(tree, ["c"], False) + |> should.equal(tree.Leaf(tree.Float(12.34)) |> Ok()) + query.get(tree, ["d"], False) + |> should.equal(tree.Leaf(tree.Bool(False)) |> Ok()) + query.get(tree, ["e"], False) + |> should.equal(tree.Leaf(tree.Null) |> Ok()) +} + +pub fn tree_test() { + let tree = tree.new() + let assert Ok(tree) = + query.set(tree, ["a", "b", "c", "d"], tree.String("foo")) + let assert Ok(tree) = query.set(tree, ["a", "b2", "c"], tree.String("bar")) + let assert Ok(tree) = query.set(tree, ["a", "b"], tree.String("new foo")) + let assert Ok(tree) = query.set(tree, ["a2", "b3"], tree.Int(42)) + let assert Ok(tree) = query.set(tree, ["a2", "b3"], tree.Int(43)) + + query.get(tree, ["a", "b"], False) + |> should.equal(tree.Leaf(tree.String("new foo")) |> Ok()) + + query.get(tree, ["a", "b2"], True) + |> should.equal( + tree.Node(dict.new() |> dict.insert("c", tree.Leaf(tree.String("bar")))) + |> Ok(), + ) + query.get(tree, ["a2", "b3"], False) + |> should.equal(tree.Leaf(tree.Int(43)) |> Ok()) +} + +pub fn json_test() { + let tree = tree.new() + let assert Ok(tree) = + query.set(tree, ["a", "b", "c", "d"], tree.String("foo")) + let assert Ok(tree) = query.set(tree, ["a", "b2", "c"], tree.String("bar")) + let assert Ok(tree) = query.set(tree, ["a", "b"], tree.String("new foo")) + let assert Ok(tree) = query.set(tree, ["a2", "b3"], tree.Int(42)) + let assert Ok(tree) = query.set(tree, ["a2", "b3"], tree.Int(43)) + + let json = tree |> json_serde.serialize + let assert Ok(decoded) = json |> json_serde.deserialize() + + should.equal(tree, decoded) +} + +pub fn config_test() { + let config = config.load("./test/vault.json") + should.equal( + config.Config("./snapshots", 12_345, ["tree1", "tree2", "tree3"]), + config, + ) +} diff --git a/test/vault.json b/test/vault.json index 44c7d6a..6ad2cb7 100644 --- a/test/vault.json +++ b/test/vault.json @@ -1,4 +1,9 @@ { "snapshots_path": "./snapshots", - "port": 12345 + "port": 12345, + "forest": [ + "tree1", + "tree2", + "tree3" + ] } \ No newline at end of file