diff --git a/Cargo.lock b/Cargo.lock index afb7d72..6ca6af6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -198,6 +198,12 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.8" @@ -214,6 +220,12 @@ version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "getrandom" version = "0.2.12" @@ -235,6 +247,42 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hcl-edit" +version = "0.7.6" +source = "git+https://github.com/lapce/hcl-rs?rev=fb0ac2875760a8219899f5a4d774d0996a5b06dd#fb0ac2875760a8219899f5a4d774d0996a5b06dd" +dependencies = [ + "fnv", + "hcl-primitives", + "vecmap-rs", + "winnow", +] + +[[package]] +name = "hcl-primitives" +version = "0.1.3" +source = "git+https://github.com/lapce/hcl-rs?rev=fb0ac2875760a8219899f5a4d774d0996a5b06dd#fb0ac2875760a8219899f5a4d774d0996a5b06dd" +dependencies = [ + "itoa", + "kstring", + "ryu", + "serde", + "unicode-ident", +] + +[[package]] +name = "hcl-rs" +version = "0.16.8" +source = "git+https://github.com/lapce/hcl-rs?rev=fb0ac2875760a8219899f5a4d774d0996a5b06dd#fb0ac2875760a8219899f5a4d774d0996a5b06dd" +dependencies = [ + "hcl-edit", + "hcl-primitives", + "indexmap", + "itoa", + "serde", + "vecmap-rs", +] + [[package]] name = "heck" version = "0.4.1" @@ -247,6 +295,17 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown", + "serde", +] + [[package]] name = "indoc" version = "2.0.5" @@ -268,6 +327,16 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "kstring" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3066350882a1cd6d950d055997f379ac37fd39f81cd4d8ed186032eb3c5747" +dependencies = [ + "serde", + "static_assertions", +] + [[package]] name = "libc" version = "0.2.153" @@ -305,6 +374,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + [[package]] name = "mio" version = "0.8.11" @@ -458,14 +533,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "rcl" -version = "0.2.0" -source = "git+https://github.com/lapce/rcl?rev=78796fe8845a2a129b21093604006501fa5cde24#78796fe8845a2a129b21093604006501fa5cde24" -dependencies = [ - "unicode-width", -] - [[package]] name = "redox_syscall" version = "0.4.1" @@ -679,8 +746,9 @@ dependencies = [ "bincode", "clap", "crossbeam-channel", + "hcl-edit", + "hcl-rs", "itertools", - "rcl", "serde", "strum", "strum_macros", @@ -695,7 +763,8 @@ name = "tiron-common" version = "0.1.5" dependencies = [ "anyhow", - "rcl", + "hcl-edit", + "hcl-rs", "serde", "uuid", ] @@ -713,9 +782,10 @@ dependencies = [ "clap", "crossbeam-channel", "documented", + "hcl-edit", + "hcl-rs", "itertools", "os_info", - "rcl", "serde", "serde_json", "tempfile", @@ -776,6 +846,15 @@ dependencies = [ "serde", ] +[[package]] +name = "vecmap-rs" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67d9c76f2c769d47dec11c6f4a9cd3be5e5e025a6bce297b07a311a3514ca97d" +dependencies = [ + "serde", +] + [[package]] name = "version_check" version = "0.9.4" @@ -942,6 +1021,15 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +[[package]] +name = "winnow" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" +dependencies = [ + "memchr", +] + [[package]] name = "zerocopy" version = "0.7.32" diff --git a/Cargo.toml b/Cargo.toml index 1306071..a101cea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,8 @@ members = [ ] [workspace.dependencies] +hcl-rs = { git = "https://github.com/lapce/hcl-rs", rev = "fb0ac2875760a8219899f5a4d774d0996a5b06dd" } +hcl-edit = { git = "https://github.com/lapce/hcl-rs", rev = "fb0ac2875760a8219899f5a4d774d0996a5b06dd" } tempfile = "3.10.1" os_info = "3.7" itertools = "0.12.1" @@ -55,5 +57,3 @@ tiron = { path = "./tiron" } tiron-tui = { path = "./tiron-tui" } tiron-node = { path = "./tiron-node" } tiron-common = { path = "./tiron-common" } -rcl = { git = "https://github.com/lapce/rcl", rev = "78796fe8845a2a129b21093604006501fa5cde24" } -# rcl = { path = "../rcl" } diff --git a/README.md b/README.md index c81cfde..122c6e3 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,10 @@ ## Features -* **No YAML:** Tiron uses a new configuration language called [rcl](https://github.com/ruuda/rcl), which is simple to write with some basic code functionalities. +* **No YAML:** Tiron uses [HCL](https://github.com/hashicorp/hcl) as the configuration language. * **Agentless:** By using SSH, Tiron connects to the remote machines without the need to install an agent first. * **TUI:** Tiron has a built in terminal user interfaces to display the outputs of the running tasks. -* **Correctness:** Tiron pre validates all the rcl files and will throw errors before the task is started to execute. +* **Correctness:** Tiron pre validates all the runbook files and will throw errors before the task is started to execute. * **Speed:** On validating all the input, Tiron also pre populates all the data for tasks, and send them to the remote machines in one go to save the roundtrips between the client and remote. ## Quickstart diff --git a/examples/example_tiron_project/jobs/job1/main.rcl b/examples/example_tiron_project/jobs/job1/main.rcl deleted file mode 100644 index c6401bb..0000000 --- a/examples/example_tiron_project/jobs/job1/main.rcl +++ /dev/null @@ -1,26 +0,0 @@ - -[ - { - action = "copy", - params = { - src = "./files/test.rcl", - dest = "/tmp/test.conf" - } - }, - { - name = "Install httpd and mariadb", - action = "package", - params = { - name = [apache, "mariadb-connector-c"], - state = "latest", - }, - }, - // { - // name = "Install ntpdate", - // action = "package", - // params = { - // name = "ntpdate", - // state = "present", - // } - // }, -] \ No newline at end of file diff --git a/examples/example_tiron_project/jobs/job1/main.tr b/examples/example_tiron_project/jobs/job1/main.tr new file mode 100644 index 0000000..5da3696 --- /dev/null +++ b/examples/example_tiron_project/jobs/job1/main.tr @@ -0,0 +1,18 @@ +use "test.tr" { + job "job2" {} +} + +job "job1" { + action "copy" { + name = "the first action" + params { + src = "/tmp/test.tr" + dest = "/tmp/test.conf" + } + } + action "job" { + params { + name = "job2" + } + } +} \ No newline at end of file diff --git a/examples/example_tiron_project/jobs/job1/test.tr b/examples/example_tiron_project/jobs/job1/test.tr new file mode 100644 index 0000000..d2d1b6e --- /dev/null +++ b/examples/example_tiron_project/jobs/job1/test.tr @@ -0,0 +1,9 @@ +job "job2" { + action "copy" { + name = "the first action in job2" + params { + src = "/tmp/test.tr" + dest = "/tmp/test.conf" + } + } +} \ No newline at end of file diff --git a/examples/example_tiron_project/jobs/test_job.rcl b/examples/example_tiron_project/jobs/test_job.rcl deleted file mode 100644 index e69de29..0000000 diff --git a/examples/example_tiron_project/main.rcl b/examples/example_tiron_project/main.rcl deleted file mode 100644 index 39ca3b0..0000000 --- a/examples/example_tiron_project/main.rcl +++ /dev/null @@ -1,31 +0,0 @@ -[ - { - name = "initial run", - hosts = "production", - remote_user = "dz", - become = true, - actions = [ - { action = "copy", params = {src = "/tmp/test.rcl", dest = "/tmp/test.conf" }}, - { action = "job", params = {name = "job1"} }, - ], - }, - { - hosts = "production", - remote_user = "dz", - actions = [ - { action = "copy", params = {src = "/tmp/test.rcl", dest = "/tmp/test.conf" }}, - ], - }, - { - hosts = ["group1", "production"], - remote_user = "dz", - actions = [ - { action = "copy", params = {src = "/tmp/test.rcl", dest = "/tmp/test.conf" }}, - { action = "job", params = {name = "job1"} }, - { action = "job", params = {name = "job1"} }, - { action = "job", params = {name = "job1"} }, - { action = "job", params = {name = "job1"} }, - { action = "job", params = {name = "job1"} }, - ], - }, -] diff --git a/examples/example_tiron_project/main.tr b/examples/example_tiron_project/main.tr new file mode 100644 index 0000000..a6289c3 --- /dev/null +++ b/examples/example_tiron_project/main.tr @@ -0,0 +1,71 @@ +use "jobs/job1/main.tr" { + job "job1" { + } +} + +use "tiron.tr" { + group "group2" {} +} + +group "production" { + host "localhost" { + apache = "apache2" + } +} + +group "gropu3" { + group "group2" {} +} + +run "production" { + name = "initial run" + remote_user = "dz" + become = true + + action "package" { + params { + name = [apache, "mariadb-connector-c", "${apache}"] + state = "present" + } + } + + action "copy" { + params { + src = "/tmp/test.tr" + dest = "/tmp/test.conf" + } + } + + action "job" { + name = "run job1" + params { + name = "job1" + } + } +} + +run "group2" { + remote_user = "dz" + + action "job" { + params { + name = "job1" + } + } + action "copy" { + params { + src = "/tmp/test.tr" + dest = "/tmp/test.conf" + } + } + action "job" { + params { + name = "job1" + } + } + action "job" { + params { + name = "job1" + } + } +} diff --git a/examples/example_tiron_project/projects/main.rcl b/examples/example_tiron_project/projects/main.rcl deleted file mode 100644 index bbbe13f..0000000 --- a/examples/example_tiron_project/projects/main.rcl +++ /dev/null @@ -1,30 +0,0 @@ -[ - { - name = "initial run", - hosts = "production", - remote_user = "dz", - actions = [ - { action = "copy", params = {src = "/tmp/test.rcl", dest = "/tmp/test.conf" }}, - { action = "job", params = {name = "job1"} }, - ], - }, - { - hosts = "production", - remote_user = "dz", - actions = [ - { action = "copy", params = {src = "/tmp/test.rcl", dest = "/tmp/test.conf" }}, - ], - }, - { - hosts = ["group1", "production"], - remote_user = "dz", - actions = [ - { action = "copy", params = {src = "/tmp/test.rcl", dest = "/tmp/test.conf" }}, - { action = "job", params = {name = "job1"} }, - { action = "job", params = {name = "job1"} }, - { action = "job", params = {name = "job1"} }, - { action = "job", params = {name = "job1"} }, - { action = "job", params = {name = "job1"} }, - ], - }, -] diff --git a/examples/example_tiron_project/tiron.rcl b/examples/example_tiron_project/tiron.rcl deleted file mode 100644 index fc63fe4..0000000 --- a/examples/example_tiron_project/tiron.rcl +++ /dev/null @@ -1,29 +0,0 @@ -{ - groups = { - production = { - hosts = [ - { host = "localhost" }, - ], - vars = { - var1 = "val1", - var2 = "val2", - var3 = "val3", - apache = "httpd", - }, - }, - group1 = { - hosts = [ - { group = "production" }, - { - host = "machine1", - vars = { - var1 = "val1", - var2 = "val2", - var3 = "val3", - apache = "apache", - }, - }, - ] - }, - }, -} diff --git a/examples/example_tiron_project/tiron.tr b/examples/example_tiron_project/tiron.tr new file mode 100644 index 0000000..4629572 --- /dev/null +++ b/examples/example_tiron_project/tiron.tr @@ -0,0 +1,17 @@ +group "group1" { + host "machine1" {} +} + +group "group2" { + host "machine1" { + var1 = "machine1_var1" + } + + group "group1" { + var3 = "var3" + } + + apache = "apache2" + var1 = "var1" + var2 = "var2" +} diff --git a/tiron-common/Cargo.toml b/tiron-common/Cargo.toml index 1075d5c..d401819 100644 --- a/tiron-common/Cargo.toml +++ b/tiron-common/Cargo.toml @@ -4,7 +4,8 @@ version.workspace = true edition.workspace = true [dependencies] +hcl-rs = { workspace = true } +hcl-edit = { workspace = true } anyhow = { workspace = true } serde = { workspace = true } -rcl = { workspace = true } uuid = { workspace = true } diff --git a/tiron-common/src/error.rs b/tiron-common/src/error.rs new file mode 100644 index 0000000..3f2849c --- /dev/null +++ b/tiron-common/src/error.rs @@ -0,0 +1,227 @@ +use std::{io::Write, ops::Range, path::PathBuf}; + +use anyhow::Result; + +/// The runbook file path and content +pub struct Origin { + pub cwd: PathBuf, + pub path: PathBuf, + pub data: String, +} + +impl Origin { + pub fn error(&self, message: impl Into, span: &Option>) -> Error { + Error::new(message.into()).with_origin(self, span) + } +} + +pub struct Error { + pub message: String, + pub location: Option, +} + +pub struct ErrorLocation { + pub path: PathBuf, + pub line_content: String, + pub line: usize, + pub start_col: usize, + pub end_col: usize, +} + +impl Error { + pub fn new(message: impl Into) -> Self { + Self { + message: message.into(), + location: None, + } + } + + pub fn with_origin(mut self, origin: &Origin, span: &Option>) -> Self { + if let Some(span) = span { + let line_begin = origin.data[..span.start] + .as_bytes() + .iter() + .rev() + .position(|&b| b == b'\n') + .map_or(0, |pos| span.start - pos); + + let line_content = origin.data[line_begin..] + .as_bytes() + .iter() + .position(|&b| b == b'\n') + .map_or(&origin.data[line_begin..], |pos| { + &origin.data[line_begin..line_begin + pos] + }); + + let line = origin.data[..span.start] + .as_bytes() + .iter() + .filter(|&&b| b == b'\n') + .count() + + 1; + let start_col = span.start - line_begin + 1; + let end_col = span.start - line_begin + span.len(); + self.location = Some(ErrorLocation { + path: origin.path.clone(), + line_content: line_content.to_string(), + line, + start_col, + end_col, + }); + } + self + } + + pub fn from_hcl(err: hcl_edit::parser::Error, path: PathBuf) -> Error { + Error { + message: err.message().to_string(), + location: Some(ErrorLocation { + path, + line_content: err.line().to_string(), + line: err.location().line(), + start_col: err.location().column(), + end_col: err.location().column(), + }), + } + } + + pub fn err(self) -> Result { + Err(self) + } + + pub fn report_stderr(&self) -> Result<()> { + let mut result = Vec::new(); + result.push(Segment::from("Error: ").with_markup(Markup::Error)); + result.push(self.message.clone().into()); + result.push("\n".into()); + if let Some(location) = &self.location { + let line_len = location.line.to_string().len(); + + result.push(" ".repeat(line_len + 1).into()); + result.push(Segment::from("--> ").with_markup(Markup::Error)); + let path = location.path.to_string_lossy(); + result.push(path.as_ref().into()); + let line_col = format!(":{}:{}\n", location.line, location.start_col); + result.push(line_col.as_str().into()); + + result.push(" ".repeat(line_len + 2).into()); + result.push(Segment::from("╷\n").with_markup(Markup::Error)); + result.push(Segment::from(format!(" {} ", location.line)).with_markup(Markup::Error)); + result.push(Segment::from("│ ").with_markup(Markup::Error)); + result.push(location.line_content.clone().into()); + result.push("\n".into()); + result.push(" ".repeat(line_len + 2).into()); + result.push(Segment::from("╵").with_markup(Markup::Error)); + result.push(" ".repeat(location.start_col).into()); + result.push("^".into()); + for _ in location.start_col..location.end_col { + result.push("~".into()); + } + result.push("\n".into()); + } + + let stderr = std::io::stderr(); + let mut out = stderr.lock(); + let mut markup = Markup::None; + for seg in result { + if markup != seg.markup { + markup = seg.markup; + out.write_all(switch_ansi(markup).as_bytes())?; + } + out.write_all(seg.s.as_bytes())?; + } + + std::process::exit(1); + } +} + +struct Segment { + s: String, + markup: Markup, +} + +impl Segment { + pub fn with_markup(mut self, markup: Markup) -> Self { + self.markup = markup; + self + } +} + +impl From<&str> for Segment { + fn from(value: &str) -> Segment { + Segment { + s: value.to_string(), + markup: Markup::None, + } + } +} + +impl From for Segment { + fn from(value: String) -> Segment { + Segment { + s: value, + markup: Markup::None, + } + } +} + +/// A markup hint, used to apply color and other markup to output. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Markup { + /// No special markup applied, default formatting. + None, + + /// Used for error message reporting, styled in bold. + Error, + /// Used for error message reporting, styled in bold. + Warning, + /// Used for trace message reporting, styled in bold. + Trace, + + /// Make something stand out in error messages. + /// + /// We use this to play a similar role as backticks in Markdown, + /// to clarify visually where the boundaries of a quotation are. + Highlight, + + // These are meant for syntax highlighting. + Builtin, + Comment, + Escape, + Field, + Keyword, + Number, + String, + Type, +} + +/// Return the ANSI escape code to switch to style `markup`. +pub fn switch_ansi(markup: Markup) -> &'static str { + let reset = "\x1b[0m"; + let bold_blue = "\x1b[34;1m"; + let bold_green = "\x1b[32;1m"; + let bold_red = "\x1b[31;1m"; + let bold_yellow = "\x1b[33;1m"; + let blue = "\x1b[34m"; + let cyan = "\x1b[36m"; + let magenta = "\x1b[35m"; + let red = "\x1b[31m"; + let white = "\x1b[37m"; + let yellow = "\x1b[33m"; + + match markup { + Markup::None => reset, + Markup::Error => bold_red, + Markup::Warning => bold_yellow, + Markup::Trace => bold_blue, + Markup::Highlight => white, + Markup::Builtin => red, + Markup::Comment => white, + Markup::Field => blue, + Markup::Keyword => bold_green, + Markup::Number => cyan, + Markup::String => red, + Markup::Escape => yellow, + Markup::Type => magenta, + } +} diff --git a/tiron-common/src/lib.rs b/tiron-common/src/lib.rs index 05fdf15..0039a0a 100644 --- a/tiron-common/src/lib.rs +++ b/tiron-common/src/lib.rs @@ -1,4 +1,6 @@ pub mod action; +pub mod error; pub mod event; pub mod node; pub mod run; +pub mod value; diff --git a/tiron-common/src/value.rs b/tiron-common/src/value.rs new file mode 100644 index 0000000..6f29e2f --- /dev/null +++ b/tiron-common/src/value.rs @@ -0,0 +1,132 @@ +use std::ops::Range; + +use hcl::{ + eval::{Context, Evaluate}, + Map, Number, Value, +}; +use hcl_edit::{expr::Expression, Span}; + +use crate::error::{Error, Origin}; + +/// A wrapper type for attaching span information to a value. +#[derive(Debug, Clone, Eq)] +pub struct Spanned { + value: T, + span: Option>, +} + +impl PartialEq for Spanned +where + T: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.value == other.value + } +} + +impl Spanned { + /// Creates a new `Spanned` from a `T`. + pub fn new(value: T) -> Spanned { + Spanned { value, span: None } + } + + fn with_span(mut self, span: Option>) -> Spanned { + self.span = span; + self + } + + /// Returns a reference to the wrapped value. + pub fn value(&self) -> &T { + &self.value + } + + pub fn span(&self) -> &Option> { + &self.span + } +} + +/// Represents a value that is `null`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Null; + +/// Represents any valid decorated HCL value. +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum SpannedValue { + /// Represents a HCL null value. + Null(Spanned), + /// Represents a HCL boolean. + Bool(Spanned), + /// Represents a HCL number, either integer or float. + Number(Spanned), + /// Represents a HCL string. + String(Spanned), + /// Represents a HCL array. + Array(Spanned>), + /// Represents a HCL object. + Object(Spanned>), +} + +impl SpannedValue { + pub fn span(&self) -> &Option> { + match self { + SpannedValue::Null(v) => v.span(), + SpannedValue::Bool(v) => v.span(), + SpannedValue::Number(v) => v.span(), + SpannedValue::String(v) => v.span(), + SpannedValue::Array(v) => v.span(), + SpannedValue::Object(v) => v.span(), + } + } + + pub fn from_value(value: Value, span: Option>) -> SpannedValue { + match value { + Value::Null => SpannedValue::Null(Spanned::new(Null).with_span(span)), + Value::Bool(bool) => SpannedValue::Bool(Spanned::new(bool).with_span(span)), + Value::Number(v) => SpannedValue::Number(Spanned::new(v).with_span(span)), + Value::String(v) => SpannedValue::String(Spanned::new(v).with_span(span)), + Value::Array(array) => SpannedValue::Array( + Spanned::new( + array + .into_iter() + .map(|v| SpannedValue::from_value(v, span.clone())) + .collect(), + ) + .with_span(span), + ), + + Value::Object(map) => SpannedValue::Object( + Spanned::new( + map.into_iter() + .map(|(key, v)| (key, SpannedValue::from_value(v, span.clone()))) + .collect(), + ) + .with_span(span), + ), + } + } + + pub fn from_expression( + origin: &Origin, + ctx: &Context, + expr: hcl_edit::expr::Expression, + ) -> Result { + let span = expr.span(); + match expr { + Expression::Array(exprs) => { + let mut values = Vec::new(); + for expr in exprs.into_iter() { + let value = SpannedValue::from_expression(origin, ctx, expr)?; + values.push(value); + } + Ok(SpannedValue::Array(Spanned::new(values).with_span(span))) + } + _ => { + let expr: hcl::Expression = expr.into(); + let v: hcl::Value = expr + .evaluate(ctx) + .map_err(|e| origin.error(e.to_string(), &span))?; + Ok(SpannedValue::from_value(v, span)) + } + } + } +} diff --git a/tiron-node/Cargo.toml b/tiron-node/Cargo.toml index d8f14cd..e1513a8 100644 --- a/tiron-node/Cargo.toml +++ b/tiron-node/Cargo.toml @@ -4,13 +4,14 @@ version.workspace = true edition.workspace = true [dependencies] +hcl-rs = { workspace = true } +hcl-edit = { workspace = true } itertools = { workspace = true } tempfile = { workspace = true } serde_json = { workspace = true } os_info = { workspace = true } documented = { workspace = true } uuid = { workspace = true } -rcl = { workspace = true } clap = { workspace = true } serde = { workspace = true } anyhow = { workspace = true } diff --git a/tiron-node/src/action/command.rs b/tiron-node/src/action/command.rs index e3de81b..991eca6 100644 --- a/tiron-node/src/action/command.rs +++ b/tiron-node/src/action/command.rs @@ -1,17 +1,20 @@ use std::{ io::{BufRead, BufReader}, - path::Path, process::{ExitStatus, Stdio}, }; use anyhow::{anyhow, Result}; use crossbeam_channel::Sender; use documented::{Documented, DocumentedFields}; -use rcl::{error::Error, runtime::Value}; use serde::{Deserialize, Serialize}; -use tiron_common::action::{ActionId, ActionMessage, ActionOutputLevel}; +use tiron_common::{ + action::{ActionId, ActionMessage, ActionOutputLevel}, + error::Error, +}; -use super::{Action, ActionDoc, ActionParamBaseType, ActionParamDoc, ActionParamType}; +use super::{ + Action, ActionDoc, ActionParamBaseType, ActionParamDoc, ActionParamType, ActionParams, +}; pub fn run_command( id: ActionId, @@ -92,48 +95,46 @@ impl Action for CommandAction { "command".to_string() } - fn input(&self, _cwd: &Path, params: Option<&Value>) -> Result, Error> { - let Some(params) = params else { - return Error::new("can't find params").err(); - }; - let Value::Dict(dict, dict_span) = params else { - return Error::new("params should be a Dict") - .with_origin(*params.span()) - .err(); - }; - let Some(cmd) = dict.get(&Value::String("cmd".into(), None)) else { - return Error::new("can't find cmd").with_origin(*dict_span).err(); - }; - let Value::String(cmd, _) = cmd else { - return Error::new("cmd should be a string") - .with_origin(*cmd.span()) - .err(); - }; - let args = if let Some(args) = dict.get(&Value::String("args".into(), None)) { - let Value::List(args_value) = args else { - return Error::new("args should be a list") - .with_origin(*args.span()) - .err(); - }; - let mut args = Vec::new(); - for arg in args_value.iter() { - let Value::String(arg, _) = arg else { - return Error::new("args should be a list of strings") - .with_origin(*arg.span()) - .err(); - }; - args.push(arg.to_string()); - } + fn doc(&self) -> ActionDoc { + ActionDoc { + description: Self::DOCS.to_string(), + params: vec![ + ActionParamDoc { + name: "cmd".to_string(), + required: true, + description: Self::get_field_docs("cmd").unwrap_or_default().to_string(), + type_: vec![ActionParamType::String], + }, + ActionParamDoc { + name: "args".to_string(), + required: false, + description: Self::get_field_docs("args").unwrap_or_default().to_string(), + type_: vec![ActionParamType::List(ActionParamBaseType::String)], + }, + ], + } + } + + fn input(&self, params: ActionParams) -> Result, Error> { + let cmd = params.expect_string(0); + + let args = if let Some(list) = params.list(1) { + let args = list + .iter() + .map(|v| v.expect_string().to_string()) + .collect::>(); Some(args) } else { None }; + let input = CommandAction { cmd: cmd.to_string(), args: args.unwrap_or_default(), }; let input = bincode::serialize(&input).map_err(|e| { - Error::new(format!("serialize action input error: {e}")).with_origin(*params.span()) + Error::new(format!("serialize action input error: {e}")) + .with_origin(params.origin, ¶ms.span) })?; Ok(input) } @@ -152,24 +153,4 @@ impl Action for CommandAction { Err(anyhow!("command failed")) } } - - fn doc(&self) -> ActionDoc { - ActionDoc { - description: Self::DOCS.to_string(), - params: vec![ - ActionParamDoc { - name: "cmd".to_string(), - required: true, - description: Self::get_field_docs("cmd").unwrap_or_default().to_string(), - type_: vec![ActionParamType::String], - }, - ActionParamDoc { - name: "args".to_string(), - required: false, - description: Self::get_field_docs("args").unwrap_or_default().to_string(), - type_: vec![ActionParamType::List(ActionParamBaseType::String)], - }, - ], - } - } } diff --git a/tiron-node/src/action/copy.rs b/tiron-node/src/action/copy.rs index b3d8476..cc2ec62 100644 --- a/tiron-node/src/action/copy.rs +++ b/tiron-node/src/action/copy.rs @@ -1,13 +1,17 @@ -use std::{io::Write, path::Path}; +use std::io::Write; use anyhow::{anyhow, Result}; use crossbeam_channel::Sender; use documented::{Documented, DocumentedFields}; -use rcl::{error::Error, runtime::Value}; use serde::{Deserialize, Serialize}; -use tiron_common::action::{ActionId, ActionMessage}; +use tiron_common::{ + action::{ActionId, ActionMessage}, + error::Error, +}; -use super::{command::run_command, Action, ActionDoc, ActionParamDoc, ActionParamType}; +use super::{ + command::run_command, Action, ActionDoc, ActionParamDoc, ActionParamType, ActionParams, +}; /// Copy the file to the remote machine #[derive(Default, Clone, Serialize, Deserialize, Documented, DocumentedFields)] @@ -48,41 +52,22 @@ impl Action for CopyAction { } } - fn input(&self, cwd: &Path, params: Option<&Value>) -> Result, Error> { - let Some(value) = params else { - return Error::new("can't find params").err(); - }; - let Value::Dict(dict, dict_span) = value else { - return Error::new("params should be a Dict") - .with_origin(*value.span()) - .err(); - }; - let Some(src) = dict.get(&Value::String("src".into(), None)) else { - return Error::new("can't find src").with_origin(*dict_span).err(); - }; - let Value::String(src, src_span) = src else { - return Error::new("src isn't string") - .with_origin(*src.span()) - .err(); - }; - let src_file = cwd.join(src.as_ref()); + fn input(&self, params: ActionParams) -> Result, Error> { + let (src, src_span) = params.expect_string_with_span(0); + let src_file = params.origin.cwd.join(src); let meta = src_file .metadata() - .map_err(|_| Error::new("can't find src file").with_origin(*src_span))?; + .map_err(|_| Error::new("can't find src file").with_origin(params.origin, src_span))?; if !meta.is_file() { - return Error::new("src isn't a file").with_origin(*src_span).err(); + return Error::new("src isn't a file") + .with_origin(params.origin, src_span) + .err(); } - let content = std::fs::read(&src_file) - .map_err(|e| Error::new(format!("read src file error: {e}")).with_origin(*src_span))?; + let content = std::fs::read(&src_file).map_err(|e| { + Error::new(format!("read src file error: {e}")).with_origin(params.origin, src_span) + })?; - let Some(dest) = dict.get(&Value::String("dest".into(), None)) else { - return Error::new("can't find dest").with_origin(*dict_span).err(); - }; - let Value::String(dest, _) = dest else { - return Error::new("dest isn't string") - .with_origin(*dest.span()) - .err(); - }; + let dest = params.expect_string(1); let input = CopyAction { src: src_file.to_string_lossy().to_string(), @@ -90,7 +75,8 @@ impl Action for CopyAction { dest: dest.to_string(), }; let input = bincode::serialize(&input).map_err(|e| { - Error::new(format!("serialize action input error: {e}")).with_origin(*value.span()) + Error::new(format!("serialize action input error: {e}")) + .with_origin(params.origin, ¶ms.span) })?; Ok(input) diff --git a/tiron-node/src/action/file.rs b/tiron-node/src/action/file.rs index 8862c59..a1fa994 100644 --- a/tiron-node/src/action/file.rs +++ b/tiron-node/src/action/file.rs @@ -1,10 +1,12 @@ use std::path::PathBuf; use documented::{Documented, DocumentedFields}; -use rcl::{error::Error, runtime::Value}; use serde::{Deserialize, Serialize}; +use tiron_common::error::Error; -use super::{Action, ActionDoc, ActionParamBaseValue, ActionParamDoc, ActionParamType}; +use super::{ + Action, ActionDoc, ActionParamBaseValue, ActionParamDoc, ActionParamType, ActionParams, +}; #[derive(Default, Clone, Serialize, Deserialize)] pub enum FileState { @@ -36,49 +38,53 @@ impl Action for FileAction { "file".to_string() } - fn input(&self, _cwd: &std::path::Path, params: Option<&Value>) -> Result, Error> { - let Some(params) = params else { - return Error::new("can't find params").err(); - }; - let Value::Dict(dict, dict_span) = params else { - return Error::new("params should be a Dict") - .with_origin(*params.span()) - .err(); - }; - let Some(path) = dict.get(&Value::String("path".into(), None)) else { - return Error::new("can't find path").with_origin(*dict_span).err(); - }; - let Value::String(path, _) = path else { - return Error::new("path should be a string") - .with_origin(*path.span()) - .err(); - }; + fn doc(&self) -> ActionDoc { + ActionDoc { + description: Self::DOCS.to_string(), + params: vec![ + ActionParamDoc { + name: "path".to_string(), + required: true, + description: Self::get_field_docs("path").unwrap_or_default().to_string(), + type_: vec![ActionParamType::String], + }, + ActionParamDoc { + name: "state".to_string(), + required: false, + description: Self::get_field_docs("state") + .unwrap_or_default() + .to_string(), + type_: vec![ActionParamType::Enum(vec![ + ActionParamBaseValue::String("file".to_string()), + ActionParamBaseValue::String("absent".to_string()), + ActionParamBaseValue::String("directory".to_string()), + ])], + }, + ], + } + } + + fn input(&self, params: ActionParams) -> Result, Error> { + let path = params.expect_string(0); let mut input = FileAction { path: path.to_string(), ..Default::default() }; - if let Some(state) = dict.get(&Value::String("state".into(), None)) { - let Value::String(state, state_span) = state else { - return Error::new("state should be a string") - .with_origin(*state.span()) - .err(); - }; - let state = match state.as_ref() { + if let Some(state) = params.base(1) { + let state = state.expect_string(); + let state = match state { "file" => FileState::File, "absent" => FileState::Absent, "directory" => FileState::Directory, - _ => { - return Error::new("state is invalid") - .with_origin(*state_span) - .err() - } + _ => unreachable!(), }; input.state = state; - }; + } let input = bincode::serialize(&input).map_err(|e| { - Error::new(format!("serialize action input error: {e}")).with_origin(*params.span()) + Error::new(format!("serialize action input error: {e}")) + .with_origin(params.origin, ¶ms.span) })?; Ok(input) } @@ -108,30 +114,4 @@ impl Action for FileAction { } Ok("".to_string()) } - - fn doc(&self) -> ActionDoc { - ActionDoc { - description: Self::DOCS.to_string(), - params: vec![ - ActionParamDoc { - name: "path".to_string(), - required: true, - description: Self::get_field_docs("path").unwrap_or_default().to_string(), - type_: vec![ActionParamType::String], - }, - ActionParamDoc { - name: "state".to_string(), - required: false, - description: Self::get_field_docs("state") - .unwrap_or_default() - .to_string(), - type_: vec![ActionParamType::Enum(vec![ - ActionParamBaseValue::String("file".to_string()), - ActionParamBaseValue::String("absent".to_string()), - ActionParamBaseValue::String("directory".to_string()), - ])], - }, - ], - } - } } diff --git a/tiron-node/src/action/git.rs b/tiron-node/src/action/git.rs index 9cf15fc..7a030db 100644 --- a/tiron-node/src/action/git.rs +++ b/tiron-node/src/action/git.rs @@ -1,9 +1,11 @@ use anyhow::anyhow; use documented::{Documented, DocumentedFields}; -use rcl::{error::Error, runtime::Value}; use serde::{Deserialize, Serialize}; +use tiron_common::error::Error; -use super::{command::run_command, Action, ActionDoc, ActionParamDoc, ActionParamType}; +use super::{ + command::run_command, Action, ActionDoc, ActionParamDoc, ActionParamType, ActionParams, +}; /// Manage Git repositories #[derive(Default, Clone, Serialize, Deserialize, Documented, DocumentedFields)] @@ -39,38 +41,17 @@ impl Action for GitAction { } } - fn input(&self, _cwd: &std::path::Path, params: Option<&Value>) -> Result, Error> { - let Some(params) = params else { - return Error::new("can't find params").err(); - }; - let Value::Dict(dict, dict_span) = params else { - return Error::new("params should be a Dict") - .with_origin(*params.span()) - .err(); - }; - let Some(repo) = dict.get(&Value::String("repo".into(), None)) else { - return Error::new("can't find repo").with_origin(*dict_span).err(); - }; - let Value::String(repo, _) = repo else { - return Error::new("repo should be a string") - .with_origin(*repo.span()) - .err(); - }; - let Some(dest) = dict.get(&Value::String("dest".into(), None)) else { - return Error::new("can't find dest").with_origin(*dict_span).err(); - }; - let Value::String(dest, _) = dest else { - return Error::new("dest should be a string") - .with_origin(*dest.span()) - .err(); - }; + fn input(&self, params: ActionParams) -> Result, Error> { + let repo = params.expect_string(0); + let dest = params.expect_string(1); let input = GitAction { repo: repo.to_string(), dest: dest.to_string(), }; let input = bincode::serialize(&input).map_err(|e| { - Error::new(format!("serialize action input error: {e}")).with_origin(*params.span()) + Error::new(format!("serialize action input error: {e}")) + .with_origin(params.origin, ¶ms.span) })?; Ok(input) } diff --git a/tiron-node/src/action/mod.rs b/tiron-node/src/action/mod.rs index e292737..85d21c0 100644 --- a/tiron-node/src/action/mod.rs +++ b/tiron-node/src/action/mod.rs @@ -5,12 +5,15 @@ mod file; mod git; mod package; -use std::{collections::BTreeMap, fmt::Display}; +use std::{collections::HashMap, fmt::Display, ops::Range}; use crossbeam_channel::Sender; use itertools::Itertools; -use rcl::{error::Error, runtime::Value, source::Span}; -use tiron_common::action::{ActionId, ActionMessage}; +use tiron_common::{ + action::{ActionId, ActionMessage}, + error::{Error, Origin}, + value::SpannedValue, +}; pub trait Action { /// name of the action @@ -18,11 +21,7 @@ pub trait Action { fn doc(&self) -> ActionDoc; - fn input( - &self, - cwd: &std::path::Path, - params: Option<&rcl::runtime::Value>, - ) -> Result, Error>; + fn input(&self, params: ActionParams) -> Result, Error>; fn execute( &self, @@ -37,11 +36,11 @@ pub enum ActionParamBaseType { } impl ActionParamBaseType { - fn parse(&self, value: &Value) -> Option { + fn parse_value(&self, value: &SpannedValue) -> Option { match self { ActionParamBaseType::String => { - if let Value::String(s, _) = value { - return Some(ActionParamBaseValue::String(s.to_string())); + if let SpannedValue::String(s) = value { + return Some(ActionParamBaseValue::String(s.value().to_string())); } } } @@ -59,29 +58,32 @@ impl Display for ActionParamBaseType { pub enum ActionParamType { String, - Boolean, + Bool, List(ActionParamBaseType), Enum(Vec), } impl ActionParamType { - fn parse(&self, value: &Value) -> Option { + fn parse_attr(&self, value: &SpannedValue) -> Option { match self { ActionParamType::String => { - if let Value::String(s, _) = value { - return Some(ActionParamValue::String(s.to_string())); + if let SpannedValue::String(s) = value { + return Some(ActionParamValue::String( + s.value().to_string(), + value.span().to_owned(), + )); } } - ActionParamType::Boolean => { - if let Value::Bool(v) = value { - return Some(ActionParamValue::Boolean(*v)); + ActionParamType::Bool => { + if let SpannedValue::Bool(v) = value { + return Some(ActionParamValue::Bool(*v.value())); } } ActionParamType::List(base) => { - if let Value::List(v) = value { + if let SpannedValue::Array(v) = value { let mut items = Vec::new(); - for v in v.iter() { - let base = base.parse(v)?; + for v in v.value().iter() { + let base = base.parse_value(v)?; items.push(base); } return Some(ActionParamValue::List(items)); @@ -89,12 +91,13 @@ impl ActionParamType { } ActionParamType::Enum(options) => { for option in options { - if option.match_value(value) { + if option.match_value_new(value) { return Some(ActionParamValue::Base(option.clone())); } } } } + None } } @@ -103,7 +106,7 @@ impl Display for ActionParamType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ActionParamType::String => f.write_str("String"), - ActionParamType::Boolean => f.write_str("Boolean"), + ActionParamType::Bool => f.write_str("Boolean"), ActionParamType::List(t) => f.write_str(&format!("List of {t}")), ActionParamType::Enum(t) => f.write_str(&format!( "Enum of {}", @@ -124,31 +127,33 @@ pub struct ActionParamDoc { } impl ActionParamDoc { - pub fn parse_param( + fn parse_attrs( &self, - dict: &BTreeMap, - dict_span: Option, + origin: &Origin, + attrs: &HashMap, ) -> Result, Error> { - let param = dict.get(&Value::String(self.name.clone().into(), None)); + let param = attrs.get(&self.name); + if let Some(param) = param { for type_ in &self.type_ { - if let Some(value) = type_.parse(param) { + if let Some(value) = type_.parse_attr(param) { return Ok(Some(value)); } } - return Error::new(format!( - "{} type should be {}", - self.name, - self.type_.iter().map(|t| t.to_string()).join(" or ") - )) - .with_origin(*param.span()) - .err(); + return origin + .error( + format!( + "{} type should be {}", + self.name, + self.type_.iter().map(|t| t.to_string()).join(" or ") + ), + param.span(), + ) + .err(); } if self.required { - return Error::new(format!("can't find {}", self.name,)) - .with_origin(dict_span) - .err(); + return Error::new(format!("can't find {} in params, it's required", self.name)).err(); } Ok(None) @@ -161,53 +166,137 @@ pub struct ActionDoc { } impl ActionDoc { - pub fn parse_params( + pub fn parse_attrs<'a>( &self, - params: Option<&Value>, - ) -> Result>, Error> { - let Some(value) = params else { - return Error::new("can't find params").err(); - }; - let Value::Dict(dict, dict_span) = value else { - return Error::new("params should be a Dict") - .with_origin(*value.span()) - .err(); - }; - + origin: &'a Origin, + attrs: &HashMap, + ) -> Result, Error> { let mut values = Vec::new(); for param in &self.params { - let value = param.parse_param(dict, *dict_span)?; + let value = param.parse_attrs(origin, attrs)?; values.push(value); } - Ok(values) + Ok(ActionParams { + origin, + span: None, + values, + }) + } +} + +pub struct ActionParams<'a> { + pub origin: &'a Origin, + pub span: Option>, + pub values: Vec>, +} + +impl<'a> ActionParams<'a> { + pub fn expect_string(&self, i: usize) -> &str { + self.values[i].as_ref().unwrap().expect_string() + } + + pub fn expect_string_with_span(&self, i: usize) -> (&str, &Option>) { + self.values[i].as_ref().unwrap().expect_string_with_span() + } + + pub fn base(&self, i: usize) -> Option<&ActionParamBaseValue> { + self.values[i].as_ref().map(|v| v.expect_base()) + } + + pub fn expect_base(&self, i: usize) -> &ActionParamBaseValue { + self.values[i].as_ref().unwrap().expect_base() + } + + pub fn list(&self, i: usize) -> Option<&[ActionParamBaseValue]> { + self.values[i].as_ref().map(|v| v.expect_list()) } } pub enum ActionParamValue { - String(String), - Boolean(bool), + String(String, Option>), + Bool(bool), List(Vec), Base(ActionParamBaseValue), } +impl ActionParamValue { + pub fn string(&self) -> Option<&str> { + if let ActionParamValue::String(s, _) = self { + Some(s) + } else { + None + } + } + + pub fn string_with_span(&self) -> Option<(&str, &Option>)> { + if let ActionParamValue::String(s, span) = self { + Some((s, span)) + } else { + None + } + } + + pub fn list(&self) -> Option<&[ActionParamBaseValue]> { + if let ActionParamValue::List(l) = self { + Some(l) + } else { + None + } + } + + pub fn base(&self) -> Option<&ActionParamBaseValue> { + if let ActionParamValue::Base(v) = self { + Some(v) + } else { + None + } + } + + pub fn expect_string(&self) -> &str { + self.string().unwrap() + } + + pub fn expect_string_with_span(&self) -> (&str, &Option>) { + self.string_with_span().unwrap() + } + + pub fn expect_list(&self) -> &[ActionParamBaseValue] { + self.list().unwrap() + } + + pub fn expect_base(&self) -> &ActionParamBaseValue { + self.base().unwrap() + } +} + #[derive(Clone)] pub enum ActionParamBaseValue { String(String), } impl ActionParamBaseValue { - fn match_value(&self, value: &Value) -> bool { + fn match_value_new(&self, value: &SpannedValue) -> bool { match self { ActionParamBaseValue::String(base) => { - if let Value::String(s, _) = value { - return base == &s.to_string(); + if let SpannedValue::String(s) = value { + return base == s.value(); } } } false } + + pub fn string(&self) -> Option<&str> { + match self { + ActionParamBaseValue::String(s) => Some(s), + } + } + + pub fn expect_string(&self) -> &str { + self.string().unwrap() + } } impl Display for ActionParamBaseValue { diff --git a/tiron-node/src/action/package/mod.rs b/tiron-node/src/action/package/mod.rs index 6155cf3..72246bc 100644 --- a/tiron-node/src/action/package/mod.rs +++ b/tiron-node/src/action/package/mod.rs @@ -3,14 +3,17 @@ mod provider; use anyhow::anyhow; use crossbeam_channel::Sender; use documented::{Documented, DocumentedFields}; -use rcl::{error::Error, runtime::Value}; use serde::{Deserialize, Serialize}; -use tiron_common::action::{ActionId, ActionMessage}; +use tiron_common::{ + action::{ActionId, ActionMessage}, + error::Error, +}; use self::provider::PackageProvider; use super::{ Action, ActionDoc, ActionParamBaseType, ActionParamBaseValue, ActionParamDoc, ActionParamType, + ActionParams, }; #[derive(Default, Clone, Serialize, Deserialize)] @@ -38,65 +41,61 @@ impl Action for PackageAction { "package".to_string() } - fn input( - &self, - _cwd: &std::path::Path, - params: Option<&rcl::runtime::Value>, - ) -> Result, Error> { - let Some(params) = params else { - return Error::new("can't find params").err(); - }; - let Value::Dict(dict, dict_span) = params else { - return Error::new("params should be a Dict") - .with_origin(*params.span()) - .err(); - }; - let Some(name) = dict.get(&Value::String("name".into(), None)) else { - return Error::new("can't find name").with_origin(*dict_span).err(); - }; - let names = match name { - Value::String(name, _) => vec![name.to_string()], - Value::List(name) => { - let mut names = Vec::new(); - for name in name.iter() { - let Value::String(name, _) = name else { - return Error::new("name should be a string") - .with_origin(*name.span()) - .err(); - }; - names.push(name.to_string()); - } - names - } - _ => { - return Error::new("name should be either a string or a list") - .with_origin(*name.span()) - .err(); - } - }; + fn doc(&self) -> ActionDoc { + ActionDoc { + description: PackageAction::DOCS.to_string(), + params: vec![ + ActionParamDoc { + name: "name".to_string(), + required: true, + description: PackageAction::get_field_docs("name") + .unwrap_or_default() + .to_string(), + type_: vec![ + ActionParamType::String, + ActionParamType::List(ActionParamBaseType::String), + ], + }, + ActionParamDoc { + name: "state".to_string(), + required: true, + description: PackageAction::get_field_docs("state") + .unwrap_or_default() + .to_string(), + type_: vec![ActionParamType::Enum(vec![ + ActionParamBaseValue::String("present".to_string()), + ActionParamBaseValue::String("absent".to_string()), + ActionParamBaseValue::String("latest".to_string()), + ])], + }, + ], + } + } - let Some(state) = dict.get(&Value::String("state".into(), None)) else { - return Error::new("can't find state").with_origin(*dict_span).err(); - }; - let Value::String(state, state_span) = state else { - return Error::new("state should be a string") - .with_origin(*state.span()) - .err(); + fn input(&self, params: ActionParams) -> Result, Error> { + let name = params.values[0].as_ref().unwrap(); + let names = if let Some(s) = name.string() { + vec![s.to_string()] + } else { + let list = name.expect_list(); + list.iter().map(|v| v.expect_string().to_string()).collect() }; - let state = match state.as_ref() { + + let state = params.expect_base(1); + let state = state.expect_string(); + let state = match state { "present" => PackageState::Present, "absent" => PackageState::Absent, "latest" => PackageState::Latest, _ => { - return Error::new("state is invalid") - .with_origin(*state_span) - .err() + unreachable!(); } }; let input = PackageAction { name: names, state }; let input = bincode::serialize(&input).map_err(|e| { - Error::new(format!("serialize action input error: {e}")).with_origin(*params.span()) + Error::new(format!("serialize action input error: {e}")) + .with_origin(params.origin, ¶ms.span) })?; Ok(input) } @@ -117,35 +116,4 @@ impl Action for PackageAction { Err(anyhow!("package failed")) } } - - fn doc(&self) -> ActionDoc { - ActionDoc { - description: PackageAction::DOCS.to_string(), - params: vec![ - ActionParamDoc { - name: "name".to_string(), - required: true, - description: PackageAction::get_field_docs("name") - .unwrap_or_default() - .to_string(), - type_: vec![ - ActionParamType::String, - ActionParamType::List(ActionParamBaseType::String), - ], - }, - ActionParamDoc { - name: "state".to_string(), - required: true, - description: PackageAction::get_field_docs("state") - .unwrap_or_default() - .to_string(), - type_: vec![ActionParamType::Enum(vec![ - ActionParamBaseValue::String("present".to_string()), - ActionParamBaseValue::String("absent".to_string()), - ActionParamBaseValue::String("latest".to_string()), - ])], - }, - ], - } - } } diff --git a/tiron/Cargo.toml b/tiron/Cargo.toml index 2140bd7..150c678 100644 --- a/tiron/Cargo.toml +++ b/tiron/Cargo.toml @@ -4,6 +4,8 @@ version.workspace = true edition.workspace = true [dependencies] +hcl-rs = { workspace = true } +hcl-edit = { workspace = true } itertools = { workspace = true } clap = { workspace = true } crossbeam-channel = { workspace = true } @@ -13,7 +15,6 @@ serde = { workspace = true } bincode = { workspace = true } anyhow = { workspace = true } uuid = { workspace = true } -rcl = { workspace = true } tiron-tui = { workspace = true } tiron-node = { workspace = true } tiron-common = { workspace = true } diff --git a/tiron/src/action.rs b/tiron/src/action.rs index 76124b7..2a7d9a5 100644 --- a/tiron/src/action.rs +++ b/tiron/src/action.rs @@ -1,116 +1,264 @@ -use std::{collections::HashMap, path::Path}; +use std::collections::HashMap; use anyhow::Result; -use rcl::{error::Error, loader::Loader, runtime::Value}; -use tiron_common::action::{ActionData, ActionId}; +use hcl::eval::Context; +use hcl_edit::{ + structure::{Block, BlockLabel, Structure}, + Span, +}; +use tiron_common::{ + action::{ActionData, ActionId}, + error::Error, + value::SpannedValue, +}; use tiron_node::action::data::all_actions; -use crate::{config::Config, job::Job}; +use crate::core::Runbook; pub fn parse_actions( - loader: &mut Loader, - cwd: &Path, - value: &Value, - vars: &HashMap, - job_depth: &mut i32, - config: &Config, + runbook: &Runbook, + block: &Block, + vars: &HashMap, ) -> Result, Error> { - let Value::List(action_values) = value else { - return Error::new("actions should be a list") - .with_origin(*value.span()) - .err(); - }; - let all_actions = all_actions(); + let mut ctx = Context::new(); + for (name, var) in vars { + ctx.declare_var(name.to_string(), var.to_owned()); + } + let mut actions = Vec::new(); - for action_value in action_values.iter() { - let Value::Dict(dict, dict_span) = action_value else { - return Error::new("action should be a dict") - .with_origin(*value.span()) - .err(); - }; - let Some(action) = dict.get(&Value::String("action".into(), None)) else { - return Error::new("missing action key in action") - .with_origin(*dict_span) - .err(); - }; - let Value::String(action_name, action_name_span) = action else { - return Error::new("action key should be string") - .with_origin(*action.span()) - .err(); - }; - - let name = if let Some(name) = dict.get(&Value::String("name".into(), None)) { - let Value::String(name, _) = name else { - return Error::new("name should be string") - .with_origin(*name.span()) - .err(); - }; - Some(name.to_string()) - } else { - None - }; - - if action_name.as_ref() == "job" { - let Some(params) = dict.get(&Value::String("params".into(), None)) else { - return Error::new("job needs params").with_origin(*dict_span).err(); - }; - let Value::Dict(params, params_span) = params else { - return Error::new("params should be a dict") - .with_origin(*params.span()) - .err(); - }; - let Some(job_name) = params.get(&Value::String("name".into(), None)) else { - return Error::new("missing job name in action") - .with_origin(*params_span) - .err(); - }; - let Value::String(job_name, job_name_span) = job_name else { - return Error::new("job name should be string") - .with_origin(*job_name.span()) - .err(); - }; - *job_depth += 1; - if *job_depth > 500 { - return Error::new("job name might have a endless loop here") - .with_origin(*job_name_span) - .err(); - } - let mut job_actions = Job::load( - loader, - *job_name_span, - cwd, - job_name, - vars, - job_depth, - config, - )?; - *job_depth -= 1; - - actions.append(&mut job_actions); - } else { - let Some(action) = all_actions.get(action_name.as_ref()) else { - return Error::new("action can't be found") - .with_origin(*action_name_span) - .err(); - }; - let params = dict.get(&Value::String("params".into(), None)); - let _ = action.doc().parse_params(params)?; - let input = action.input(cwd, params).map_err(|e| { - let mut e = e; - if e.origin.is_none() { - e.origin = *dict_span + for s in block.body.iter() { + if let Structure::Block(block) = s { + if block.ident.as_str() == "action" { + if block.labels.is_empty() { + return runbook + .origin + .error("No action name", &block.ident.span()) + .err(); + } + if block.labels.len() > 1 { + return runbook + .origin + .error("You can only have one action name", &block.labels[1].span()) + .err(); + } + let BlockLabel::String(action_name) = &block.labels[0] else { + return runbook + .origin + .error("action name should be a string", &block.labels[0].span()) + .err(); + }; + + let params = block.body.iter().find_map(|s| { + s.as_block() + .filter(|&block| block.ident.as_str() == "params") + }); + + let name = block.body.iter().find_map(|s| { + s.as_attribute() + .filter(|a| a.key.as_str() == "name") + .map(|a| &a.value) + }); + let name = if let Some(name) = name { + let name = + SpannedValue::from_expression(&runbook.origin, &ctx, name.to_owned())?; + let SpannedValue::String(s) = name else { + return runbook + .origin + .error("name should be a string", name.span()) + .err(); + }; + Some(s.value().to_string()) + } else { + None + }; + + let params = params.ok_or_else(|| { + runbook + .origin + .error("action doesn't have params", &block.ident.span()) + })?; + + let mut attrs = HashMap::new(); + for s in params.body.iter() { + if let Some(a) = s.as_attribute() { + let v = SpannedValue::from_expression( + &runbook.origin, + &ctx, + a.value.to_owned(), + )?; + attrs.insert(a.key.to_string(), v); + } + } + + if action_name.as_str() == "job" { + let job_name = attrs.get("name").ok_or_else(|| { + runbook + .origin + .error("job doesn't have name in params", ¶ms.ident.span()) + })?; + let SpannedValue::String(job_name) = job_name else { + return runbook + .origin + .error("job name should be a string", job_name.span()) + .err(); + }; + let job = runbook.jobs.get(job_name.value()).ok_or_else(|| { + runbook.origin.error("can't find job name", job_name.span()) + })?; + + let runbook = if let Some(imported) = &job.imported { + runbook.imports.get(imported).ok_or_else(|| { + runbook + .origin + .error("can't find imported job", job_name.span()) + })? + } else { + runbook + }; + + actions.append(&mut parse_actions(runbook, &job.block, vars)?); + } else { + let Some(action) = all_actions.get(action_name.as_str()) else { + return runbook + .origin + .error( + format!("action {} can't be found", action_name.as_str()), + &block.labels[0].span(), + ) + .err(); + }; + + let params = + action + .doc() + .parse_attrs(&runbook.origin, &attrs) + .map_err(|e| { + let mut e = e; + if e.location.is_none() { + e = e.with_origin(&runbook.origin, ¶ms.ident.span()); + } + e + })?; + let input = action.input(params)?; + actions.push(ActionData { + id: ActionId::new(), + name: name.unwrap_or_else(|| action_name.to_string()), + action: action_name.to_string(), + input, + }); } - e - })?; - actions.push(ActionData { - id: ActionId::new(), - name: name.unwrap_or_else(|| action_name.to_string()), - action: action_name.to_string(), - input, - }); + } } } Ok(actions) } + +// pub fn parse_actions( +// loader: &mut Loader, +// cwd: &Path, +// value: &Value, +// vars: &HashMap, +// job_depth: &mut i32, +// config: &Config, +// ) -> Result, Error> { +// let Value::List(action_values) = value else { +// return Error::new("actions should be a list") +// .with_origin(*value.span()) +// .err(); +// }; + +// let all_actions = all_actions(); + +// let mut actions = Vec::new(); +// for action_value in action_values.iter() { +// let Value::Dict(dict, dict_span) = action_value else { +// return Error::new("action should be a dict") +// .with_origin(*value.span()) +// .err(); +// }; +// let Some(action) = dict.get(&Value::String("action".into(), None)) else { +// return Error::new("missing action key in action") +// .with_origin(*dict_span) +// .err(); +// }; +// let Value::String(action_name, action_name_span) = action else { +// return Error::new("action key should be string") +// .with_origin(*action.span()) +// .err(); +// }; + +// let name = if let Some(name) = dict.get(&Value::String("name".into(), None)) { +// let Value::String(name, _) = name else { +// return Error::new("name should be string") +// .with_origin(*name.span()) +// .err(); +// }; +// Some(name.to_string()) +// } else { +// None +// }; + +// if action_name.as_ref() == "job" { +// let Some(params) = dict.get(&Value::String("params".into(), None)) else { +// return Error::new("job needs params").with_origin(*dict_span).err(); +// }; +// let Value::Dict(params, params_span) = params else { +// return Error::new("params should be a dict") +// .with_origin(*params.span()) +// .err(); +// }; +// let Some(job_name) = params.get(&Value::String("name".into(), None)) else { +// return Error::new("missing job name in action") +// .with_origin(*params_span) +// .err(); +// }; +// let Value::String(job_name, job_name_span) = job_name else { +// return Error::new("job name should be string") +// .with_origin(*job_name.span()) +// .err(); +// }; +// *job_depth += 1; +// if *job_depth > 500 { +// return Error::new("job name might have a endless loop here") +// .with_origin(*job_name_span) +// .err(); +// } +// let mut job_actions = Job::load( +// loader, +// *job_name_span, +// cwd, +// job_name, +// vars, +// job_depth, +// config, +// )?; +// *job_depth -= 1; + +// actions.append(&mut job_actions); +// } else { +// let Some(action) = all_actions.get(action_name.as_ref()) else { +// return Error::new("action can't be found") +// .with_origin(*action_name_span) +// .err(); +// }; +// let params = dict.get(&Value::String("params".into(), None)); +// let params = action.doc().parse_params(params)?; +// let input = action.input(cwd, params).map_err(|e| { +// let mut e = e; +// if e.origin.is_none() { +// e.origin = *dict_span +// } +// e +// })?; +// actions.push(ActionData { +// id: ActionId::new(), +// name: name.unwrap_or_else(|| action_name.to_string()), +// action: action_name.to_string(), +// input, +// }); +// } +// } +// Ok(actions) +// } diff --git a/tiron/src/cli.rs b/tiron/src/cli.rs index f48780e..57de65a 100644 --- a/tiron/src/cli.rs +++ b/tiron/src/cli.rs @@ -14,13 +14,13 @@ pub enum CliCmd { /// Run Tiron runbooks Run { /// The runbooks for Tiron to run. - /// Default to main.rcl if unspecified + /// Default to main.tr if unspecified runbooks: Vec, }, /// Check Tiron runbooks Check { /// The runbooks for Tiron to check. - /// Default to main.rcl if unspecified + /// Default to main.tr if unspecified runbooks: Vec, }, /// Show Tiron action docs diff --git a/tiron/src/config.rs b/tiron/src/config.rs index 77f8c32..66f4ddb 100644 --- a/tiron/src/config.rs +++ b/tiron/src/config.rs @@ -1,293 +1,31 @@ -use std::{ - collections::{BTreeMap, HashMap}, - path::PathBuf, -}; +use std::{collections::HashMap, path::PathBuf}; -use anyhow::{anyhow, Result}; use crossbeam_channel::Sender; -use rcl::{error::Error, loader::Loader, markup::MarkupMode, runtime::Value}; use tiron_tui::event::AppEvent; -use crate::{core::print_warn, node::Node}; - +#[derive(Clone)] pub enum HostOrGroup { Host(String), Group(String), } +#[derive(Clone)] pub struct HostOrGroupConfig { - host: HostOrGroup, - vars: HashMap, + pub host: HostOrGroup, + pub vars: HashMap, } +#[derive(Clone)] pub struct GroupConfig { - hosts: Vec, - vars: HashMap, + pub hosts: Vec, + pub vars: HashMap, + pub imported: Option, } pub struct Config { pub tx: Sender, - groups: HashMap, + pub groups: HashMap, pub project_folder: PathBuf, } -impl Config { - pub fn load(loader: &mut Loader, tx: &Sender) -> Result { - let mut cwd = std::env::current_dir() - .map_err(|e| Error::new(format!("can't get current directory {e}")))?; - - let mut path = cwd.join("tiron.rcl"); - while !path.exists() { - cwd = cwd - .parent() - .map(|p| p.to_path_buf()) - .ok_or_else(|| Error::new("can't find tiron.rcl"))?; - path = cwd.join("tiron.rcl"); - } - - let data = std::fs::read_to_string(&path) - .map_err(|e| Error::new(format!("can't reading config. Error: {e}")))?; - - let id = loader.load_string(data, Some(path.to_string_lossy().to_string()), 0); - let value = loader.evaluate( - &mut rcl::typecheck::prelude(), - &mut rcl::runtime::prelude(), - id, - &mut rcl::tracer::StderrTracer::new(Some(MarkupMode::Ansi)), - )?; - - let Value::Dict(mut dict, dict_span) = value else { - return Error::new("root should be dict") - .with_origin(*value.span()) - .err(); - }; - - let mut config = Config { - tx: tx.clone(), - groups: HashMap::new(), - project_folder: cwd, - }; - - if let Some(groups) = dict.remove(&Value::String("groups".into(), None)) { - let Value::Dict(groups, groups_span) = groups else { - return Error::new("hosts should be dict") - .with_origin(dict_span) - .err(); - }; - for (key, group) in groups.iter() { - let Value::String(group_name, _) = key else { - return Error::new("group key should be a string") - .with_origin(groups_span) - .err(); - }; - let group = Self::parse_group(&groups, group_name, group)?; - config.groups.insert(group_name.to_string(), group); - } - } - - for (key, _) in dict { - let warn = Error::new("key here is unsed") - .warning() - .with_origin(*key.span()); - print_warn(warn, loader); - } - - Ok(config) - } - - fn parse_group( - groups: &BTreeMap, - group_name: &str, - value: &Value, - ) -> Result { - let Value::Dict(group, group_span) = value else { - return Error::new("group value should be a dict") - .with_origin(*value.span()) - .err(); - }; - let mut group_config = GroupConfig { - hosts: Vec::new(), - vars: HashMap::new(), - }; - let Some(group_hosts) = group.get(&Value::String("hosts".into(), None)) else { - return Error::new("group should have hosts") - .with_origin(*group_span) - .err(); - }; - let Value::List(group_hosts) = group_hosts else { - return Error::new("group value should be a list") - .with_origin(*group_hosts.span()) - .err(); - }; - - for host in group_hosts.iter() { - let host_config = Self::parse_group_entry(groups, group_name, host)?; - group_config.hosts.push(host_config); - } - - if let Some(vars) = group.get(&Value::String("vars".into(), None)) { - let Value::Dict(vars, _) = vars else { - return Error::new("group entry vars should be a dict") - .with_origin(*vars.span()) - .err(); - }; - for (key, var) in vars.iter() { - let Value::String(key, _) = key else { - return Error::new("group entry vars key should be a string") - .with_origin(*key.span()) - .err(); - }; - group_config.vars.insert(key.to_string(), var.clone()); - } - } - - Ok(group_config) - } - - fn parse_group_entry( - groups: &BTreeMap, - group_name: &str, - value: &Value, - ) -> Result { - let Value::Dict(host, host_span) = value else { - return Error::new("group entry should be a dict") - .with_origin(*value.span()) - .err(); - }; - - if host.contains_key(&Value::String("host".into(), None)) - && host.contains_key(&Value::String("group".into(), None)) - { - return Error::new("group entry can't have host and group at the same time") - .with_origin(*host_span) - .err(); - } - - let host_or_group = if let Some(v) = host.get(&Value::String("host".into(), None)) { - let Value::String(v, _) = v else { - return Error::new("group entry host value should be a string") - .with_origin(*v.span()) - .err(); - }; - HostOrGroup::Host(v.to_string()) - } else if let Some(v) = host.get(&Value::String("group".into(), None)) { - let Value::String(v, v_span) = v else { - return Error::new("group entry group value should be a string") - .with_origin(*v.span()) - .err(); - }; - if v.as_ref() == group_name { - return Error::new("group entry group can't point to itself") - .with_origin(*v_span) - .err(); - } - if !groups.contains_key(&Value::String(v.clone(), None)) { - return Error::new("group entry group doesn't exist") - .with_origin(*v_span) - .err(); - } - - HostOrGroup::Group(v.to_string()) - } else { - return Error::new("group entry should have either host or group") - .with_origin(*host_span) - .err(); - }; - let mut host_config = HostOrGroupConfig { - host: host_or_group, - vars: HashMap::new(), - }; - - if let Some(vars) = host.get(&Value::String("vars".into(), None)) { - let Value::Dict(vars, _) = vars else { - return Error::new("group entry vars should be a dict") - .with_origin(*vars.span()) - .err(); - }; - for (key, var) in vars.iter() { - let Value::String(key, _) = key else { - return Error::new("group entry vars key should be a string") - .with_origin(*key.span()) - .err(); - }; - host_config.vars.insert(key.to_string(), var.clone()); - } - } - - Ok(host_config) - } - - pub fn hosts_from_name(&self, name: &str) -> Result> { - if self.groups.contains_key(name) { - return self.hosts_from_group(name); - } else { - for group in self.groups.values() { - for host in &group.hosts { - if let HostOrGroup::Host(host_name) = &host.host { - if host_name == name { - return Ok(vec![Node::new( - host_name.to_string(), - host.vars.clone(), - &self.tx, - )]); - } - } - } - } - } - Err(anyhow!("can't find host with name {name}")) - } - - fn hosts_from_group(&self, group: &str) -> Result> { - let Some(group) = self.groups.get(group) else { - return Err(anyhow!("hosts doesn't have group {group}")); - }; - - let mut hosts = Vec::new(); - for host_or_group in &group.hosts { - let mut local_hosts = match &host_or_group.host { - HostOrGroup::Host(name) => { - vec![Node::new( - name.to_string(), - host_or_group.vars.clone(), - &self.tx, - )] - } - HostOrGroup::Group(group) => { - let mut local_hosts = self.hosts_from_group(group)?; - for host in local_hosts.iter_mut() { - for (key, val) in &host_or_group.vars { - if !host.vars.contains_key(key) { - if key == "remote_user" && host.remote_user.is_none() { - host.remote_user = if let Value::String(s, _) = val { - Some(s.to_string()) - } else { - None - }; - } - host.vars.insert(key.to_string(), val.clone()); - } - } - } - local_hosts - } - }; - for host in local_hosts.iter_mut() { - for (key, val) in &group.vars { - if !host.vars.contains_key(key) { - if key == "remote_user" && host.remote_user.is_none() { - host.remote_user = if let Value::String(s, _) = val { - Some(s.to_string()) - } else { - None - }; - } - host.vars.insert(key.to_string(), val.clone()); - } - } - } - hosts.append(&mut local_hosts); - } - Ok(hosts) - } -} +impl Config {} diff --git a/tiron/src/core.rs b/tiron/src/core.rs index 8a40e64..6fb6287 100644 --- a/tiron/src/core.rs +++ b/tiron/src/core.rs @@ -1,24 +1,24 @@ -use std::{ - io::Write, - path::{Path, PathBuf}, -}; +use std::{collections::HashMap, path::PathBuf}; -use anyhow::Result; +use anyhow::{anyhow, Result}; use clap::Parser; -use itertools::Itertools; -use rcl::{ - ast::{Expr, Seq, Yield}, - error::Error, - loader::Loader, - markup::{MarkupMode, MarkupString}, - pprint::{self, Doc}, +use crossbeam_channel::Sender; +use hcl::eval::{Context, Evaluate}; +use hcl_edit::{ + structure::{Block, BlockLabel, Structure}, + Span, }; +use itertools::Itertools; + +use tiron_common::error::{Error, Origin}; use tiron_node::action::data::all_actions; use tiron_tui::event::{AppEvent, RunEvent}; +use uuid::Uuid; use crate::{ cli::{Cli, CliCmd}, - config::Config, + config::{Config, GroupConfig, HostOrGroup, HostOrGroupConfig}, + job::Job, node::Node, run::Run, }; @@ -32,9 +32,8 @@ pub fn cmd() { } else { runbooks }; - let mut loader = rcl::loader::Loader::new(); - if let Err(e) = run(&mut loader, runbooks, false) { - print_fatal_error(e, &loader); + if let Err(e) = run(runbooks, false) { + let _ = e.report_stderr(); } } CliCmd::Check { runbooks } => { @@ -43,8 +42,7 @@ pub fn cmd() { } else { runbooks }; - let mut loader = rcl::loader::Loader::new(); - match run(&mut loader, runbooks, true) { + match run(runbooks, true) { Ok(runbooks) => { println!("successfully checked"); for runbook in runbooks { @@ -52,7 +50,7 @@ pub fn cmd() { } } Err(e) => { - print_fatal_error(e, &loader); + let _ = e.report_stderr(); } } } @@ -60,48 +58,640 @@ pub fn cmd() { } } -fn print_fatal_error(err: Error, loader: &Loader) -> ! { - let inputs = loader.as_inputs(); - let err_doc = err.report(&inputs); - print_doc_stderr(err_doc); - // Regardless of whether printing to stderr failed or not, the error was - // fatal, so we exit with code 1. - std::process::exit(1); -} +// fn print_fatal_error(err: Error, loader: &Loader) -> ! { +// let inputs = loader.as_inputs(); +// let err_doc = err.report(&inputs); +// print_doc_stderr(err_doc); +// // Regardless of whether printing to stderr failed or not, the error was +// // fatal, so we exit with code 1. +// std::process::exit(1); +// } -pub fn print_warn(err: Error, loader: &Loader) { - let inputs = loader.as_inputs(); - let err_doc = err.report(&inputs); - print_doc_stderr(err_doc); -} +// pub fn print_warn(err: Error, loader: &Loader) { +// let inputs = loader.as_inputs(); +// let err_doc = err.report(&inputs); +// print_doc_stderr(err_doc); +// } + +// fn print_doc_stderr(doc: Doc) { +// let stderr = std::io::stderr(); +// let markup = MarkupMode::Ansi; +// let cfg = pprint::Config { width: 80 }; +// let result = doc.println(&cfg); +// let mut out = stderr.lock(); +// print_string(markup, result, &mut out); +// } -fn print_doc_stderr(doc: Doc) { - let stderr = std::io::stderr(); - let markup = MarkupMode::Ansi; - let cfg = pprint::Config { width: 80 }; - let result = doc.println(&cfg); - let mut out = stderr.lock(); - print_string(markup, result, &mut out); +// fn print_string(mode: MarkupMode, data: MarkupString, out: &mut dyn Write) { +// let res = data.write_bytes(mode, out); +// if res.is_err() { +// // If we fail to print to stdout/stderr, there is no point in +// // printing an error, just exit then. +// std::process::exit(1); +// } +// } + +pub struct Runbook { + groups: HashMap, + pub jobs: HashMap, + // the imported runbooks + pub imports: HashMap, + runs: Vec, + // the origin data of the runbook + pub origin: Origin, + tx: Sender, + // the imported level of the runbook, this is to detect circular imports + level: usize, } -fn print_string(mode: MarkupMode, data: MarkupString, out: &mut dyn Write) { - let res = data.write_bytes(mode, out); - if res.is_err() { - // If we fail to print to stdout/stderr, there is no point in - // printing an error, just exit then. - std::process::exit(1); +impl Runbook { + pub fn new(path: PathBuf, tx: Sender, level: usize) -> Result { + let cwd = path.parent().ok_or_else(|| { + Error::new(format!("can't find parent for {}", path.to_string_lossy())) + })?; + + let data = std::fs::read_to_string(&path).map_err(|e| { + Error::new(format!( + "can't read runbook {} error: {e}", + path.to_string_lossy() + )) + })?; + + let origin = Origin { + cwd: cwd.to_path_buf(), + path, + data, + }; + let runbook = Self { + origin, + groups: HashMap::new(), + jobs: HashMap::new(), + imports: HashMap::new(), + runs: Vec::new(), + tx, + level, + }; + + Ok(runbook) + } + + pub fn parse(&mut self, parse_run: bool) -> Result<(), Error> { + let body = hcl_edit::parser::parse_body(&self.origin.data) + .map_err(|e| Error::from_hcl(e, self.origin.path.clone()))?; + + for structure in body.iter() { + if let Structure::Block(block) = structure { + match block.ident.as_str() { + "use" => { + self.parse_use(block)?; + } + "group" => { + self.parse_group(block)?; + } + "job" => { + self.parse_job(block)?; + } + "run" => { + if parse_run { + // for imported runbook, we don't need to parse runs + self.parse_run(block)?; + } + } + _ => {} + } + } + } + + Ok(()) + } + + fn parse_run(&mut self, block: &Block) -> Result<(), Error> { + let mut hosts: Vec = Vec::new(); + if block.labels.is_empty() { + return self + .origin + .error("You need put group name after run", &block.ident.span()) + .err(); + } + if block.labels.len() > 1 { + return self + .origin + .error( + "You can only have one group name to run", + &block.labels[1].span(), + ) + .err(); + } + let BlockLabel::String(name) = &block.labels[0] else { + return self + .origin + .error("group name should be a string", &block.labels[0].span()) + .err(); + }; + for node in self + .hosts_from_name(name.as_str()) + .map_err(|e| self.origin.error(e.to_string(), &block.labels[0].span()))? + { + if !hosts.iter().any(|n| n.host == node.host) { + hosts.push(node); + } + } + + let hosts = if hosts.is_empty() { + vec![Node { + id: Uuid::new_v4(), + host: "localhost".to_string(), + vars: HashMap::new(), + remote_user: None, + become_: false, + actions: Vec::new(), + tx: self.tx.clone(), + }] + } else { + hosts + }; + let run = Run::from_block(self, None, block, hosts)?; + self.runs.push(run); + Ok(()) + } + + fn parse_group(&mut self, block: &Block) -> Result<(), Error> { + if block.labels.is_empty() { + return self + .origin + .error("group name doesn't exit", &block.ident.span()) + .err(); + } + if block.labels.len() > 1 { + return self + .origin + .error("group should only have one name", &block.labels[1].span()) + .err(); + } + let BlockLabel::String(name) = &block.labels[0] else { + return self + .origin + .error("group name should be a string", &block.labels[0].span()) + .err(); + }; + + if self.groups.contains_key(name.as_str()) { + return self + .origin + .error("group name already exists", &block.labels[0].span()) + .err(); + } + + let mut group_config = GroupConfig { + hosts: Vec::new(), + vars: HashMap::new(), + imported: None, + }; + + let ctx = Context::new(); + for structure in block.body.iter() { + match structure { + Structure::Attribute(a) => { + let expr: hcl::Expression = a.value.to_owned().into(); + let v: hcl::Value = expr + .evaluate(&ctx) + .map_err(|e| Error::new(e.to_string().replace('\n', " ")))?; + group_config.vars.insert(a.key.to_string(), v); + } + Structure::Block(block) => { + let host_or_group = self.parse_group_entry(name, block)?; + group_config.hosts.push(host_or_group); + } + } + } + + self.groups.insert(name.to_string(), group_config); + + Ok(()) + } + + fn parse_group_entry( + &self, + group_name: &str, + block: &Block, + ) -> Result { + let host_or_group = match block.ident.as_str() { + "host" => { + if block.labels.is_empty() { + return self + .origin + .error("host name doesn't exit", &block.ident.span()) + .err(); + } + if block.labels.len() > 1 { + return self + .origin + .error("host should only have one name", &block.labels[1].span()) + .err(); + } + + let BlockLabel::String(name) = &block.labels[0] else { + return self + .origin + .error("host name should be a string", &block.labels[0].span()) + .err(); + }; + + HostOrGroup::Host(name.to_string()) + } + "group" => { + if block.labels.is_empty() { + return self + .origin + .error("group name doesn't exit", &block.ident.span()) + .err(); + } + if block.labels.len() > 1 { + return self + .origin + .error("group should only have one name", &block.labels[1].span()) + .err(); + } + + let BlockLabel::String(name) = &block.labels[0] else { + return self + .origin + .error("group name should be a string", &block.labels[0].span()) + .err(); + }; + + if name.as_str() == group_name { + return self + .origin + .error("group can't point to itself", &block.labels[0].span()) + .err(); + } + + if !self.groups.contains_key(name.as_str()) { + return self + .origin + .error( + format!("group {} doesn't exist", name.as_str()), + &block.labels[0].span(), + ) + .err(); + } + + HostOrGroup::Group(name.to_string()) + } + _ => { + return self + .origin + .error("you can only have host or group", &block.ident.span()) + .err() + } + }; + + let mut host_config = HostOrGroupConfig { + host: host_or_group, + vars: HashMap::new(), + }; + + let ctx = Context::new(); + for structure in block.body.iter() { + if let Structure::Attribute(a) = structure { + let expr: hcl::Expression = a.value.to_owned().into(); + let v: hcl::Value = expr + .evaluate(&ctx) + .map_err(|e| Error::new(e.to_string().replace('\n', " ")))?; + host_config.vars.insert(a.key.to_string(), v); + } + } + + Ok(host_config) + } + + fn parse_use(&mut self, block: &Block) -> Result<(), Error> { + if block.labels.is_empty() { + return self + .origin + .error("use needs a path", &block.ident.span()) + .err(); + } + if block.labels.len() > 1 { + return self + .origin + .error( + "You can only have one path for use", + &block.labels[1].span(), + ) + .err(); + } + let BlockLabel::String(name) = &block.labels[0] else { + return self + .origin + .error("path should be a string", &block.labels[0].span()) + .err(); + }; + + let path = self.origin.cwd.join(name.as_str()); + + let mut runbook = Runbook::new(path, self.tx.clone(), self.level + 1)?; + runbook.parse(false).map_err(|e| { + let mut e = e; + if e.location.is_none() { + e = e.with_origin(&self.origin, &block.labels[0].span()); + } + e + })?; + + let path = self + .origin + .cwd + .join(name.as_str()) + .canonicalize() + .map_err(|e| { + Error::new(format!("can't canonicalize path: {e}")) + .with_origin(&self.origin, &block.labels[0].span()) + })?; + if self.imports.contains_key(&path) { + return self + .origin + .error("path already imported", &block.labels[0].span()) + .err(); + } + + for structure in block.body.iter() { + if let Structure::Block(block) = structure { + match block.ident.as_str() { + "job" => { + self.parse_use_job(&runbook, block)?; + } + "group" => { + self.parse_use_group(&runbook, block)?; + } + _ => {} + } + } + } + + self.imports.insert(path, runbook); + + Ok(()) + } + + fn parse_use_job(&mut self, imported: &Runbook, block: &Block) -> Result<(), Error> { + if block.labels.is_empty() { + return self + .origin + .error("use job needs a job name", &block.ident.span()) + .err(); + } + if block.labels.len() > 1 { + return self + .origin + .error("You can only use one job name", &block.labels[1].span()) + .err(); + } + let BlockLabel::String(name) = &block.labels[0] else { + return self + .origin + .error("job name should be a string", &block.labels[0].span()) + .err(); + }; + + let as_name = block.body.iter().find_map(|s| { + s.as_attribute().and_then(|a| { + if a.key.as_str() == "as" { + Some(a.value.as_str()?) + } else { + None + } + }) + }); + + let imported_name = as_name.unwrap_or(name.as_str()); + if self.jobs.contains_key(imported_name) { + return self + .origin + .error("job name already exists", &block.labels[0].span()) + .err(); + } + + let mut job = imported + .jobs + .get(name.as_str()) + .ok_or_else(|| { + self.origin.error( + "job name can't be imported, it doesn't exit in the imported runbook", + &block.labels[0].span(), + ) + })? + .clone(); + job.imported = Some(imported.origin.path.clone()); + + self.jobs.insert(imported_name.to_string(), job.to_owned()); + + Ok(()) + } + + fn hosts_from_name(&self, name: &str) -> Result> { + if self.groups.contains_key(name) { + return self.hosts_from_group(name); + } else { + for group in self.groups.values() { + for host in &group.hosts { + if let HostOrGroup::Host(host_name) = &host.host { + if host_name == name { + return Ok(vec![Node::new( + host_name.to_string(), + host.vars.clone(), + &self.tx, + )]); + } + } + } + } + } + Err(anyhow!("can't find host with name {name}")) + } + + fn parse_use_group(&mut self, imported: &Runbook, block: &Block) -> Result<(), Error> { + if block.labels.is_empty() { + return self + .origin + .error("use group needs a group name", &block.ident.span()) + .err(); + } + if block.labels.len() > 1 { + return self + .origin + .error("You can only use one group name", &block.labels[1].span()) + .err(); + } + let BlockLabel::String(name) = &block.labels[0] else { + return self + .origin + .error("group name should be a string", &block.labels[0].span()) + .err(); + }; + + let as_name = block.body.iter().find_map(|s| { + s.as_attribute().and_then(|a| { + if a.key.as_str() == "as" { + Some(a.value.as_str()?) + } else { + None + } + }) + }); + + let imported_name = as_name.unwrap_or(name.as_str()); + if self.groups.contains_key(imported_name) { + return self + .origin + .error("group name already exists", &block.labels[0].span()) + .err(); + } + + let mut group = imported + .groups + .get(name.as_str()) + .ok_or_else(|| { + self.origin.error( + "group name can't be imported, it doesn't exit in the imported runbook", + &block.labels[0].span(), + ) + })? + .clone(); + group.imported = Some(imported.origin.path.clone()); + + self.groups.insert(imported_name.to_string(), group); + + Ok(()) + } + + fn hosts_from_group(&self, group: &str) -> Result> { + let Some(group) = self.groups.get(group) else { + return Err(anyhow!("hosts doesn't have group {group}")); + }; + + let runbook = if let Some(imported) = &group.imported { + self.imports + .get(imported) + .ok_or_else(|| anyhow!("can't find imported"))? + } else { + self + }; + + let mut hosts = Vec::new(); + for host_or_group in &group.hosts { + let mut local_hosts = match &host_or_group.host { + HostOrGroup::Host(name) => { + vec![Node::new( + name.to_string(), + host_or_group.vars.clone(), + &self.tx, + )] + } + HostOrGroup::Group(group) => { + let mut local_hosts = runbook.hosts_from_group(group)?; + for host in local_hosts.iter_mut() { + for (key, val) in &host_or_group.vars { + if !host.vars.contains_key(key) { + if key == "remote_user" && host.remote_user.is_none() { + host.remote_user = if let hcl::Value::String(s) = val { + Some(s.to_string()) + } else { + None + }; + } + host.vars.insert(key.to_string(), val.clone()); + } + } + } + local_hosts + } + }; + for host in local_hosts.iter_mut() { + for (key, val) in &group.vars { + if !host.vars.contains_key(key) { + if key == "remote_user" && host.remote_user.is_none() { + host.remote_user = if let hcl::Value::String(s) = val { + Some(s.to_string()) + } else { + None + }; + } + host.vars.insert(key.to_string(), val.clone()); + } + } + } + hosts.append(&mut local_hosts); + } + Ok(hosts) + } + + fn parse_job(&mut self, block: &Block) -> Result<(), Error> { + if block.labels.is_empty() { + return Error::new("job needs a name").err(); + } + if block.labels.len() > 1 { + return Error::new("You can only have one job name").err(); + } + let BlockLabel::String(name) = &block.labels[0] else { + return Error::new("job name should be a string").err(); + }; + + if self.jobs.contains_key(name.as_str()) { + return Error::new("job name already exists").err(); + } + + // for s in block.body.iter() { + // if let Structure::Block(block) = s { + // if block.ident.as_str() == "action" && block.labels.len() == 1 { + // let BlockLabel::String(action_name) = &block.labels[0] else { + // return Error::new("action name should be a string").err(); + // }; + // if action_name.as_str() == "job" { + // let job_name = block + // .body + // .iter() + // .find_map(|s| { + // s.as_attribute().and_then(|a| { + // if a.key.as_str() == "name" { + // a.value.as_str() + // } else { + // None + // } + // }) + // }) + // .ok_or_else(|| Error::new("job don't have name"))?; + // } + // } + // } + // } + + self.jobs.insert( + name.to_string(), + Job { + block: block.to_owned(), + imported: None, + }, + ); + + Ok(()) } } -pub fn run(loader: &mut Loader, runbooks: Vec, check: bool) -> Result, Error> { +pub fn run(runbooks: Vec, check: bool) -> Result, Error> { let mut app = tiron_tui::app::App::new(); - let config = Config::load(loader, &app.tx)?; + let config = Config { + tx: app.tx.clone(), + groups: HashMap::new(), + project_folder: PathBuf::from("."), + }; let runbooks: Vec = runbooks .iter() .map(|name| { - let file_name = if !name.ends_with(".rcl") { - format!("{name}.rcl") + let file_name = if !name.ends_with(".tr") { + format!("{name}.tr") } else { name.to_string() }; @@ -113,11 +703,13 @@ pub fn run(loader: &mut Loader, runbooks: Vec, check: bool) -> Result>, Error> = runbooks - .iter() - .map(|name| parse_runbook(loader, name, &config)) - .collect(); - let runs: Vec = runs?.into_iter().flatten().collect(); + let mut runs = Vec::new(); + for path in runbooks.iter() { + let mut runbook = Runbook::new(path.to_path_buf(), config.tx.clone(), 0)?; + runbook.parse(true)?; + runs.push(runbook.runs); + } + let runs: Vec = runs.into_iter().flatten().collect(); if !check { app.runs = runs.iter().map(|run| run.to_panel()).collect(); @@ -138,111 +730,12 @@ pub fn run(loader: &mut Loader, runbooks: Vec, check: bool) -> Result Result, Error> { - let cwd = path - .parent() - .ok_or_else(|| Error::new(format!("can't find parent for {}", path.to_string_lossy())))?; - - let data = std::fs::read_to_string(path).map_err(|e| { - Error::new(format!( - "can't read runbook {} error: {e}", - path.to_string_lossy() - )) - })?; - - let id = loader.load_string(data.clone(), Some(path.to_string_lossy().to_string()), 0); - - let ast = loader.get_unchecked_ast(id)?; - - let mut runs = Vec::new(); - let Expr::BracketLit { elements, open } = ast else { - return Error::new("runbook should be a list").err(); - }; - for seq in elements { - let mut hosts: Vec = Vec::new(); - let mut name: Option = None; - - let Seq::Yield(Yield::Elem { value, span }) = seq else { - return Error::new("run should be a dict") - .with_origin(Some(open)) - .err(); - }; - let Expr::BraceLit { elements, .. } = *value else { - return Error::new("run should be a dict") - .with_origin(Some(span)) - .err(); - }; - - for seq in elements { - if let Seq::Yield(Yield::Assoc { - key, - value, - value_span, - .. - }) = seq - { - if let Expr::StringLit(s, hosts_span) = *key { - if s.as_ref() == "hosts" { - if let Expr::StringLit(s, span) = *value { - for node in config - .hosts_from_name(s.as_ref()) - .map_err(|e| Error::new(e.to_string()).with_origin(span))? - { - if !hosts.iter().any(|n| n.host == node.host) { - hosts.push(node); - } - } - } else if let Expr::BracketLit { elements, open } = *value { - for seq in elements { - let Seq::Yield(Yield::Elem { value, span }) = seq else { - return Error::new("hosts should be list of strings") - .with_origin(Some(open)) - .err(); - }; - let Expr::StringLit(s, span) = *value else { - return Error::new("hosts should be list of strings") - .with_origin(Some(span)) - .err(); - }; - for node in config - .hosts_from_name(s.as_ref()) - .map_err(|e| Error::new(e.to_string()).with_origin(span))? - { - if !hosts.iter().any(|n| n.host == node.host) { - hosts.push(node); - } - } - } - } else { - return Error::new("hosts should be a string or list of strings") - .with_origin(hosts_span) - .err(); - } - } else if s.as_ref() == "name" { - let Expr::StringLit(s, _) = *value else { - return Error::new("run name should be a string") - .with_origin(Some(value_span)) - .err(); - }; - name = Some(s.to_string()); - } - } - } - } - let run = Run::from_runbook(loader, cwd, name, span, hosts, config)?; - runs.push(run); - } - - Ok(runs) -} - fn action_doc(name: Option) { let actions = all_actions(); if let Some(name) = name { diff --git a/tiron/src/job.rs b/tiron/src/job.rs index 4b15f11..56429e7 100644 --- a/tiron/src/job.rs +++ b/tiron/src/job.rs @@ -1,91 +1,11 @@ -use std::{ - collections::HashMap, - path::{Path, PathBuf}, -}; +use std::path::PathBuf; -use anyhow::{anyhow, Result}; -use rcl::{error::Error, loader::Loader, markup::MarkupMode, runtime::Value, source::Span}; -use tiron_common::action::ActionData; +use hcl_edit::structure::Block; -use crate::{action::parse_actions, config::Config, run::value_to_type}; - -pub struct Job {} - -impl Job { - pub fn load( - loader: &mut Loader, - origin: Option, - cwd: &Path, - name: &str, - vars: &HashMap, - job_depth: &mut i32, - config: &Config, - ) -> Result, Error> { - let (content, path) = Self::load_file(cwd, name, config) - .map_err(|e| Error::new(e.to_string()).with_origin(origin))?; - let parent = path.parent().ok_or_else(|| { - Error::new(format!( - "job path {} doesn't have parent folder", - path.to_string_lossy() - )) - .with_origin(origin) - })?; - - let id = loader.load_string(content, Some(path.to_string_lossy().to_string()), 0); - let mut type_env = rcl::typecheck::prelude(); - let mut env = rcl::runtime::prelude(); - for (name, value) in vars { - type_env.push(name.as_str().into(), value_to_type(value)); - env.push(name.as_str().into(), value.clone()); - } - let value = loader.evaluate( - &mut type_env, - &mut env, - id, - &mut rcl::tracer::StderrTracer::new(Some(MarkupMode::Ansi)), - )?; - - let actions = parse_actions(loader, parent, &value, vars, job_depth, config)?; - - Ok(actions) - } - - fn load_file(cwd: &Path, name: &str, config: &Config) -> Result<(String, PathBuf)> { - if let Ok(content) = Self::load_file_from_folder(cwd, name) { - return Ok(content); - } - - if let Ok(content) = Self::load_file_from_folder(&config.project_folder.join("jobs"), name) - { - return Ok(content); - } - - Err(anyhow!("can't find job {name}")) - } - - fn load_file_from_folder(cwd: &Path, name: &str) -> Result<(String, PathBuf)> { - { - let path = cwd.join(name); - if path.is_dir() { - let path = path.join("main.rcl"); - if let Ok(content) = std::fs::read_to_string(&path) { - return Ok((content, path)); - } - } - } - - { - let name = if name.ends_with(".rcl") { - name.to_string() - } else { - format!("{name}.rcl") - }; - let path = cwd.join(name); - if let Ok(content) = std::fs::read_to_string(&path) { - return Ok((content, path)); - } - } - - Err(anyhow!("can't find job {name}")) - } +#[derive(Clone)] +pub struct Job { + pub block: Block, + pub imported: Option, } + +impl Job {} diff --git a/tiron/src/node.rs b/tiron/src/node.rs index f284656..14a19ae 100644 --- a/tiron/src/node.rs +++ b/tiron/src/node.rs @@ -2,7 +2,6 @@ use std::collections::HashMap; use anyhow::Result; use crossbeam_channel::{Receiver, Sender}; -use rcl::runtime::Value; use tiron_common::{ action::{ActionData, ActionMessage}, node::NodeMessage, @@ -21,28 +20,34 @@ pub struct Node { pub host: String, pub remote_user: Option, pub become_: bool, - pub vars: HashMap, + pub vars: HashMap, pub actions: Vec, pub tx: Sender, } impl Node { - pub fn new(host: String, vars: HashMap, tx: &Sender) -> Self { + pub fn new(host: String, new_vars: HashMap, tx: &Sender) -> Self { Self { id: Uuid::new_v4(), host, - remote_user: vars.get("remote_user").and_then(|v| { - if let Value::String(s, _) = v { + remote_user: new_vars.get("remote_user").and_then(|v| { + if let hcl::Value::String(s) = v { Some(s.to_string()) } else { None } }), - become_: vars + become_: new_vars .get("become") - .map(|v| if let Value::Bool(b) = v { *b } else { false }) + .map(|v| { + if let hcl::Value::Bool(b) = v { + *b + } else { + false + } + }) .unwrap_or(false), - vars, + vars: new_vars, actions: Vec::new(), tx: tx.clone(), } diff --git a/tiron/src/run.rs b/tiron/src/run.rs index dc5ab1b..4235029 100644 --- a/tiron/src/run.rs +++ b/tiron/src/run.rs @@ -1,19 +1,10 @@ -use std::{collections::HashMap, path::Path, sync::Arc}; - use anyhow::Result; -use rcl::{ - error::Error, - loader::Loader, - markup::MarkupMode, - pprint::Doc, - runtime::Value, - source::Span, - types::{SourcedType, Type}, -}; +use hcl_edit::structure::Block; +use tiron_common::error::Error; use tiron_tui::run::{ActionSection, HostSection, RunPanel}; use uuid::Uuid; -use crate::{action::parse_actions, config::Config, node::Node}; +use crate::{action::parse_actions, core::Runbook, node::Node}; pub struct Run { pub id: Uuid, @@ -22,34 +13,12 @@ pub struct Run { } impl Run { - pub fn from_runbook( - loader: &mut Loader, - cwd: &Path, + pub fn from_block( + runbook: &Runbook, name: Option, - origin: Span, + block: &Block, hosts: Vec, - config: &Config, ) -> Result { - let doc = origin.doc(); - let start_line = { - let doc = loader.get_doc(doc); - origin.start_line(doc.data) - }; - - let hosts = if hosts.is_empty() { - vec![Node { - id: Uuid::new_v4(), - host: "localhost".to_string(), - vars: HashMap::new(), - remote_user: None, - become_: false, - actions: Vec::new(), - tx: config.tx.clone(), - }] - } else { - hosts - }; - let mut run = Run { id: Uuid::new_v4(), name, @@ -57,67 +26,14 @@ impl Run { }; for host in run.hosts.iter_mut() { - let doc = loader.get_doc(doc); - let content = origin.resolve(doc.data); - let id = loader.load_string( - content.to_string(), - Some(doc.name.to_string()), - start_line.saturating_sub(1), - ); - let mut type_env = rcl::typecheck::prelude(); - let mut env = rcl::runtime::prelude(); - for (name, value) in &host.vars { - type_env.push(name.as_str().into(), value_to_type(value)); - env.push(name.as_str().into(), value.clone()); - } - let value = loader.evaluate( - &mut type_env, - &mut env, - id, - &mut rcl::tracer::StderrTracer::new(Some(MarkupMode::Ansi)), - )?; - - let Value::Dict(dict, dict_span) = value else { - return Error::new("run should be a dict") - .with_origin(*value.span()) - .err(); - }; - let Some(value) = dict.get(&Value::String("actions".into(), None)) else { - return Error::new("run should have actions") - .with_origin(dict_span) - .err(); - }; - - if let Some(remote_user) = dict.get(&Value::String("remote_user".into(), None)) { - let Value::String(remote_user, _) = remote_user else { - return Error::new("remote_user should be a string") - .with_origin(*remote_user.span()) - .err(); - }; - if host.remote_user.is_none() { - host.remote_user = Some(remote_user.to_string()); - } - } - - if let Some(become_) = dict.get(&Value::String("become".into(), None)) { - let Value::Bool(b) = become_ else { - return Error::new("become should be a bool") - .with_origin(*become_.span()) - .err(); - }; - if *b { - host.become_ = true; - } - } - - let mut job_depth = 0; - let actions = parse_actions(loader, cwd, value, &host.vars, &mut job_depth, config) - .map_err(|mut e| { - e.message = - Doc::string(format!("parsing actions for host {} error: ", host.host)) - + e.message; - e - })?; + let actions = parse_actions(runbook, block, &host.vars).map_err(|e| { + let mut e = e; + e.message = format!( + "error when parsing actions for host {}: {}", + host.host, e.message + ); + e + })?; host.actions = actions; } @@ -167,43 +83,3 @@ impl Run { RunPanel::new(self.id, self.name.clone(), hosts) } } - -pub fn value_to_type(value: &Value) -> SourcedType { - let type_ = match value { - Value::Null => Type::Null, - Value::Bool(_) => Type::Bool, - Value::Int(_) => Type::Int, - Value::String(_, _) => Type::String, - Value::List(list) => Type::List(Arc::new(if let Some(v) = list.first() { - value_to_type(v) - } else { - SourcedType::any() - })), - Value::Set(v) => Type::Set(Arc::new(if let Some(v) = v.first() { - value_to_type(v) - } else { - SourcedType::any() - })), - Value::Dict(v, _) => { - Type::Dict(Arc::new(if let Some((key, value)) = v.first_key_value() { - rcl::types::Dict { - key: value_to_type(key), - value: value_to_type(value), - } - } else { - rcl::types::Dict { - key: SourcedType::any(), - value: SourcedType::any(), - } - })) - } - Value::Function(f) => Type::Function(f.type_.clone()), - Value::BuiltinFunction(f) => Type::Function(Arc::new((f.type_)())), - Value::BuiltinMethod(_) => Type::Any, - }; - - SourcedType { - type_, - source: rcl::type_source::Source::None, - } -}