Compare commits
10 Commits
866d2dc255
...
ad9ef045cb
| Author | SHA1 | Date | |
|---|---|---|---|
| ad9ef045cb | |||
| b47bff55af | |||
| a64acafbc9 | |||
| 7679c46e5a | |||
| 62a76e9d92 | |||
| b6bba8fcd1 | |||
| 324aeb7cac | |||
| 0388c9249b | |||
| 7ee4ec2cf0 | |||
| 0906e227d5 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,4 +4,5 @@
|
||||
erl_crash.dump
|
||||
|
||||
/snapshots
|
||||
/test/snapshots
|
||||
config.json
|
||||
@ -13,11 +13,10 @@ version = "1.0.0"
|
||||
# https://gleam.run/writing-gleam/gleam-toml/.
|
||||
|
||||
[dependencies]
|
||||
gleam_stdlib = ">= 0.45.0 and < 2.0.0"
|
||||
gleam_stdlib = ">= 0.50.0 and < 1.0.0"
|
||||
gleam_otp = ">= 0.15.0 and < 1.0.0"
|
||||
gleam_json = ">= 2.1.0 and < 3.0.0"
|
||||
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"
|
||||
|
||||
@ -3,41 +3,41 @@
|
||||
|
||||
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 = "birl", version = "1.8.0", build_tools = ["gleam"], requirements = ["gleam_regexp", "gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "2AC7BA26F998E3DFADDB657148BD5DDFE966958AD4D6D6957DD0D22E5B56C400" },
|
||||
{ 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_ansi", version = "1.4.2", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "479DEDC748D08B310C9FEB9C4CBEC46B95C874F7F4F2844304D6D20CA78A8BB5" },
|
||||
{ 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 = "gleam_erlang", version = "0.34.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "0C38F2A128BAA0CEF17C3000BD2097EB80634E239CE31A86400C4416A5D0FDCC" },
|
||||
{ name = "gleam_http", version = "4.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "0A62451FC85B98062E0907659D92E6A89F5F3C0FBE4AB8046C99936BF6F91DBC" },
|
||||
{ name = "gleam_httpc", version = "4.1.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "1A38507AF26CACA145248733688703EADCB734EA971D4E34FB97B7613DECF132" },
|
||||
{ name = "gleam_json", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "C55C5C2B318533A8072D221C5E06E5A75711C129E420DD1CE463342106012E5D" },
|
||||
{ name = "gleam_otp", version = "0.16.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "50DA1539FC8E8FA09924EB36A67A2BBB0AD6B27BCDED5A7EF627057CF69D035E" },
|
||||
{ name = "gleam_regexp", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "7F5E0C0BBEB3C58E57C9CB05FA9002F970C85AD4A63BA1E55CBCB35C15809179" },
|
||||
{ name = "gleam_stdlib", version = "0.54.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "723BA61A2BAE8D67406E59DD88CEA1B3C3F266FC8D70F64BE9FEC81B4505B927" },
|
||||
{ name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" },
|
||||
{ name = "glearray", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "2CDF973B9ECE4A1AB8FBFB719AA37D9F3F1FF947B260C1B21ED5B608B52BC111" },
|
||||
{ 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 = "gleeunit", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "0E6C83834BA65EDCAAF4FE4FB94AC697D9262D83E6F58A750D63C9F6C8A9D9FF" },
|
||||
{ name = "glisten", version = "7.0.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "1A53CF9FB3231A93FF7F1BD519A43DC968C1722F126CDD278403A78725FC5189" },
|
||||
{ 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 = "ranger", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_yielder"], otp_app = "ranger", source = "hex", outer_checksum = "C8988E8F8CDBD3E7F4D8F2E663EF76490390899C2B2885A6432E942495B3E854" },
|
||||
{ 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 = "snag", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "7E9F06390040EB5FAB392CE642771484136F2EC103A92AE11BA898C8167E6E17" },
|
||||
{ name = "spinner", version = "1.3.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "21BDE7FF9D7D9ACBB4086C0D5C86F0A90CE6B0F3CB593B41D03384AE7724B5B4" },
|
||||
{ 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" },
|
||||
{ name = "tom", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0910EE688A713994515ACAF1F486A4F05752E585B9E3209D8F35A85B234C2719" },
|
||||
]
|
||||
|
||||
[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" }
|
||||
gleam_stdlib = { version = ">= 0.50.0 and < 1.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" }
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import gleam/dynamic
|
||||
import gleam/dynamic/decode
|
||||
import gleam/io
|
||||
import gleam/json
|
||||
|
||||
@ -23,13 +23,13 @@ pub fn load(path) -> Config {
|
||||
}
|
||||
Ok(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, _)
|
||||
{
|
||||
use snapshots_path <- decode.field("snapshots_path", decode.string)
|
||||
use port <- decode.field("port", decode.int)
|
||||
use forest <- decode.field("forest", decode.list(decode.string))
|
||||
decode.success(Config(snapshots_path:, port:, forest:))
|
||||
}
|
||||
|> json.parse(data, _)
|
||||
|
||||
case config_tree {
|
||||
Error(_) -> handle_malformed()
|
||||
|
||||
@ -2,15 +2,14 @@ import gleam/dict.{type Dict}
|
||||
import gleam/erlang/process.{type Subject}
|
||||
import gleam/list
|
||||
import gleam/otp/actor
|
||||
import gleam/string
|
||||
|
||||
import gleam/result.{try}
|
||||
import json_serde
|
||||
import query
|
||||
import simplifile
|
||||
import tree
|
||||
import tree_events.{
|
||||
type TreeEvents, GetEvent, RecursiveGetEvent, SetEvent, Shutdown,
|
||||
}
|
||||
import tree_events.{type TreeEvents, GetEvent, SetEvent, Shutdown, Snapshot}
|
||||
|
||||
pub type Message {
|
||||
Message
|
||||
@ -77,15 +76,7 @@ pub fn load(snapshots_path, names) {
|
||||
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)
|
||||
let res = query.get(tree, path)
|
||||
actor.send(
|
||||
client,
|
||||
res |> result.map(fn(tree) { tree |> json_serde.serialize }),
|
||||
@ -95,8 +86,8 @@ fn handle_message(event: TreeEvents, tree) {
|
||||
SetEvent(path, data, client) -> {
|
||||
let res = query.set(tree, path, data)
|
||||
case res {
|
||||
Error(_) -> {
|
||||
actor.send(client, res |> result.map(fn(_) { "Ok" }))
|
||||
Error(err) -> {
|
||||
actor.send(client, res |> result.map(fn(_) { err |> string.inspect }))
|
||||
actor.continue(tree)
|
||||
}
|
||||
Ok(tree) -> {
|
||||
@ -105,13 +96,21 @@ fn handle_message(event: TreeEvents, 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)
|
||||
}
|
||||
Snapshot(filepath, client) -> {
|
||||
actor.send(client, snapshot(tree, filepath))
|
||||
actor.continue(tree)
|
||||
}
|
||||
|
||||
Shutdown -> {
|
||||
actor.Stop(process.Normal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn snapshot(tree, filepath) {
|
||||
let json = tree |> json_serde.serialize()
|
||||
case simplifile.write(filepath, json) {
|
||||
Error(err) -> Error(err |> simplifile.describe_error)
|
||||
Ok(_) -> Ok("Ok")
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
//// Functions to allow serialization from Tree to Json and back!
|
||||
|
||||
import decode/zero
|
||||
import gleam/dict
|
||||
import gleam/dynamic
|
||||
import gleam/dynamic/decode
|
||||
|
||||
import gleam/json.{bool, float, int, null, object, string}
|
||||
import gleam/list
|
||||
@ -49,19 +49,28 @@ fn leaf_to_json(tree: tree.Tree) -> json.Json {
|
||||
}
|
||||
}
|
||||
|
||||
/// Decodes a full tree (which root is tree.Root) from JSON
|
||||
pub fn deserialize(string) {
|
||||
case json.decode(string, zero.run(_, zero.dynamic)) {
|
||||
case json.parse(string, decode.dynamic) {
|
||||
Error(_) -> Error(DecodingError)
|
||||
Ok(data) -> data |> tree_decoder(True) |> Ok
|
||||
}
|
||||
}
|
||||
|
||||
/// Decodes a subtree (which root is NOT tree.Root) from JSON
|
||||
pub fn deserialize_sub(string) {
|
||||
case json.parse(string, decode.dynamic) {
|
||||
Error(_) -> Error(DecodingError)
|
||||
Ok(data) -> data |> tree_decoder(False) |> Ok
|
||||
}
|
||||
}
|
||||
|
||||
fn tree_decoder(data, root) {
|
||||
let _ = case dynamic.classify(data), root {
|
||||
"Dict", True -> {
|
||||
let children =
|
||||
zero.dict(zero.string, zero.dynamic)
|
||||
|> zero.run(data, _)
|
||||
decode.dict(decode.string, decode.dynamic)
|
||||
|> decode.run(data, _)
|
||||
|
||||
case children {
|
||||
Error(_) -> panic as "EncodingError"
|
||||
@ -74,8 +83,8 @@ fn tree_decoder(data, root) {
|
||||
|
||||
"Dict", False -> {
|
||||
let children =
|
||||
zero.dict(zero.string, zero.dynamic)
|
||||
|> zero.run(data, _)
|
||||
decode.dict(decode.string, decode.dynamic)
|
||||
|> decode.run(data, _)
|
||||
|
||||
case children {
|
||||
Error(_) -> panic as "EncodingError"
|
||||
@ -96,19 +105,19 @@ fn tree_decoder(data, root) {
|
||||
pub fn decode_leafdata(data) {
|
||||
case dynamic.classify(data) {
|
||||
"Int" -> {
|
||||
use value <- result.try(dynamic.int(data))
|
||||
use value <- result.try(decode.run(data, decode.int))
|
||||
Ok(tree.Int(value))
|
||||
}
|
||||
"Float" -> {
|
||||
use value <- result.try(dynamic.float(data))
|
||||
use value <- result.try(decode.run(data, decode.float))
|
||||
Ok(tree.Float(value))
|
||||
}
|
||||
"Bool" -> {
|
||||
use value <- result.try(dynamic.bool(data))
|
||||
use value <- result.try(decode.run(data, decode.bool))
|
||||
Ok(tree.Bool(value))
|
||||
}
|
||||
"String" -> {
|
||||
use value <- result.try(dynamic.string(data))
|
||||
use value <- result.try(decode.run(data, decode.string))
|
||||
Ok(tree.String(value))
|
||||
}
|
||||
_ -> Ok(tree.Null)
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import gleam/dynamic
|
||||
import gleam/json
|
||||
import gleam/string
|
||||
import json_serde
|
||||
import query_error.{type QueryError, QueryError}
|
||||
@ -10,7 +8,7 @@ import tree_events
|
||||
///
|
||||
/// Query formats:
|
||||
///
|
||||
/// - get | rget (path)
|
||||
/// - get (path)
|
||||
/// - set (path) (data)
|
||||
pub fn parse(query: String) {
|
||||
let args = query |> string.split(" ")
|
||||
@ -23,20 +21,11 @@ pub fn parse(query: String) {
|
||||
|> 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
|
||||
let data = data |> json_serde.deserialize_sub
|
||||
case data {
|
||||
Error(_) -> Error(QueryError("Wrong data format."))
|
||||
Ok(data) ->
|
||||
@ -50,16 +39,11 @@ pub fn parse(query: String) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(tree, path, recursive) -> Result(tree.Tree, QueryError) {
|
||||
{
|
||||
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 get(tree, path) -> Result(tree.Tree, QueryError) {
|
||||
let res = tree.get(tree, path)
|
||||
case res {
|
||||
Error(_) -> Error(QueryError("There is no such node."))
|
||||
Ok(tree) -> Ok(tree)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -18,10 +18,6 @@ pub fn new() {
|
||||
Root(dict.new())
|
||||
}
|
||||
|
||||
/// Traverses the tree along the given path.
|
||||
///
|
||||
/// If the last found node is a Leaf, returns it.
|
||||
/// Else returns Nil
|
||||
pub fn get(tree, path) {
|
||||
let res = get_step(tree, path)
|
||||
case res {
|
||||
@ -30,15 +26,6 @@ pub fn get(tree, path) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursive Get: returns whole requsted subtree
|
||||
pub fn rget(tree, path) {
|
||||
let res = get_step(tree, path)
|
||||
case res {
|
||||
Error(_) -> Error(Nil)
|
||||
Ok(_) as res -> res
|
||||
}
|
||||
}
|
||||
|
||||
fn get_step(tree: Tree, path: List(String)) -> Result(Tree, Nil) {
|
||||
case tree, path {
|
||||
Root(_) as rt, [] -> Ok(rt)
|
||||
@ -64,10 +51,10 @@ pub fn set(tree, path, data) {
|
||||
res
|
||||
}
|
||||
|
||||
fn set_step(tree: Tree, path: List(String), data: LeafData) {
|
||||
fn set_step(tree: Tree, path: List(String), data: Tree) {
|
||||
case tree, path {
|
||||
Leaf(_), [child] -> Node(dict.new() |> dict.insert(child, Leaf(data)))
|
||||
Node(children), [child] -> Node(children |> dict.insert(child, Leaf(data)))
|
||||
Leaf(_), [child] -> Node(dict.new() |> dict.insert(child, data))
|
||||
Node(children), [child] -> Node(children |> dict.insert(child, data))
|
||||
Leaf(_), [head, ..tail] ->
|
||||
Node(
|
||||
dict.new()
|
||||
@ -87,7 +74,7 @@ fn set_step(tree: Tree, path: List(String), data: LeafData) {
|
||||
Root(children), [head] ->
|
||||
Root(
|
||||
children
|
||||
|> dict.insert(head, Leaf(data)),
|
||||
|> dict.insert(head, data),
|
||||
)
|
||||
|
||||
Root(children), [head, ..tail] ->
|
||||
|
||||
@ -5,16 +5,13 @@ 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,
|
||||
data: tree.Tree,
|
||||
reply_with: Subject(Result(String, QueryError)),
|
||||
)
|
||||
|
||||
Shutdown(filepath: String)
|
||||
Snapshot(filepath: String, reply_with: Subject(Result(String, String)))
|
||||
|
||||
Shutdown
|
||||
}
|
||||
|
||||
@ -3,23 +3,36 @@ import forest
|
||||
import gleam/bit_array
|
||||
import gleam/bytes_tree
|
||||
import gleam/dict
|
||||
import gleam/erlang
|
||||
import gleam/erlang/process
|
||||
import gleam/int
|
||||
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
|
||||
import spinner
|
||||
|
||||
pub fn main() {
|
||||
let config = config.load("./config.json") |> io.debug
|
||||
init("./treevault.json")
|
||||
process.sleep_forever()
|
||||
}
|
||||
|
||||
pub fn init(config_path) {
|
||||
io.println("🌳 Hello from Treevault! 🌳")
|
||||
let spinner =
|
||||
spinner.new("Loading configuration file...")
|
||||
|> spinner.with_colour(ansi.pink)
|
||||
|> spinner.start
|
||||
let config = config.load(config_path)
|
||||
spinner |> spinner.stop
|
||||
|
||||
let spinner =
|
||||
spinner.new("Loading trees...")
|
||||
|> spinner.with_colour(ansi.pink)
|
||||
|> spinner.start
|
||||
let forest = case forest.load(config.snapshots_path, config.forest) {
|
||||
Error(err) -> {
|
||||
let reason = err |> string.inspect
|
||||
@ -27,7 +40,15 @@ pub fn main() {
|
||||
}
|
||||
Ok(forest) -> forest
|
||||
}
|
||||
spinner |> spinner.stop
|
||||
|
||||
forest |> serve(config.port)
|
||||
io.println(
|
||||
"All set! Listening on port " <> config.port |> int.to_string <> ".",
|
||||
)
|
||||
}
|
||||
|
||||
fn serve(forest: forest.Forest, port: Int) {
|
||||
let assert Ok(_) =
|
||||
glisten.handler(fn(_conn) { #(Nil, option.None) }, fn(msg, state, conn) {
|
||||
let assert glisten.Packet(msg) = msg
|
||||
@ -44,82 +65,32 @@ pub fn main() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
let result = run_query(forest, root, evt)
|
||||
case result {
|
||||
Ok(ok) -> glisten.send(conn, ok |> bytes_tree.from_string)
|
||||
Error(err) -> glisten.send(conn, err |> bytes_tree.from_string)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
actor.continue(state)
|
||||
})
|
||||
|> glisten.serve(config.port)
|
||||
|> glisten.serve(port)
|
||||
|
||||
io.println(
|
||||
"Hello from Treevault! 🌳\n
|
||||
To exit gracefully, use \"exit\".
|
||||
To list available commands, use \"help\".
|
||||
",
|
||||
)
|
||||
|
||||
read_next(forest)
|
||||
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 run_query(forest: forest.Forest, root, evt) {
|
||||
let tree = forest.trees |> dict.get(root)
|
||||
case tree {
|
||||
Error(_) -> Error("No such tree.")
|
||||
Ok(tree) -> {
|
||||
let res = tree.1 |> process.call_forever(evt)
|
||||
let _ = case res {
|
||||
Error(query_error.QueryError(reason)) -> Error(reason)
|
||||
Ok(res) -> Ok(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
@ -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.Leaf(tree.Int(i)))
|
||||
tree |> fill(i + 1)
|
||||
}
|
||||
}
|
||||
@ -50,8 +50,7 @@ fn get_100k(tree, i) {
|
||||
100_000 -> Nil
|
||||
_ -> {
|
||||
let rint = int.random(1_000_000)
|
||||
let assert Ok(_) =
|
||||
tree |> query.get([".", rint |> int.to_string()], False)
|
||||
let assert Ok(_) = tree |> query.get([".", rint |> int.to_string()])
|
||||
get_100k(tree, i + 1)
|
||||
}
|
||||
}
|
||||
@ -82,7 +81,7 @@ fn tree_bench() {
|
||||
fn fill_tree(tree, paths) {
|
||||
paths
|
||||
|> list.fold(tree, fn(tree, path) {
|
||||
let assert Ok(tree) = tree |> query.set(path, tree.String("foo"))
|
||||
let assert Ok(tree) = tree |> query.set(path, tree.Leaf(tree.String("foo")))
|
||||
tree
|
||||
})
|
||||
}
|
||||
@ -98,7 +97,7 @@ fn setup_nodes_tasks(tree, paths) {
|
||||
fn get_nodes(tree, paths) {
|
||||
paths
|
||||
|> list.fold(tree, fn(tree, path) {
|
||||
let _ = tree |> query.get(path, True)
|
||||
let _ = tree |> query.get(path)
|
||||
tree
|
||||
})
|
||||
}
|
||||
@ -1,15 +1,15 @@
|
||||
import gleam/erlang/process
|
||||
import gleam/otp/task
|
||||
import gleeunit
|
||||
import gleeunit/should
|
||||
import mug
|
||||
import treevault
|
||||
|
||||
pub fn main() {
|
||||
server_test()
|
||||
gleeunit.main()
|
||||
}
|
||||
|
||||
pub fn server_test() {
|
||||
let _ = task.async(treevault.main)
|
||||
process.start(fn() { treevault.init("non-existent-file.json") }, True)
|
||||
process.sleep(2000)
|
||||
|
||||
let assert Ok(socket) =
|
||||
@ -31,8 +31,27 @@ pub fn server_test() {
|
||||
|
||||
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(Nil) = mug.send(socket, <<"get root":utf8>>)
|
||||
let assert Ok(packet) = mug.receive(socket, timeout_milliseconds: 100)
|
||||
|
||||
packet |> should.equal(<<"{\"a\":2,\"b\":\"foo\"}":utf8>>)
|
||||
|
||||
let assert Ok(Nil) =
|
||||
mug.send(socket, <<
|
||||
"set root.c {\"a\":{\"b\":{\"c\":{\"d\":{\"e\":{\"f\":true}}}}}}":utf8,
|
||||
>>)
|
||||
let assert Ok(_) = mug.receive(socket, timeout_milliseconds: 100)
|
||||
let assert Ok(Nil) = mug.send(socket, <<"get root.c":utf8>>)
|
||||
let assert Ok(packet) = mug.receive(socket, timeout_milliseconds: 100)
|
||||
packet
|
||||
|> should.equal(<<
|
||||
"{\"a\":{\"b\":{\"c\":{\"d\":{\"e\":{\"f\":true}}}}}}":utf8,
|
||||
>>)
|
||||
|
||||
let assert Ok(Nil) = mug.send(socket, <<"set root.a {\"baz\":\"bar\"}":utf8>>)
|
||||
let assert Ok(_) = mug.receive(socket, timeout_milliseconds: 100)
|
||||
let assert Ok(Nil) = mug.send(socket, <<"get root.a":utf8>>)
|
||||
let assert Ok(packet) = mug.receive(socket, timeout_milliseconds: 100)
|
||||
packet
|
||||
|> should.equal(<<"{\"baz\":\"bar\"}":utf8>>)
|
||||
}
|
||||
|
||||
16
test/treevault.json
Normal file
16
test/treevault.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"snapshots_path": "./test/snapshots",
|
||||
"port": 12345,
|
||||
"forest": [
|
||||
"tree0",
|
||||
"tree1",
|
||||
"tree2",
|
||||
"tree3",
|
||||
"tree4",
|
||||
"tree5",
|
||||
"tree6",
|
||||
"tree7",
|
||||
"tree8",
|
||||
"tree9"
|
||||
]
|
||||
}
|
||||
@ -18,53 +18,57 @@ pub fn path_parse_test() {
|
||||
|
||||
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)
|
||||
let assert Ok(tree) = query.set(tree, ["a"], tree.Leaf(tree.String("foo")))
|
||||
let assert Ok(tree) = query.set(tree, ["b"], tree.Leaf(tree.Int(42)))
|
||||
let assert Ok(tree) = query.set(tree, ["c"], tree.Leaf(tree.Float(12.34)))
|
||||
let assert Ok(tree) = query.set(tree, ["d"], tree.Leaf(tree.Bool(False)))
|
||||
let assert Ok(tree) = query.set(tree, ["e"], tree.Leaf(tree.Null))
|
||||
|
||||
query.get(tree, ["a"], False)
|
||||
query.get(tree, ["a"])
|
||||
|> should.equal(tree.Leaf(tree.String("foo")) |> Ok())
|
||||
query.get(tree, ["b"], False)
|
||||
query.get(tree, ["b"])
|
||||
|> should.equal(tree.Leaf(tree.Int(42)) |> Ok())
|
||||
query.get(tree, ["c"], False)
|
||||
query.get(tree, ["c"])
|
||||
|> should.equal(tree.Leaf(tree.Float(12.34)) |> Ok())
|
||||
query.get(tree, ["d"], False)
|
||||
query.get(tree, ["d"])
|
||||
|> should.equal(tree.Leaf(tree.Bool(False)) |> Ok())
|
||||
query.get(tree, ["e"], False)
|
||||
query.get(tree, ["e"])
|
||||
|> 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.set(tree, ["a", "b", "c", "d"], tree.Leaf(tree.String("foo")))
|
||||
let assert Ok(tree) =
|
||||
query.set(tree, ["a", "b2", "c"], tree.Leaf(tree.String("bar")))
|
||||
let assert Ok(tree) =
|
||||
query.set(tree, ["a", "b"], tree.Leaf(tree.String("new foo")))
|
||||
let assert Ok(tree) = query.set(tree, ["a2", "b3"], tree.Leaf(tree.Int(42)))
|
||||
let assert Ok(tree) = query.set(tree, ["a2", "b3"], tree.Leaf(tree.Int(43)))
|
||||
|
||||
query.get(tree, ["a", "b"], False)
|
||||
query.get(tree, ["a", "b"])
|
||||
|> should.equal(tree.Leaf(tree.String("new foo")) |> Ok())
|
||||
|
||||
query.get(tree, ["a", "b2"], True)
|
||||
query.get(tree, ["a", "b2"])
|
||||
|> should.equal(
|
||||
tree.Node(dict.new() |> dict.insert("c", tree.Leaf(tree.String("bar"))))
|
||||
|> Ok(),
|
||||
)
|
||||
query.get(tree, ["a2", "b3"], False)
|
||||
query.get(tree, ["a2", "b3"])
|
||||
|> 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))
|
||||
query.set(tree, ["a", "b", "c", "d"], tree.Leaf(tree.String("foo")))
|
||||
let assert Ok(tree) =
|
||||
query.set(tree, ["a", "b2", "c"], tree.Leaf(tree.String("bar")))
|
||||
let assert Ok(tree) =
|
||||
query.set(tree, ["a", "b"], tree.Leaf(tree.String("new foo")))
|
||||
let assert Ok(tree) = query.set(tree, ["a2", "b3"], tree.Leaf(tree.Int(42)))
|
||||
let assert Ok(tree) = query.set(tree, ["a2", "b3"], tree.Leaf(tree.Int(43)))
|
||||
|
||||
let json = tree |> json_serde.serialize
|
||||
let assert Ok(decoded) = json |> json_serde.deserialize()
|
||||
@ -73,9 +77,12 @@ pub fn json_test() {
|
||||
}
|
||||
|
||||
pub fn config_test() {
|
||||
let config = config.load("./test/vault.json")
|
||||
let config = config.load("./test/treevault.json")
|
||||
should.equal(
|
||||
config.Config("./snapshots", 12_345, ["tree1", "tree2", "tree3"]),
|
||||
config.Config("./test/snapshots", 12_345, [
|
||||
"tree0", "tree1", "tree2", "tree3", "tree4", "tree5", "tree6", "tree7",
|
||||
"tree8", "tree9",
|
||||
]),
|
||||
config,
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
{
|
||||
"snapshots_path": "./snapshots",
|
||||
"port": 12345,
|
||||
"forest": [
|
||||
"tree1",
|
||||
"tree2",
|
||||
"tree3"
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user