full implementation!
needs heavy testing and benchmarking
This commit is contained in:
parent
f307747c76
commit
866d2dc255
3
.gitignore
vendored
3
.gitignore
vendored
@ -2,3 +2,6 @@
|
|||||||
*.ez
|
*.ez
|
||||||
/build
|
/build
|
||||||
erl_crash.dump
|
erl_crash.dump
|
||||||
|
|
||||||
|
/snapshots
|
||||||
|
config.json
|
||||||
@ -1,4 +1,4 @@
|
|||||||
name = "revault"
|
name = "treevault"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
|
||||||
# Fill out these fields if you intend to generate HTML documentation or publish
|
# 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"
|
decode = ">= 0.5.0 and < 1.0.0"
|
||||||
glisten = ">= 7.0.0 and < 8.0.0"
|
glisten = ">= 7.0.0 and < 8.0.0"
|
||||||
gleam_erlang = ">= 0.33.0 and < 1.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]
|
[dev-dependencies]
|
||||||
gleeunit = ">= 1.0.0 and < 2.0.0"
|
gleeunit = ">= 1.0.0 and < 2.0.0"
|
||||||
birl = ">= 1.7.1 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"
|
||||||
|
|||||||
@ -2,28 +2,45 @@
|
|||||||
# You typically do not need to edit this file
|
# You typically do not need to edit this file
|
||||||
|
|
||||||
packages = [
|
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 = "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 = "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 = "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_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_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_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 = "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 = "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 = "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 = "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.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 = "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 = "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]
|
[requirements]
|
||||||
birl = { version = ">= 1.7.1 and < 2.0.0" }
|
birl = { version = ">= 1.7.1 and < 2.0.0" }
|
||||||
decode = { version = ">= 0.5.0 and < 1.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_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_json = { version = ">= 2.1.0 and < 3.0.0" }
|
||||||
gleam_otp = { version = ">= 0.15.0 and < 1.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.45.0 and < 2.0.0" }
|
||||||
|
gleescript = { version = ">= 1.4.0 and < 2.0.0" }
|
||||||
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
|
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
|
||||||
glisten = { version = ">= 7.0.0 and < 8.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" }
|
simplifile = { version = ">= 2.2.0 and < 3.0.0" }
|
||||||
|
spinner = { version = ">= 1.3.0 and < 2.0.0" }
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
|
import gleam/dynamic
|
||||||
import gleam/io
|
import gleam/io
|
||||||
import gleam/result
|
import gleam/json
|
||||||
|
|
||||||
import gleam/string
|
import gleam/string
|
||||||
import json_serde
|
|
||||||
import simplifile
|
import simplifile
|
||||||
import tree
|
|
||||||
|
|
||||||
pub type Config {
|
pub type Config {
|
||||||
Config(snapshots_path: String, port: Int)
|
Config(snapshots_path: String, port: Int, forest: List(String))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default() -> Config {
|
fn default() -> Config {
|
||||||
Config(snapshots_path: "snapshots", port: 8080)
|
Config(snapshots_path: "snapshots", port: 8080, forest: ["root"])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load(path) -> Config {
|
pub fn load(path) -> Config {
|
||||||
@ -22,27 +22,18 @@ pub fn load(path) -> Config {
|
|||||||
default()
|
default()
|
||||||
}
|
}
|
||||||
Ok(data) -> {
|
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 {
|
case config_tree {
|
||||||
Error(_) -> handle_malformed()
|
Error(_) -> handle_malformed()
|
||||||
Ok(tree) -> {
|
Ok(config) -> config
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
117
src/forest.gleam
Normal file
117
src/forest.gleam
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -93,8 +93,8 @@ fn tree_decoder(data, root) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn leaf_decoder(data) {
|
pub fn decode_leafdata(data) {
|
||||||
let decoded = case dynamic.classify(data) {
|
case dynamic.classify(data) {
|
||||||
"Int" -> {
|
"Int" -> {
|
||||||
use value <- result.try(dynamic.int(data))
|
use value <- result.try(dynamic.int(data))
|
||||||
Ok(tree.Int(value))
|
Ok(tree.Int(value))
|
||||||
@ -113,7 +113,9 @@ fn leaf_decoder(data) {
|
|||||||
}
|
}
|
||||||
_ -> Ok(tree.Null)
|
_ -> Ok(tree.Null)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
use data <- result.try(decoded)
|
fn leaf_decoder(data) {
|
||||||
|
use data <- result.try(decode_leafdata(data))
|
||||||
Ok(tree.Leaf(data))
|
Ok(tree.Leaf(data))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,57 @@
|
|||||||
|
import gleam/dynamic
|
||||||
|
import gleam/json
|
||||||
import gleam/string
|
import gleam/string
|
||||||
|
import json_serde
|
||||||
|
import query_error.{type QueryError, QueryError}
|
||||||
import tree
|
import tree
|
||||||
|
import tree_events
|
||||||
|
|
||||||
pub type QueryError {
|
/// Parses a string query into corresponding TreeEvent
|
||||||
QueryError(String)
|
///
|
||||||
|
/// 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) {
|
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 {
|
let res = case recursive {
|
||||||
False -> tree.get(tree, path)
|
False -> tree.get(tree, path)
|
||||||
_ -> tree.rget(tree, path)
|
_ -> tree.rget(tree, path)
|
||||||
@ -20,19 +62,14 @@ pub fn get(tree, path, recursive) -> Result(tree.Tree, QueryError) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set(tree, path, data) -> Result(tree.Tree, QueryError) {
|
pub fn set(tree, path, data) -> Result(tree.Tree, QueryError) {
|
||||||
let path = path |> parse
|
Ok(tree.set(tree, path, data))
|
||||||
case path {
|
|
||||||
Error(err) -> Error(err)
|
|
||||||
Ok(path) -> Ok(tree.set(tree, path, data))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse(path) -> Result(List(String), QueryError) {
|
pub fn parse_path(path) {
|
||||||
case path |> string.split(".") {
|
case path |> string.split(".") {
|
||||||
[head, ..tail] if head == "" -> Ok(tail)
|
[head, ..tail] -> Ok(#(head, tail))
|
||||||
_ -> Error(QueryError("Bad path."))
|
_ -> Error(QueryError("Bad path."))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/query_error.gleam
Normal file
3
src/query_error.gleam
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub type QueryError {
|
||||||
|
QueryError(String)
|
||||||
|
}
|
||||||
@ -1,6 +0,0 @@
|
|||||||
import config
|
|
||||||
import gleam/io
|
|
||||||
|
|
||||||
pub fn main() {
|
|
||||||
config.load("./config.json") |> io.debug
|
|
||||||
}
|
|
||||||
20
src/tree_events.gleam
Normal file
20
src/tree_events.gleam
Normal file
@ -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)
|
||||||
|
}
|
||||||
125
src/treevault.gleam
Normal file
125
src/treevault.gleam
Normal file
@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -39,7 +39,7 @@ fn fill(tree, i) {
|
|||||||
1_000_000 -> tree
|
1_000_000 -> tree
|
||||||
_ -> {
|
_ -> {
|
||||||
let assert Ok(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)
|
tree |> fill(i + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -51,7 +51,7 @@ fn get_100k(tree, i) {
|
|||||||
_ -> {
|
_ -> {
|
||||||
let rint = int.random(1_000_000)
|
let rint = int.random(1_000_000)
|
||||||
let assert Ok(_) =
|
let assert Ok(_) =
|
||||||
tree |> query.get("." <> rint |> int.to_string(), False)
|
tree |> query.get([".", rint |> int.to_string()], False)
|
||||||
get_100k(tree, i + 1)
|
get_100k(tree, i + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -104,7 +104,6 @@ fn get_nodes(tree, paths) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn path(i: Int) {
|
fn path(i: Int) {
|
||||||
{ i |> int.to_string() |> string.to_graphemes() }
|
{ i |> int.to_string() |> string.to_graphemes }
|
||||||
|> list.fold("", fn(path, s) { path <> s <> "." })
|
|> list.reverse()
|
||||||
|> string.reverse()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
38
test/server_test.gleam
Normal file
38
test/server_test.gleam
Normal file
@ -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>>)
|
||||||
|
}
|
||||||
81
test/treevault_test.gleam
Normal file
81
test/treevault_test.gleam
Normal file
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,4 +1,9 @@
|
|||||||
{
|
{
|
||||||
"snapshots_path": "./snapshots",
|
"snapshots_path": "./snapshots",
|
||||||
"port": 12345
|
"port": 12345,
|
||||||
|
"forest": [
|
||||||
|
"tree1",
|
||||||
|
"tree2",
|
||||||
|
"tree3"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user