Migrate repository
This commit is contained in:
commit
ef57a42eb6
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
target/
|
3906
Cargo.lock
generated
Normal file
3906
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
Cargo.toml
Normal file
40
Cargo.toml
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[package]
|
||||||
|
name = "whale"
|
||||||
|
version = "0.3.0"
|
||||||
|
description = "Data-oriented programming language and interactive shell."
|
||||||
|
authors = ["jeff <dev@jeffa.io.com>"]
|
||||||
|
repository = "https://git.jeffa.io/jeff/whale.git"
|
||||||
|
homepage = "https://git.jeffa.io/jeff/whale"
|
||||||
|
readme = "README.md"
|
||||||
|
license = "MIT"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "whale"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "whale_lib"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rand = "0.8.5"
|
||||||
|
chrono = "0.4.26"
|
||||||
|
eframe = "0.22.0"
|
||||||
|
trash = "3.0.3"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
rayon = "1.7.0"
|
||||||
|
serde = { version = "1.0.171", features = ["derive"] }
|
||||||
|
sys-info = "0.9.1"
|
||||||
|
sysinfo = "0.29.6"
|
||||||
|
toml = "0.7.6"
|
||||||
|
toml_edit = "0.19.14"
|
||||||
|
comfy-table = "7.0.1"
|
||||||
|
reedline = "0.22.0"
|
||||||
|
clap = { version = "4.3.19", features = ["derive"] }
|
||||||
|
nu-ansi-term = "0.49"
|
||||||
|
git2 = "0.17.2"
|
||||||
|
csv = "1.2.2"
|
||||||
|
json = "0.12.4"
|
||||||
|
reqwest = { version = "0.11.18", features = ["blocking", "json"] }
|
||||||
|
serde_json = "1.0.104"
|
||||||
|
egui_extras = "0.22.0"
|
286
README.md
Normal file
286
README.md
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
# Dust
|
||||||
|
|
||||||
|
Dust is a data-oriented programming language and interactive shell. The command-line interface can also be run as a singular command or it can run a file. Dust is minimal, easy to read and easy to learn by example. Your code will always do exactly what it looks like it's going to do.
|
||||||
|
|
||||||
|
A basic dust program:
|
||||||
|
|
||||||
|
```dust
|
||||||
|
output "Hello world!"
|
||||||
|
```
|
||||||
|
|
||||||
|
Dust can do two things at the same time:
|
||||||
|
|
||||||
|
```dust
|
||||||
|
async (
|
||||||
|
'output "will this one finish first?"',
|
||||||
|
'output "or will this one?"'
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Display CSV data as a line plot in a GUI window:
|
||||||
|
|
||||||
|
```dust
|
||||||
|
read("examples/assets/faithful.csv")
|
||||||
|
-> from_csv(input)
|
||||||
|
-> get_rows(input)
|
||||||
|
-> transform(input, 'input:get(1)')
|
||||||
|
-> plot(input)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Data Visualization
|
||||||
|
|
||||||
|
Aside from downloading and processing data, dust is able to display it. Unfortunately the command line is a text interface that struggles to render data in the form of charts and graphs. That's why dust is able to instantly spin up GUI windows with beautifully rendered data. Plots, graphs and charts are available from directly within dust. No external tools are needed.
|
||||||
|
|
||||||
|
[bar_graph_demo.webm](https://github.com/solaeus/whale/assets/112188538/deba6e3c-35d4-47e9-9db9-2045ff2e7c9c)
|
||||||
|
|
||||||
|
### Powerful Tooling
|
||||||
|
|
||||||
|
Built-in tools called **macros** reduce complex tasks to plain, simple code.
|
||||||
|
|
||||||
|
```whale
|
||||||
|
download "https://api.sampleapis.com/futurama/cast"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pipelines
|
||||||
|
|
||||||
|
Like a pipe in bash, zsh or fish, the yield operator `->` evaluates the expression on the left and passes it as input to the expression on the right. That input is always assigned to the **`input` variable** for that context. These expressions may simply contain a value or they can call a macro or function that returns a value.
|
||||||
|
|
||||||
|
```whale
|
||||||
|
download "https://api.sampleapis.com/futurama/cast" -> output(input)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Format conversion
|
||||||
|
|
||||||
|
Effortlessly convert between whale and formats like JSON, CSV and TOML.
|
||||||
|
|
||||||
|
```whale
|
||||||
|
download "https://api.sampleapis.com/futurama/cast"
|
||||||
|
-> from_json(input)
|
||||||
|
-> to_csv(input)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Structured data
|
||||||
|
|
||||||
|
Unlike a traditional command line shell, whale can represent data with more than just strings. Lists, maps and tables are everywhere in whale. When you pull in external data, it is easy to deserialize it into whale.
|
||||||
|
|
||||||
|
```whale
|
||||||
|
download "https://api.sampleapis.com/futurama/cast"
|
||||||
|
-> input:from_json()
|
||||||
|
-> input:get(0)
|
||||||
|
-> input.name
|
||||||
|
```
|
||||||
|
|
||||||
|
### Disk Management
|
||||||
|
|
||||||
|
Whale scripts are clear and easy-to-maintain. You can manage disks with sets of key-value pairs instead of remembering positional arguments.
|
||||||
|
|
||||||
|
```
|
||||||
|
new_disk.name = "My Files"
|
||||||
|
new_disk.filesystem = "btrfs";
|
||||||
|
new_disk.path = "/dev/sdb";
|
||||||
|
new_disk.label = "gpt";
|
||||||
|
new_disk.range = (0, 8000);
|
||||||
|
|
||||||
|
new_disk:partition();
|
||||||
|
```
|
||||||
|
|
||||||
|
### First-Class Functions
|
||||||
|
|
||||||
|
Assign custom functions to variables. Functions can return any value or none at all. Use functions to build structured data or automate tasks.
|
||||||
|
|
||||||
|
```whale
|
||||||
|
User = '
|
||||||
|
this.name = input.0;
|
||||||
|
this.age = input.1;
|
||||||
|
this
|
||||||
|
';
|
||||||
|
|
||||||
|
user_0 = User("bob", "44");
|
||||||
|
user_1 = User("mary", "77");
|
||||||
|
```
|
||||||
|
|
||||||
|
This "fetch" function will download JSON data and parse it.
|
||||||
|
|
||||||
|
```
|
||||||
|
fetch = '
|
||||||
|
raw_data = download(input);
|
||||||
|
from_json(raw_data)
|
||||||
|
';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Dust is an experimental project under active development. At this stage, features come and go and the API is always changing. It should not be considered for serious use yet.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
You must have the default rust toolchain installed and up-to-date. Clone the repository and run `cargo run` to start the interactive shell. To see other command line options, use `cargo run -- --help`.
|
||||||
|
|
||||||
|
## The Whale Programming Language
|
||||||
|
|
||||||
|
Dust is a hard fork of [evalexpr]; a simple expression language. Dust's core language features maintain this simplicity. But it can manage large, complex sets of data and perform complicated tasks through macros. It should not take long for a new user to learn the language, especially with the assistance of the shell.
|
||||||
|
|
||||||
|
### Variables and Data Types
|
||||||
|
|
||||||
|
Variables have two parts: a key and a value. The key is always a text string. The value can be any of the following data types:
|
||||||
|
|
||||||
|
- string
|
||||||
|
- integer
|
||||||
|
- floating point value
|
||||||
|
- boolean
|
||||||
|
- list
|
||||||
|
- map
|
||||||
|
- table
|
||||||
|
- function
|
||||||
|
- time
|
||||||
|
- empty
|
||||||
|
|
||||||
|
Here are some examples of variables in dust.
|
||||||
|
|
||||||
|
```whale
|
||||||
|
x = 1;
|
||||||
|
y = "hello, it is " + now().time;
|
||||||
|
z = "42.42";
|
||||||
|
|
||||||
|
list = (3, 2, x);
|
||||||
|
big_list = (x, y, z, list)
|
||||||
|
map.x = "foobar";
|
||||||
|
function = 'output "I'm a function"';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Macros
|
||||||
|
|
||||||
|
**Macros** are dust's built-in tools. Some of them can reconfigure your whole system while others are do very little. They may accept different inputs, or none at all. Macros in the `random` group can be run without input, but the `random_integer` macro can optionally take two numbers as in inclusive range.
|
||||||
|
|
||||||
|
```whale
|
||||||
|
die_roll = random_integer(1, 6);
|
||||||
|
d20_roll = random_integer(1, 20);
|
||||||
|
coin_flip = random_boolean();
|
||||||
|
```
|
||||||
|
|
||||||
|
Other macros can be found by pressing TAB in the interactive shell.
|
||||||
|
|
||||||
|
```whale
|
||||||
|
message = "I hate dust.";
|
||||||
|
replace(message, "hate", "love");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lists
|
||||||
|
|
||||||
|
Lists are sequential collections. They can be built by grouping values with parentheses and separating them with commas. Values can be indexed by their position to access their contents. Lists are used to represent rows in tables and most macros take a list as an argument.
|
||||||
|
|
||||||
|
```whale
|
||||||
|
list = (true, 42, "Ok");
|
||||||
|
|
||||||
|
assert_eq(get(list, 0), true);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Maps
|
||||||
|
|
||||||
|
Maps are flexible collections with arbitrary key-value pairs, similar to JSON objects. Under the hood, all of dust's runtime variables are stored in a map, so, as with variables, the key is always a string.
|
||||||
|
|
||||||
|
```whale
|
||||||
|
reminder.message = "Buy milk";
|
||||||
|
reminder.tags = ("groceries", "home");
|
||||||
|
|
||||||
|
json = to_json(reminder);
|
||||||
|
append_to_file(json, "info.txt");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tables
|
||||||
|
|
||||||
|
Tables are strict collections, each row must have a value for each column. Empty cells must be explicitly set to an empty value. Querying a table is similar to SQL.
|
||||||
|
|
||||||
|
```whale
|
||||||
|
animals.all = create_table (
|
||||||
|
("name", "species", "age"),
|
||||||
|
("rover", "cat", 14),
|
||||||
|
("spot", "snake", 9),
|
||||||
|
("bob", "giraffe", 2),
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
The macros `create_table` and `insert` make sure that all of the memory used to hold the rows is allocated at once, so it is good practice to group your rows together instead of using a call for each row.
|
||||||
|
|
||||||
|
```whale
|
||||||
|
animals.all:insert(
|
||||||
|
("eliza", "ostrich", 4),
|
||||||
|
("pat", "white rhino", 7),
|
||||||
|
("jim", "walrus", 9)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq(animals:length(), 6);
|
||||||
|
|
||||||
|
animals.by_name = animals:sort_by("name");
|
||||||
|
```
|
||||||
|
|
||||||
|
### The Yield Operator
|
||||||
|
|
||||||
|
Like a pipe in bash, zsh or fish, the yield operator evaluates the expression on the left and passes it as input to the expression on the right. That input is always assigned to the **`input` variable** for that context. These expressions may simply contain a value or they can call a macro or function that returns a value.
|
||||||
|
|
||||||
|
```whale
|
||||||
|
"Hello dust!" -> output(input)
|
||||||
|
```
|
||||||
|
|
||||||
|
This can be useful when working on the command line but to make a script easier to read or to avoid fetching the same resource multiple times, we can also declare variables. You should use `->` and variables together to write short, elegant scripts.
|
||||||
|
|
||||||
|
```whale
|
||||||
|
json = download("https://api.sampleapis.com/futurama/characters");
|
||||||
|
from_json(json)
|
||||||
|
-> select(input, "name");
|
||||||
|
-> get(input, 4)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Functions
|
||||||
|
|
||||||
|
Functions are first-class values in dust, so they are assigned to variables like any other value. The function body is wrapped in single parentheses. To call a function, it's just like calling a macro: simply pass it an argument or use an empty set of parentheses to pass an empty value.
|
||||||
|
|
||||||
|
In the function bod, the **`input` variable** represents whatever value is passed to the function when called.
|
||||||
|
|
||||||
|
```whale
|
||||||
|
say_hi = 'output "hi"';
|
||||||
|
add_one = 'input + 1';
|
||||||
|
|
||||||
|
assert_eq(add_one(3), 4);
|
||||||
|
say_hi();
|
||||||
|
```
|
||||||
|
|
||||||
|
This function simply passes the input to the shell's standard output.
|
||||||
|
|
||||||
|
```whale
|
||||||
|
print = 'output(input)';
|
||||||
|
```
|
||||||
|
|
||||||
|
Because functions are stored in variables, we can use collections like maps to
|
||||||
|
organize them.
|
||||||
|
|
||||||
|
```whale
|
||||||
|
math.add = 'input.0 + input.1';
|
||||||
|
math.subtract = 'input.0 - input.1';
|
||||||
|
|
||||||
|
assert_eq(math.add(2, 2), 4);
|
||||||
|
assert_eq(math.subtract(100, 1), 99);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Time
|
||||||
|
|
||||||
|
Whale can record, parse and convert time values. Whale can parse TOML datetime
|
||||||
|
values or can create time values using macros.
|
||||||
|
|
||||||
|
```whale
|
||||||
|
dob = from_toml("1979-05-27T07:32:00-08:00")
|
||||||
|
|
||||||
|
output "Date of birth = " + local(dob);
|
||||||
|
```
|
||||||
|
|
||||||
|
```whale
|
||||||
|
time = now();
|
||||||
|
|
||||||
|
output "Universal time is " + time:utc();
|
||||||
|
output "Local time is " + time:local();
|
||||||
|
```
|
||||||
|
|
||||||
|
[dnf]: https://dnf.readthedocs.io/en/latest/index.html
|
||||||
|
[evalexpr]: https://github.com/ISibboI/evalexpr
|
9
examples/cpu_monitor.whale
Normal file
9
examples/cpu_monitor.whale
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
loop('
|
||||||
|
speed = cpu_speed();
|
||||||
|
message = "Current CPU clock: " + string(speed);
|
||||||
|
|
||||||
|
output(message);
|
||||||
|
wait 300;
|
||||||
|
');
|
||||||
|
|
||||||
|
|
4
examples/fetch.whale
Normal file
4
examples/fetch.whale
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
raw_data = download "https://api.sampleapis.com/futurama/cast";
|
||||||
|
data = raw_data:from_json();
|
||||||
|
first = data:get 0;
|
||||||
|
first.name
|
1
examples/hello_world.whale
Normal file
1
examples/hello_world.whale
Normal file
@ -0,0 +1 @@
|
|||||||
|
output "Hello world!"
|
7
examples/map.whale
Normal file
7
examples/map.whale
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
x = 1;
|
||||||
|
y = 2;
|
||||||
|
z = 3;
|
||||||
|
a = "foo";
|
||||||
|
b = "bar";
|
||||||
|
c = "foobar";
|
||||||
|
d = ("abc", "123", "xyz");
|
14
examples/random_user_data.whale
Normal file
14
examples/random_user_data.whale
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
create_user = '
|
||||||
|
names = ("bob", "bill", "mary", "susan");
|
||||||
|
(random_integer(), names:random(), random_integer(0, 100))
|
||||||
|
';
|
||||||
|
|
||||||
|
create_table(
|
||||||
|
("id", "name", "age"),
|
||||||
|
(
|
||||||
|
repeat (
|
||||||
|
create_user,
|
||||||
|
10
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
29
examples/table.whale
Normal file
29
examples/table.whale
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
table = create_table (
|
||||||
|
("text", "number", "bool"),
|
||||||
|
(
|
||||||
|
("a", 1, true),
|
||||||
|
("b", 2, true),
|
||||||
|
("a", 3, true)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
test_table = create_table (
|
||||||
|
("text", "bool"),
|
||||||
|
(
|
||||||
|
("a", true),
|
||||||
|
("b", true),
|
||||||
|
("a", true)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_equal(table:select("text", "bool"), test_table);
|
||||||
|
|
||||||
|
test_table = create_table (
|
||||||
|
("text", "number", "bool"),
|
||||||
|
(
|
||||||
|
("a", 1, true),
|
||||||
|
("a", 3, true)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_equal(table:where('text == "a"'), test_table);
|
21
examples/toolbox.whale
Normal file
21
examples/toolbox.whale
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
enable_copr_repositories = ("varlad/helix");
|
||||||
|
enable_rpm_repositories (
|
||||||
|
"https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-38.noarch.rpm",
|
||||||
|
"https://mirrors.rpmfusion.org/free/fedora/rpmfusion-nonfree-release-38.noarch.rpm"
|
||||||
|
);
|
||||||
|
install_package (
|
||||||
|
"bat",
|
||||||
|
"egl-wayland",
|
||||||
|
"fd-find",
|
||||||
|
"fish",
|
||||||
|
"fzf",
|
||||||
|
"lsd",
|
||||||
|
"kitty",
|
||||||
|
"helix",
|
||||||
|
"libwayland-cursor",
|
||||||
|
"libwayland-egl",
|
||||||
|
"wl-clipboard"
|
||||||
|
);
|
||||||
|
|
||||||
|
sh "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh";
|
||||||
|
sh "cargo install starship zoxide";
|
3
examples/wait.whale
Normal file
3
examples/wait.whale
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
output "foo...";
|
||||||
|
wait 1;
|
||||||
|
output "bar!";
|
615
src/error.rs
Normal file
615
src/error.rs
Normal file
@ -0,0 +1,615 @@
|
|||||||
|
//! Error and Result types.
|
||||||
|
//!
|
||||||
|
//! To deal with errors from dependencies, either create a new error variant
|
||||||
|
//! or use the MacroFailure variant if the error can only occur inside a macro.
|
||||||
|
use crate::{
|
||||||
|
operator::Operator, token::PartialToken, value::value_type::ValueType, value::Value, MacroInfo,
|
||||||
|
Node,
|
||||||
|
};
|
||||||
|
|
||||||
|
use std::{fmt, io, time::SystemTimeError};
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum Error {
|
||||||
|
/// A row was inserted to a table with the wrong amount of values.
|
||||||
|
WrongColumnAmount {
|
||||||
|
expected: usize,
|
||||||
|
actual: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// An operator was called with the wrong amount of arguments.
|
||||||
|
ExpectedOperatorArgumentAmount {
|
||||||
|
expected: usize,
|
||||||
|
actual: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A function was called with the wrong amount of arguments.
|
||||||
|
ExpectedFunctionArgumentAmount {
|
||||||
|
identifier: String,
|
||||||
|
expected: usize,
|
||||||
|
actual: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A function was called with the wrong amount of arguments.
|
||||||
|
ExpectedAtLeastFunctionArgumentAmount {
|
||||||
|
identifier: String,
|
||||||
|
minimum: usize,
|
||||||
|
actual: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
ExpectedString {
|
||||||
|
actual: Value,
|
||||||
|
},
|
||||||
|
|
||||||
|
ExpectedInt {
|
||||||
|
actual: Value,
|
||||||
|
},
|
||||||
|
|
||||||
|
ExpectedFloat {
|
||||||
|
actual: Value,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// An integer, floating point or value was expected.
|
||||||
|
ExpectedNumber {
|
||||||
|
actual: Value,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// An integer, floating point or string value was expected.
|
||||||
|
ExpectedNumberOrString {
|
||||||
|
actual: Value,
|
||||||
|
},
|
||||||
|
|
||||||
|
ExpectedBoolean {
|
||||||
|
actual: Value,
|
||||||
|
},
|
||||||
|
|
||||||
|
ExpectedList {
|
||||||
|
actual: Value,
|
||||||
|
},
|
||||||
|
|
||||||
|
ExpectedFixedLenList {
|
||||||
|
expected_len: usize,
|
||||||
|
actual: Value,
|
||||||
|
},
|
||||||
|
|
||||||
|
ExpectedEmpty {
|
||||||
|
actual: Value,
|
||||||
|
},
|
||||||
|
|
||||||
|
ExpectedMap {
|
||||||
|
actual: Value,
|
||||||
|
},
|
||||||
|
|
||||||
|
ExpectedTable {
|
||||||
|
actual: Value,
|
||||||
|
},
|
||||||
|
|
||||||
|
ExpectedFunction {
|
||||||
|
actual: Value,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A string, list, map or table value was expected.
|
||||||
|
ExpectedCollection {
|
||||||
|
actual: Value,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Tried to append a child to a leaf node.
|
||||||
|
/// Leaf nodes cannot have children.
|
||||||
|
AppendedToLeafNode(Node),
|
||||||
|
|
||||||
|
/// Tried to append a child to a node such that the precedence of the child
|
||||||
|
/// is not higher. This error should never occur. If it does, please file a
|
||||||
|
/// bug report.
|
||||||
|
PrecedenceViolation,
|
||||||
|
|
||||||
|
/// A `VariableIdentifier` operation did not find its value in the context.
|
||||||
|
VariableIdentifierNotFound(String),
|
||||||
|
|
||||||
|
/// A `FunctionIdentifier` operation did not find its value in the context.
|
||||||
|
FunctionIdentifierNotFound(String),
|
||||||
|
|
||||||
|
/// A value has the wrong type.
|
||||||
|
/// Only use this if there is no other error that describes the expected and
|
||||||
|
/// provided types in more detail.
|
||||||
|
TypeError {
|
||||||
|
/// The expected types.
|
||||||
|
expected: &'static [ValueType],
|
||||||
|
/// The actual value.
|
||||||
|
actual: Value,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A macro or function was called with the wrong type of input.
|
||||||
|
MacroArgumentType {
|
||||||
|
/// The macro that was called.
|
||||||
|
macro_info: MacroInfo<'static>,
|
||||||
|
/// The actual value.
|
||||||
|
actual: Value,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// An operator is used with a wrong combination of types.
|
||||||
|
WrongTypeCombination {
|
||||||
|
/// The operator that whose evaluation caused the error.
|
||||||
|
operator: Operator,
|
||||||
|
/// The types that were used in the operator causing it to fail.
|
||||||
|
actual: Vec<ValueType>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// An opening brace without a matching closing brace was found.
|
||||||
|
UnmatchedLBrace,
|
||||||
|
|
||||||
|
/// A closing brace without a matching opening brace was found.
|
||||||
|
UnmatchedRBrace,
|
||||||
|
|
||||||
|
/// Left of an opening brace or right of a closing brace is a token that does not expect the brace next to it.
|
||||||
|
/// For example, writing `4(5)` would yield this error, as the `4` does not have any operands.
|
||||||
|
MissingOperatorOutsideOfBrace,
|
||||||
|
|
||||||
|
/// A `PartialToken` is unmatched, such that it cannot be combined into a full `Token`.
|
||||||
|
/// This happens if for example a single `=` is found, surrounded by whitespace.
|
||||||
|
/// It is not a token, but it is part of the string representation of some tokens.
|
||||||
|
UnmatchedPartialToken {
|
||||||
|
/// The unmatched partial token.
|
||||||
|
first: PartialToken,
|
||||||
|
/// The token that follows the unmatched partial token and that cannot be matched to the partial token, or `None`, if `first` is the last partial token in the stream.
|
||||||
|
second: Option<PartialToken>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// An addition operation performed by Rust failed.
|
||||||
|
AdditionError {
|
||||||
|
/// The first argument of the addition.
|
||||||
|
augend: Value,
|
||||||
|
/// The second argument of the addition.
|
||||||
|
addend: Value,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A subtraction operation performed by Rust failed.
|
||||||
|
SubtractionError {
|
||||||
|
/// The first argument of the subtraction.
|
||||||
|
minuend: Value,
|
||||||
|
/// The second argument of the subtraction.
|
||||||
|
subtrahend: Value,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A negation operation performed by Rust failed.
|
||||||
|
NegationError {
|
||||||
|
/// The argument of the negation.
|
||||||
|
argument: Value,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A multiplication operation performed by Rust failed.
|
||||||
|
MultiplicationError {
|
||||||
|
/// The first argument of the multiplication.
|
||||||
|
multiplicand: Value,
|
||||||
|
/// The second argument of the multiplication.
|
||||||
|
multiplier: Value,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A division operation performed by Rust failed.
|
||||||
|
DivisionError {
|
||||||
|
/// The first argument of the division.
|
||||||
|
dividend: Value,
|
||||||
|
/// The second argument of the division.
|
||||||
|
divisor: Value,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A modulation operation performed by Rust failed.
|
||||||
|
ModulationError {
|
||||||
|
/// The first argument of the modulation.
|
||||||
|
dividend: Value,
|
||||||
|
/// The second argument of the modulation.
|
||||||
|
divisor: Value,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A regular expression could not be parsed
|
||||||
|
InvalidRegex {
|
||||||
|
/// The invalid regular expression
|
||||||
|
regex: String,
|
||||||
|
/// Failure message from the regex engine
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A modification was attempted on a `Context` that does not allow modifications.
|
||||||
|
ContextNotMutable,
|
||||||
|
|
||||||
|
/// An escape sequence within a string literal is illegal.
|
||||||
|
IllegalEscapeSequence(String),
|
||||||
|
|
||||||
|
/// This context does not allow enabling builtin functions.
|
||||||
|
BuiltinFunctionsCannotBeEnabled,
|
||||||
|
|
||||||
|
/// This context does not allow disabling builtin functions.
|
||||||
|
BuiltinFunctionsCannotBeDisabled,
|
||||||
|
|
||||||
|
/// The function failed due to an external error.
|
||||||
|
MacroFailure(String),
|
||||||
|
|
||||||
|
/// A custom error explained by its message.
|
||||||
|
CustomMessage(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<csv::Error> for Error {
|
||||||
|
fn from(value: csv::Error) -> Self {
|
||||||
|
Error::MacroFailure(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<json::Error> for Error {
|
||||||
|
fn from(value: json::Error) -> Self {
|
||||||
|
Error::MacroFailure(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<io::Error> for Error {
|
||||||
|
fn from(value: std::io::Error) -> Self {
|
||||||
|
Error::MacroFailure(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<git2::Error> for Error {
|
||||||
|
fn from(value: git2::Error) -> Self {
|
||||||
|
Error::MacroFailure(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<reqwest::Error> for Error {
|
||||||
|
fn from(value: reqwest::Error) -> Self {
|
||||||
|
Error::MacroFailure(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Error> for Error {
|
||||||
|
fn from(value: serde_json::Error) -> Self {
|
||||||
|
Error::MacroFailure(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<sys_info::Error> for Error {
|
||||||
|
fn from(value: sys_info::Error) -> Self {
|
||||||
|
Error::MacroFailure(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SystemTimeError> for Error {
|
||||||
|
fn from(value: SystemTimeError) -> Self {
|
||||||
|
Error::MacroFailure(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<trash::Error> for Error {
|
||||||
|
fn from(value: trash::Error) -> Self {
|
||||||
|
Error::MacroFailure(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
pub(crate) fn expect_operator_argument_amount(actual: usize, expected: usize) -> Result<()> {
|
||||||
|
if actual == expected {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::ExpectedOperatorArgumentAmount { expected, actual })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn expect_function_argument_amount(
|
||||||
|
identifier: &str,
|
||||||
|
actual: usize,
|
||||||
|
expected: usize,
|
||||||
|
) -> Result<()> {
|
||||||
|
if actual == expected {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::ExpectedFunctionArgumentAmount {
|
||||||
|
identifier: identifier.to_string(),
|
||||||
|
expected,
|
||||||
|
actual,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn expected_minimum_function_argument_amount(
|
||||||
|
identifier: &str,
|
||||||
|
actual: usize,
|
||||||
|
minimum: usize,
|
||||||
|
) -> Result<()> {
|
||||||
|
if actual >= minimum {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::ExpectedAtLeastFunctionArgumentAmount {
|
||||||
|
identifier: identifier.to_string(),
|
||||||
|
minimum,
|
||||||
|
actual,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn type_error(actual: Value, expected: &'static [ValueType]) -> Self {
|
||||||
|
Error::TypeError { actual, expected }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wrong_type_combination(operator: Operator, actual: Vec<ValueType>) -> Self {
|
||||||
|
Error::WrongTypeCombination { operator, actual }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expected_string(actual: Value) -> Self {
|
||||||
|
Error::ExpectedString { actual }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expected_int(actual: Value) -> Self {
|
||||||
|
Error::ExpectedInt { actual }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expected_float(actual: Value) -> Self {
|
||||||
|
Error::ExpectedFloat { actual }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expected_number(actual: Value) -> Self {
|
||||||
|
Error::ExpectedNumber { actual }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expected_number_or_string(actual: Value) -> Self {
|
||||||
|
Error::ExpectedNumberOrString { actual }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expected_boolean(actual: Value) -> Self {
|
||||||
|
Error::ExpectedBoolean { actual }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expected_list(actual: Value) -> Self {
|
||||||
|
Error::ExpectedList { actual }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expected_fixed_len_list(expected_len: usize, actual: Value) -> Self {
|
||||||
|
Error::ExpectedFixedLenList {
|
||||||
|
expected_len,
|
||||||
|
actual,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expected_empty(actual: Value) -> Self {
|
||||||
|
Error::ExpectedEmpty { actual }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expected_map(actual: Value) -> Self {
|
||||||
|
Error::ExpectedMap { actual }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expected_table(actual: Value) -> Self {
|
||||||
|
Error::ExpectedTable { actual }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expected_function(actual: Value) -> Self {
|
||||||
|
Error::ExpectedFunction { actual }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expected_collection(actual: Value) -> Self {
|
||||||
|
Error::ExpectedCollection { actual }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn unmatched_partial_token(
|
||||||
|
first: PartialToken,
|
||||||
|
second: Option<PartialToken>,
|
||||||
|
) -> Self {
|
||||||
|
Error::UnmatchedPartialToken { first, second }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn addition_error(augend: Value, addend: Value) -> Self {
|
||||||
|
Error::AdditionError { augend, addend }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn subtraction_error(minuend: Value, subtrahend: Value) -> Self {
|
||||||
|
Error::SubtractionError {
|
||||||
|
minuend,
|
||||||
|
subtrahend,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn negation_error(argument: Value) -> Self {
|
||||||
|
Error::NegationError { argument }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn multiplication_error(multiplicand: Value, multiplier: Value) -> Self {
|
||||||
|
Error::MultiplicationError {
|
||||||
|
multiplicand,
|
||||||
|
multiplier,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn division_error(dividend: Value, divisor: Value) -> Self {
|
||||||
|
Error::DivisionError { dividend, divisor }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn modulation_error(dividend: Value, divisor: Value) -> Self {
|
||||||
|
Error::ModulationError { dividend, divisor }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs `EvalexprError::InvalidRegex(regex)`
|
||||||
|
pub fn invalid_regex(regex: String, message: String) -> Self {
|
||||||
|
Error::InvalidRegex { regex, message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `Ok(())` if the given value is a string or a numeric.
|
||||||
|
pub fn expect_number_or_string(actual: &Value) -> Result<()> {
|
||||||
|
match actual {
|
||||||
|
Value::String(_) | Value::Float(_) | Value::Integer(_) => Ok(()),
|
||||||
|
_ => Err(Error::expected_number_or_string(actual.clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `Ok(())` if the given value is a String, List, Map or Table.
|
||||||
|
pub fn _expect_collection(actual: &Value) -> Result<()> {
|
||||||
|
match actual {
|
||||||
|
Value::String(_) | Value::List(_) | Value::Map(_) | Value::Table(_) => Ok(()),
|
||||||
|
_ => Err(Error::expected_collection(actual.clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for Error {}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
use Error::*;
|
||||||
|
|
||||||
|
match self {
|
||||||
|
ExpectedOperatorArgumentAmount { expected, actual } => write!(
|
||||||
|
f,
|
||||||
|
"An operator expected {} arguments, but got {}.",
|
||||||
|
expected, actual
|
||||||
|
),
|
||||||
|
ExpectedFunctionArgumentAmount {
|
||||||
|
expected,
|
||||||
|
actual,
|
||||||
|
identifier,
|
||||||
|
} => write!(
|
||||||
|
f,
|
||||||
|
"{identifier} expected {expected} arguments, but got {actual}.",
|
||||||
|
),
|
||||||
|
ExpectedAtLeastFunctionArgumentAmount {
|
||||||
|
minimum,
|
||||||
|
actual,
|
||||||
|
identifier,
|
||||||
|
} => write!(
|
||||||
|
f,
|
||||||
|
"{identifier} expected a minimum of {minimum} arguments, but got {actual}.",
|
||||||
|
),
|
||||||
|
ExpectedString { actual } => {
|
||||||
|
write!(f, "Expected a Value::String, but got {:?}.", actual)
|
||||||
|
}
|
||||||
|
ExpectedInt { actual } => write!(f, "Expected a Value::Int, but got {:?}.", actual),
|
||||||
|
ExpectedFloat { actual } => write!(f, "Expected a Value::Float, but got {:?}.", actual),
|
||||||
|
ExpectedNumber { actual } => write!(
|
||||||
|
f,
|
||||||
|
"Expected a Value::Float or Value::Int, but got {:?}.",
|
||||||
|
actual
|
||||||
|
),
|
||||||
|
ExpectedNumberOrString { actual } => write!(
|
||||||
|
f,
|
||||||
|
"Expected a Value::Number or a Value::String, but got {:?}.",
|
||||||
|
actual
|
||||||
|
),
|
||||||
|
ExpectedBoolean { actual } => {
|
||||||
|
write!(f, "Expected a Value::Boolean, but got {:?}.", actual)
|
||||||
|
}
|
||||||
|
ExpectedList { actual } => write!(f, "Expected a Value::Tuple, but got {:?}.", actual),
|
||||||
|
ExpectedFixedLenList {
|
||||||
|
expected_len,
|
||||||
|
actual,
|
||||||
|
} => write!(
|
||||||
|
f,
|
||||||
|
"Expected a Value::Tuple of len {}, but got {:?}.",
|
||||||
|
expected_len, actual
|
||||||
|
),
|
||||||
|
ExpectedEmpty { actual } => write!(f, "Expected a Value::Empty, but got {:?}.", actual),
|
||||||
|
ExpectedMap { actual } => write!(f, "Expected a Value::Map, but got {:?}.", actual),
|
||||||
|
ExpectedTable { actual } => write!(f, "Expected a Value::Table, but got {:?}.", actual),
|
||||||
|
ExpectedFunction { actual } => {
|
||||||
|
write!(f, "Expected Value::Function, but got {:?}.", actual)
|
||||||
|
}
|
||||||
|
ExpectedCollection { actual } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Expected a string, list, map or table, but got {:?}.",
|
||||||
|
actual
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AppendedToLeafNode(node) => write!(f, "Syntax error at \"{node}\"."),
|
||||||
|
PrecedenceViolation => write!(
|
||||||
|
f,
|
||||||
|
"Tried to append a node to another node with higher precedence."
|
||||||
|
),
|
||||||
|
VariableIdentifierNotFound(identifier) => write!(
|
||||||
|
f,
|
||||||
|
"Variable identifier is not bound to anything by context: {:?}.",
|
||||||
|
identifier
|
||||||
|
),
|
||||||
|
FunctionIdentifierNotFound(identifier) => write!(
|
||||||
|
f,
|
||||||
|
"Function identifier is not bound to anything by context: {:?}.",
|
||||||
|
identifier
|
||||||
|
),
|
||||||
|
TypeError { expected, actual } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Type Error. The value {actual} is not one of the following: {expected:?}.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
WrongTypeCombination { operator, actual } => write!(
|
||||||
|
f,
|
||||||
|
"The operator {:?} was called with a wrong combination of types: {:?}",
|
||||||
|
operator, actual
|
||||||
|
),
|
||||||
|
UnmatchedLBrace => write!(f, "Found an unmatched opening parenthesis '('."),
|
||||||
|
UnmatchedRBrace => write!(f, "Found an unmatched closing parenthesis ')'."),
|
||||||
|
MissingOperatorOutsideOfBrace { .. } => write!(
|
||||||
|
f,
|
||||||
|
"Found an opening parenthesis that is preceded by something that does not take \
|
||||||
|
any arguments on the right, or found a closing parenthesis that is succeeded by \
|
||||||
|
something that does not take any arguments on the left."
|
||||||
|
),
|
||||||
|
UnmatchedPartialToken { first, second } => {
|
||||||
|
if let Some(second) = second {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Found a partial token '{}' that should not be followed by '{}'.",
|
||||||
|
first, second
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Found a partial token '{}' that should be followed by another partial \
|
||||||
|
token.",
|
||||||
|
first
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AdditionError { augend, addend } => write!(f, "Error adding {} + {}", augend, addend),
|
||||||
|
SubtractionError {
|
||||||
|
minuend,
|
||||||
|
subtrahend,
|
||||||
|
} => write!(f, "Error subtracting {} - {}", minuend, subtrahend),
|
||||||
|
NegationError { argument } => write!(f, "Error negating -{}", argument),
|
||||||
|
MultiplicationError {
|
||||||
|
multiplicand,
|
||||||
|
multiplier,
|
||||||
|
} => write!(f, "Error multiplying {} * {}", multiplicand, multiplier),
|
||||||
|
DivisionError { dividend, divisor } => {
|
||||||
|
write!(f, "Error dividing {} / {}", dividend, divisor)
|
||||||
|
}
|
||||||
|
ModulationError { dividend, divisor } => {
|
||||||
|
write!(f, "Error modulating {} % {}", dividend, divisor)
|
||||||
|
}
|
||||||
|
InvalidRegex { regex, message } => write!(
|
||||||
|
f,
|
||||||
|
"Regular expression {:?} is invalid: {:?}",
|
||||||
|
regex, message
|
||||||
|
),
|
||||||
|
ContextNotMutable => write!(f, "Cannot manipulate context"),
|
||||||
|
BuiltinFunctionsCannotBeEnabled => {
|
||||||
|
write!(f, "This context does not allow enabling builtin functions")
|
||||||
|
}
|
||||||
|
BuiltinFunctionsCannotBeDisabled => {
|
||||||
|
write!(f, "This context does not allow disabling builtin functions")
|
||||||
|
}
|
||||||
|
IllegalEscapeSequence(string) => write!(f, "Illegal escape sequence: {}", string),
|
||||||
|
MacroFailure(message) => write!(f, "Function failure: {}", message),
|
||||||
|
CustomMessage(message) => write!(f, "Error: {}", message),
|
||||||
|
WrongColumnAmount { expected, actual } => write!(
|
||||||
|
f,
|
||||||
|
"Wrong number of columns for this table. Expected {expected}, found {actual}."
|
||||||
|
),
|
||||||
|
MacroArgumentType {
|
||||||
|
macro_info,
|
||||||
|
actual,
|
||||||
|
} => write!(
|
||||||
|
f,
|
||||||
|
"Wrong argument of type {:?} was passed to {}. Expected one of the following types: {:?}.",
|
||||||
|
actual.value_type(),
|
||||||
|
macro_info.identifier,
|
||||||
|
macro_info.inputs
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
58
src/interface.rs
Normal file
58
src/interface.rs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
use crate::{token, tree, Result, Value, VariableMap};
|
||||||
|
|
||||||
|
/// Evaluate the given expression string.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use whale_lib::*;
|
||||||
|
/// assert_eq!(eval("1 + 2 + 3"), Ok(Value::from(6)));
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// *See the [crate doc](index.html) for more examples and explanations of the expression format.*
|
||||||
|
pub fn eval(string: &str) -> Result<Value> {
|
||||||
|
let mut context = VariableMap::new();
|
||||||
|
let eval = eval_with_context(string, &mut context);
|
||||||
|
|
||||||
|
match eval {
|
||||||
|
Ok(output) => {
|
||||||
|
if output.is_empty() {
|
||||||
|
Ok(Value::Map(context))
|
||||||
|
} else {
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluate the given expression string with the given context.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use whale_lib::*;
|
||||||
|
/// let mut context = VariableMap::new();
|
||||||
|
/// context.set_value("one".into(), 1.into()).unwrap(); // Do proper error handling here
|
||||||
|
/// context.set_value("two".into(), 2.into()).unwrap(); // Do proper error handling here
|
||||||
|
/// context.set_value("three".into(), 3.into()).unwrap(); // Do proper error handling here
|
||||||
|
/// assert_eq!(eval_with_context("one + two + three", &context), Ok(Value::from(6)));
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// *See the [crate doc](index.html) for more examples and explanations of the expression format.*
|
||||||
|
pub fn eval_with_context(string: &str, context: &mut VariableMap) -> Result<Value> {
|
||||||
|
let split = string.split_once("::");
|
||||||
|
|
||||||
|
if let Some((left, right)) = split {
|
||||||
|
let left_result = tree::tokens_to_operator_tree(token::tokenize(left)?)?
|
||||||
|
.eval_with_context_mut(context)?;
|
||||||
|
|
||||||
|
context.set_value("input", left_result)?;
|
||||||
|
|
||||||
|
let right_result = eval_with_context(right, context)?;
|
||||||
|
|
||||||
|
Ok(right_result)
|
||||||
|
} else {
|
||||||
|
tree::tokens_to_operator_tree(token::tokenize(string)?)?.eval_with_context_mut(context)
|
||||||
|
}
|
||||||
|
}
|
23
src/lib.rs
Normal file
23
src/lib.rs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
#![doc = include_str!("../README.md")]
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
pub use crate::{
|
||||||
|
error::*,
|
||||||
|
interface::*,
|
||||||
|
macros::*,
|
||||||
|
operator::Operator,
|
||||||
|
token::PartialToken,
|
||||||
|
tree::Node,
|
||||||
|
value::{
|
||||||
|
function::Function, table::Table, time::Time, value_type::ValueType,
|
||||||
|
variable_map::VariableMap, Value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
mod error;
|
||||||
|
mod interface;
|
||||||
|
mod macros;
|
||||||
|
mod operator;
|
||||||
|
mod token;
|
||||||
|
mod tree;
|
||||||
|
mod value;
|
561
src/macros/collections.rs
Normal file
561
src/macros/collections.rs
Normal file
@ -0,0 +1,561 @@
|
|||||||
|
//! Macros for collection values: strings, lists, maps and tables.
|
||||||
|
|
||||||
|
use crate::{Error, Macro, MacroInfo, Result, Table, Value, VariableMap};
|
||||||
|
|
||||||
|
pub struct Transform;
|
||||||
|
|
||||||
|
impl Macro for Transform {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "transform",
|
||||||
|
description: "Change each value with a function.",
|
||||||
|
group: "collections",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let argument = argument.as_list()?;
|
||||||
|
let value = &argument[0];
|
||||||
|
let function = argument[1].as_function()?;
|
||||||
|
|
||||||
|
match value {
|
||||||
|
Value::String(_string) => todo!(),
|
||||||
|
Value::Float(_) => todo!(),
|
||||||
|
Value::Integer(_) => todo!(),
|
||||||
|
Value::Boolean(_) => todo!(),
|
||||||
|
Value::List(list) => {
|
||||||
|
let mut mapped_list = Vec::with_capacity(list.len());
|
||||||
|
|
||||||
|
for value in list {
|
||||||
|
let mut context = VariableMap::new();
|
||||||
|
|
||||||
|
context.set_value("input", value.clone())?;
|
||||||
|
|
||||||
|
let mapped_value = function.run_with_context(&mut context)?;
|
||||||
|
|
||||||
|
mapped_list.push(mapped_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::List(mapped_list))
|
||||||
|
}
|
||||||
|
Value::Empty => todo!(),
|
||||||
|
Value::Map(_map) => todo!(),
|
||||||
|
Value::Table(_) => todo!(),
|
||||||
|
Value::Function(_) => todo!(),
|
||||||
|
Value::Time(_) => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct String;
|
||||||
|
|
||||||
|
impl Macro for String {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "string",
|
||||||
|
description: "Stringify a value.",
|
||||||
|
group: "collections",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let string = match argument.clone() {
|
||||||
|
Value::String(string) => string,
|
||||||
|
Value::List(_list) => todo!(),
|
||||||
|
Value::Map(_map) => todo!(),
|
||||||
|
Value::Table(_table) => todo!(),
|
||||||
|
Value::Function(function) => function.to_string(),
|
||||||
|
Value::Float(float) => float.to_string(),
|
||||||
|
Value::Integer(integer) => integer.to_string(),
|
||||||
|
Value::Boolean(boolean) => boolean.to_string(),
|
||||||
|
Value::Time(_) => todo!(),
|
||||||
|
Value::Empty => todo!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Value::String(string))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Count;
|
||||||
|
|
||||||
|
impl Macro for Count {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "count",
|
||||||
|
description: "Return the number of items in a value.",
|
||||||
|
group: "collections",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let len = match argument {
|
||||||
|
Value::String(string) => string.len(),
|
||||||
|
Value::List(list) => list.len(),
|
||||||
|
Value::Map(map) => map.len(),
|
||||||
|
Value::Table(table) => table.len(),
|
||||||
|
Value::Function(_)
|
||||||
|
| Value::Float(_)
|
||||||
|
| Value::Integer(_)
|
||||||
|
| Value::Boolean(_)
|
||||||
|
| Value::Time(_) => 1,
|
||||||
|
Value::Empty => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Value::Integer(len as i64))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CreateTable;
|
||||||
|
|
||||||
|
impl Macro for CreateTable {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "create_table",
|
||||||
|
description: "Define a new table with a list of column names and list of rows.",
|
||||||
|
group: "collections",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let argument = argument.as_list()?;
|
||||||
|
|
||||||
|
let column_name_inputs = argument[0].as_list()?;
|
||||||
|
let mut column_names = Vec::with_capacity(column_name_inputs.len());
|
||||||
|
|
||||||
|
for name in column_name_inputs {
|
||||||
|
column_names.push(name.as_string()?.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let column_count = column_names.len();
|
||||||
|
let rows = argument[1].as_list()?;
|
||||||
|
let mut table = Table::new(column_names);
|
||||||
|
|
||||||
|
for row in rows {
|
||||||
|
let row = row.as_fixed_len_list(column_count)?;
|
||||||
|
|
||||||
|
table.insert(row.clone()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::Table(table))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Rows;
|
||||||
|
|
||||||
|
impl Macro for Rows {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "rows",
|
||||||
|
description: "Extract a table's rows as a list.",
|
||||||
|
group: "collections",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let table = argument.as_table()?;
|
||||||
|
|
||||||
|
let rows = table
|
||||||
|
.rows()
|
||||||
|
.iter()
|
||||||
|
.map(|row| Value::List(row.clone()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(Value::List(rows))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Get;
|
||||||
|
|
||||||
|
impl Macro for Get {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "get",
|
||||||
|
description: "Retrieve a value from a collection.",
|
||||||
|
group: "collections",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let argument = argument.as_list()?;
|
||||||
|
|
||||||
|
let collection = &argument[0];
|
||||||
|
let index = argument[1].as_int()?;
|
||||||
|
|
||||||
|
if let Ok(list) = collection.as_list() {
|
||||||
|
if let Some(value) = list.get(index as usize) {
|
||||||
|
return Ok(value.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(Value::Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Insert;
|
||||||
|
|
||||||
|
impl Macro for Insert {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "insert",
|
||||||
|
description: "Add new rows to a table.",
|
||||||
|
group: "collections",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let argument = argument.as_list()?;
|
||||||
|
let new_rows = &argument[1..];
|
||||||
|
let mut table = argument[0].as_table()?.clone();
|
||||||
|
|
||||||
|
table.reserve(new_rows.len());
|
||||||
|
|
||||||
|
for row in new_rows {
|
||||||
|
let row = row.as_list()?.clone();
|
||||||
|
|
||||||
|
table.insert(row)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::Table(table))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Select;
|
||||||
|
|
||||||
|
impl Macro for Select {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "select",
|
||||||
|
description: "Extract one or more values based on their key.",
|
||||||
|
group: "collections",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let arguments = argument.as_fixed_len_list(2)?;
|
||||||
|
let collection = &arguments[0];
|
||||||
|
|
||||||
|
if let Value::List(list) = collection {
|
||||||
|
let mut selected = Vec::new();
|
||||||
|
|
||||||
|
let index = arguments[1].as_int()?;
|
||||||
|
let value = list.get(index as usize);
|
||||||
|
|
||||||
|
if let Some(value) = value {
|
||||||
|
selected.push(value.clone());
|
||||||
|
return Ok(Value::List(selected));
|
||||||
|
} else {
|
||||||
|
return Ok(Value::List(selected));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut column_names = Vec::new();
|
||||||
|
|
||||||
|
if let Value::List(columns) = &arguments[1] {
|
||||||
|
for column in columns {
|
||||||
|
let name = column.as_string()?;
|
||||||
|
|
||||||
|
column_names.push(name.clone());
|
||||||
|
}
|
||||||
|
} else if let Value::String(column) = &arguments[1] {
|
||||||
|
column_names.push(column.clone());
|
||||||
|
} else {
|
||||||
|
todo!()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Value::Map(map) = collection {
|
||||||
|
let mut selected = VariableMap::new();
|
||||||
|
|
||||||
|
for (key, value) in map.inner() {
|
||||||
|
if column_names.contains(key) {
|
||||||
|
selected.set_value(key, value.clone())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(Value::Map(selected));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Value::Table(table) = collection {
|
||||||
|
let selected = table.select(&column_names);
|
||||||
|
|
||||||
|
return Ok(Value::Table(selected));
|
||||||
|
}
|
||||||
|
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ForEach;
|
||||||
|
|
||||||
|
impl Macro for ForEach {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "for_each",
|
||||||
|
description: "Run an operation on every item in a collection.",
|
||||||
|
group: "collections",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let argument = argument.as_list()?;
|
||||||
|
|
||||||
|
Error::expected_minimum_function_argument_amount(
|
||||||
|
self.info().identifier,
|
||||||
|
2,
|
||||||
|
argument.len(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let table = argument[0].as_table()?;
|
||||||
|
let columns = argument[1].as_list()?;
|
||||||
|
let mut column_names = Vec::new();
|
||||||
|
|
||||||
|
for column in columns {
|
||||||
|
let name = column.as_string()?;
|
||||||
|
|
||||||
|
column_names.push(name.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let selected = table.select(&column_names);
|
||||||
|
|
||||||
|
Ok(Value::Table(selected))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Where;
|
||||||
|
|
||||||
|
impl Macro for Where {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "where",
|
||||||
|
description: "Keep rows matching a predicate.",
|
||||||
|
group: "collections",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let argument_list = argument.as_list()?;
|
||||||
|
Error::expect_function_argument_amount(self.info().identifier, argument_list.len(), 2)?;
|
||||||
|
|
||||||
|
let collection = &argument_list[0];
|
||||||
|
let function = argument_list[1].as_function()?;
|
||||||
|
|
||||||
|
if let Ok(list) = collection.as_list() {
|
||||||
|
let mut context = VariableMap::new();
|
||||||
|
let mut new_list = Vec::new();
|
||||||
|
|
||||||
|
for value in list {
|
||||||
|
context.set_value("input", value.clone())?;
|
||||||
|
let keep_row = function.run_with_context(&mut context)?.as_boolean()?;
|
||||||
|
|
||||||
|
if keep_row {
|
||||||
|
new_list.push(value.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(Value::List(new_list));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(map) = collection.as_map() {
|
||||||
|
let mut context = VariableMap::new();
|
||||||
|
let mut new_map = VariableMap::new();
|
||||||
|
|
||||||
|
for (key, value) in map.inner() {
|
||||||
|
if let Ok(map) = value.as_map() {
|
||||||
|
for (key, value) in map.inner() {
|
||||||
|
context.set_value(key, value.clone())?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
context.set_value("input", value.clone())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let keep_row = function.run_with_context(&mut context)?.as_boolean()?;
|
||||||
|
|
||||||
|
if keep_row {
|
||||||
|
new_map.set_value(key, value.clone())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(Value::Map(new_map));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(table) = collection.as_table() {
|
||||||
|
let mut context = VariableMap::new();
|
||||||
|
let mut new_table = Table::new(table.column_names().clone());
|
||||||
|
|
||||||
|
for row in table.rows() {
|
||||||
|
for (column_index, cell) in row.iter().enumerate() {
|
||||||
|
let column_name = table.column_names().get(column_index).unwrap();
|
||||||
|
|
||||||
|
context.set_value(column_name, cell.clone())?;
|
||||||
|
}
|
||||||
|
let keep_row = function.run_with_context(&mut context)?.as_boolean()?;
|
||||||
|
|
||||||
|
if keep_row {
|
||||||
|
new_table.insert(row.clone())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(Value::Table(new_table));
|
||||||
|
}
|
||||||
|
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::Function;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn where_from_non_collections() {
|
||||||
|
Where
|
||||||
|
.run(&Value::List(vec![
|
||||||
|
Value::Integer(1),
|
||||||
|
Value::Function(Function::new("input == 1")),
|
||||||
|
]))
|
||||||
|
.unwrap_err();
|
||||||
|
Where
|
||||||
|
.run(&Value::List(vec![
|
||||||
|
Value::Float(1.0),
|
||||||
|
Value::Function(Function::new("input == 1.0")),
|
||||||
|
]))
|
||||||
|
.unwrap_err();
|
||||||
|
Where
|
||||||
|
.run(&Value::List(vec![
|
||||||
|
Value::Boolean(true),
|
||||||
|
Value::Function(Function::new("input == true")),
|
||||||
|
]))
|
||||||
|
.unwrap_err();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn where_from_list() {
|
||||||
|
let arguments = Value::List(vec![
|
||||||
|
Value::List(vec![Value::Integer(1), Value::Integer(2)]),
|
||||||
|
Value::Function(Function::new("input == 1")),
|
||||||
|
]);
|
||||||
|
let select = Where.run(&arguments).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(Value::List(vec![Value::Integer(1)]), select);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn where_from_map() {
|
||||||
|
let mut map = VariableMap::new();
|
||||||
|
|
||||||
|
map.set_value("foo", Value::Integer(1)).unwrap();
|
||||||
|
map.set_value("bar", Value::Integer(2)).unwrap();
|
||||||
|
|
||||||
|
let arguments = Value::List(vec![
|
||||||
|
Value::Map(map),
|
||||||
|
Value::Function(Function::new("input == 1")),
|
||||||
|
]);
|
||||||
|
let select = Where.run(&arguments).unwrap();
|
||||||
|
|
||||||
|
let mut map = VariableMap::new();
|
||||||
|
|
||||||
|
map.set_value("foo", Value::Integer(1)).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(Value::Map(map), select);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn where_from_table() {
|
||||||
|
let mut table = Table::new(vec!["foo".to_string(), "bar".to_string()]);
|
||||||
|
|
||||||
|
table
|
||||||
|
.insert(vec![Value::Integer(1), Value::Integer(2)])
|
||||||
|
.unwrap();
|
||||||
|
table
|
||||||
|
.insert(vec![Value::Integer(3), Value::Integer(4)])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let arguments = Value::List(vec![
|
||||||
|
Value::Table(table),
|
||||||
|
Value::Function(Function::new("foo == 1")),
|
||||||
|
]);
|
||||||
|
let select = Where.run(&arguments).unwrap();
|
||||||
|
let mut table = Table::new(vec!["foo".to_string(), "bar".to_string()]);
|
||||||
|
|
||||||
|
table
|
||||||
|
.insert(vec![Value::Integer(1), Value::Integer(2)])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(Value::Table(table), select);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn select_from_non_collections() {
|
||||||
|
Select
|
||||||
|
.run(&Value::List(vec![Value::Integer(1), Value::Integer(1)]))
|
||||||
|
.unwrap_err();
|
||||||
|
Select
|
||||||
|
.run(&Value::List(vec![Value::Float(1.0), Value::Float(1.0)]))
|
||||||
|
.unwrap_err();
|
||||||
|
Select
|
||||||
|
.run(&Value::List(vec![
|
||||||
|
Value::Boolean(true),
|
||||||
|
Value::Boolean(true),
|
||||||
|
]))
|
||||||
|
.unwrap_err();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn select_from_list() {
|
||||||
|
let arguments = Value::List(vec![
|
||||||
|
Value::List(vec![Value::Integer(1), Value::Integer(2)]),
|
||||||
|
Value::Integer(0),
|
||||||
|
]);
|
||||||
|
let select = Select.run(&arguments).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(Value::List(vec![Value::Integer(1)]), select);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn select_from_map() {
|
||||||
|
let mut map = VariableMap::new();
|
||||||
|
|
||||||
|
map.set_value("foo", Value::Integer(1)).unwrap();
|
||||||
|
map.set_value("bar", Value::Integer(2)).unwrap();
|
||||||
|
|
||||||
|
let arguments = Value::List(vec![Value::Map(map), Value::String("foo".to_string())]);
|
||||||
|
let select = Select.run(&arguments).unwrap();
|
||||||
|
|
||||||
|
let mut map = VariableMap::new();
|
||||||
|
|
||||||
|
map.set_value("foo", Value::Integer(1)).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(Value::Map(map), select);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn select_from_table() {
|
||||||
|
let mut table = Table::new(vec!["foo".to_string(), "bar".to_string()]);
|
||||||
|
|
||||||
|
table
|
||||||
|
.insert(vec![Value::Integer(1), Value::Integer(2)])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let arguments = Value::List(vec![Value::Table(table), Value::String("foo".to_string())]);
|
||||||
|
let select = Select.run(&arguments).unwrap();
|
||||||
|
|
||||||
|
let mut table = Table::new(vec!["foo".to_string()]);
|
||||||
|
|
||||||
|
table.insert(vec![Value::Integer(1)]).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(Value::Table(table), select);
|
||||||
|
}
|
||||||
|
}
|
119
src/macros/command.rs
Normal file
119
src/macros/command.rs
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use crate::{Macro, MacroInfo, Result, Value};
|
||||||
|
|
||||||
|
pub struct Sh;
|
||||||
|
|
||||||
|
impl Macro for Sh {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "sh",
|
||||||
|
description: "Pass input to the Bourne Shell.",
|
||||||
|
group: "command",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let argument = argument.as_string()?;
|
||||||
|
|
||||||
|
Command::new("sh").arg("-c").arg(argument).spawn()?.wait()?;
|
||||||
|
|
||||||
|
Ok(Value::Empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Bash;
|
||||||
|
|
||||||
|
impl Macro for Bash {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "bash",
|
||||||
|
description: "Pass input to the Bourne Again Shell.",
|
||||||
|
group: "command",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let argument = argument.as_string()?;
|
||||||
|
|
||||||
|
Command::new("bash")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(argument)
|
||||||
|
.spawn()?
|
||||||
|
.wait()?;
|
||||||
|
|
||||||
|
Ok(Value::Empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub struct Fish;
|
||||||
|
|
||||||
|
impl Macro for Fish {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "fish",
|
||||||
|
description: "Pass input to the fish shell.",
|
||||||
|
group: "command",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let argument = argument.as_string()?;
|
||||||
|
|
||||||
|
Command::new("fish")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(argument)
|
||||||
|
.spawn()?
|
||||||
|
.wait()?;
|
||||||
|
|
||||||
|
Ok(Value::Empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Zsh;
|
||||||
|
|
||||||
|
impl Macro for Zsh {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "zsh",
|
||||||
|
description: "Pass input to the Z shell.",
|
||||||
|
group: "command",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let argument = argument.as_string()?;
|
||||||
|
|
||||||
|
Command::new("zsh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(argument)
|
||||||
|
.spawn()?
|
||||||
|
.wait()?;
|
||||||
|
|
||||||
|
Ok(Value::Empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Raw;
|
||||||
|
|
||||||
|
impl Macro for Raw {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "raw",
|
||||||
|
description: "Run input as a command without a shell",
|
||||||
|
group: "command",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let argument = argument.as_string()?;
|
||||||
|
|
||||||
|
Command::new(argument).spawn()?.wait()?;
|
||||||
|
|
||||||
|
Ok(Value::Empty)
|
||||||
|
}
|
||||||
|
}
|
151
src/macros/data_formats.rs
Normal file
151
src/macros/data_formats.rs
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
//! Convert values to and from data formats like JSON and TOML.
|
||||||
|
|
||||||
|
use crate::{Macro, MacroInfo, Result, Table, Value};
|
||||||
|
|
||||||
|
pub struct FromJson;
|
||||||
|
|
||||||
|
impl Macro for FromJson {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "from_json",
|
||||||
|
description: "Get a whale value from a JSON string.",
|
||||||
|
group: "data",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let argument = argument.as_string()?;
|
||||||
|
let value = serde_json::from_str(argument)?;
|
||||||
|
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ToJson;
|
||||||
|
|
||||||
|
impl Macro for ToJson {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "to_json",
|
||||||
|
description: "Create a JSON string from a whale value.",
|
||||||
|
group: "data",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let json = serde_json::to_string(argument)?;
|
||||||
|
|
||||||
|
Ok(Value::String(json))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FromCsv;
|
||||||
|
|
||||||
|
impl Macro for FromCsv {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "from_csv",
|
||||||
|
description: "Create a whale value from a CSV string.",
|
||||||
|
group: "data",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let csv = argument.as_string()?;
|
||||||
|
let mut reader = csv::Reader::from_reader(csv.as_bytes());
|
||||||
|
|
||||||
|
let headers = reader
|
||||||
|
.headers()?
|
||||||
|
.iter()
|
||||||
|
.map(|header| header.trim().trim_matches('"').to_string())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut table = Table::new(headers);
|
||||||
|
|
||||||
|
for result in reader.records() {
|
||||||
|
let row = result?
|
||||||
|
.iter()
|
||||||
|
.map(|column| {
|
||||||
|
let column = column.trim().trim_matches('"').trim_matches('\'');
|
||||||
|
|
||||||
|
if let Ok(integer) = column.parse::<i64>() {
|
||||||
|
Value::Integer(integer)
|
||||||
|
} else if let Ok(float) = column.parse::<f64>() {
|
||||||
|
Value::Float(float)
|
||||||
|
} else {
|
||||||
|
Value::String(column.to_string())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
table.insert(row)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::Table(table))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ToCsv;
|
||||||
|
|
||||||
|
impl Macro for ToCsv {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "to_csv",
|
||||||
|
description: "Convert a value to a string of comma-separated values.",
|
||||||
|
group: "data",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
let mut writer = csv::Writer::from_writer(&mut buffer);
|
||||||
|
|
||||||
|
match argument {
|
||||||
|
Value::String(string) => {
|
||||||
|
writer.write_record([string])?;
|
||||||
|
}
|
||||||
|
Value::Float(float) => {
|
||||||
|
writer.write_record(&[float.to_string()])?;
|
||||||
|
}
|
||||||
|
Value::Integer(integer) => {
|
||||||
|
writer.write_record(&[integer.to_string()])?;
|
||||||
|
}
|
||||||
|
Value::Boolean(boolean) => {
|
||||||
|
writer.write_record(&[boolean.to_string()])?;
|
||||||
|
}
|
||||||
|
Value::List(list) => {
|
||||||
|
let string_list = list.iter().map(|value| value.to_string());
|
||||||
|
|
||||||
|
writer.write_record(string_list)?;
|
||||||
|
}
|
||||||
|
Value::Empty => {}
|
||||||
|
Value::Map(map) => {
|
||||||
|
writer.write_record(map.inner().keys())?;
|
||||||
|
writer.write_record(map.inner().values().map(|value| value.to_string()))?;
|
||||||
|
}
|
||||||
|
Value::Table(table) => {
|
||||||
|
writer.write_record(table.column_names())?;
|
||||||
|
|
||||||
|
for row in table.rows() {
|
||||||
|
let row_string = row.iter().map(|value| value.to_string());
|
||||||
|
|
||||||
|
writer.write_record(row_string)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::Function(_) => todo!(),
|
||||||
|
Value::Time(time) => {
|
||||||
|
writer.write_record(&[time.to_string()])?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.flush()?;
|
||||||
|
|
||||||
|
Ok(Value::String(
|
||||||
|
String::from_utf8_lossy(writer.get_ref()).to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
123
src/macros/disks.rs
Normal file
123
src/macros/disks.rs
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use sysinfo::{DiskExt, System, SystemExt};
|
||||||
|
|
||||||
|
use crate::{Macro, MacroInfo, Result, Table, Value};
|
||||||
|
|
||||||
|
pub struct ListDisks;
|
||||||
|
|
||||||
|
impl Macro for ListDisks {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "list_disks",
|
||||||
|
description: "List all block devices.",
|
||||||
|
group: "disks",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
argument.as_empty()?;
|
||||||
|
|
||||||
|
let mut sys = System::new_all();
|
||||||
|
sys.refresh_all();
|
||||||
|
|
||||||
|
let mut disk_table = Table::new(vec![
|
||||||
|
"name".to_string(),
|
||||||
|
"kind".to_string(),
|
||||||
|
"file system".to_string(),
|
||||||
|
"mount point".to_string(),
|
||||||
|
"total space".to_string(),
|
||||||
|
"available space".to_string(),
|
||||||
|
"is removable".to_string(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
for disk in sys.disks() {
|
||||||
|
let name = disk.name().to_string_lossy().to_string();
|
||||||
|
let kind = disk.kind();
|
||||||
|
let file_system = String::from_utf8_lossy(disk.file_system()).to_string();
|
||||||
|
let mount_point = disk.mount_point().to_str().unwrap().to_string();
|
||||||
|
let total_space = disk.total_space() as i64;
|
||||||
|
let available_space = disk.available_space() as i64;
|
||||||
|
let is_removable = disk.is_removable();
|
||||||
|
|
||||||
|
let row = vec![
|
||||||
|
Value::String(name),
|
||||||
|
Value::String(format!("{kind:?}")),
|
||||||
|
Value::String(file_system),
|
||||||
|
Value::String(mount_point),
|
||||||
|
Value::Integer(total_space),
|
||||||
|
Value::Integer(available_space),
|
||||||
|
Value::Boolean(is_removable),
|
||||||
|
];
|
||||||
|
|
||||||
|
disk_table.insert(row)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::Table(disk_table))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Partition;
|
||||||
|
|
||||||
|
impl Macro for Partition {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "partition",
|
||||||
|
description: "Partition a disk, clearing its content.",
|
||||||
|
group: "disks",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let argument = argument.as_map()?;
|
||||||
|
let path = argument
|
||||||
|
.get_value("path")?
|
||||||
|
.unwrap_or(Value::Empty)
|
||||||
|
.as_string()?
|
||||||
|
.clone();
|
||||||
|
let label = argument
|
||||||
|
.get_value("label")?
|
||||||
|
.unwrap_or(Value::Empty)
|
||||||
|
.as_string()?
|
||||||
|
.clone();
|
||||||
|
let name = argument
|
||||||
|
.get_value("name")?
|
||||||
|
.unwrap_or(Value::Empty)
|
||||||
|
.as_string()?
|
||||||
|
.clone();
|
||||||
|
let filesystem = argument
|
||||||
|
.get_value("filesystem")?
|
||||||
|
.unwrap_or(Value::Empty)
|
||||||
|
.as_string()?
|
||||||
|
.clone();
|
||||||
|
let range = argument
|
||||||
|
.get_value("range")?
|
||||||
|
.unwrap_or(Value::Empty)
|
||||||
|
.as_list()?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
if range.len() != 2 {
|
||||||
|
return Err(crate::Error::ExpectedFixedLenList {
|
||||||
|
expected_len: 2,
|
||||||
|
actual: Value::List(range),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let range_start = range[0].as_string()?;
|
||||||
|
let range_end = range[1].as_string()?;
|
||||||
|
|
||||||
|
let script = format!(
|
||||||
|
"sudo parted {path} mklabel {label} mkpart {name} {filesystem} {range_start} {range_end}"
|
||||||
|
);
|
||||||
|
|
||||||
|
Command::new("fish")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(&script)
|
||||||
|
.spawn()?
|
||||||
|
.wait()?;
|
||||||
|
|
||||||
|
Ok(Value::Empty)
|
||||||
|
}
|
||||||
|
}
|
454
src/macros/filesystem.rs
Normal file
454
src/macros/filesystem.rs
Normal file
@ -0,0 +1,454 @@
|
|||||||
|
//! Tools for files and directories.
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
fs::{self, OpenOptions},
|
||||||
|
io::{Read, Write as IoWrite},
|
||||||
|
path::PathBuf,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{Error, Macro, MacroInfo, Result, Table, Time, Value, ValueType};
|
||||||
|
|
||||||
|
pub struct Append;
|
||||||
|
|
||||||
|
impl Macro for Append {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "append",
|
||||||
|
description: "Append data to a file.",
|
||||||
|
group: "filesystem",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let arguments = argument.as_fixed_len_list(2)?;
|
||||||
|
let path = arguments[0].as_string()?;
|
||||||
|
let content = arguments[1].as_string()?;
|
||||||
|
let mut file = OpenOptions::new().append(true).open(path)?;
|
||||||
|
|
||||||
|
file.write_all(content.as_bytes())?;
|
||||||
|
|
||||||
|
Ok(Value::Empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CreateDir;
|
||||||
|
|
||||||
|
impl Macro for CreateDir {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "create_dir",
|
||||||
|
description: "Create one or more directories.",
|
||||||
|
group: "filesystem",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let path = argument.as_string()?;
|
||||||
|
fs::create_dir_all(path)?;
|
||||||
|
|
||||||
|
Ok(Value::Empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FileMetadata;
|
||||||
|
|
||||||
|
impl Macro for FileMetadata {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "file_metadata",
|
||||||
|
description: "Get metadata for files.",
|
||||||
|
group: "filesystem",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let path_string = argument.as_string()?;
|
||||||
|
let metadata = PathBuf::from(path_string).metadata()?;
|
||||||
|
let created = metadata.accessed()?.elapsed()?.as_secs() / 60;
|
||||||
|
let accessed = metadata.accessed()?.elapsed()?.as_secs() / 60;
|
||||||
|
let modified = metadata.modified()?.elapsed()?.as_secs() / 60;
|
||||||
|
let read_only = metadata.permissions().readonly();
|
||||||
|
let size = metadata.len();
|
||||||
|
|
||||||
|
let mut file_table = Table::new(vec![
|
||||||
|
"path".to_string(),
|
||||||
|
"size".to_string(),
|
||||||
|
"created".to_string(),
|
||||||
|
"accessed".to_string(),
|
||||||
|
"modified".to_string(),
|
||||||
|
"read only".to_string(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
file_table.insert(vec![
|
||||||
|
Value::String(path_string.clone()),
|
||||||
|
Value::Integer(size as i64),
|
||||||
|
Value::Integer(created as i64),
|
||||||
|
Value::Integer(accessed as i64),
|
||||||
|
Value::Integer(modified as i64),
|
||||||
|
Value::Boolean(read_only),
|
||||||
|
])?;
|
||||||
|
|
||||||
|
Ok(Value::Table(file_table))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ReadDir;
|
||||||
|
|
||||||
|
impl Macro for ReadDir {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "read_dir",
|
||||||
|
description: "Read the content of a directory.",
|
||||||
|
group: "filesystem",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let path = if let Ok(path) = argument.as_string() {
|
||||||
|
path
|
||||||
|
} else if argument.is_empty() {
|
||||||
|
"."
|
||||||
|
} else {
|
||||||
|
return Err(Error::TypeError {
|
||||||
|
expected: &[ValueType::Empty, ValueType::String],
|
||||||
|
actual: argument.clone(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
let dir = fs::read_dir(path)?;
|
||||||
|
let mut file_table = Table::new(vec![
|
||||||
|
"path".to_string(),
|
||||||
|
"size".to_string(),
|
||||||
|
"created".to_string(),
|
||||||
|
"accessed".to_string(),
|
||||||
|
"modified".to_string(),
|
||||||
|
"read only".to_string(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
for entry in dir {
|
||||||
|
let entry = entry?;
|
||||||
|
let file_type = entry.file_type()?;
|
||||||
|
let file_name = if file_type.is_dir() {
|
||||||
|
let name = entry.file_name().into_string().unwrap_or_default();
|
||||||
|
|
||||||
|
format!("{name}/")
|
||||||
|
} else {
|
||||||
|
entry.file_name().into_string().unwrap_or_default()
|
||||||
|
};
|
||||||
|
let metadata = entry.path().metadata()?;
|
||||||
|
|
||||||
|
let created_timestamp = metadata.accessed()?;
|
||||||
|
let created = Time::from(created_timestamp);
|
||||||
|
|
||||||
|
let accessed_timestamp = metadata.accessed()?;
|
||||||
|
let accessed = Time::from(accessed_timestamp);
|
||||||
|
|
||||||
|
let modified_timestamp = metadata.modified()?;
|
||||||
|
let modified = Time::from(modified_timestamp);
|
||||||
|
|
||||||
|
let read_only = metadata.permissions().readonly();
|
||||||
|
let size = metadata.len();
|
||||||
|
|
||||||
|
file_table.insert(vec![
|
||||||
|
Value::String(file_name),
|
||||||
|
Value::Integer(size as i64),
|
||||||
|
Value::Time(created),
|
||||||
|
Value::Time(accessed),
|
||||||
|
Value::Time(modified),
|
||||||
|
Value::Boolean(read_only),
|
||||||
|
])?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::Table(file_table))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ReadFile;
|
||||||
|
|
||||||
|
impl Macro for ReadFile {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "read_file",
|
||||||
|
description: "Read file contents.",
|
||||||
|
group: "filesystem",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let path = argument.as_string()?;
|
||||||
|
let mut contents = String::new();
|
||||||
|
|
||||||
|
OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.create(false)
|
||||||
|
.open(path)?
|
||||||
|
.read_to_string(&mut contents)?;
|
||||||
|
|
||||||
|
Ok(Value::String(contents))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RemoveDir;
|
||||||
|
|
||||||
|
impl Macro for RemoveDir {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "remove_dir",
|
||||||
|
description: "Remove directories.",
|
||||||
|
group: "filesystem",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let path = argument.as_string()?;
|
||||||
|
fs::remove_file(path)?;
|
||||||
|
|
||||||
|
Ok(Value::Empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MoveDir;
|
||||||
|
|
||||||
|
impl Macro for MoveDir {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "move_dir",
|
||||||
|
description: "Move a directory to a new path.",
|
||||||
|
group: "filesystem",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let argument = argument.as_list()?;
|
||||||
|
|
||||||
|
Error::expect_function_argument_amount(self.info().identifier, argument.len(), 2)?;
|
||||||
|
|
||||||
|
let current_path = argument[0].as_string()?;
|
||||||
|
let target_path = argument[1].as_string()?;
|
||||||
|
let file_list = ReadDir.run(&Value::String(current_path.clone()))?;
|
||||||
|
|
||||||
|
for path in file_list.as_list()? {
|
||||||
|
let path = PathBuf::from(path.as_string()?);
|
||||||
|
let new_path = PathBuf::from(&target_path).join(&path);
|
||||||
|
|
||||||
|
if path.is_file() {
|
||||||
|
fs::copy(&path, target_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if path.is_symlink() && path.symlink_metadata()?.is_file() {
|
||||||
|
fs::copy(&path, new_path)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::Empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Trash;
|
||||||
|
|
||||||
|
impl Macro for Trash {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "trash",
|
||||||
|
description: "Move a file or directory to the trash.",
|
||||||
|
group: "filesystem",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let path = argument.as_string()?;
|
||||||
|
|
||||||
|
trash::delete(path)?;
|
||||||
|
|
||||||
|
Ok(Value::Empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Write;
|
||||||
|
|
||||||
|
impl Macro for Write {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "write",
|
||||||
|
description: "Write data to a file.",
|
||||||
|
group: "filesystem",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let strings = argument.as_list()?;
|
||||||
|
|
||||||
|
Error::expect_function_argument_amount(self.info().identifier, strings.len(), 2)?;
|
||||||
|
|
||||||
|
let path = strings.first().unwrap().as_string()?;
|
||||||
|
let mut file = OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
|
.open(path)?;
|
||||||
|
|
||||||
|
for content in &strings[1..] {
|
||||||
|
let content = content.to_string();
|
||||||
|
|
||||||
|
file.write_all(content.as_bytes())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::Empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RemoveFile;
|
||||||
|
|
||||||
|
impl Macro for RemoveFile {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "write",
|
||||||
|
description: "Write data to a file.",
|
||||||
|
group: "filesystem",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let strings = argument.as_list()?;
|
||||||
|
|
||||||
|
Error::expect_function_argument_amount(self.info().identifier, strings.len(), 2)?;
|
||||||
|
|
||||||
|
let _path = strings.first().unwrap().as_string()?;
|
||||||
|
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Watch;
|
||||||
|
|
||||||
|
impl Macro for Watch {
|
||||||
|
fn info(&self) -> crate::MacroInfo<'static> {
|
||||||
|
crate::MacroInfo {
|
||||||
|
identifier: "watch",
|
||||||
|
description: "Wait until a file changes.",
|
||||||
|
group: "filesystem",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let argument = argument.as_string()?;
|
||||||
|
let path = PathBuf::from(argument);
|
||||||
|
let modified_old = path.metadata()?.modified()?;
|
||||||
|
let wait_time = loop {
|
||||||
|
let modified_new = path.metadata()?.modified()?;
|
||||||
|
|
||||||
|
if modified_old != modified_new {
|
||||||
|
break modified_new
|
||||||
|
.duration_since(modified_old)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_millis() as i64;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Value::Integer(wait_time))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_dir() {
|
||||||
|
let path = PathBuf::from("./target/create_dir/");
|
||||||
|
let path_value = Value::String(path.to_string_lossy().to_string());
|
||||||
|
let _ = std::fs::remove_file(&path);
|
||||||
|
|
||||||
|
CreateDir.run(&path_value).unwrap();
|
||||||
|
|
||||||
|
assert!(path.is_dir());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_dir_nested() {
|
||||||
|
let path = PathBuf::from("./target/create_dir/nested");
|
||||||
|
let path_value = Value::String(path.to_string_lossy().to_string());
|
||||||
|
let _ = std::fs::remove_file(&path);
|
||||||
|
|
||||||
|
CreateDir.run(&path_value).unwrap();
|
||||||
|
|
||||||
|
assert!(path.is_dir());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write() {
|
||||||
|
let path = PathBuf::from("./target/write.txt");
|
||||||
|
let path_value = Value::String(path.to_string_lossy().to_string());
|
||||||
|
let message = "hiya".to_string();
|
||||||
|
let message_value = Value::String(message.clone());
|
||||||
|
let _ = std::fs::remove_file(&path);
|
||||||
|
|
||||||
|
Write
|
||||||
|
.run(&Value::List(vec![path_value, message_value]))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(path.is_file());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn append() {
|
||||||
|
let path = PathBuf::from("./target/append.txt");
|
||||||
|
let path_value = Value::String(path.to_string_lossy().to_string());
|
||||||
|
let message = "hiya".to_string();
|
||||||
|
let message_value = Value::String(message.clone());
|
||||||
|
let _ = std::fs::remove_file(&path);
|
||||||
|
|
||||||
|
Write
|
||||||
|
.run(&Value::List(vec![
|
||||||
|
path_value.clone(),
|
||||||
|
message_value.clone(),
|
||||||
|
]))
|
||||||
|
.unwrap();
|
||||||
|
Append
|
||||||
|
.run(&Value::List(vec![path_value, message_value]))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let read = fs::read_to_string(&path).unwrap();
|
||||||
|
|
||||||
|
assert_eq!("hiyahiya", read);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_file() {
|
||||||
|
let path = PathBuf::from("./target/read_file.txt");
|
||||||
|
let path_value = Value::String(path.to_string_lossy().to_string());
|
||||||
|
let message = "hiya".to_string();
|
||||||
|
let message_value = Value::String(message.clone());
|
||||||
|
let _ = std::fs::remove_file(&path);
|
||||||
|
|
||||||
|
Write
|
||||||
|
.run(&Value::List(vec![path_value.clone(), message_value]))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let test = ReadFile.run(&path_value).unwrap();
|
||||||
|
let read = fs::read_to_string(&path).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(test, Value::String(read));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remove_file() {
|
||||||
|
let path = PathBuf::from("./target/remove_file.txt");
|
||||||
|
let path_value = Value::String(path.to_string_lossy().to_string());
|
||||||
|
let _ = std::fs::File::create(&path);
|
||||||
|
|
||||||
|
RemoveFile.run(&path_value).unwrap();
|
||||||
|
|
||||||
|
assert!(!path.exists());
|
||||||
|
}
|
||||||
|
}
|
126
src/macros/general.rs
Normal file
126
src/macros/general.rs
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
use std::{fs, thread::sleep, time::Duration};
|
||||||
|
|
||||||
|
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
|
||||||
|
|
||||||
|
use crate::{Function, Macro, MacroInfo, Result, Value};
|
||||||
|
|
||||||
|
pub struct Output;
|
||||||
|
|
||||||
|
impl Macro for Output {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "output",
|
||||||
|
description: "Print a value.",
|
||||||
|
group: "general",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
println!("{argument}");
|
||||||
|
|
||||||
|
Ok(Value::Empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub struct Repeat;
|
||||||
|
|
||||||
|
impl Macro for Repeat {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "repeat",
|
||||||
|
description: "Run a function the given number of times.",
|
||||||
|
group: "general",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let argument = argument.as_list()?;
|
||||||
|
let function = argument[0].as_function()?;
|
||||||
|
let count = argument[1].as_int()?;
|
||||||
|
let mut result_list = Vec::with_capacity(count as usize);
|
||||||
|
|
||||||
|
for _ in 0..count {
|
||||||
|
let result = function.run()?;
|
||||||
|
|
||||||
|
result_list.push(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::List(result_list))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Run;
|
||||||
|
|
||||||
|
impl Macro for Run {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "run",
|
||||||
|
description: "Run a whale file.",
|
||||||
|
group: "general",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let path = argument.as_string()?;
|
||||||
|
let file_contents = fs::read_to_string(path)?;
|
||||||
|
|
||||||
|
Function::new(&file_contents).run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Async;
|
||||||
|
|
||||||
|
impl Macro for Async {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "async",
|
||||||
|
description: "Run functions in parallel.",
|
||||||
|
group: "general",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let argument_list = argument.as_list()?;
|
||||||
|
let results = argument_list
|
||||||
|
.par_iter()
|
||||||
|
.map(|value| {
|
||||||
|
let function = if let Ok(function) = value.as_function() {
|
||||||
|
function
|
||||||
|
} else {
|
||||||
|
return value.clone();
|
||||||
|
};
|
||||||
|
|
||||||
|
match function.run() {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => Value::String(error.to_string()),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(Value::List(results))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Wait;
|
||||||
|
|
||||||
|
impl Macro for Wait {
|
||||||
|
fn info(&self) -> crate::MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "wait",
|
||||||
|
description: "Wait for the given number of milliseconds.",
|
||||||
|
group: "general",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let argument = argument.as_int()?;
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(argument as u64));
|
||||||
|
|
||||||
|
Ok(Value::Empty)
|
||||||
|
}
|
||||||
|
}
|
375
src/macros/gui.rs
Normal file
375
src/macros/gui.rs
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
use eframe::{
|
||||||
|
egui::{
|
||||||
|
plot::{Bar, BarChart, Line, Plot as EguiPlot, PlotPoints},
|
||||||
|
CentralPanel, Context, Direction, Layout, RichText, ScrollArea, Ui,
|
||||||
|
},
|
||||||
|
emath::Align,
|
||||||
|
epaint::{Color32, Stroke},
|
||||||
|
run_native, NativeOptions,
|
||||||
|
};
|
||||||
|
use egui_extras::{Column, StripBuilder, TableBuilder};
|
||||||
|
|
||||||
|
use crate::{eval_with_context, Error, Macro, MacroInfo, Result, Table, Value, VariableMap};
|
||||||
|
|
||||||
|
pub struct CreateLine;
|
||||||
|
|
||||||
|
impl Macro for CreateLine {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "create_line",
|
||||||
|
description: "Create a map value to be shown as a line in a plot.",
|
||||||
|
group: "gui",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let argument = argument.as_list()?;
|
||||||
|
let index = argument[0].as_int()?.clone();
|
||||||
|
let height = argument[1].as_float()?.clone();
|
||||||
|
let name = argument[2].clone();
|
||||||
|
let mut line = VariableMap::new();
|
||||||
|
|
||||||
|
line.set_value("index", Value::Float(index as f64))?;
|
||||||
|
line.set_value("height", Value::Float(height))?;
|
||||||
|
line.set_value("name", name)?;
|
||||||
|
|
||||||
|
Ok(Value::Map(line))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BarGraph;
|
||||||
|
|
||||||
|
impl Macro for BarGraph {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "bar_graph",
|
||||||
|
description: "Render a list of values as a bar graph.",
|
||||||
|
group: "gui",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let argument = argument.as_list()?;
|
||||||
|
let mut bars = Vec::new();
|
||||||
|
|
||||||
|
for (index, value) in argument.into_iter().enumerate() {
|
||||||
|
let list = value.as_list()?;
|
||||||
|
let mut name = None;
|
||||||
|
let mut height = None;
|
||||||
|
|
||||||
|
for value in list {
|
||||||
|
match value {
|
||||||
|
Value::Float(float) => {
|
||||||
|
if height.is_none() {
|
||||||
|
height = Some(float);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::Integer(integer) => {}
|
||||||
|
Value::String(string) => name = Some(string),
|
||||||
|
Value::Boolean(_)
|
||||||
|
| Value::List(_)
|
||||||
|
| Value::Map(_)
|
||||||
|
| Value::Table(_)
|
||||||
|
| Value::Time(_)
|
||||||
|
| Value::Function(_)
|
||||||
|
| Value::Empty => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let height =
|
||||||
|
match height {
|
||||||
|
Some(height) => *height,
|
||||||
|
None => return Err(Error::CustomMessage(
|
||||||
|
"Could not create bar graph. No float value was found to use as a height."
|
||||||
|
.to_string(),
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let bar = Bar::new(index as f64, height).name(name.unwrap_or(&"".to_string()));
|
||||||
|
|
||||||
|
bars.push(bar);
|
||||||
|
}
|
||||||
|
|
||||||
|
run_native(
|
||||||
|
"bar_graph",
|
||||||
|
NativeOptions::default(),
|
||||||
|
Box::new(|_cc| Box::new(BarGraphGui::new(bars))),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Ok(Value::Empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BarGraphGui {
|
||||||
|
bars: Vec<Bar>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BarGraphGui {
|
||||||
|
fn new(data: Vec<Bar>) -> Self {
|
||||||
|
Self { bars: data }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl eframe::App for BarGraphGui {
|
||||||
|
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
|
||||||
|
CentralPanel::default().show(ctx, |ui| {
|
||||||
|
EguiPlot::new("bar_graph").show(ui, |plot_ui| {
|
||||||
|
plot_ui.bar_chart(BarChart::new(self.bars.clone()).color(Color32::RED));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Plot;
|
||||||
|
|
||||||
|
impl Macro for Plot {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "plot",
|
||||||
|
description: "Render a list of numbers as a scatter plot graph.",
|
||||||
|
group: "gui",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let argument = argument.as_list()?;
|
||||||
|
let mut floats = Vec::new();
|
||||||
|
|
||||||
|
for value in argument {
|
||||||
|
if let Ok(float) = value.as_float() {
|
||||||
|
floats.push(float);
|
||||||
|
} else if let Ok(integer) = value.as_int() {
|
||||||
|
floats.push(integer as f64);
|
||||||
|
} else {
|
||||||
|
return Err(Error::expected_number(value.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run_native(
|
||||||
|
"plot",
|
||||||
|
NativeOptions {
|
||||||
|
resizable: true,
|
||||||
|
centered: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Box::new(|_cc| Box::new(PlotGui::new(floats))),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Ok(Value::Empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PlotGui {
|
||||||
|
data: Vec<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlotGui {
|
||||||
|
fn new(data: Vec<f64>) -> Self {
|
||||||
|
Self { data }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl eframe::App for PlotGui {
|
||||||
|
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
|
||||||
|
CentralPanel::default().show(ctx, |ui| {
|
||||||
|
EguiPlot::new("plot").show(ui, |plot_ui| {
|
||||||
|
let points = self
|
||||||
|
.data
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, value)| [index as f64, *value])
|
||||||
|
.collect::<PlotPoints>();
|
||||||
|
let line = Line::new(points);
|
||||||
|
plot_ui.line(line);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Gui;
|
||||||
|
|
||||||
|
impl Macro for Gui {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "gui",
|
||||||
|
description: "Display a value in a GUI window.",
|
||||||
|
group: "gui",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let argument = argument.clone();
|
||||||
|
|
||||||
|
run_native(
|
||||||
|
&argument.value_type().to_string(),
|
||||||
|
NativeOptions::default(),
|
||||||
|
Box::new(|_cc| Box::new(GuiApp::new(Ok(argument)))),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Ok(Value::Empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GuiApp {
|
||||||
|
text_edit_buffer: String,
|
||||||
|
whale_context: VariableMap,
|
||||||
|
eval_result: Result<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GuiApp {
|
||||||
|
pub fn new(result: Result<Value>) -> Self {
|
||||||
|
GuiApp {
|
||||||
|
text_edit_buffer: String::new(),
|
||||||
|
whale_context: VariableMap::new(),
|
||||||
|
eval_result: result,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn table_ui(&mut self, table: &Table, ui: &mut Ui) {
|
||||||
|
TableBuilder::new(ui)
|
||||||
|
.resizable(true)
|
||||||
|
.striped(true)
|
||||||
|
.columns(Column::remainder(), table.column_names().len())
|
||||||
|
.header(30.0, |mut row| {
|
||||||
|
for name in table.column_names() {
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.label(name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.body(|body| {
|
||||||
|
body.rows(20.0, table.rows().len(), |index, mut row| {
|
||||||
|
let row_data = table.rows().get(index).unwrap();
|
||||||
|
|
||||||
|
for cell_data in row_data {
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.label(cell_data.to_string());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl eframe::App for GuiApp {
|
||||||
|
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
|
||||||
|
CentralPanel::default().show(ctx, |ui| {
|
||||||
|
ui.with_layout(
|
||||||
|
Layout {
|
||||||
|
main_dir: Direction::TopDown,
|
||||||
|
main_wrap: false,
|
||||||
|
main_align: Align::Center,
|
||||||
|
main_justify: false,
|
||||||
|
cross_align: Align::Center,
|
||||||
|
cross_justify: true,
|
||||||
|
},
|
||||||
|
|ui| {
|
||||||
|
ui.text_edit_multiline(&mut self.text_edit_buffer);
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
let clear = ui.button("clear");
|
||||||
|
let submit = ui.button("submit");
|
||||||
|
|
||||||
|
if clear.clicked() {
|
||||||
|
self.text_edit_buffer.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if submit.clicked() {
|
||||||
|
self.eval_result =
|
||||||
|
eval_with_context(&self.text_edit_buffer, &mut self.whale_context);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
StripBuilder::new(ui)
|
||||||
|
.sizes(egui_extras::Size::remainder(), 1)
|
||||||
|
.vertical(|mut strip| {
|
||||||
|
strip.cell(|ui| {
|
||||||
|
let rectangle = ui.available_rect_before_wrap();
|
||||||
|
let corner = 5.0;
|
||||||
|
let border = Stroke::new(1.0, Color32::DARK_GREEN);
|
||||||
|
let item_size = 20.0;
|
||||||
|
|
||||||
|
ui.painter().rect_stroke(rectangle, corner, border);
|
||||||
|
|
||||||
|
match &self.eval_result {
|
||||||
|
Ok(value) => match value {
|
||||||
|
Value::String(string) => {
|
||||||
|
ui.label(RichText::new(string).size(item_size));
|
||||||
|
}
|
||||||
|
Value::Float(float) => {
|
||||||
|
ui.label(RichText::new(float.to_string()).size(item_size));
|
||||||
|
}
|
||||||
|
Value::Integer(integer) => {
|
||||||
|
ui.label(RichText::new(integer.to_string()).size(item_size));
|
||||||
|
}
|
||||||
|
Value::Boolean(boolean) => {
|
||||||
|
ui.label(RichText::new(boolean.to_string()).size(item_size));
|
||||||
|
}
|
||||||
|
Value::List(list) => {
|
||||||
|
for value in list {
|
||||||
|
ui.label(RichText::new(value.to_string()).size(item_size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::Map(_) => todo!(),
|
||||||
|
Value::Table(table) => {
|
||||||
|
ScrollArea::both().show(ui, |ui| {
|
||||||
|
TableBuilder::new(ui)
|
||||||
|
.resizable(true)
|
||||||
|
.striped(true)
|
||||||
|
.vscroll(true)
|
||||||
|
.columns(
|
||||||
|
Column::remainder(),
|
||||||
|
table.column_names().len(),
|
||||||
|
)
|
||||||
|
.header(20.0, |mut row| {
|
||||||
|
for name in table.column_names() {
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.label(name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.body(|body| {
|
||||||
|
body.rows(
|
||||||
|
20.0,
|
||||||
|
table.rows().len(),
|
||||||
|
|index, mut row| {
|
||||||
|
let row_data =
|
||||||
|
table.rows().get(index).unwrap();
|
||||||
|
|
||||||
|
for value in row_data {
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.label(value.to_string());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Value::Function(_) => todo!(),
|
||||||
|
Value::Empty => {}
|
||||||
|
Value::Time(_) => todo!(),
|
||||||
|
},
|
||||||
|
Err(error) => {
|
||||||
|
let rectangle = ui.available_rect_before_wrap();
|
||||||
|
let corner = 5.0;
|
||||||
|
let border = Stroke::new(1.0, Color32::DARK_RED);
|
||||||
|
|
||||||
|
ui.painter().rect_stroke(rectangle, corner, border);
|
||||||
|
ui.label(error.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
81
src/macros/logic.rs
Normal file
81
src/macros/logic.rs
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
use crate::{Error, Macro, MacroInfo, Result, Value, ValueType};
|
||||||
|
|
||||||
|
pub struct If;
|
||||||
|
|
||||||
|
impl Macro for If {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "if",
|
||||||
|
description: "Evaluates the first argument. If true, it does the second argument. If false, it does the third argument",
|
||||||
|
group: "general",
|
||||||
|
inputs: vec![
|
||||||
|
ValueType::List(vec![
|
||||||
|
ValueType::Boolean,
|
||||||
|
ValueType::Any,
|
||||||
|
ValueType::Any
|
||||||
|
]),
|
||||||
|
ValueType::List(vec![
|
||||||
|
ValueType::Function,
|
||||||
|
ValueType::Any,
|
||||||
|
ValueType::Any
|
||||||
|
])],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let argument = argument.as_fixed_len_list(3)?;
|
||||||
|
let (condition, if_true, if_false) = (&argument[0], &argument[1], &argument[2]);
|
||||||
|
|
||||||
|
let condition_is_true = if let Ok(boolean) = condition.as_boolean() {
|
||||||
|
boolean
|
||||||
|
} else if let Ok(function) = condition.as_function() {
|
||||||
|
function.run()?.as_boolean()?
|
||||||
|
} else {
|
||||||
|
return Err(Error::TypeError {
|
||||||
|
expected: &[ValueType::Boolean, ValueType::Function],
|
||||||
|
actual: condition.clone(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let should_yield = if condition_is_true { if_true } else { if_false };
|
||||||
|
|
||||||
|
if let Ok(function) = should_yield.as_function() {
|
||||||
|
function.run()
|
||||||
|
} else {
|
||||||
|
Ok(should_yield.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Loop;
|
||||||
|
|
||||||
|
impl Macro for Loop {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "loop",
|
||||||
|
description: "Repeats a function until the program ends.",
|
||||||
|
group: "general",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let function = argument.as_function()?;
|
||||||
|
|
||||||
|
function.run()?;
|
||||||
|
|
||||||
|
Loop.run(argument)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct While;
|
||||||
|
|
||||||
|
impl Macro for While {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, _argument: &Value) -> Result<Value> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
368
src/macros/mod.rs
Normal file
368
src/macros/mod.rs
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
//! This module contains whale's built-in macro system. Every macro is listed
|
||||||
|
//! alphabetically. Use [call_macro] to check an identifier against every macro.
|
||||||
|
//!
|
||||||
|
//! ## Writing macros
|
||||||
|
//!
|
||||||
|
//! - snake case identifier, this is enforced by a test
|
||||||
|
//! - the type name should be the identifier in upper camel case
|
||||||
|
//! - always verify user input, this creates helpful errors
|
||||||
|
//! - the description should be brief, it will display in the shell
|
||||||
|
//! - maintain alphabetical order
|
||||||
|
//!
|
||||||
|
//! ## Usage
|
||||||
|
//!
|
||||||
|
//! Macros can be used in Rust by passing a Value to the run method.
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! let value = Value::List(vec![1, 2,3]);
|
||||||
|
//! let count = Count.run(value).as_string().unwrap();
|
||||||
|
//!
|
||||||
|
//! assert_eq!(count, 3);
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use crate::{Result, Value, ValueType};
|
||||||
|
|
||||||
|
pub mod collections;
|
||||||
|
pub mod command;
|
||||||
|
pub mod data_formats;
|
||||||
|
pub mod disks;
|
||||||
|
pub mod filesystem;
|
||||||
|
pub mod general;
|
||||||
|
pub mod gui;
|
||||||
|
pub mod logic;
|
||||||
|
pub mod network;
|
||||||
|
pub mod package_management;
|
||||||
|
pub mod random;
|
||||||
|
pub mod system;
|
||||||
|
pub mod test;
|
||||||
|
pub mod time;
|
||||||
|
|
||||||
|
/// Master list of all macros.
|
||||||
|
///
|
||||||
|
/// This list is used to match identifiers with macros and to provide info to
|
||||||
|
/// the shell.
|
||||||
|
pub const MACRO_LIST: [&'static dyn Macro; 56] = [
|
||||||
|
&collections::Count,
|
||||||
|
&collections::CreateTable,
|
||||||
|
&collections::Get,
|
||||||
|
&collections::Insert,
|
||||||
|
&collections::Rows,
|
||||||
|
&collections::Select,
|
||||||
|
&collections::String,
|
||||||
|
&collections::Transform,
|
||||||
|
&collections::Where,
|
||||||
|
&command::Bash,
|
||||||
|
&command::Fish,
|
||||||
|
&command::Raw,
|
||||||
|
&command::Sh,
|
||||||
|
&command::Zsh,
|
||||||
|
&data_formats::FromCsv,
|
||||||
|
&data_formats::ToCsv,
|
||||||
|
&data_formats::FromJson,
|
||||||
|
&data_formats::ToJson,
|
||||||
|
&disks::ListDisks,
|
||||||
|
&disks::Partition,
|
||||||
|
&filesystem::Append,
|
||||||
|
&filesystem::CreateDir,
|
||||||
|
&filesystem::FileMetadata,
|
||||||
|
&filesystem::MoveDir,
|
||||||
|
&filesystem::ReadDir,
|
||||||
|
&filesystem::ReadFile,
|
||||||
|
&filesystem::RemoveDir,
|
||||||
|
&filesystem::Trash,
|
||||||
|
&filesystem::Watch,
|
||||||
|
&filesystem::Write,
|
||||||
|
&general::Async,
|
||||||
|
&general::Output,
|
||||||
|
&general::Repeat,
|
||||||
|
&general::Run,
|
||||||
|
&general::Wait,
|
||||||
|
&gui::BarGraph,
|
||||||
|
&gui::Plot,
|
||||||
|
&gui::Gui,
|
||||||
|
&logic::If,
|
||||||
|
&logic::Loop,
|
||||||
|
&network::Download,
|
||||||
|
&package_management::CoprRepositories,
|
||||||
|
&package_management::EnableRpmRepositories,
|
||||||
|
&package_management::InstallPackage,
|
||||||
|
&package_management::UninstallPackage,
|
||||||
|
&package_management::UpgradePackages,
|
||||||
|
&random::Random,
|
||||||
|
&random::RandomBoolean,
|
||||||
|
&random::RandomFloat,
|
||||||
|
&random::RandomInteger,
|
||||||
|
&random::RandomString,
|
||||||
|
&system::CpuSpeed,
|
||||||
|
&test::Assert,
|
||||||
|
&test::AssertEqual,
|
||||||
|
&time::Local,
|
||||||
|
&time::Now,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// A whale macro function.
|
||||||
|
pub trait Macro: Sync + Send {
|
||||||
|
fn info(&self) -> MacroInfo<'static>;
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information needed for each macro.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct MacroInfo<'a> {
|
||||||
|
/// Text pattern that triggers this macro.
|
||||||
|
pub identifier: &'a str,
|
||||||
|
|
||||||
|
/// User-facing information about how the macro works.
|
||||||
|
pub description: &'a str,
|
||||||
|
|
||||||
|
/// Category used to sort macros in the shell.
|
||||||
|
pub group: &'a str,
|
||||||
|
|
||||||
|
pub inputs: Vec<ValueType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub struct SystemInfo;
|
||||||
|
|
||||||
|
// impl Macro for SystemInfo {
|
||||||
|
// fn info(&self) -> MacroInfo<'static> {
|
||||||
|
// MacroInfo {
|
||||||
|
// identifier: "system_info",
|
||||||
|
// description: "Get information on the system.",
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn run(&self, argument: &Value) -> crate::Result<Value> {
|
||||||
|
// argument.as_empty()?;
|
||||||
|
|
||||||
|
// let mut map = VariableMap::new();
|
||||||
|
|
||||||
|
// map.set_value("hostname", Value::String(hostname()?))?;
|
||||||
|
|
||||||
|
// Ok(Value::Map(map))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub struct Sort;
|
||||||
|
|
||||||
|
// impl Macro for Sort {
|
||||||
|
// fn info(&self) -> MacroInfo<'static> {
|
||||||
|
// MacroInfo {
|
||||||
|
// identifier: "sort",
|
||||||
|
// description: "Apply default ordering.",
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
// if let Ok(mut list) = argument.as_list().cloned() {
|
||||||
|
// list.sort();
|
||||||
|
|
||||||
|
// Ok(Value::List(list))
|
||||||
|
// } else if let Ok(map) = argument.as_map().cloned() {
|
||||||
|
// Ok(Value::Map(map))
|
||||||
|
// } else if let Ok(mut table) = argument.as_table().cloned() {
|
||||||
|
// table.sort();
|
||||||
|
|
||||||
|
// Ok(Value::Table(table))
|
||||||
|
// } else {
|
||||||
|
// Err(crate::Error::ExpectedList {
|
||||||
|
// actual: argument.clone(),
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub struct Map;
|
||||||
|
|
||||||
|
// impl Macro for Map {
|
||||||
|
// fn info(&self) -> MacroInfo<'static> {
|
||||||
|
// MacroInfo {
|
||||||
|
// identifier: "map",
|
||||||
|
// description: "Create a map from a value.",
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
// match argument {
|
||||||
|
// Value::String(_) => todo!(),
|
||||||
|
// Value::Float(_) => todo!(),
|
||||||
|
// Value::Integer(_) => todo!(),
|
||||||
|
// Value::Boolean(_) => todo!(),
|
||||||
|
// Value::List(_) => todo!(),
|
||||||
|
// Value::Map(_) => todo!(),
|
||||||
|
// Value::Table(table) => Ok(Value::Map(VariableMap::from(table))),
|
||||||
|
// Value::Function(_) => todo!(),
|
||||||
|
// Value::Empty => todo!(),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub struct Status;
|
||||||
|
|
||||||
|
// impl Macro for Status {
|
||||||
|
// fn info(&self) -> MacroInfo<'static> {
|
||||||
|
// MacroInfo {
|
||||||
|
// identifier: "git_status",
|
||||||
|
// description: "Get the repository status for the current directory.",
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
// argument.as_empty()?;
|
||||||
|
|
||||||
|
// let repo = Repository::open(".")?;
|
||||||
|
// let mut table = Table::new(vec![
|
||||||
|
// "path".to_string(),
|
||||||
|
// "status".to_string(),
|
||||||
|
// "staged".to_string(),
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
// for entry in repo.statuses(None)?.into_iter() {
|
||||||
|
// let (status, staged) = {
|
||||||
|
// if entry.status().is_wt_new() {
|
||||||
|
// ("created".to_string(), false)
|
||||||
|
// } else if entry.status().is_wt_deleted() {
|
||||||
|
// ("deleted".to_string(), false)
|
||||||
|
// } else if entry.status().is_wt_modified() {
|
||||||
|
// ("modified".to_string(), false)
|
||||||
|
// } else if entry.status().is_index_new() {
|
||||||
|
// ("created".to_string(), true)
|
||||||
|
// } else if entry.status().is_index_deleted() {
|
||||||
|
// ("deleted".to_string(), true)
|
||||||
|
// } else if entry.status().is_index_modified() {
|
||||||
|
// ("modified".to_string(), true)
|
||||||
|
// } else if entry.status().is_ignored() {
|
||||||
|
// continue;
|
||||||
|
// } else {
|
||||||
|
// ("".to_string(), false)
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// let path = entry.path().unwrap().to_string();
|
||||||
|
|
||||||
|
// table.insert(vec![
|
||||||
|
// Value::String(path),
|
||||||
|
// Value::String(status),
|
||||||
|
// Value::Boolean(staged),
|
||||||
|
// ])?;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Ok(Value::Table(table))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub struct DocumentConvert;
|
||||||
|
|
||||||
|
// impl Macro for DocumentConvert {
|
||||||
|
// fn info(&self) -> MacroInfo<'static> {
|
||||||
|
// MacroInfo {
|
||||||
|
// identifier: "convert_document",
|
||||||
|
// description: "Convert a file's contents to a format and set the extension.",
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
// let argument = argument.as_list()?;
|
||||||
|
|
||||||
|
// if argument.len() != 3 {
|
||||||
|
// return Err(Error::WrongFunctionArgumentAmount {
|
||||||
|
// expected: 3,
|
||||||
|
// actual: argument.len(),
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let (path, from, to) = (
|
||||||
|
// argument[0].as_string()?,
|
||||||
|
// argument[1].as_string()?,
|
||||||
|
// argument[2].as_string()?,
|
||||||
|
// );
|
||||||
|
// let mut file_name = PathBuf::from(&path);
|
||||||
|
// file_name.set_extension(to);
|
||||||
|
// let new_file_name = file_name.to_str().unwrap();
|
||||||
|
// let script = format!("pandoc --from {from} --to {to} --output {new_file_name} {path}");
|
||||||
|
|
||||||
|
// Command::new("fish").arg("-c").arg(script).spawn()?.wait()?;
|
||||||
|
|
||||||
|
// Ok(Value::Empty)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub struct Trash;
|
||||||
|
|
||||||
|
// impl Macro for Trash {
|
||||||
|
// fn info(&self) -> MacroInfo<'static> {
|
||||||
|
// MacroInfo {
|
||||||
|
// identifier: "trash_dir",
|
||||||
|
// description: "Move a directory to the trash.",
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
// let path = argument.as_string()?;
|
||||||
|
|
||||||
|
// trash::delete(path)?;
|
||||||
|
|
||||||
|
// Ok(Value::Empty)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub struct Get;
|
||||||
|
|
||||||
|
// impl Macro for Get {
|
||||||
|
// fn info(&self) -> MacroInfo<'static> {
|
||||||
|
// MacroInfo {
|
||||||
|
// identifier: "get",
|
||||||
|
// description: "Extract a value from a collection.",
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
// let argument_list = argument.as_list()?;
|
||||||
|
// let collection = &argument_list[0];
|
||||||
|
// let index = &argument_list[1];
|
||||||
|
|
||||||
|
// if let Ok(list) = collection.as_list() {
|
||||||
|
// let index = index.as_int()?;
|
||||||
|
// let value = list.get(index as usize).unwrap_or(&Value::Empty);
|
||||||
|
|
||||||
|
// return Ok(value.clone());
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if let Ok(table) = collection.as_table() {
|
||||||
|
// let index = index.as_int()?;
|
||||||
|
// let get_row = table.get(index as usize);
|
||||||
|
|
||||||
|
// if let Some(row) = get_row {
|
||||||
|
// return Ok(Value::List(row.clone()));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Err(Error::TypeError {
|
||||||
|
// expected: &[
|
||||||
|
// ValueType::List,
|
||||||
|
// ValueType::Map,
|
||||||
|
// ValueType::Table,
|
||||||
|
// ValueType::String,
|
||||||
|
// ],
|
||||||
|
// actual: collection.clone(),
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn macro_formatting() {
|
||||||
|
for function in MACRO_LIST {
|
||||||
|
let identifier = function.info().identifier;
|
||||||
|
|
||||||
|
assert_eq!(identifier.to_lowercase(), identifier);
|
||||||
|
assert!(identifier.is_ascii());
|
||||||
|
assert!(!identifier.is_empty());
|
||||||
|
assert!(!identifier.contains(' '));
|
||||||
|
assert!(!identifier.contains(':'));
|
||||||
|
assert!(!identifier.contains('.'));
|
||||||
|
assert!(!identifier.contains('-'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
src/macros/network.rs
Normal file
23
src/macros/network.rs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
//! Macros for network access.
|
||||||
|
|
||||||
|
use crate::{Macro, MacroInfo, Result, Value};
|
||||||
|
|
||||||
|
pub struct Download;
|
||||||
|
|
||||||
|
impl Macro for Download {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "download",
|
||||||
|
description: "Fetch a network resource.",
|
||||||
|
group: "network",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let argument = argument.as_string()?;
|
||||||
|
let output = reqwest::blocking::get(argument)?.text()?;
|
||||||
|
|
||||||
|
Ok(Value::String(output))
|
||||||
|
}
|
||||||
|
}
|
170
src/macros/package_management.rs
Normal file
170
src/macros/package_management.rs
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use crate::{Error, Macro, MacroInfo, Result, Value};
|
||||||
|
|
||||||
|
pub struct CoprRepositories;
|
||||||
|
|
||||||
|
impl Macro for CoprRepositories {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "enable_copr_repository",
|
||||||
|
description: "Enable one or more COPR repositories.",
|
||||||
|
group: "package management",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let repo_list_string = if let Ok(repo) = argument.as_string().cloned() {
|
||||||
|
repo
|
||||||
|
} else if let Ok(repos) = argument.as_list() {
|
||||||
|
repos.iter().map(|value| value.to_string() + " ").collect()
|
||||||
|
} else {
|
||||||
|
return Err(crate::Error::ExpectedString {
|
||||||
|
actual: argument.clone(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Command::new("fish")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(format!("sudo dnf -y copr enable {repo_list_string}"))
|
||||||
|
.spawn()?
|
||||||
|
.wait()?;
|
||||||
|
|
||||||
|
Ok(Value::Empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct InstallPackage;
|
||||||
|
|
||||||
|
impl Macro for InstallPackage {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "install_package",
|
||||||
|
description: "Install one or more packages.",
|
||||||
|
group: "package management",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let package_list_string = if let Ok(package) = argument.as_string().cloned() {
|
||||||
|
package
|
||||||
|
} else if let Ok(packages) = argument.as_list() {
|
||||||
|
packages
|
||||||
|
.iter()
|
||||||
|
.map(|value| value.to_string() + " ")
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
return Err(Error::ExpectedString {
|
||||||
|
actual: argument.clone(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Command::new("fish")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(format!("sudo dnf -y install {package_list_string}"))
|
||||||
|
.spawn()?
|
||||||
|
.wait()?;
|
||||||
|
|
||||||
|
Ok(Value::Empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EnableRpmRepositories;
|
||||||
|
|
||||||
|
impl Macro for EnableRpmRepositories {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "enable_rpm_repositories",
|
||||||
|
description: "Enable one or more RPM repositories.",
|
||||||
|
group: "package management",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
if let Ok(repo) = argument.as_string() {
|
||||||
|
Command::new("fish")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(format!("sudo dnf -y config-manager --add-repo {repo}"))
|
||||||
|
.spawn()?
|
||||||
|
.wait()?;
|
||||||
|
} else if let Ok(repos) = argument.as_list() {
|
||||||
|
for repo in repos {
|
||||||
|
Command::new("fish")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(format!("sudo dnf -y config-manager --add-repo {repo}"))
|
||||||
|
.spawn()?
|
||||||
|
.wait()?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(crate::Error::ExpectedString {
|
||||||
|
actual: argument.clone(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Value::Empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UninstallPackage;
|
||||||
|
|
||||||
|
impl Macro for UninstallPackage {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "uninstall_package",
|
||||||
|
description: "Uninstall one or more packages.",
|
||||||
|
group: "package management",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let package_list_string = if let Ok(package) = argument.as_string().cloned() {
|
||||||
|
package
|
||||||
|
} else if let Ok(packages) = argument.as_list() {
|
||||||
|
packages
|
||||||
|
.iter()
|
||||||
|
.map(|value| value.to_string() + " ")
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
return Err(Error::ExpectedString {
|
||||||
|
actual: argument.clone(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Command::new("fish")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(format!("sudo dnf -y remove {package_list_string}"))
|
||||||
|
.spawn()?
|
||||||
|
.wait()?;
|
||||||
|
|
||||||
|
Ok(Value::Empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UpgradePackages;
|
||||||
|
|
||||||
|
impl Macro for UpgradePackages {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "upgrade_packages",
|
||||||
|
description: "Upgrade all installed packages.",
|
||||||
|
group: "package management",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
argument.as_empty()?;
|
||||||
|
|
||||||
|
Command::new("fish")
|
||||||
|
.arg("-c")
|
||||||
|
.arg("sudo dnf -y upgrade")
|
||||||
|
.spawn()?
|
||||||
|
.wait()?;
|
||||||
|
|
||||||
|
Ok(Value::Empty)
|
||||||
|
}
|
||||||
|
}
|
149
src/macros/random.rs
Normal file
149
src/macros/random.rs
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
use std::convert::TryInto;
|
||||||
|
|
||||||
|
use rand::{random, thread_rng, Rng};
|
||||||
|
|
||||||
|
use crate::{Error, Macro, MacroInfo, Result, Value};
|
||||||
|
|
||||||
|
pub struct RandomBoolean;
|
||||||
|
|
||||||
|
impl Macro for RandomBoolean {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "random_boolean",
|
||||||
|
description: "Create a random boolean.",
|
||||||
|
group: "random",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
argument.as_empty()?;
|
||||||
|
|
||||||
|
let boolean = rand::thread_rng().gen();
|
||||||
|
|
||||||
|
Ok(Value::Boolean(boolean))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RandomInteger;
|
||||||
|
|
||||||
|
impl Macro for RandomInteger {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "random_integer",
|
||||||
|
description: "Create a random integer.",
|
||||||
|
group: "random",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
match argument {
|
||||||
|
Value::Integer(max) => {
|
||||||
|
let integer = rand::thread_rng().gen_range(0..*max);
|
||||||
|
|
||||||
|
Ok(Value::Integer(integer))
|
||||||
|
}
|
||||||
|
Value::List(min_max) => {
|
||||||
|
Error::expect_function_argument_amount(self.info().identifier, min_max.len(), 2)?;
|
||||||
|
|
||||||
|
let min = min_max.get(0).unwrap().as_int()?;
|
||||||
|
let max = min_max.get(1).unwrap().as_int()? + 1;
|
||||||
|
let integer = rand::thread_rng().gen_range(min..max);
|
||||||
|
|
||||||
|
Ok(Value::Integer(integer))
|
||||||
|
}
|
||||||
|
Value::Empty => Ok(crate::Value::Integer(random())),
|
||||||
|
_ => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RandomString;
|
||||||
|
|
||||||
|
impl Macro for RandomString {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "random_string",
|
||||||
|
description: "Generate a random string.",
|
||||||
|
group: "random",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
match argument {
|
||||||
|
Value::Integer(length) => {
|
||||||
|
let length: usize = length.unsigned_abs().try_into().unwrap_or(0);
|
||||||
|
let mut random = String::with_capacity(length);
|
||||||
|
|
||||||
|
for _ in 0..length {
|
||||||
|
let random_char = thread_rng().gen_range('A'..='z').to_string();
|
||||||
|
|
||||||
|
random.push_str(&random_char);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::String(random))
|
||||||
|
}
|
||||||
|
Value::Empty => {
|
||||||
|
let mut random = String::with_capacity(10);
|
||||||
|
|
||||||
|
for _ in 0..10 {
|
||||||
|
let random_char = thread_rng().gen_range('A'..='z').to_string();
|
||||||
|
|
||||||
|
random.push_str(&random_char);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::String(random))
|
||||||
|
}
|
||||||
|
_ => Err(Error::ExpectedEmpty {
|
||||||
|
actual: argument.clone(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RandomFloat;
|
||||||
|
|
||||||
|
impl Macro for RandomFloat {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "random_float",
|
||||||
|
description: "Generate a random floating point value between 0 and 1.",
|
||||||
|
group: "random",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
argument.as_empty()?;
|
||||||
|
|
||||||
|
Ok(Value::Float(random()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Random;
|
||||||
|
|
||||||
|
impl Macro for Random {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "random",
|
||||||
|
description: "Select a random item from a collection.",
|
||||||
|
group: "random",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
if let Ok(list) = argument.as_list() {
|
||||||
|
let random_index = thread_rng().gen_range(0..list.len());
|
||||||
|
let random_item = list.get(random_index).unwrap();
|
||||||
|
|
||||||
|
Ok(random_item.clone())
|
||||||
|
} else {
|
||||||
|
Err(Error::ExpectedCollection {
|
||||||
|
actual: argument.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
src/macros/system.rs
Normal file
24
src/macros/system.rs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
use sys_info::cpu_speed;
|
||||||
|
|
||||||
|
use crate::{Macro, MacroInfo, Result, Value};
|
||||||
|
|
||||||
|
pub struct CpuSpeed;
|
||||||
|
|
||||||
|
impl Macro for CpuSpeed {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "cpu_speed",
|
||||||
|
description: "Return the current processor speed in megahertz.",
|
||||||
|
group: "system",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
argument.as_empty()?;
|
||||||
|
|
||||||
|
let speed = cpu_speed().unwrap_or_default() as i64;
|
||||||
|
|
||||||
|
Ok(Value::Integer(speed))
|
||||||
|
}
|
||||||
|
}
|
42
src/macros/test.rs
Normal file
42
src/macros/test.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
use crate::{Macro, MacroInfo, Result, Value};
|
||||||
|
|
||||||
|
pub struct Assert;
|
||||||
|
|
||||||
|
impl Macro for Assert {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "assert",
|
||||||
|
description: "Panic if a boolean is false.",
|
||||||
|
group: "test",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let boolean = argument.as_boolean()?;
|
||||||
|
|
||||||
|
assert!(boolean);
|
||||||
|
|
||||||
|
Ok(Value::Empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AssertEqual;
|
||||||
|
|
||||||
|
impl Macro for AssertEqual {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "assert_equal",
|
||||||
|
description: "Panic if two values do not match.",
|
||||||
|
group: "test",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &Value) -> Result<Value> {
|
||||||
|
let arguments = argument.as_fixed_len_list(2)?;
|
||||||
|
assert_eq!(arguments[0], arguments[1]);
|
||||||
|
|
||||||
|
Ok(Value::Empty)
|
||||||
|
}
|
||||||
|
}
|
43
src/macros/time.rs
Normal file
43
src/macros/time.rs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use crate::{Macro, MacroInfo, Result, Time, Value};
|
||||||
|
|
||||||
|
pub struct Now;
|
||||||
|
|
||||||
|
impl Macro for Now {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "now",
|
||||||
|
description: "Return the current time.",
|
||||||
|
group: "time",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &crate::Value) -> Result<Value> {
|
||||||
|
argument.as_empty()?;
|
||||||
|
|
||||||
|
let time = Time::monotonic(Instant::now());
|
||||||
|
|
||||||
|
Ok(Value::Time(time))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Local;
|
||||||
|
|
||||||
|
impl Macro for Local {
|
||||||
|
fn info(&self) -> MacroInfo<'static> {
|
||||||
|
MacroInfo {
|
||||||
|
identifier: "local",
|
||||||
|
description: "Show a time value adjusted for the current time zone.",
|
||||||
|
group: "time",
|
||||||
|
inputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: &crate::Value) -> Result<Value> {
|
||||||
|
let argument = argument.as_time()?;
|
||||||
|
|
||||||
|
Ok(Value::String(argument.as_local()))
|
||||||
|
}
|
||||||
|
}
|
273
src/main.rs
Normal file
273
src/main.rs
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
//! Command line interface for the whale programming language.
|
||||||
|
use clap::Parser;
|
||||||
|
use eframe::{run_native, NativeOptions};
|
||||||
|
use nu_ansi_term::{Color, Style};
|
||||||
|
use reedline::{
|
||||||
|
default_emacs_keybindings, ColumnarMenu, Completer, DefaultHinter, DefaultPrompt,
|
||||||
|
DefaultPromptSegment, EditCommand, Emacs, FileBackedHistory, KeyCode, KeyModifiers, Reedline,
|
||||||
|
ReedlineEvent, ReedlineMenu, Signal, Span, Suggestion,
|
||||||
|
};
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
fs::{self, read_to_string},
|
||||||
|
path::PathBuf,
|
||||||
|
};
|
||||||
|
|
||||||
|
use whale_lib::{
|
||||||
|
eval, eval_with_context, gui::GuiApp, Macro, MacroInfo, Result, Value, VariableMap, MACRO_LIST,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Command-line arguments to be parsed.
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(author, version, about, long_about = None)]
|
||||||
|
struct Args {
|
||||||
|
/// Whale source code to evaluate.
|
||||||
|
#[arg(short, long)]
|
||||||
|
command: Option<String>,
|
||||||
|
|
||||||
|
/// Location of the file to run.
|
||||||
|
#[arg(short, long)]
|
||||||
|
path: Option<String>,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
gui: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
if !args.gui && args.path.is_none() && args.command.is_none() {
|
||||||
|
return run_cli_shell();
|
||||||
|
}
|
||||||
|
|
||||||
|
let eval_result = if let Some(path) = args.path {
|
||||||
|
let file_contents = read_to_string(path).unwrap();
|
||||||
|
|
||||||
|
eval(&file_contents)
|
||||||
|
} else if let Some(command) = args.command {
|
||||||
|
eval(&command)
|
||||||
|
} else {
|
||||||
|
Ok(Value::Empty)
|
||||||
|
};
|
||||||
|
|
||||||
|
if args.gui {
|
||||||
|
return run_gui_shell(eval_result);
|
||||||
|
}
|
||||||
|
|
||||||
|
match eval_result {
|
||||||
|
Ok(value) => println!("{value}"),
|
||||||
|
Err(error) => eprintln!("{error}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_gui_shell(result: Result<Value>) {
|
||||||
|
run_native(
|
||||||
|
&Value::Empty.to_string(),
|
||||||
|
NativeOptions::default(),
|
||||||
|
Box::new(|_cc| Box::new(GuiApp::new(result))),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_cli_shell() {
|
||||||
|
let mut context = VariableMap::new();
|
||||||
|
let mut line_editor = setup_reedline();
|
||||||
|
let prompt = DefaultPrompt {
|
||||||
|
left_prompt: DefaultPromptSegment::WorkingDirectory,
|
||||||
|
right_prompt: DefaultPromptSegment::CurrentDateTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let sig = line_editor.read_line(&prompt);
|
||||||
|
|
||||||
|
match sig {
|
||||||
|
Ok(Signal::Success(buffer)) => {
|
||||||
|
let eval_result = eval_with_context(&buffer, &mut context);
|
||||||
|
|
||||||
|
match eval_result {
|
||||||
|
Ok(value) => println!("{value}"),
|
||||||
|
Err(error) => eprintln!("{error}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Signal::CtrlD) | Ok(Signal::CtrlC) => {
|
||||||
|
println!("\nExit");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
signal => {
|
||||||
|
println!("Unhandled signal: {:?}", signal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WhaleCompeleter {
|
||||||
|
macro_list: Vec<Suggestion>,
|
||||||
|
files: Vec<Suggestion>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WhaleCompeleter {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
WhaleCompeleter {
|
||||||
|
macro_list: Vec::new(),
|
||||||
|
files: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_macro_list(&mut self, macro_list: Vec<&'static dyn Macro>) -> &mut Self {
|
||||||
|
self.macro_list = macro_list
|
||||||
|
.iter()
|
||||||
|
.map(|r#macro| {
|
||||||
|
let MacroInfo {
|
||||||
|
identifier,
|
||||||
|
description,
|
||||||
|
group,
|
||||||
|
inputs,
|
||||||
|
} = r#macro.info();
|
||||||
|
|
||||||
|
let description = format!("{description} | {group}");
|
||||||
|
let inputs = inputs
|
||||||
|
.iter()
|
||||||
|
.map(|value_type| value_type.to_string())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Suggestion {
|
||||||
|
value: identifier.to_string(),
|
||||||
|
description: Some(description),
|
||||||
|
extra: Some(inputs),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
self.macro_list
|
||||||
|
.sort_by_key(|suggestion| suggestion.extra.clone());
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_suggestions(&mut self, start: usize, end: usize) -> Vec<Suggestion> {
|
||||||
|
let macro_suggestions = self
|
||||||
|
.macro_list
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|suggestion| Suggestion {
|
||||||
|
span: Span { start, end },
|
||||||
|
..suggestion
|
||||||
|
});
|
||||||
|
let file_suggestions = self.files.iter().cloned().map(|suggestion| Suggestion {
|
||||||
|
span: Span { start, end },
|
||||||
|
..suggestion
|
||||||
|
});
|
||||||
|
|
||||||
|
file_suggestions.chain(macro_suggestions).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_files(&mut self, mut path: &str) {
|
||||||
|
if path.starts_with('\"') {
|
||||||
|
path = &path[1..];
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = PathBuf::from(path);
|
||||||
|
|
||||||
|
if !path.is_dir() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.files = fs::read_dir(path)
|
||||||
|
.unwrap()
|
||||||
|
.map(|entry| {
|
||||||
|
let path = entry.unwrap().path();
|
||||||
|
let path = path.to_string_lossy();
|
||||||
|
|
||||||
|
Suggestion {
|
||||||
|
value: format!("\"{path}\""),
|
||||||
|
description: None,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Completer for WhaleCompeleter {
|
||||||
|
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
|
||||||
|
let split = line.split(' ');
|
||||||
|
let current_word = split.last().unwrap_or("");
|
||||||
|
let start = pos.saturating_sub(current_word.len());
|
||||||
|
let end = line.len();
|
||||||
|
|
||||||
|
self.update_files(current_word);
|
||||||
|
self.get_suggestions(start, end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_reedline() -> Reedline {
|
||||||
|
let mut completer = Box::new(WhaleCompeleter::new());
|
||||||
|
|
||||||
|
completer.set_macro_list(MACRO_LIST.to_vec());
|
||||||
|
|
||||||
|
let completion_menu = Box::new(
|
||||||
|
ColumnarMenu::default()
|
||||||
|
.with_name("completion_menu")
|
||||||
|
.with_columns(1)
|
||||||
|
.with_text_style(Style {
|
||||||
|
foreground: Some(Color::White),
|
||||||
|
is_dimmed: false,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.with_description_text_style(Style {
|
||||||
|
is_dimmed: true,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.with_selected_text_style(Style {
|
||||||
|
is_bold: true,
|
||||||
|
background: Some(Color::Black),
|
||||||
|
foreground: Some(Color::White),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut keybindings = default_emacs_keybindings();
|
||||||
|
keybindings.add_binding(
|
||||||
|
KeyModifiers::NONE,
|
||||||
|
KeyCode::Tab,
|
||||||
|
ReedlineEvent::UntilFound(vec![
|
||||||
|
ReedlineEvent::Menu("completion_menu".to_string()),
|
||||||
|
ReedlineEvent::MenuNext,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
keybindings.add_binding(
|
||||||
|
KeyModifiers::SHIFT,
|
||||||
|
KeyCode::Tab,
|
||||||
|
ReedlineEvent::UntilFound(vec![
|
||||||
|
ReedlineEvent::Menu("completion_menu".to_string()),
|
||||||
|
ReedlineEvent::MenuPrevious,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
keybindings.add_binding(
|
||||||
|
KeyModifiers::ALT,
|
||||||
|
KeyCode::Enter,
|
||||||
|
ReedlineEvent::Edit(vec![EditCommand::InsertNewline]),
|
||||||
|
);
|
||||||
|
|
||||||
|
let edit_mode = Box::new(Emacs::new(keybindings));
|
||||||
|
let history = Box::new(
|
||||||
|
FileBackedHistory::with_file(100, "target/history.txt".into())
|
||||||
|
.expect("Error configuring shell history file."),
|
||||||
|
);
|
||||||
|
let mut hinter = DefaultHinter::default();
|
||||||
|
|
||||||
|
hinter = hinter.with_style(Style {
|
||||||
|
foreground: Some(Color::Yellow),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
Reedline::create()
|
||||||
|
.with_completer(completer)
|
||||||
|
.with_menu(ReedlineMenu::EngineCompleter(completion_menu))
|
||||||
|
.with_edit_mode(edit_mode)
|
||||||
|
.with_history(history)
|
||||||
|
.with_hinter(Box::new(hinter))
|
||||||
|
.with_partial_completions(true)
|
||||||
|
.with_quick_completions(true)
|
||||||
|
}
|
547
src/operator.rs
Normal file
547
src/operator.rs
Normal file
@ -0,0 +1,547 @@
|
|||||||
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
|
||||||
|
use crate::{error::*, value::Value, Result, VariableMap};
|
||||||
|
|
||||||
|
/// An enum that represents operators in the operator tree.
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
pub enum Operator {
|
||||||
|
/// A root node in the operator tree.
|
||||||
|
/// The whole expression is stored under a root node, as well as each subexpression surrounded by parentheses.
|
||||||
|
RootNode,
|
||||||
|
|
||||||
|
/// A binary addition operator.
|
||||||
|
Add,
|
||||||
|
/// A binary subtraction operator.
|
||||||
|
Sub,
|
||||||
|
/// A unary negation operator.
|
||||||
|
Neg,
|
||||||
|
/// A binary multiplication operator.
|
||||||
|
Mul,
|
||||||
|
/// A binary division operator.
|
||||||
|
Div,
|
||||||
|
/// A binary modulo operator.
|
||||||
|
Mod,
|
||||||
|
/// A binary exponentiation operator.
|
||||||
|
Exp,
|
||||||
|
|
||||||
|
/// A binary equality comparator.
|
||||||
|
Eq,
|
||||||
|
/// A binary inequality comparator.
|
||||||
|
Neq,
|
||||||
|
/// A binary greater-than comparator.
|
||||||
|
Gt,
|
||||||
|
/// A binary lower-than comparator.
|
||||||
|
Lt,
|
||||||
|
/// A binary greater-than-or-equal comparator.
|
||||||
|
Geq,
|
||||||
|
/// A binary lower-than-or-equal comparator.
|
||||||
|
Leq,
|
||||||
|
/// A binary logical and operator.
|
||||||
|
And,
|
||||||
|
/// A binary logical or operator.
|
||||||
|
Or,
|
||||||
|
/// A binary logical not operator.
|
||||||
|
Not,
|
||||||
|
|
||||||
|
/// A binary assignment operator.
|
||||||
|
Assign,
|
||||||
|
/// A binary add-assign operator.
|
||||||
|
AddAssign,
|
||||||
|
/// A binary subtract-assign operator.
|
||||||
|
SubAssign,
|
||||||
|
/// A binary multiply-assign operator.
|
||||||
|
MulAssign,
|
||||||
|
/// A binary divide-assign operator.
|
||||||
|
DivAssign,
|
||||||
|
/// A binary modulo-assign operator.
|
||||||
|
ModAssign,
|
||||||
|
/// A binary exponentiate-assign operator.
|
||||||
|
ExpAssign,
|
||||||
|
/// A binary and-assign operator.
|
||||||
|
AndAssign,
|
||||||
|
/// A binary or-assign operator.
|
||||||
|
OrAssign,
|
||||||
|
|
||||||
|
/// An n-ary tuple constructor.
|
||||||
|
Tuple,
|
||||||
|
/// An n-ary subexpression chain.
|
||||||
|
Chain,
|
||||||
|
|
||||||
|
/// A constant value.
|
||||||
|
Const {
|
||||||
|
/** The value of the constant. */
|
||||||
|
value: Value,
|
||||||
|
},
|
||||||
|
/// A write to a variable identifier.
|
||||||
|
VariableIdentifierWrite {
|
||||||
|
/// The identifier of the variable.
|
||||||
|
identifier: String,
|
||||||
|
},
|
||||||
|
/// A read from a variable identifier.
|
||||||
|
VariableIdentifierRead {
|
||||||
|
/// The identifier of the variable.
|
||||||
|
identifier: String,
|
||||||
|
},
|
||||||
|
/// A function identifier.
|
||||||
|
FunctionIdentifier {
|
||||||
|
/// The identifier of the function.
|
||||||
|
identifier: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Operator {
|
||||||
|
pub(crate) fn value(value: Value) -> Self {
|
||||||
|
Operator::Const { value }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn variable_identifier_write(identifier: String) -> Self {
|
||||||
|
Operator::VariableIdentifierWrite { identifier }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn variable_identifier_read(identifier: String) -> Self {
|
||||||
|
Operator::VariableIdentifierRead { identifier }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn function_identifier(identifier: String) -> Self {
|
||||||
|
Operator::FunctionIdentifier { identifier }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the precedence of the operator.
|
||||||
|
/// A high precedence means that the operator has priority to be deeper in the tree.
|
||||||
|
pub(crate) const fn precedence(&self) -> i32 {
|
||||||
|
use crate::operator::Operator::*;
|
||||||
|
match self {
|
||||||
|
RootNode => 200,
|
||||||
|
|
||||||
|
Add | Sub => 95,
|
||||||
|
Neg => 110,
|
||||||
|
Mul | Div | Mod => 100,
|
||||||
|
Exp => 120,
|
||||||
|
|
||||||
|
Eq | Neq | Gt | Lt | Geq | Leq => 80,
|
||||||
|
And => 75,
|
||||||
|
Or => 70,
|
||||||
|
Not => 110,
|
||||||
|
|
||||||
|
Assign | AddAssign | SubAssign | MulAssign | DivAssign | ModAssign | ExpAssign
|
||||||
|
| AndAssign | OrAssign => 50,
|
||||||
|
|
||||||
|
Tuple => 40,
|
||||||
|
Chain => 0,
|
||||||
|
|
||||||
|
Const { .. } => 200,
|
||||||
|
VariableIdentifierWrite { .. } | VariableIdentifierRead { .. } => 200,
|
||||||
|
FunctionIdentifier { .. } => 190,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if chains of operators with the same precedence as this one should be evaluated left-to-right,
|
||||||
|
/// and false if they should be evaluated right-to-left.
|
||||||
|
/// Left-to-right chaining has priority if operators with different order but same precedence are chained.
|
||||||
|
pub(crate) const fn is_left_to_right(&self) -> bool {
|
||||||
|
use crate::operator::Operator::*;
|
||||||
|
!matches!(self, Assign | FunctionIdentifier { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if chains of this operator should be flattened into one operator with many arguments.
|
||||||
|
pub(crate) const fn is_sequence(&self) -> bool {
|
||||||
|
use crate::operator::Operator::*;
|
||||||
|
matches!(self, Tuple | Chain)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True if this operator is a leaf, meaning it accepts no arguments.
|
||||||
|
// Make this a const fn as soon as whatever is missing gets stable (issue #57563)
|
||||||
|
pub(crate) fn is_leaf(&self) -> bool {
|
||||||
|
self.max_argument_amount() == Some(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the maximum amount of arguments required by this operator.
|
||||||
|
pub(crate) const fn max_argument_amount(&self) -> Option<usize> {
|
||||||
|
use crate::operator::Operator::*;
|
||||||
|
match self {
|
||||||
|
Add | Sub | Mul | Div | Mod | Exp | Eq | Neq | Gt | Lt | Geq | Leq | And | Or
|
||||||
|
| Assign | AddAssign | SubAssign | MulAssign | DivAssign | ModAssign | ExpAssign
|
||||||
|
| AndAssign | OrAssign => Some(2),
|
||||||
|
Tuple | Chain => None,
|
||||||
|
Not | Neg | RootNode => Some(1),
|
||||||
|
Const { .. } => Some(0),
|
||||||
|
VariableIdentifierWrite { .. } | VariableIdentifierRead { .. } => Some(0),
|
||||||
|
FunctionIdentifier { .. } => Some(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if this operator is unary, i.e. it requires exactly one argument.
|
||||||
|
pub(crate) fn is_unary(&self) -> bool {
|
||||||
|
self.max_argument_amount() == Some(1) && *self != Operator::RootNode
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates the operator with the given arguments and context.
|
||||||
|
pub(crate) fn eval(&self, arguments: &[Value], context: &VariableMap) -> Result<Value> {
|
||||||
|
use crate::operator::Operator::*;
|
||||||
|
|
||||||
|
match self {
|
||||||
|
RootNode => {
|
||||||
|
if let Some(first) = arguments.first() {
|
||||||
|
Ok(first.clone())
|
||||||
|
} else {
|
||||||
|
Ok(Value::Empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Add => {
|
||||||
|
Error::expect_operator_argument_amount(arguments.len(), 2)?;
|
||||||
|
expect_number_or_string(&arguments[0])?;
|
||||||
|
expect_number_or_string(&arguments[1])?;
|
||||||
|
|
||||||
|
if let (Ok(a), Ok(b)) = (arguments[0].as_string(), arguments[1].as_string()) {
|
||||||
|
let mut result = String::with_capacity(a.len() + b.len());
|
||||||
|
result.push_str(a);
|
||||||
|
result.push_str(b);
|
||||||
|
Ok(Value::String(result))
|
||||||
|
} else if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) {
|
||||||
|
let result = a.checked_add(b);
|
||||||
|
if let Some(result) = result {
|
||||||
|
Ok(Value::Integer(result))
|
||||||
|
} else {
|
||||||
|
Err(Error::addition_error(
|
||||||
|
arguments[0].clone(),
|
||||||
|
arguments[1].clone(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} else if let (Ok(a), Ok(b)) = (arguments[0].as_number(), arguments[1].as_number())
|
||||||
|
{
|
||||||
|
Ok(Value::Float(a + b))
|
||||||
|
} else {
|
||||||
|
Err(Error::wrong_type_combination(
|
||||||
|
self.clone(),
|
||||||
|
vec![
|
||||||
|
arguments.get(0).unwrap().into(),
|
||||||
|
arguments.get(1).unwrap().into(),
|
||||||
|
],
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Sub => {
|
||||||
|
Error::expect_operator_argument_amount(arguments.len(), 2)?;
|
||||||
|
arguments[0].as_number()?;
|
||||||
|
arguments[1].as_number()?;
|
||||||
|
|
||||||
|
if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) {
|
||||||
|
let result = a.checked_sub(b);
|
||||||
|
if let Some(result) = result {
|
||||||
|
Ok(Value::Integer(result))
|
||||||
|
} else {
|
||||||
|
Err(Error::subtraction_error(
|
||||||
|
arguments[0].clone(),
|
||||||
|
arguments[1].clone(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(Value::Float(
|
||||||
|
arguments[0].as_number()? - arguments[1].as_number()?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Neg => {
|
||||||
|
Error::expect_operator_argument_amount(arguments.len(), 1)?;
|
||||||
|
arguments[0].as_number()?;
|
||||||
|
|
||||||
|
if let Ok(a) = arguments[0].as_int() {
|
||||||
|
let result = a.checked_neg();
|
||||||
|
if let Some(result) = result {
|
||||||
|
Ok(Value::Integer(result))
|
||||||
|
} else {
|
||||||
|
Err(Error::negation_error(arguments[0].clone()))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(Value::Float(-arguments[0].as_number()?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Mul => {
|
||||||
|
Error::expect_operator_argument_amount(arguments.len(), 2)?;
|
||||||
|
arguments[0].as_number()?;
|
||||||
|
arguments[1].as_number()?;
|
||||||
|
|
||||||
|
if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) {
|
||||||
|
let result = a.checked_mul(b);
|
||||||
|
if let Some(result) = result {
|
||||||
|
Ok(Value::Integer(result))
|
||||||
|
} else {
|
||||||
|
Err(Error::multiplication_error(
|
||||||
|
arguments[0].clone(),
|
||||||
|
arguments[1].clone(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(Value::Float(
|
||||||
|
arguments[0].as_number()? * arguments[1].as_number()?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Div => {
|
||||||
|
Error::expect_operator_argument_amount(arguments.len(), 2)?;
|
||||||
|
arguments[0].as_number()?;
|
||||||
|
arguments[1].as_number()?;
|
||||||
|
|
||||||
|
if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) {
|
||||||
|
let result = a.checked_div(b);
|
||||||
|
if let Some(result) = result {
|
||||||
|
Ok(Value::Integer(result))
|
||||||
|
} else {
|
||||||
|
Err(Error::division_error(
|
||||||
|
arguments[0].clone(),
|
||||||
|
arguments[1].clone(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(Value::Float(
|
||||||
|
arguments[0].as_number()? / arguments[1].as_number()?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Mod => {
|
||||||
|
Error::expect_operator_argument_amount(arguments.len(), 2)?;
|
||||||
|
arguments[0].as_number()?;
|
||||||
|
arguments[1].as_number()?;
|
||||||
|
|
||||||
|
if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) {
|
||||||
|
let result = a.checked_rem(b);
|
||||||
|
if let Some(result) = result {
|
||||||
|
Ok(Value::Integer(result))
|
||||||
|
} else {
|
||||||
|
Err(Error::modulation_error(
|
||||||
|
arguments[0].clone(),
|
||||||
|
arguments[1].clone(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(Value::Float(
|
||||||
|
arguments[0].as_number()? % arguments[1].as_number()?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Exp => {
|
||||||
|
Error::expect_operator_argument_amount(arguments.len(), 2)?;
|
||||||
|
arguments[0].as_number()?;
|
||||||
|
arguments[1].as_number()?;
|
||||||
|
|
||||||
|
Ok(Value::Float(
|
||||||
|
arguments[0].as_number()?.powf(arguments[1].as_number()?),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Eq => {
|
||||||
|
Error::expect_operator_argument_amount(arguments.len(), 2)?;
|
||||||
|
|
||||||
|
Ok(Value::Boolean(arguments[0] == arguments[1]))
|
||||||
|
}
|
||||||
|
Neq => {
|
||||||
|
Error::expect_operator_argument_amount(arguments.len(), 2)?;
|
||||||
|
|
||||||
|
Ok(Value::Boolean(arguments[0] != arguments[1]))
|
||||||
|
}
|
||||||
|
Gt => {
|
||||||
|
Error::expect_operator_argument_amount(arguments.len(), 2)?;
|
||||||
|
expect_number_or_string(&arguments[0])?;
|
||||||
|
expect_number_or_string(&arguments[1])?;
|
||||||
|
|
||||||
|
if let (Ok(a), Ok(b)) = (arguments[0].as_string(), arguments[1].as_string()) {
|
||||||
|
Ok(Value::Boolean(a > b))
|
||||||
|
} else if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) {
|
||||||
|
Ok(Value::Boolean(a > b))
|
||||||
|
} else {
|
||||||
|
Ok(Value::Boolean(
|
||||||
|
arguments[0].as_number()? > arguments[1].as_number()?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Lt => {
|
||||||
|
Error::expect_operator_argument_amount(arguments.len(), 2)?;
|
||||||
|
expect_number_or_string(&arguments[0])?;
|
||||||
|
expect_number_or_string(&arguments[1])?;
|
||||||
|
|
||||||
|
if let (Ok(a), Ok(b)) = (arguments[0].as_string(), arguments[1].as_string()) {
|
||||||
|
Ok(Value::Boolean(a < b))
|
||||||
|
} else if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) {
|
||||||
|
Ok(Value::Boolean(a < b))
|
||||||
|
} else {
|
||||||
|
Ok(Value::Boolean(
|
||||||
|
arguments[0].as_number()? < arguments[1].as_number()?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Geq => {
|
||||||
|
Error::expect_operator_argument_amount(arguments.len(), 2)?;
|
||||||
|
expect_number_or_string(&arguments[0])?;
|
||||||
|
expect_number_or_string(&arguments[1])?;
|
||||||
|
|
||||||
|
if let (Ok(a), Ok(b)) = (arguments[0].as_string(), arguments[1].as_string()) {
|
||||||
|
Ok(Value::Boolean(a >= b))
|
||||||
|
} else if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) {
|
||||||
|
Ok(Value::Boolean(a >= b))
|
||||||
|
} else {
|
||||||
|
Ok(Value::Boolean(
|
||||||
|
arguments[0].as_number()? >= arguments[1].as_number()?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Leq => {
|
||||||
|
Error::expect_operator_argument_amount(arguments.len(), 2)?;
|
||||||
|
expect_number_or_string(&arguments[0])?;
|
||||||
|
expect_number_or_string(&arguments[1])?;
|
||||||
|
|
||||||
|
if let (Ok(a), Ok(b)) = (arguments[0].as_string(), arguments[1].as_string()) {
|
||||||
|
Ok(Value::Boolean(a <= b))
|
||||||
|
} else if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) {
|
||||||
|
Ok(Value::Boolean(a <= b))
|
||||||
|
} else {
|
||||||
|
Ok(Value::Boolean(
|
||||||
|
arguments[0].as_number()? <= arguments[1].as_number()?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
And => {
|
||||||
|
Error::expect_operator_argument_amount(arguments.len(), 2)?;
|
||||||
|
let a = arguments[0].as_boolean()?;
|
||||||
|
let b = arguments[1].as_boolean()?;
|
||||||
|
|
||||||
|
Ok(Value::Boolean(a && b))
|
||||||
|
}
|
||||||
|
Or => {
|
||||||
|
Error::expect_operator_argument_amount(arguments.len(), 2)?;
|
||||||
|
let a = arguments[0].as_boolean()?;
|
||||||
|
let b = arguments[1].as_boolean()?;
|
||||||
|
|
||||||
|
Ok(Value::Boolean(a || b))
|
||||||
|
}
|
||||||
|
Not => {
|
||||||
|
Error::expect_operator_argument_amount(arguments.len(), 1)?;
|
||||||
|
let a = arguments[0].as_boolean()?;
|
||||||
|
|
||||||
|
Ok(Value::Boolean(!a))
|
||||||
|
}
|
||||||
|
Assign | AddAssign | SubAssign | MulAssign | DivAssign | ModAssign | ExpAssign
|
||||||
|
| AndAssign | OrAssign => Err(Error::ContextNotMutable),
|
||||||
|
Tuple => Ok(Value::List(arguments.into())),
|
||||||
|
Chain => {
|
||||||
|
if arguments.is_empty() {
|
||||||
|
return Err(Error::expect_operator_argument_amount(0, 1).unwrap_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(arguments.last().cloned().unwrap_or(Value::Empty))
|
||||||
|
}
|
||||||
|
Const { value } => {
|
||||||
|
Error::expect_operator_argument_amount(arguments.len(), 0)?;
|
||||||
|
|
||||||
|
Ok(value.clone())
|
||||||
|
}
|
||||||
|
VariableIdentifierWrite { identifier } => {
|
||||||
|
Error::expect_operator_argument_amount(arguments.len(), 0)?;
|
||||||
|
|
||||||
|
Ok(identifier.clone().into())
|
||||||
|
}
|
||||||
|
VariableIdentifierRead { identifier } => {
|
||||||
|
Error::expect_operator_argument_amount(arguments.len(), 0)?;
|
||||||
|
|
||||||
|
if let Some(value) = context.get_value(identifier)? {
|
||||||
|
Ok(value)
|
||||||
|
} else {
|
||||||
|
Err(Error::VariableIdentifierNotFound(identifier.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FunctionIdentifier { identifier } => {
|
||||||
|
Error::expect_operator_argument_amount(arguments.len(), 1)?;
|
||||||
|
let arguments = &arguments[0];
|
||||||
|
|
||||||
|
context.call_function(identifier, arguments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates the operator with the given arguments and mutable context.
|
||||||
|
pub(crate) fn eval_mut(&self, arguments: &[Value], context: &mut VariableMap) -> Result<Value> {
|
||||||
|
use crate::operator::Operator::*;
|
||||||
|
match self {
|
||||||
|
Assign => {
|
||||||
|
Error::expect_operator_argument_amount(arguments.len(), 2)?;
|
||||||
|
let target = arguments[0].as_string()?;
|
||||||
|
context.set_value(target, arguments[1].clone())?;
|
||||||
|
|
||||||
|
Ok(Value::Empty)
|
||||||
|
}
|
||||||
|
AddAssign | SubAssign | MulAssign | DivAssign | ModAssign | ExpAssign | AndAssign
|
||||||
|
| OrAssign => {
|
||||||
|
Error::expect_operator_argument_amount(arguments.len(), 2)?;
|
||||||
|
|
||||||
|
let target = arguments[0].as_string()?;
|
||||||
|
let left_value = Operator::VariableIdentifierRead {
|
||||||
|
identifier: target.clone(),
|
||||||
|
}
|
||||||
|
.eval(&Vec::new(), context)?;
|
||||||
|
let arguments = vec![left_value, arguments[1].clone()];
|
||||||
|
|
||||||
|
let result = match self {
|
||||||
|
AddAssign => Operator::Add.eval(&arguments, context),
|
||||||
|
SubAssign => Operator::Sub.eval(&arguments, context),
|
||||||
|
MulAssign => Operator::Mul.eval(&arguments, context),
|
||||||
|
DivAssign => Operator::Div.eval(&arguments, context),
|
||||||
|
ModAssign => Operator::Mod.eval(&arguments, context),
|
||||||
|
ExpAssign => Operator::Exp.eval(&arguments, context),
|
||||||
|
AndAssign => Operator::And.eval(&arguments, context),
|
||||||
|
OrAssign => Operator::Or.eval(&arguments, context),
|
||||||
|
_ => unreachable!(
|
||||||
|
"Forgot to add a match arm for an assign operation: {}",
|
||||||
|
self
|
||||||
|
),
|
||||||
|
}?;
|
||||||
|
context.set_value(target, result)?;
|
||||||
|
|
||||||
|
Ok(Value::Empty)
|
||||||
|
}
|
||||||
|
_ => self.eval(arguments, context),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Operator {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
use crate::operator::Operator::*;
|
||||||
|
match self {
|
||||||
|
RootNode => Ok(()),
|
||||||
|
Add => write!(f, "+"),
|
||||||
|
Sub => write!(f, "-"),
|
||||||
|
Neg => write!(f, "-"),
|
||||||
|
Mul => write!(f, "*"),
|
||||||
|
Div => write!(f, "/"),
|
||||||
|
Mod => write!(f, "%"),
|
||||||
|
Exp => write!(f, "^"),
|
||||||
|
|
||||||
|
Eq => write!(f, "=="),
|
||||||
|
Neq => write!(f, "!="),
|
||||||
|
Gt => write!(f, ">"),
|
||||||
|
Lt => write!(f, "<"),
|
||||||
|
Geq => write!(f, ">="),
|
||||||
|
Leq => write!(f, "<="),
|
||||||
|
And => write!(f, "&&"),
|
||||||
|
Or => write!(f, "||"),
|
||||||
|
Not => write!(f, "!"),
|
||||||
|
|
||||||
|
Assign => write!(f, " = "),
|
||||||
|
AddAssign => write!(f, " += "),
|
||||||
|
SubAssign => write!(f, " -= "),
|
||||||
|
MulAssign => write!(f, " *= "),
|
||||||
|
DivAssign => write!(f, " /= "),
|
||||||
|
ModAssign => write!(f, " %= "),
|
||||||
|
ExpAssign => write!(f, " ^= "),
|
||||||
|
AndAssign => write!(f, " &&= "),
|
||||||
|
OrAssign => write!(f, " ||= "),
|
||||||
|
|
||||||
|
Tuple => write!(f, ", "),
|
||||||
|
Chain => write!(f, "; "),
|
||||||
|
|
||||||
|
Const { value } => write!(f, "{}", value),
|
||||||
|
VariableIdentifierWrite { identifier } | VariableIdentifierRead { identifier } => {
|
||||||
|
write!(f, "{}", identifier)
|
||||||
|
}
|
||||||
|
FunctionIdentifier { identifier } => write!(f, "{}", identifier),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
562
src/token.rs
Normal file
562
src/token.rs
Normal file
@ -0,0 +1,562 @@
|
|||||||
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
|
pub enum Token {
|
||||||
|
// Arithmetic
|
||||||
|
Plus,
|
||||||
|
Minus,
|
||||||
|
Star,
|
||||||
|
Slash,
|
||||||
|
Percent,
|
||||||
|
Hat,
|
||||||
|
|
||||||
|
// Logic
|
||||||
|
Eq,
|
||||||
|
Neq,
|
||||||
|
Gt,
|
||||||
|
Lt,
|
||||||
|
Geq,
|
||||||
|
Leq,
|
||||||
|
And,
|
||||||
|
Or,
|
||||||
|
Not,
|
||||||
|
|
||||||
|
// Precedence
|
||||||
|
LBrace,
|
||||||
|
RBrace,
|
||||||
|
|
||||||
|
// Assignment
|
||||||
|
Assign,
|
||||||
|
PlusAssign,
|
||||||
|
MinusAssign,
|
||||||
|
StarAssign,
|
||||||
|
SlashAssign,
|
||||||
|
PercentAssign,
|
||||||
|
HatAssign,
|
||||||
|
AndAssign,
|
||||||
|
OrAssign,
|
||||||
|
|
||||||
|
// Special
|
||||||
|
Comma,
|
||||||
|
Semicolon,
|
||||||
|
Yield(String, String),
|
||||||
|
|
||||||
|
// Values, Variables and Functions
|
||||||
|
Identifier(String),
|
||||||
|
Float(f64),
|
||||||
|
Int(i64),
|
||||||
|
Boolean(bool),
|
||||||
|
String(String),
|
||||||
|
Function(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A partial token is an input character whose meaning depends on the characters around it.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum PartialToken {
|
||||||
|
/// A partial token that unambiguously maps to a single token.
|
||||||
|
Token(Token),
|
||||||
|
/// A partial token that is a literal.
|
||||||
|
Literal(String),
|
||||||
|
/// A plus character '+'.
|
||||||
|
Plus,
|
||||||
|
/// A minus character '-'.
|
||||||
|
Minus,
|
||||||
|
/// A star character '*'.
|
||||||
|
Star,
|
||||||
|
/// A slash character '/'.
|
||||||
|
Slash,
|
||||||
|
/// A percent character '%'.
|
||||||
|
Percent,
|
||||||
|
/// A hat character '^'.
|
||||||
|
Hat,
|
||||||
|
/// A whitespace character, e.g. ' '.
|
||||||
|
Whitespace,
|
||||||
|
/// An equal-to character '='.
|
||||||
|
Eq,
|
||||||
|
/// An exclamation mark character '!'.
|
||||||
|
ExclamationMark,
|
||||||
|
/// A greater-than character '>'.
|
||||||
|
Gt,
|
||||||
|
/// A lower-than character '<'.
|
||||||
|
Lt,
|
||||||
|
/// An ampersand character '&'.
|
||||||
|
Ampersand,
|
||||||
|
/// A vertical bar character '|'.
|
||||||
|
VerticalBar,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make this a const fn as soon as is_whitespace and to_string get stable (issue #57563)
|
||||||
|
fn char_to_partial_token(c: char) -> PartialToken {
|
||||||
|
match c {
|
||||||
|
'+' => PartialToken::Plus,
|
||||||
|
'-' => PartialToken::Minus,
|
||||||
|
'*' => PartialToken::Star,
|
||||||
|
'/' => PartialToken::Slash,
|
||||||
|
'%' => PartialToken::Percent,
|
||||||
|
'^' => PartialToken::Hat,
|
||||||
|
|
||||||
|
'(' => PartialToken::Token(Token::LBrace),
|
||||||
|
')' => PartialToken::Token(Token::RBrace),
|
||||||
|
|
||||||
|
',' => PartialToken::Token(Token::Comma),
|
||||||
|
';' => PartialToken::Token(Token::Semicolon),
|
||||||
|
|
||||||
|
'=' => PartialToken::Eq,
|
||||||
|
'!' => PartialToken::ExclamationMark,
|
||||||
|
'>' => PartialToken::Gt,
|
||||||
|
'<' => PartialToken::Lt,
|
||||||
|
'&' => PartialToken::Ampersand,
|
||||||
|
'|' => PartialToken::VerticalBar,
|
||||||
|
|
||||||
|
c => {
|
||||||
|
if c.is_whitespace() {
|
||||||
|
PartialToken::Whitespace
|
||||||
|
} else {
|
||||||
|
PartialToken::Literal(c.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Token {
|
||||||
|
#[cfg(not(tarpaulin_include))]
|
||||||
|
pub(crate) const fn is_leftsided_value(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Token::Plus => false,
|
||||||
|
Token::Minus => false,
|
||||||
|
Token::Star => false,
|
||||||
|
Token::Slash => false,
|
||||||
|
Token::Percent => false,
|
||||||
|
Token::Hat => false,
|
||||||
|
|
||||||
|
Token::Eq => false,
|
||||||
|
Token::Neq => false,
|
||||||
|
Token::Gt => false,
|
||||||
|
Token::Lt => false,
|
||||||
|
Token::Geq => false,
|
||||||
|
Token::Leq => false,
|
||||||
|
Token::And => false,
|
||||||
|
Token::Or => false,
|
||||||
|
Token::Not => false,
|
||||||
|
|
||||||
|
Token::LBrace => true,
|
||||||
|
Token::RBrace => false,
|
||||||
|
|
||||||
|
Token::Comma => false,
|
||||||
|
Token::Semicolon => false,
|
||||||
|
Token::Yield(_, _) => false,
|
||||||
|
|
||||||
|
Token::Assign => false,
|
||||||
|
Token::PlusAssign => false,
|
||||||
|
Token::MinusAssign => false,
|
||||||
|
Token::StarAssign => false,
|
||||||
|
Token::SlashAssign => false,
|
||||||
|
Token::PercentAssign => false,
|
||||||
|
Token::HatAssign => false,
|
||||||
|
Token::AndAssign => false,
|
||||||
|
Token::OrAssign => false,
|
||||||
|
|
||||||
|
Token::Identifier(_) => true,
|
||||||
|
Token::Float(_) => true,
|
||||||
|
Token::Int(_) => true,
|
||||||
|
Token::Boolean(_) => true,
|
||||||
|
Token::String(_) => true,
|
||||||
|
Token::Function(_) => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(tarpaulin_include))]
|
||||||
|
pub(crate) const fn is_rightsided_value(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Token::Plus => false,
|
||||||
|
Token::Minus => false,
|
||||||
|
Token::Star => false,
|
||||||
|
Token::Slash => false,
|
||||||
|
Token::Percent => false,
|
||||||
|
Token::Hat => false,
|
||||||
|
|
||||||
|
Token::Eq => false,
|
||||||
|
Token::Neq => false,
|
||||||
|
Token::Gt => false,
|
||||||
|
Token::Lt => false,
|
||||||
|
Token::Geq => false,
|
||||||
|
Token::Leq => false,
|
||||||
|
Token::And => false,
|
||||||
|
Token::Or => false,
|
||||||
|
Token::Not => false,
|
||||||
|
|
||||||
|
Token::LBrace => false,
|
||||||
|
Token::RBrace => true,
|
||||||
|
|
||||||
|
Token::Comma => false,
|
||||||
|
Token::Semicolon => false,
|
||||||
|
Token::Yield(_, _) => false,
|
||||||
|
|
||||||
|
Token::Assign => false,
|
||||||
|
Token::PlusAssign => false,
|
||||||
|
Token::MinusAssign => false,
|
||||||
|
Token::StarAssign => false,
|
||||||
|
Token::SlashAssign => false,
|
||||||
|
Token::PercentAssign => false,
|
||||||
|
Token::HatAssign => false,
|
||||||
|
Token::AndAssign => false,
|
||||||
|
Token::OrAssign => false,
|
||||||
|
|
||||||
|
Token::Identifier(_) => true,
|
||||||
|
Token::Float(_) => true,
|
||||||
|
Token::Int(_) => true,
|
||||||
|
Token::Boolean(_) => true,
|
||||||
|
Token::String(_) => true,
|
||||||
|
Token::Function(_) => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(tarpaulin_include))]
|
||||||
|
pub(crate) fn is_assignment(&self) -> bool {
|
||||||
|
use Token::*;
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
Assign
|
||||||
|
| PlusAssign
|
||||||
|
| MinusAssign
|
||||||
|
| StarAssign
|
||||||
|
| SlashAssign
|
||||||
|
| PercentAssign
|
||||||
|
| HatAssign
|
||||||
|
| AndAssign
|
||||||
|
| OrAssign
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses an escape sequence within a string literal.
|
||||||
|
fn parse_escape_sequence<Iter: Iterator<Item = char>>(iter: &mut Iter) -> Result<char> {
|
||||||
|
match iter.next() {
|
||||||
|
Some('"') => Ok('"'),
|
||||||
|
Some('\\') => Ok('\\'),
|
||||||
|
Some(c) => Err(Error::IllegalEscapeSequence(format!("\\{}", c))),
|
||||||
|
None => Err(Error::IllegalEscapeSequence("\\".to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a string value from the given character iterator.
|
||||||
|
///
|
||||||
|
/// The first character from the iterator is interpreted as first character of the string.
|
||||||
|
/// The string is terminated by a double quote `"`.
|
||||||
|
/// Occurrences of `"` within the string can be escaped with `\`.
|
||||||
|
/// The backslash needs to be escaped with another backslash `\`.
|
||||||
|
fn parse_string_literal<Iter: Iterator<Item = char>>(mut iter: &mut Iter) -> Result<PartialToken> {
|
||||||
|
let mut result = String::new();
|
||||||
|
|
||||||
|
while let Some(c) = iter.next() {
|
||||||
|
match c {
|
||||||
|
'"' => break,
|
||||||
|
'\\' => result.push(parse_escape_sequence(&mut iter)?),
|
||||||
|
c => result.push(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(PartialToken::Token(Token::String(result)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_function<Iter: Iterator<Item = char>>(mut iter: &mut Iter) -> Result<PartialToken> {
|
||||||
|
let mut result = String::new();
|
||||||
|
|
||||||
|
while let Some(c) = iter.next() {
|
||||||
|
match c {
|
||||||
|
'\'' => break,
|
||||||
|
'\\' => result.push(parse_escape_sequence(&mut iter)?),
|
||||||
|
c => result.push(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(PartialToken::Token(Token::Function(result)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a string to a vector of partial tokens.
|
||||||
|
fn str_to_partial_tokens(string: &str) -> Result<Vec<PartialToken>> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
let mut iter = string.chars().peekable();
|
||||||
|
|
||||||
|
while let Some(c) = iter.next() {
|
||||||
|
if c == '"' {
|
||||||
|
result.push(parse_string_literal(&mut iter)?);
|
||||||
|
} else if c == '\'' {
|
||||||
|
result.push(parse_function(&mut iter)?)
|
||||||
|
} else {
|
||||||
|
let partial_token = char_to_partial_token(c);
|
||||||
|
|
||||||
|
let if_let_successful =
|
||||||
|
if let (Some(PartialToken::Literal(last)), PartialToken::Literal(literal)) =
|
||||||
|
(result.last_mut(), &partial_token)
|
||||||
|
{
|
||||||
|
last.push_str(literal);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
if !if_let_successful {
|
||||||
|
result.push(partial_token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves all partial tokens by converting them to complex tokens.
|
||||||
|
fn partial_tokens_to_tokens(mut tokens: &[PartialToken]) -> Result<Vec<Token>> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
while !tokens.is_empty() {
|
||||||
|
let first = tokens[0].clone();
|
||||||
|
let second = tokens.get(1).cloned();
|
||||||
|
let third = tokens.get(2).cloned();
|
||||||
|
let mut cutoff = 2;
|
||||||
|
|
||||||
|
result.extend(
|
||||||
|
match first {
|
||||||
|
PartialToken::Token(token) => {
|
||||||
|
cutoff = 1;
|
||||||
|
Some(token)
|
||||||
|
}
|
||||||
|
PartialToken::Plus => match second {
|
||||||
|
Some(PartialToken::Eq) => Some(Token::PlusAssign),
|
||||||
|
_ => {
|
||||||
|
cutoff = 1;
|
||||||
|
Some(Token::Plus)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PartialToken::Minus => match second {
|
||||||
|
Some(PartialToken::Eq) => Some(Token::MinusAssign),
|
||||||
|
_ => {
|
||||||
|
cutoff = 1;
|
||||||
|
Some(Token::Minus)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PartialToken::Star => match second {
|
||||||
|
Some(PartialToken::Eq) => Some(Token::StarAssign),
|
||||||
|
_ => {
|
||||||
|
cutoff = 1;
|
||||||
|
Some(Token::Star)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PartialToken::Slash => match second {
|
||||||
|
Some(PartialToken::Eq) => Some(Token::SlashAssign),
|
||||||
|
_ => {
|
||||||
|
cutoff = 1;
|
||||||
|
Some(Token::Slash)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PartialToken::Percent => match second {
|
||||||
|
Some(PartialToken::Eq) => Some(Token::PercentAssign),
|
||||||
|
_ => {
|
||||||
|
cutoff = 1;
|
||||||
|
Some(Token::Percent)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PartialToken::Hat => match second {
|
||||||
|
Some(PartialToken::Eq) => Some(Token::HatAssign),
|
||||||
|
_ => {
|
||||||
|
cutoff = 1;
|
||||||
|
Some(Token::Hat)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PartialToken::Literal(literal) => {
|
||||||
|
cutoff = 1;
|
||||||
|
if let Ok(number) = parse_dec_or_hex(&literal) {
|
||||||
|
Some(Token::Int(number))
|
||||||
|
} else if let Ok(number) = literal.parse::<f64>() {
|
||||||
|
Some(Token::Float(number))
|
||||||
|
} else if let Ok(boolean) = literal.parse::<bool>() {
|
||||||
|
Some(Token::Boolean(boolean))
|
||||||
|
} else {
|
||||||
|
// If there are two tokens following this one, check if the next one is
|
||||||
|
// a plus or a minus. If so, then attempt to parse all three tokens as a
|
||||||
|
// scientific notation number of the form `<coefficient>e{+,-}<exponent>`,
|
||||||
|
// for example [Literal("10e"), Minus, Literal("3")] => "1e-3".parse().
|
||||||
|
match (second, third) {
|
||||||
|
(Some(second), Some(third))
|
||||||
|
if second == PartialToken::Minus
|
||||||
|
|| second == PartialToken::Plus =>
|
||||||
|
{
|
||||||
|
if let Ok(number) =
|
||||||
|
format!("{}{}{}", literal, second, third).parse::<f64>()
|
||||||
|
{
|
||||||
|
cutoff = 3;
|
||||||
|
Some(Token::Float(number))
|
||||||
|
} else {
|
||||||
|
Some(Token::Identifier(literal.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Some(Token::Identifier(literal.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PartialToken::Whitespace => {
|
||||||
|
cutoff = 1;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
PartialToken::Eq => match second {
|
||||||
|
Some(PartialToken::Eq) => Some(Token::Eq),
|
||||||
|
_ => {
|
||||||
|
cutoff = 1;
|
||||||
|
Some(Token::Assign)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PartialToken::ExclamationMark => match second {
|
||||||
|
Some(PartialToken::Eq) => Some(Token::Neq),
|
||||||
|
_ => {
|
||||||
|
cutoff = 1;
|
||||||
|
Some(Token::Not)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PartialToken::Gt => match second {
|
||||||
|
Some(PartialToken::Eq) => Some(Token::Geq),
|
||||||
|
_ => {
|
||||||
|
cutoff = 1;
|
||||||
|
Some(Token::Gt)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PartialToken::Lt => match second {
|
||||||
|
Some(PartialToken::Eq) => Some(Token::Leq),
|
||||||
|
_ => {
|
||||||
|
cutoff = 1;
|
||||||
|
Some(Token::Lt)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PartialToken::Ampersand => match second {
|
||||||
|
Some(PartialToken::Ampersand) => match third {
|
||||||
|
Some(PartialToken::Eq) => {
|
||||||
|
cutoff = 3;
|
||||||
|
Some(Token::AndAssign)
|
||||||
|
}
|
||||||
|
_ => Some(Token::And),
|
||||||
|
},
|
||||||
|
_ => return Err(Error::unmatched_partial_token(first, second)),
|
||||||
|
},
|
||||||
|
PartialToken::VerticalBar => match second {
|
||||||
|
Some(PartialToken::VerticalBar) => match third {
|
||||||
|
Some(PartialToken::Eq) => {
|
||||||
|
cutoff = 3;
|
||||||
|
Some(Token::OrAssign)
|
||||||
|
}
|
||||||
|
_ => Some(Token::Or),
|
||||||
|
},
|
||||||
|
_ => return Err(Error::unmatched_partial_token(first, second)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.into_iter(),
|
||||||
|
);
|
||||||
|
|
||||||
|
tokens = &tokens[cutoff..];
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Token {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
use self::Token::*;
|
||||||
|
match self {
|
||||||
|
Plus => write!(f, "+"),
|
||||||
|
Minus => write!(f, "-"),
|
||||||
|
Star => write!(f, "*"),
|
||||||
|
Slash => write!(f, "/"),
|
||||||
|
Percent => write!(f, "%"),
|
||||||
|
Hat => write!(f, "^"),
|
||||||
|
|
||||||
|
// Logic
|
||||||
|
Eq => write!(f, "=="),
|
||||||
|
Neq => write!(f, "!="),
|
||||||
|
Gt => write!(f, ">"),
|
||||||
|
Lt => write!(f, "<"),
|
||||||
|
Geq => write!(f, ">="),
|
||||||
|
Leq => write!(f, "<="),
|
||||||
|
And => write!(f, "&&"),
|
||||||
|
Or => write!(f, "||"),
|
||||||
|
Not => write!(f, "!"),
|
||||||
|
|
||||||
|
// Precedence
|
||||||
|
LBrace => write!(f, "("),
|
||||||
|
RBrace => write!(f, ")"),
|
||||||
|
|
||||||
|
// Assignment
|
||||||
|
Assign => write!(f, "="),
|
||||||
|
PlusAssign => write!(f, "+="),
|
||||||
|
MinusAssign => write!(f, "-="),
|
||||||
|
StarAssign => write!(f, "*="),
|
||||||
|
SlashAssign => write!(f, "/="),
|
||||||
|
PercentAssign => write!(f, "%="),
|
||||||
|
HatAssign => write!(f, "^="),
|
||||||
|
AndAssign => write!(f, "&&="),
|
||||||
|
OrAssign => write!(f, "||="),
|
||||||
|
|
||||||
|
// Special
|
||||||
|
Comma => write!(f, ","),
|
||||||
|
Semicolon => write!(f, ";"),
|
||||||
|
|
||||||
|
// Values => write!(f, ""), Variables and Functions
|
||||||
|
Identifier(identifier) => identifier.fmt(f),
|
||||||
|
Float(float) => float.fmt(f),
|
||||||
|
Int(int) => int.fmt(f),
|
||||||
|
Boolean(boolean) => boolean.fmt(f),
|
||||||
|
String(string) => fmt::Debug::fmt(string, f),
|
||||||
|
Function(string) => write!(f, "'{string}'"),
|
||||||
|
Yield(_, _) => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for PartialToken {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
use self::PartialToken::*;
|
||||||
|
match self {
|
||||||
|
Token(token) => token.fmt(f),
|
||||||
|
Literal(literal) => literal.fmt(f),
|
||||||
|
Whitespace => write!(f, " "),
|
||||||
|
Plus => write!(f, "+"),
|
||||||
|
Minus => write!(f, "-"),
|
||||||
|
Star => write!(f, "*"),
|
||||||
|
Slash => write!(f, "/"),
|
||||||
|
Percent => write!(f, "%"),
|
||||||
|
Hat => write!(f, "^"),
|
||||||
|
Eq => write!(f, "="),
|
||||||
|
ExclamationMark => write!(f, "!"),
|
||||||
|
Gt => write!(f, ">"),
|
||||||
|
Lt => write!(f, "<"),
|
||||||
|
Ampersand => write!(f, "&"),
|
||||||
|
VerticalBar => write!(f, "|"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn tokenize(string: &str) -> Result<Vec<Token>> {
|
||||||
|
partial_tokens_to_tokens(&str_to_partial_tokens(string)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_dec_or_hex(literal: &str) -> std::result::Result<i64, std::num::ParseIntError> {
|
||||||
|
if let Some(literal) = literal.strip_prefix("0x") {
|
||||||
|
literal.parse()
|
||||||
|
} else {
|
||||||
|
literal.parse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::token::{tokenize, Token};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn assignment_lhs_is_identifier() {
|
||||||
|
let tokens = tokenize("a = 1").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
tokens.as_slice(),
|
||||||
|
[
|
||||||
|
Token::Identifier("a".to_string()),
|
||||||
|
Token::Assign,
|
||||||
|
Token::Int(1)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
94
src/tree/iter.rs
Normal file
94
src/tree/iter.rs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
use crate::{operator::Operator, Node};
|
||||||
|
use std::slice::{Iter, IterMut};
|
||||||
|
|
||||||
|
/// An iterator that traverses an operator tree in pre-order.
|
||||||
|
pub struct NodeIter<'a> {
|
||||||
|
stack: Vec<Iter<'a, Node>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> NodeIter<'a> {
|
||||||
|
fn new(node: &'a Node) -> Self {
|
||||||
|
NodeIter {
|
||||||
|
stack: vec![node.children.iter()],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for NodeIter<'a> {
|
||||||
|
type Item = &'a Node;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
loop {
|
||||||
|
let mut result = None;
|
||||||
|
|
||||||
|
if let Some(last) = self.stack.last_mut() {
|
||||||
|
if let Some(next) = last.next() {
|
||||||
|
result = Some(next);
|
||||||
|
} else {
|
||||||
|
// Can not fail because we just borrowed last.
|
||||||
|
// We just checked that the iterator is empty, so we can safely discard it.
|
||||||
|
let _ = self.stack.pop().unwrap();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(result) = result {
|
||||||
|
self.stack.push(result.children.iter());
|
||||||
|
return Some(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An iterator that mutably traverses an operator tree in pre-order.
|
||||||
|
pub struct OperatorIterMut<'a> {
|
||||||
|
stack: Vec<IterMut<'a, Node>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> OperatorIterMut<'a> {
|
||||||
|
fn new(node: &'a mut Node) -> Self {
|
||||||
|
OperatorIterMut {
|
||||||
|
stack: vec![node.children.iter_mut()],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for OperatorIterMut<'a> {
|
||||||
|
type Item = &'a mut Operator;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
loop {
|
||||||
|
let mut result = None;
|
||||||
|
|
||||||
|
if let Some(last) = self.stack.last_mut() {
|
||||||
|
if let Some(next) = last.next() {
|
||||||
|
result = Some(next);
|
||||||
|
} else {
|
||||||
|
// Can not fail because we just borrowed last.
|
||||||
|
// We just checked that the iterator is empty, so we can safely discard it.
|
||||||
|
let _ = self.stack.pop().unwrap();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(result) = result {
|
||||||
|
self.stack.push(result.children.iter_mut());
|
||||||
|
return Some(&mut result.operator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Node {
|
||||||
|
/// Returns an iterator over all nodes in this tree.
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = &Node> {
|
||||||
|
NodeIter::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a mutable iterator over all operators in this tree.
|
||||||
|
pub fn iter_operators_mut(&mut self) -> impl Iterator<Item = &mut Operator> {
|
||||||
|
OperatorIterMut::new(self)
|
||||||
|
}
|
||||||
|
}
|
905
src/tree/mod.rs
Normal file
905
src/tree/mod.rs
Normal file
@ -0,0 +1,905 @@
|
|||||||
|
use crate::Function;
|
||||||
|
use crate::{token::Token, VariableMap};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::{Error, Result},
|
||||||
|
operator::*,
|
||||||
|
value::Value,
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
fmt::{self, Display, Formatter},
|
||||||
|
mem,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Exclude display module from coverage, as it prints not well-defined prefix notation.
|
||||||
|
#[cfg(not(tarpaulin_include))]
|
||||||
|
mod iter;
|
||||||
|
|
||||||
|
/// A node in the operator tree.
|
||||||
|
/// The operator tree is created by the crate-level `build_operator_tree` method.
|
||||||
|
/// It can be evaluated for a given context with the `Node::eval` method.
|
||||||
|
///
|
||||||
|
/// The advantage of constructing the operator tree separately from the actual evaluation is that it can be evaluated arbitrarily often with different contexts.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use evalexpr::*;
|
||||||
|
///
|
||||||
|
/// let mut context = HashMapContext::new();
|
||||||
|
/// context.set_value("alpha".into(), 2.into()).unwrap(); // Do proper error handling here
|
||||||
|
/// let node = build_operator_tree("1 + alpha").unwrap(); // Do proper error handling here
|
||||||
|
/// assert_eq!(node.eval_with_context(&context), Ok(Value::from(3)));
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
pub struct Node {
|
||||||
|
operator: Operator,
|
||||||
|
children: Vec<Node>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Node {
|
||||||
|
fn new(operator: Operator) -> Self {
|
||||||
|
Self {
|
||||||
|
children: Vec::new(),
|
||||||
|
operator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn root_node() -> Self {
|
||||||
|
Self::new(Operator::RootNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over all identifiers in this expression.
|
||||||
|
/// Each occurrence of an identifier is returned separately.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use evalexpr::*;
|
||||||
|
///
|
||||||
|
/// let tree = build_operator_tree("a + b + c * f()").unwrap(); // Do proper error handling here
|
||||||
|
/// let mut iter = tree.iter_identifiers();
|
||||||
|
/// assert_eq!(iter.next(), Some("a"));
|
||||||
|
/// assert_eq!(iter.next(), Some("b"));
|
||||||
|
/// assert_eq!(iter.next(), Some("c"));
|
||||||
|
/// assert_eq!(iter.next(), Some("f"));
|
||||||
|
/// assert_eq!(iter.next(), None);
|
||||||
|
/// ```
|
||||||
|
pub fn iter_identifiers(&self) -> impl Iterator<Item = &str> {
|
||||||
|
self.iter().filter_map(|node| match node.operator() {
|
||||||
|
Operator::VariableIdentifierWrite { identifier }
|
||||||
|
| Operator::VariableIdentifierRead { identifier }
|
||||||
|
| Operator::FunctionIdentifier { identifier } => Some(identifier.as_str()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over all identifiers in this expression, allowing mutation.
|
||||||
|
/// Each occurrence of an identifier is returned separately.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use evalexpr::*;
|
||||||
|
///
|
||||||
|
/// let mut tree = build_operator_tree("a + b + c * f()").unwrap(); // Do proper error handling here
|
||||||
|
///
|
||||||
|
/// for identifier in tree.iter_identifiers_mut() {
|
||||||
|
/// *identifier = String::from("x");
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// let mut iter = tree.iter_identifiers();
|
||||||
|
///
|
||||||
|
/// assert_eq!(iter.next(), Some("x"));
|
||||||
|
/// assert_eq!(iter.next(), Some("x"));
|
||||||
|
/// assert_eq!(iter.next(), Some("x"));
|
||||||
|
/// assert_eq!(iter.next(), Some("x"));
|
||||||
|
/// assert_eq!(iter.next(), None);
|
||||||
|
/// ```
|
||||||
|
pub fn iter_identifiers_mut(&mut self) -> impl Iterator<Item = &mut String> {
|
||||||
|
self.iter_operators_mut()
|
||||||
|
.filter_map(|operator| match operator {
|
||||||
|
Operator::VariableIdentifierWrite { identifier }
|
||||||
|
| Operator::VariableIdentifierRead { identifier }
|
||||||
|
| Operator::FunctionIdentifier { identifier } => Some(identifier),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over all variable identifiers in this expression.
|
||||||
|
/// Each occurrence of a variable identifier is returned separately.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use evalexpr::*;
|
||||||
|
///
|
||||||
|
/// let tree = build_operator_tree("a + f(b + c)").unwrap(); // Do proper error handling here
|
||||||
|
/// let mut iter = tree.iter_variable_identifiers();
|
||||||
|
/// assert_eq!(iter.next(), Some("a"));
|
||||||
|
/// assert_eq!(iter.next(), Some("b"));
|
||||||
|
/// assert_eq!(iter.next(), Some("c"));
|
||||||
|
/// assert_eq!(iter.next(), None);
|
||||||
|
/// ```
|
||||||
|
pub fn iter_variable_identifiers(&self) -> impl Iterator<Item = &str> {
|
||||||
|
self.iter().filter_map(|node| match node.operator() {
|
||||||
|
Operator::VariableIdentifierWrite { identifier }
|
||||||
|
| Operator::VariableIdentifierRead { identifier } => Some(identifier.as_str()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over all variable identifiers in this expression, allowing mutation.
|
||||||
|
/// Each occurrence of a variable identifier is returned separately.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use evalexpr::*;
|
||||||
|
///
|
||||||
|
/// let mut tree = build_operator_tree("a + b + c * f()").unwrap(); // Do proper error handling here
|
||||||
|
///
|
||||||
|
/// for identifier in tree.iter_variable_identifiers_mut() {
|
||||||
|
/// *identifier = String::from("x");
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// let mut iter = tree.iter_identifiers();
|
||||||
|
///
|
||||||
|
/// assert_eq!(iter.next(), Some("x"));
|
||||||
|
/// assert_eq!(iter.next(), Some("x"));
|
||||||
|
/// assert_eq!(iter.next(), Some("x"));
|
||||||
|
/// assert_eq!(iter.next(), Some("f"));
|
||||||
|
/// assert_eq!(iter.next(), None);
|
||||||
|
/// ```
|
||||||
|
pub fn iter_variable_identifiers_mut(&mut self) -> impl Iterator<Item = &mut String> {
|
||||||
|
self.iter_operators_mut()
|
||||||
|
.filter_map(|operator| match operator {
|
||||||
|
Operator::VariableIdentifierWrite { identifier }
|
||||||
|
| Operator::VariableIdentifierRead { identifier } => Some(identifier),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over all read variable identifiers in this expression.
|
||||||
|
/// Each occurrence of a variable identifier is returned separately.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use evalexpr::*;
|
||||||
|
///
|
||||||
|
/// let tree = build_operator_tree("d = a + f(b + c)").unwrap(); // Do proper error handling here
|
||||||
|
/// let mut iter = tree.iter_read_variable_identifiers();
|
||||||
|
/// assert_eq!(iter.next(), Some("a"));
|
||||||
|
/// assert_eq!(iter.next(), Some("b"));
|
||||||
|
/// assert_eq!(iter.next(), Some("c"));
|
||||||
|
/// assert_eq!(iter.next(), None);
|
||||||
|
/// ```
|
||||||
|
pub fn iter_read_variable_identifiers(&self) -> impl Iterator<Item = &str> {
|
||||||
|
self.iter().filter_map(|node| match node.operator() {
|
||||||
|
Operator::VariableIdentifierRead { identifier } => Some(identifier.as_str()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over all read variable identifiers in this expression, allowing mutation.
|
||||||
|
/// Each occurrence of a variable identifier is returned separately.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use evalexpr::*;
|
||||||
|
///
|
||||||
|
/// let mut tree = build_operator_tree("d = a + f(b + c)").unwrap(); // Do proper error handling here
|
||||||
|
///
|
||||||
|
/// for identifier in tree.iter_read_variable_identifiers_mut() {
|
||||||
|
/// *identifier = String::from("x");
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// let mut iter = tree.iter_identifiers();
|
||||||
|
///
|
||||||
|
/// assert_eq!(iter.next(), Some("d"));
|
||||||
|
/// assert_eq!(iter.next(), Some("x"));
|
||||||
|
/// assert_eq!(iter.next(), Some("f"));
|
||||||
|
/// assert_eq!(iter.next(), Some("x"));
|
||||||
|
/// assert_eq!(iter.next(), Some("x"));
|
||||||
|
/// assert_eq!(iter.next(), None);
|
||||||
|
/// ```
|
||||||
|
pub fn iter_read_variable_identifiers_mut(&mut self) -> impl Iterator<Item = &mut String> {
|
||||||
|
self.iter_operators_mut()
|
||||||
|
.filter_map(|operator| match operator {
|
||||||
|
Operator::VariableIdentifierRead { identifier } => Some(identifier),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over all write variable identifiers in this expression.
|
||||||
|
/// Each occurrence of a variable identifier is returned separately.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use evalexpr::*;
|
||||||
|
///
|
||||||
|
/// let tree = build_operator_tree("d = a + f(b + c)").unwrap(); // Do proper error handling here
|
||||||
|
/// let mut iter = tree.iter_write_variable_identifiers();
|
||||||
|
/// assert_eq!(iter.next(), Some("d"));
|
||||||
|
/// assert_eq!(iter.next(), None);
|
||||||
|
/// ```
|
||||||
|
pub fn iter_write_variable_identifiers(&self) -> impl Iterator<Item = &str> {
|
||||||
|
self.iter().filter_map(|node| match node.operator() {
|
||||||
|
Operator::VariableIdentifierWrite { identifier } => Some(identifier.as_str()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over all write variable identifiers in this expression, allowing mutation.
|
||||||
|
/// Each occurrence of a variable identifier is returned separately.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use evalexpr::*;
|
||||||
|
///
|
||||||
|
/// let mut tree = build_operator_tree("d = a + f(b + c)").unwrap(); // Do proper error handling here
|
||||||
|
///
|
||||||
|
/// for identifier in tree.iter_write_variable_identifiers_mut() {
|
||||||
|
/// *identifier = String::from("x");
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// let mut iter = tree.iter_identifiers();
|
||||||
|
///
|
||||||
|
/// assert_eq!(iter.next(), Some("x"));
|
||||||
|
/// assert_eq!(iter.next(), Some("a"));
|
||||||
|
/// assert_eq!(iter.next(), Some("f"));
|
||||||
|
/// assert_eq!(iter.next(), Some("b"));
|
||||||
|
/// assert_eq!(iter.next(), Some("c"));
|
||||||
|
/// assert_eq!(iter.next(), None);
|
||||||
|
/// ```
|
||||||
|
pub fn iter_write_variable_identifiers_mut(&mut self) -> impl Iterator<Item = &mut String> {
|
||||||
|
self.iter_operators_mut()
|
||||||
|
.filter_map(|operator| match operator {
|
||||||
|
Operator::VariableIdentifierWrite { identifier } => Some(identifier),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over all function identifiers in this expression.
|
||||||
|
/// Each occurrence of a function identifier is returned separately.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use evalexpr::*;
|
||||||
|
///
|
||||||
|
/// let tree = build_operator_tree("a + f(b + c)").unwrap(); // Do proper error handling here
|
||||||
|
/// let mut iter = tree.iter_function_identifiers();
|
||||||
|
/// assert_eq!(iter.next(), Some("f"));
|
||||||
|
/// assert_eq!(iter.next(), None);
|
||||||
|
/// ```
|
||||||
|
pub fn iter_function_identifiers(&self) -> impl Iterator<Item = &str> {
|
||||||
|
self.iter().filter_map(|node| match node.operator() {
|
||||||
|
Operator::FunctionIdentifier { identifier } => Some(identifier.as_str()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over all function identifiers in this expression, allowing mutation.
|
||||||
|
/// Each occurrence of a variable identifier is returned separately.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use evalexpr::*;
|
||||||
|
///
|
||||||
|
/// let mut tree = build_operator_tree("d = a + f(b + c)").unwrap(); // Do proper error handling here
|
||||||
|
///
|
||||||
|
/// for identifier in tree.iter_function_identifiers_mut() {
|
||||||
|
/// *identifier = String::from("x");
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// let mut iter = tree.iter_identifiers();
|
||||||
|
///
|
||||||
|
/// assert_eq!(iter.next(), Some("d"));
|
||||||
|
/// assert_eq!(iter.next(), Some("a"));
|
||||||
|
/// assert_eq!(iter.next(), Some("x"));
|
||||||
|
/// assert_eq!(iter.next(), Some("b"));
|
||||||
|
/// assert_eq!(iter.next(), Some("c"));
|
||||||
|
/// assert_eq!(iter.next(), None);
|
||||||
|
/// ```
|
||||||
|
pub fn iter_function_identifiers_mut(&mut self) -> impl Iterator<Item = &mut String> {
|
||||||
|
self.iter_operators_mut()
|
||||||
|
.filter_map(|operator| match operator {
|
||||||
|
Operator::FunctionIdentifier { identifier } => Some(identifier),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates the operator tree rooted at this node with the given context.
|
||||||
|
///
|
||||||
|
/// Fails, if one of the operators in the expression tree fails.
|
||||||
|
pub fn eval_with_context(&self, context: &VariableMap) -> Result<Value> {
|
||||||
|
let mut arguments = Vec::new();
|
||||||
|
for child in self.children() {
|
||||||
|
arguments.push(child.eval_with_context(context)?);
|
||||||
|
}
|
||||||
|
self.operator().eval(&arguments, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates the operator tree rooted at this node with the given mutable context.
|
||||||
|
///
|
||||||
|
/// Fails, if one of the operators in the expression tree fails.
|
||||||
|
pub fn eval_with_context_mut(&self, context: &mut VariableMap) -> Result<Value> {
|
||||||
|
let mut arguments = Vec::new();
|
||||||
|
for child in self.children() {
|
||||||
|
arguments.push(child.eval_with_context_mut(context)?);
|
||||||
|
}
|
||||||
|
self.operator().eval_mut(&arguments, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates the operator tree rooted at this node.
|
||||||
|
///
|
||||||
|
/// Fails, if one of the operators in the expression tree fails.
|
||||||
|
pub fn eval(&self) -> Result<Value> {
|
||||||
|
self.eval_with_context_mut(&mut VariableMap::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates the operator tree rooted at this node into a string with an the given context.
|
||||||
|
///
|
||||||
|
/// Fails, if one of the operators in the expression tree fails.
|
||||||
|
pub fn eval_string_with_context(&self, context: &VariableMap) -> Result<String> {
|
||||||
|
match self.eval_with_context(context) {
|
||||||
|
Ok(Value::String(string)) => Ok(string),
|
||||||
|
Ok(value) => Err(Error::expected_string(value)),
|
||||||
|
Err(error) => Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates the operator tree rooted at this node into a float with an the given context.
|
||||||
|
///
|
||||||
|
/// Fails, if one of the operators in the expression tree fails.
|
||||||
|
pub fn eval_float_with_context(&self, context: &VariableMap) -> Result<f64> {
|
||||||
|
match self.eval_with_context(context) {
|
||||||
|
Ok(Value::Float(float)) => Ok(float),
|
||||||
|
Ok(value) => Err(Error::expected_float(value)),
|
||||||
|
Err(error) => Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates the operator tree rooted at this node into an integer with an the given context.
|
||||||
|
///
|
||||||
|
/// Fails, if one of the operators in the expression tree fails.
|
||||||
|
pub fn eval_int_with_context(&self, context: &VariableMap) -> Result<i64> {
|
||||||
|
match self.eval_with_context(context) {
|
||||||
|
Ok(Value::Integer(int)) => Ok(int),
|
||||||
|
Ok(value) => Err(Error::expected_int(value)),
|
||||||
|
Err(error) => Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates the operator tree rooted at this node into a float with an the given context.
|
||||||
|
/// If the result of the expression is an integer, it is silently converted into a float.
|
||||||
|
///
|
||||||
|
/// Fails, if one of the operators in the expression tree fails.
|
||||||
|
pub fn eval_number_with_context(&self, context: &VariableMap) -> Result<f64> {
|
||||||
|
match self.eval_with_context(context) {
|
||||||
|
Ok(Value::Integer(int)) => Ok(int as f64),
|
||||||
|
Ok(Value::Float(float)) => Ok(float),
|
||||||
|
Ok(value) => Err(Error::expected_number(value)),
|
||||||
|
Err(error) => Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates the operator tree rooted at this node into a boolean with an the given context.
|
||||||
|
///
|
||||||
|
/// Fails, if one of the operators in the expression tree fails.
|
||||||
|
pub fn eval_boolean_with_context(&self, context: &VariableMap) -> Result<bool> {
|
||||||
|
match self.eval_with_context(context) {
|
||||||
|
Ok(Value::Boolean(boolean)) => Ok(boolean),
|
||||||
|
Ok(value) => Err(Error::expected_boolean(value)),
|
||||||
|
Err(error) => Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates the operator tree rooted at this node into a tuple with an the given context.
|
||||||
|
///
|
||||||
|
/// Fails, if one of the operators in the expression tree fails.
|
||||||
|
pub fn eval_tuple_with_context(&self, context: &VariableMap) -> Result<Vec<Value>> {
|
||||||
|
match self.eval_with_context(context) {
|
||||||
|
Ok(Value::List(tuple)) => Ok(tuple),
|
||||||
|
Ok(value) => Err(Error::expected_list(value)),
|
||||||
|
Err(error) => Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates the operator tree rooted at this node into an empty value with an the given context.
|
||||||
|
///
|
||||||
|
/// Fails, if one of the operators in the expression tree fails.
|
||||||
|
pub fn eval_empty_with_context(&self, context: &VariableMap) -> Result<()> {
|
||||||
|
match self.eval_with_context(context) {
|
||||||
|
Ok(Value::Empty) => Ok(()),
|
||||||
|
Ok(value) => Err(Error::expected_empty(value)),
|
||||||
|
Err(error) => Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates the operator tree rooted at this node into a string with an the given mutable context.
|
||||||
|
///
|
||||||
|
/// Fails, if one of the operators in the expression tree fails.
|
||||||
|
pub fn eval_string_with_context_mut(&self, context: &mut VariableMap) -> Result<String> {
|
||||||
|
match self.eval_with_context_mut(context) {
|
||||||
|
Ok(Value::String(string)) => Ok(string),
|
||||||
|
Ok(value) => Err(Error::expected_string(value)),
|
||||||
|
Err(error) => Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates the operator tree rooted at this node into a float with an the given mutable context.
|
||||||
|
///
|
||||||
|
/// Fails, if one of the operators in the expression tree fails.
|
||||||
|
pub fn eval_float_with_context_mut(&self, context: &mut VariableMap) -> Result<f64> {
|
||||||
|
match self.eval_with_context_mut(context) {
|
||||||
|
Ok(Value::Float(float)) => Ok(float),
|
||||||
|
Ok(value) => Err(Error::expected_float(value)),
|
||||||
|
Err(error) => Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates the operator tree rooted at this node into an integer with an the given mutable context.
|
||||||
|
///
|
||||||
|
/// Fails, if one of the operators in the expression tree fails.
|
||||||
|
pub fn eval_int_with_context_mut(&self, context: &mut VariableMap) -> Result<i64> {
|
||||||
|
match self.eval_with_context_mut(context) {
|
||||||
|
Ok(Value::Integer(int)) => Ok(int),
|
||||||
|
Ok(value) => Err(Error::expected_int(value)),
|
||||||
|
Err(error) => Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates the operator tree rooted at this node into a float with an the given mutable context.
|
||||||
|
/// If the result of the expression is an integer, it is silently converted into a float.
|
||||||
|
///
|
||||||
|
/// Fails, if one of the operators in the expression tree fails.
|
||||||
|
pub fn eval_number_with_context_mut(&self, context: &mut VariableMap) -> Result<f64> {
|
||||||
|
match self.eval_with_context_mut(context) {
|
||||||
|
Ok(Value::Integer(int)) => Ok(int as f64),
|
||||||
|
Ok(Value::Float(float)) => Ok(float),
|
||||||
|
Ok(value) => Err(Error::expected_number(value)),
|
||||||
|
Err(error) => Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates the operator tree rooted at this node into a boolean with an the given mutable context.
|
||||||
|
///
|
||||||
|
/// Fails, if one of the operators in the expression tree fails.
|
||||||
|
pub fn eval_boolean_with_context_mut(&self, context: &mut VariableMap) -> Result<bool> {
|
||||||
|
match self.eval_with_context_mut(context) {
|
||||||
|
Ok(Value::Boolean(boolean)) => Ok(boolean),
|
||||||
|
Ok(value) => Err(Error::expected_boolean(value)),
|
||||||
|
Err(error) => Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates the operator tree rooted at this node into a tuple with an the given mutable context.
|
||||||
|
///
|
||||||
|
/// Fails, if one of the operators in the expression tree fails.
|
||||||
|
pub fn eval_tuple_with_context_mut(&self, context: &mut VariableMap) -> Result<Vec<Value>> {
|
||||||
|
match self.eval_with_context_mut(context) {
|
||||||
|
Ok(Value::List(tuple)) => Ok(tuple),
|
||||||
|
Ok(value) => Err(Error::expected_list(value)),
|
||||||
|
Err(error) => Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates the operator tree rooted at this node into an empty value with an the given mutable context.
|
||||||
|
///
|
||||||
|
/// Fails, if one of the operators in the expression tree fails.
|
||||||
|
pub fn eval_empty_with_context_mut(&self, context: &mut VariableMap) -> Result<()> {
|
||||||
|
match self.eval_with_context_mut(context) {
|
||||||
|
Ok(Value::Empty) => Ok(()),
|
||||||
|
Ok(value) => Err(Error::expected_empty(value)),
|
||||||
|
Err(error) => Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates the operator tree rooted at this node into a string.
|
||||||
|
///
|
||||||
|
/// Fails, if one of the operators in the expression tree fails.
|
||||||
|
pub fn eval_string(&self) -> Result<String> {
|
||||||
|
self.eval_string_with_context_mut(&mut VariableMap::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates the operator tree rooted at this node into a float.
|
||||||
|
///
|
||||||
|
/// Fails, if one of the operators in the expression tree fails.
|
||||||
|
pub fn eval_float(&self) -> Result<f64> {
|
||||||
|
self.eval_float_with_context_mut(&mut VariableMap::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates the operator tree rooted at this node into an integer.
|
||||||
|
///
|
||||||
|
/// Fails, if one of the operators in the expression tree fails.
|
||||||
|
pub fn eval_int(&self) -> Result<i64> {
|
||||||
|
self.eval_int_with_context_mut(&mut VariableMap::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates the operator tree rooted at this node into a float.
|
||||||
|
/// If the result of the expression is an integer, it is silently converted into a float.
|
||||||
|
///
|
||||||
|
/// Fails, if one of the operators in the expression tree fails.
|
||||||
|
pub fn eval_number(&self) -> Result<f64> {
|
||||||
|
self.eval_number_with_context_mut(&mut VariableMap::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates the operator tree rooted at this node into a boolean.
|
||||||
|
///
|
||||||
|
/// Fails, if one of the operators in the expression tree fails.
|
||||||
|
pub fn eval_boolean(&self) -> Result<bool> {
|
||||||
|
self.eval_boolean_with_context_mut(&mut VariableMap::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates the operator tree rooted at this node into a tuple.
|
||||||
|
///
|
||||||
|
/// Fails, if one of the operators in the expression tree fails.
|
||||||
|
pub fn eval_tuple(&self) -> Result<Vec<Value>> {
|
||||||
|
self.eval_tuple_with_context_mut(&mut VariableMap::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates the operator tree rooted at this node into an empty value.
|
||||||
|
///
|
||||||
|
/// Fails, if one of the operators in the expression tree fails.
|
||||||
|
pub fn eval_empty(&self) -> Result<()> {
|
||||||
|
self.eval_empty_with_context_mut(&mut VariableMap::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the children of this node as a slice.
|
||||||
|
pub fn children(&self) -> &[Node] {
|
||||||
|
&self.children
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the operator associated with this node.
|
||||||
|
pub fn operator(&self) -> &Operator {
|
||||||
|
&self.operator
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a mutable reference to the vector containing the children of this node.
|
||||||
|
///
|
||||||
|
/// WARNING: Writing to this might have unexpected results, as some operators require certain amounts and types of arguments.
|
||||||
|
pub fn children_mut(&mut self) -> &mut Vec<Node> {
|
||||||
|
&mut self.children
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a mutable reference to the operator associated with this node.
|
||||||
|
///
|
||||||
|
/// WARNING: Writing to this might have unexpected results, as some operators require different amounts and types of arguments.
|
||||||
|
pub fn operator_mut(&mut self) -> &mut Operator {
|
||||||
|
&mut self.operator
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_enough_children(&self) -> bool {
|
||||||
|
Some(self.children().len()) == self.operator().max_argument_amount()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_too_many_children(&self) -> bool {
|
||||||
|
if let Some(max_argument_amount) = self.operator().max_argument_amount() {
|
||||||
|
self.children().len() > max_argument_amount
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_back_prioritized(&mut self, node: Node, is_root_node: bool) -> Result<()> {
|
||||||
|
// println!(
|
||||||
|
// "Inserting {:?} into {:?}, is_root_node = {is_root_node}",
|
||||||
|
// node.operator(),
|
||||||
|
// self.operator()
|
||||||
|
// );
|
||||||
|
// println!("Self is {:?}", self);
|
||||||
|
if self.operator().precedence() < node.operator().precedence() || node.operator().is_unary() || is_root_node
|
||||||
|
// Right-to-left chaining
|
||||||
|
|| (self.operator().precedence() == node.operator().precedence() && !self.operator().is_left_to_right() && !node.operator().is_left_to_right())
|
||||||
|
{
|
||||||
|
if self.operator().is_leaf() {
|
||||||
|
Err(Error::AppendedToLeafNode(node))
|
||||||
|
} else if self.has_enough_children() {
|
||||||
|
// Unwrap cannot fail because is_leaf being false and has_enough_children being true implies that the operator wants and has at least one child
|
||||||
|
let last_child_operator = self.children.last().unwrap().operator();
|
||||||
|
|
||||||
|
if last_child_operator.precedence()
|
||||||
|
< node.operator().precedence() || node.operator().is_unary()
|
||||||
|
// Right-to-left chaining
|
||||||
|
|| (last_child_operator.precedence()
|
||||||
|
== node.operator().precedence() && !last_child_operator.is_left_to_right() && !node.operator().is_left_to_right())
|
||||||
|
{
|
||||||
|
// println!(
|
||||||
|
// "Recursing into {:?}",
|
||||||
|
// self.children.last().unwrap().operator()
|
||||||
|
// );
|
||||||
|
// Unwrap cannot fail because is_leaf being false and has_enough_children being true implies that the operator wants and has at least one child
|
||||||
|
self.children
|
||||||
|
.last_mut()
|
||||||
|
.unwrap()
|
||||||
|
.insert_back_prioritized(node, false)
|
||||||
|
} else {
|
||||||
|
// println!("Rotating");
|
||||||
|
if node.operator().is_leaf() {
|
||||||
|
return Err(Error::AppendedToLeafNode(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap cannot fail because is_leaf being false and has_enough_children being true implies that the operator wants and has at least one child
|
||||||
|
let last_child = self.children.pop().unwrap();
|
||||||
|
// Root nodes have at most one child
|
||||||
|
// TODO I am not sure if this is the correct error
|
||||||
|
if self.operator() == &Operator::RootNode && !self.children().is_empty() {
|
||||||
|
return Err(Error::MissingOperatorOutsideOfBrace);
|
||||||
|
}
|
||||||
|
// Do not insert root nodes into root nodes.
|
||||||
|
// TODO I am not sure if this is the correct error
|
||||||
|
if self.operator() == &Operator::RootNode
|
||||||
|
&& node.operator() == &Operator::RootNode
|
||||||
|
{
|
||||||
|
return Err(Error::MissingOperatorOutsideOfBrace);
|
||||||
|
}
|
||||||
|
self.children.push(node);
|
||||||
|
let node = self.children.last_mut().unwrap();
|
||||||
|
|
||||||
|
// Root nodes have at most one child
|
||||||
|
// TODO I am not sure if this is the correct error
|
||||||
|
if node.operator() == &Operator::RootNode && !node.children().is_empty() {
|
||||||
|
return Err(Error::MissingOperatorOutsideOfBrace);
|
||||||
|
}
|
||||||
|
// Do not insert root nodes into root nodes.
|
||||||
|
// TODO I am not sure if this is the correct error
|
||||||
|
if node.operator() == &Operator::RootNode
|
||||||
|
&& last_child.operator() == &Operator::RootNode
|
||||||
|
{
|
||||||
|
return Err(Error::MissingOperatorOutsideOfBrace);
|
||||||
|
}
|
||||||
|
node.children.push(last_child);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// println!("Inserting as specified");
|
||||||
|
self.children.push(node);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(Error::PrecedenceViolation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Node {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
self.operator.fmt(f)?;
|
||||||
|
for child in self.children() {
|
||||||
|
write!(f, " {}", child)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collapse_root_stack_to(
|
||||||
|
root_stack: &mut Vec<Node>,
|
||||||
|
mut root: Node,
|
||||||
|
collapse_goal: &Node,
|
||||||
|
) -> Result<Node> {
|
||||||
|
loop {
|
||||||
|
if let Some(mut potential_higher_root) = root_stack.pop() {
|
||||||
|
// TODO I'm not sure about this >, as I have no example for different sequence operators with the same precedence
|
||||||
|
if potential_higher_root.operator().precedence() > collapse_goal.operator().precedence()
|
||||||
|
{
|
||||||
|
potential_higher_root.children.push(root);
|
||||||
|
root = potential_higher_root;
|
||||||
|
} else {
|
||||||
|
root_stack.push(potential_higher_root);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This is the only way the topmost root node could have been removed
|
||||||
|
return Err(Error::UnmatchedRBrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collapse_all_sequences(root_stack: &mut Vec<Node>) -> Result<()> {
|
||||||
|
// println!("Collapsing all sequences");
|
||||||
|
// println!("Initial root stack is: {:?}", root_stack);
|
||||||
|
let mut root = if let Some(root) = root_stack.pop() {
|
||||||
|
root
|
||||||
|
} else {
|
||||||
|
return Err(Error::UnmatchedRBrace);
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// println!("Root is: {:?}", root);
|
||||||
|
if root.operator() == &Operator::RootNode {
|
||||||
|
// This should fire if parsing something like `4(5)`
|
||||||
|
if root.has_too_many_children() {
|
||||||
|
return Err(Error::MissingOperatorOutsideOfBrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
root_stack.push(root);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(mut potential_higher_root) = root_stack.pop() {
|
||||||
|
if root.operator().is_sequence() {
|
||||||
|
potential_higher_root.children.push(root);
|
||||||
|
root = potential_higher_root;
|
||||||
|
} else {
|
||||||
|
// This should fire if parsing something like `4(5)`
|
||||||
|
if root.has_too_many_children() {
|
||||||
|
return Err(Error::MissingOperatorOutsideOfBrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
root_stack.push(potential_higher_root);
|
||||||
|
root_stack.push(root);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This is the only way the topmost root node could have been removed
|
||||||
|
return Err(Error::UnmatchedRBrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// println!("Root stack after collapsing all sequences is: {:?}", root_stack);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn tokens_to_operator_tree(tokens: Vec<Token>) -> Result<Node> {
|
||||||
|
let mut root_stack = vec![Node::root_node()];
|
||||||
|
let mut last_token_is_rightsided_value = false;
|
||||||
|
let mut token_iter = tokens.iter().peekable();
|
||||||
|
|
||||||
|
while let Some(token) = token_iter.next().cloned() {
|
||||||
|
let next = token_iter.peek().cloned();
|
||||||
|
|
||||||
|
let node = match token.clone() {
|
||||||
|
Token::Plus => Some(Node::new(Operator::Add)),
|
||||||
|
Token::Minus => {
|
||||||
|
if last_token_is_rightsided_value {
|
||||||
|
Some(Node::new(Operator::Sub))
|
||||||
|
} else {
|
||||||
|
Some(Node::new(Operator::Neg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Token::Star => Some(Node::new(Operator::Mul)),
|
||||||
|
Token::Slash => Some(Node::new(Operator::Div)),
|
||||||
|
Token::Percent => Some(Node::new(Operator::Mod)),
|
||||||
|
Token::Hat => Some(Node::new(Operator::Exp)),
|
||||||
|
|
||||||
|
Token::Eq => Some(Node::new(Operator::Eq)),
|
||||||
|
Token::Neq => Some(Node::new(Operator::Neq)),
|
||||||
|
Token::Gt => Some(Node::new(Operator::Gt)),
|
||||||
|
Token::Lt => Some(Node::new(Operator::Lt)),
|
||||||
|
Token::Geq => Some(Node::new(Operator::Geq)),
|
||||||
|
Token::Leq => Some(Node::new(Operator::Leq)),
|
||||||
|
Token::And => Some(Node::new(Operator::And)),
|
||||||
|
Token::Or => Some(Node::new(Operator::Or)),
|
||||||
|
Token::Not => Some(Node::new(Operator::Not)),
|
||||||
|
|
||||||
|
Token::LBrace => {
|
||||||
|
root_stack.push(Node::root_node());
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Token::RBrace => {
|
||||||
|
if root_stack.len() <= 1 {
|
||||||
|
return Err(Error::UnmatchedRBrace);
|
||||||
|
} else {
|
||||||
|
collapse_all_sequences(&mut root_stack)?;
|
||||||
|
root_stack.pop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Token::Assign => Some(Node::new(Operator::Assign)),
|
||||||
|
Token::PlusAssign => Some(Node::new(Operator::AddAssign)),
|
||||||
|
Token::MinusAssign => Some(Node::new(Operator::SubAssign)),
|
||||||
|
Token::StarAssign => Some(Node::new(Operator::MulAssign)),
|
||||||
|
Token::SlashAssign => Some(Node::new(Operator::DivAssign)),
|
||||||
|
Token::PercentAssign => Some(Node::new(Operator::ModAssign)),
|
||||||
|
Token::HatAssign => Some(Node::new(Operator::ExpAssign)),
|
||||||
|
Token::AndAssign => Some(Node::new(Operator::AndAssign)),
|
||||||
|
Token::OrAssign => Some(Node::new(Operator::OrAssign)),
|
||||||
|
|
||||||
|
Token::Comma => Some(Node::new(Operator::Tuple)),
|
||||||
|
Token::Semicolon => Some(Node::new(Operator::Chain)),
|
||||||
|
Token::Identifier(identifier) => {
|
||||||
|
let mut result = Some(Node::new(Operator::variable_identifier_read(
|
||||||
|
identifier.clone(),
|
||||||
|
)));
|
||||||
|
if let Some(next) = next {
|
||||||
|
if next.is_assignment() {
|
||||||
|
result = Some(Node::new(Operator::variable_identifier_write(
|
||||||
|
identifier.clone(),
|
||||||
|
)));
|
||||||
|
} else if next.is_leftsided_value() {
|
||||||
|
result = Some(Node::new(Operator::function_identifier(identifier)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
Token::Float(float) => Some(Node::new(Operator::value(Value::Float(float)))),
|
||||||
|
Token::Int(int) => Some(Node::new(Operator::value(Value::Integer(int)))),
|
||||||
|
Token::Boolean(boolean) => Some(Node::new(Operator::value(Value::Boolean(boolean)))),
|
||||||
|
Token::String(string) => Some(Node::new(Operator::value(Value::String(string)))),
|
||||||
|
Token::Function(string) => Some(Node::new(Operator::value(Value::Function(
|
||||||
|
Function::new(&string),
|
||||||
|
)))),
|
||||||
|
Token::Yield(_, _) => todo!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(mut node) = node {
|
||||||
|
// Need to pop and then repush here, because Rust 1.33.0 cannot release the mutable borrow of root_stack before the end of this complete if-statement
|
||||||
|
if let Some(mut root) = root_stack.pop() {
|
||||||
|
if node.operator().is_sequence() {
|
||||||
|
// println!("Found a sequence operator");
|
||||||
|
// println!("Stack before sequence operation: {:?}, {:?}", root_stack, root);
|
||||||
|
// If root.operator() and node.operator() are of the same variant, ...
|
||||||
|
if mem::discriminant(root.operator()) == mem::discriminant(node.operator()) {
|
||||||
|
// ... we create a new root node for the next expression in the sequence
|
||||||
|
root.children.push(Node::root_node());
|
||||||
|
root_stack.push(root);
|
||||||
|
} else if root.operator() == &Operator::RootNode {
|
||||||
|
// If the current root is an actual root node, we start a new sequence
|
||||||
|
node.children.push(root);
|
||||||
|
node.children.push(Node::root_node());
|
||||||
|
root_stack.push(Node::root_node());
|
||||||
|
root_stack.push(node);
|
||||||
|
} else {
|
||||||
|
// Otherwise, we combine the sequences based on their precedences
|
||||||
|
// TODO I'm not sure about this <, as I have no example for different sequence operators with the same precedence
|
||||||
|
if root.operator().precedence() < node.operator().precedence() {
|
||||||
|
// If the new sequence has a higher precedence, it is part of the last element of the current root sequence
|
||||||
|
if let Some(last_root_child) = root.children.pop() {
|
||||||
|
node.children.push(last_root_child);
|
||||||
|
node.children.push(Node::root_node());
|
||||||
|
root_stack.push(root);
|
||||||
|
root_stack.push(node);
|
||||||
|
} else {
|
||||||
|
// Once a sequence has been pushed on top of the stack, it also gets a child
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If the new sequence doesn't have a higher precedence, then all sequences with a higher precedence are collapsed below this one
|
||||||
|
root = collapse_root_stack_to(&mut root_stack, root, &node)?;
|
||||||
|
node.children.push(root);
|
||||||
|
root_stack.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// println!("Stack after sequence operation: {:?}", root_stack);
|
||||||
|
} else if root.operator().is_sequence() {
|
||||||
|
if let Some(mut last_root_child) = root.children.pop() {
|
||||||
|
last_root_child.insert_back_prioritized(node, true)?;
|
||||||
|
root.children.push(last_root_child);
|
||||||
|
root_stack.push(root);
|
||||||
|
} else {
|
||||||
|
// Once a sequence has been pushed on top of the stack, it also gets a child
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
root.insert_back_prioritized(node, true)?;
|
||||||
|
root_stack.push(root);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(Error::UnmatchedRBrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
last_token_is_rightsided_value = token.is_rightsided_value();
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the end, all sequences are implicitly terminated
|
||||||
|
collapse_all_sequences(&mut root_stack)?;
|
||||||
|
|
||||||
|
if root_stack.len() > 1 {
|
||||||
|
Err(Error::UnmatchedLBrace)
|
||||||
|
} else if let Some(root) = root_stack.pop() {
|
||||||
|
Ok(root)
|
||||||
|
} else {
|
||||||
|
Err(Error::UnmatchedRBrace)
|
||||||
|
}
|
||||||
|
}
|
28
src/value/function.rs
Normal file
28
src/value/function.rs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{eval, eval_with_context, Result, Value, VariableMap};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||||
|
pub struct Function(String);
|
||||||
|
|
||||||
|
impl Function {
|
||||||
|
pub fn new(body: &str) -> Self {
|
||||||
|
Function(body.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(&self) -> Result<Value> {
|
||||||
|
eval(&self.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_with_context(&self, context: &mut VariableMap) -> Result<Value> {
|
||||||
|
eval_with_context(&self.0, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Function {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
21
src/value/iter.rs
Normal file
21
src/value/iter.rs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
use crate::Value;
|
||||||
|
|
||||||
|
pub struct Iter(Value);
|
||||||
|
|
||||||
|
impl IntoIterator for Value {
|
||||||
|
type Item = Value;
|
||||||
|
|
||||||
|
type IntoIter = Iter;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
Iter(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for Iter {
|
||||||
|
type Item = Value;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
731
src/value/mod.rs
Normal file
731
src/value/mod.rs
Normal file
@ -0,0 +1,731 @@
|
|||||||
|
use crate::{
|
||||||
|
error::{Error, Result},
|
||||||
|
Function, Table, Time, ValueType, VariableMap,
|
||||||
|
};
|
||||||
|
|
||||||
|
use json::JsonValue;
|
||||||
|
use serde::{
|
||||||
|
de::{MapAccess, SeqAccess, Visitor},
|
||||||
|
ser::SerializeTuple,
|
||||||
|
Deserialize, Serialize, Serializer,
|
||||||
|
};
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
cmp::Ordering,
|
||||||
|
convert::TryFrom,
|
||||||
|
fmt::{self, Display, Formatter},
|
||||||
|
marker::PhantomData,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod function;
|
||||||
|
pub mod iter;
|
||||||
|
pub mod table;
|
||||||
|
pub mod time;
|
||||||
|
pub mod value_type;
|
||||||
|
pub mod variable_map;
|
||||||
|
|
||||||
|
/// Whale value representation.
|
||||||
|
///
|
||||||
|
/// Every whale variable has a key and a Value. Variables are represented by
|
||||||
|
/// storing them in a VariableMap. This means the map of variables is itself a
|
||||||
|
/// value that can be treated as any other.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Default)]
|
||||||
|
pub enum Value {
|
||||||
|
String(String),
|
||||||
|
Float(f64),
|
||||||
|
Integer(i64),
|
||||||
|
Boolean(bool),
|
||||||
|
List(Vec<Value>),
|
||||||
|
Map(VariableMap),
|
||||||
|
Table(Table),
|
||||||
|
Time(Time),
|
||||||
|
Function(Function),
|
||||||
|
#[default]
|
||||||
|
Empty,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Value {
|
||||||
|
pub fn value_type(&self) -> ValueType {
|
||||||
|
ValueType::from(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_table(&self) -> bool {
|
||||||
|
matches!(self, Value::Table(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_string(&self) -> bool {
|
||||||
|
matches!(self, Value::String(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_integer(&self) -> bool {
|
||||||
|
matches!(self, Value::Integer(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_float(&self) -> bool {
|
||||||
|
matches!(self, Value::Float(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_number(&self) -> bool {
|
||||||
|
matches!(self, Value::Integer(_) | Value::Float(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_boolean(&self) -> bool {
|
||||||
|
matches!(self, Value::Boolean(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_list(&self) -> bool {
|
||||||
|
matches!(self, Value::List(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
matches!(self, Value::Empty)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_map(&self) -> bool {
|
||||||
|
matches!(self, Value::Map(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_function(&self) -> bool {
|
||||||
|
matches!(self, Value::Map(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Borrows the value stored in `self` as `String`, or returns `Err` if `self` is not a `Value::String`.
|
||||||
|
pub fn as_string(&self) -> Result<&String> {
|
||||||
|
match self {
|
||||||
|
Value::String(string) => Ok(string),
|
||||||
|
value => Err(Error::expected_string(value.clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copies the value stored in `self` as `i64`, or returns `Err` if `self` is not a `Value::Int`.
|
||||||
|
pub fn as_int(&self) -> Result<i64> {
|
||||||
|
match self {
|
||||||
|
Value::Integer(i) => Ok(*i),
|
||||||
|
value => Err(Error::expected_int(value.clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copies the value stored in `self` as `f64`, or returns `Err` if `self` is not a `Value::Float`.
|
||||||
|
pub fn as_float(&self) -> Result<f64> {
|
||||||
|
match self {
|
||||||
|
Value::Float(f) => Ok(*f),
|
||||||
|
value => Err(Error::expected_float(value.clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copies the value stored in `self` as `f64`, or returns `Err` if `self` is not a `Value::Float` or `Value::Int`.
|
||||||
|
/// Note that this method silently converts `i64` to `f64`, if `self` is a `Value::Int`.
|
||||||
|
pub fn as_number(&self) -> Result<f64> {
|
||||||
|
match self {
|
||||||
|
Value::Float(f) => Ok(*f),
|
||||||
|
Value::Integer(i) => Ok(*i as f64),
|
||||||
|
value => Err(Error::expected_number(value.clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copies the value stored in `self` as `bool`, or returns `Err` if `self` is not a `Value::Boolean`.
|
||||||
|
pub fn as_boolean(&self) -> Result<bool> {
|
||||||
|
match self {
|
||||||
|
Value::Boolean(boolean) => Ok(*boolean),
|
||||||
|
value => Err(Error::expected_boolean(value.clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Borrows the value stored in `self` as `Vec<Value>`, or returns `Err` if `self` is not a `Value::List`.
|
||||||
|
pub fn as_list(&self) -> Result<&Vec<Value>> {
|
||||||
|
match self {
|
||||||
|
Value::List(list) => Ok(list),
|
||||||
|
value => Err(Error::expected_list(value.clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Borrows the value stored in `self` as `Vec<Value>`, or returns `Err` if `self` is not a `Value::List`.
|
||||||
|
pub fn into_inner_list(self) -> Result<Vec<Value>> {
|
||||||
|
match self {
|
||||||
|
Value::List(list) => Ok(list),
|
||||||
|
value => Err(Error::expected_list(value.clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Borrows the value stored in `self` as `Vec<Value>` or returns `Err` if `self` is not a `Value::Map` of the required length.
|
||||||
|
pub fn as_fixed_len_list(&self, len: usize) -> Result<&Vec<Value>> {
|
||||||
|
match self {
|
||||||
|
Value::List(tuple) => {
|
||||||
|
if tuple.len() == len {
|
||||||
|
Ok(tuple)
|
||||||
|
} else {
|
||||||
|
Err(Error::expected_fixed_len_list(len, self.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value => Err(Error::expected_list(value.clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Borrows the value stored in `self` as `Vec<Value>`, or returns `Err` if `self` is not a `Value::Map`.
|
||||||
|
pub fn as_map(&self) -> Result<&VariableMap> {
|
||||||
|
match self {
|
||||||
|
Value::Map(map) => Ok(map),
|
||||||
|
value => Err(Error::expected_map(value.clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Borrows the value stored in `self` as `Vec<Value>`, or returns `Err` if `self` is not a `Value::Table`.
|
||||||
|
pub fn as_table(&self) -> Result<&Table> {
|
||||||
|
match self {
|
||||||
|
Value::Table(table) => Ok(table),
|
||||||
|
value => Err(Error::expected_table(value.clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Borrows the value stored in `self` as `Function`, or returns `Err` if
|
||||||
|
/// `self` is not a `Value::Function`.
|
||||||
|
pub fn as_function(&self) -> Result<&Function> {
|
||||||
|
match self {
|
||||||
|
Value::Function(function) => Ok(function),
|
||||||
|
value => Err(Error::expected_function(value.clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Borrows the value stored in `self` as `Time`, or returns `Err` if
|
||||||
|
/// `self` is not a `Value::Time`.
|
||||||
|
pub fn as_time(&self) -> Result<&Time> {
|
||||||
|
match self {
|
||||||
|
Value::Time(time) => Ok(time),
|
||||||
|
value => Err(Error::expected_function(value.clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `()`, or returns`Err` if `self` is not a `Value::Tuple`.
|
||||||
|
pub fn as_empty(&self) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
Value::Empty => Ok(()),
|
||||||
|
value => Err(Error::expected_empty(value.clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an owned table, either by cloning or converting the inner value..
|
||||||
|
pub fn to_table(&self) -> Result<Table> {
|
||||||
|
match self {
|
||||||
|
Value::Table(table) => Ok(table.clone()),
|
||||||
|
Value::List(list) => Ok(Table::from(list)),
|
||||||
|
Value::Map(map) => Ok(Table::from(map)),
|
||||||
|
value => Err(Error::expected_table(value.clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for Value {}
|
||||||
|
|
||||||
|
impl PartialOrd for Value {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for Value {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
match (self, other) {
|
||||||
|
(Value::String(left), Value::String(right)) => left.cmp(right),
|
||||||
|
(Value::String(_), _) => Ordering::Greater,
|
||||||
|
(Value::Integer(left), Value::Integer(right)) => left.cmp(right),
|
||||||
|
(Value::Integer(_), _) => Ordering::Greater,
|
||||||
|
(Value::Boolean(left), Value::Boolean(right)) => left.cmp(right),
|
||||||
|
(Value::Boolean(_), _) => Ordering::Greater,
|
||||||
|
(Value::Float(left), Value::Float(right)) => left.total_cmp(right),
|
||||||
|
(Value::Float(_), _) => Ordering::Greater,
|
||||||
|
(Value::List(left), Value::List(right)) => left.cmp(right),
|
||||||
|
(Value::List(_), _) => Ordering::Greater,
|
||||||
|
(Value::Map(left), Value::Map(right)) => left.cmp(right),
|
||||||
|
(Value::Map(_), _) => Ordering::Greater,
|
||||||
|
(Value::Table(left), Value::Table(right)) => left.cmp(right),
|
||||||
|
(Value::Table(_), _) => Ordering::Greater,
|
||||||
|
(Value::Function(left), Value::Function(right)) => left.cmp(right),
|
||||||
|
(Value::Function(_), _) => Ordering::Greater,
|
||||||
|
(Value::Time(left), Value::Time(right)) => left.cmp(right),
|
||||||
|
(Value::Time(_), _) => Ordering::Greater,
|
||||||
|
(Value::Empty, Value::Empty) => Ordering::Equal,
|
||||||
|
(Value::Empty, _) => Ordering::Less,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Value {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
Value::String(inner) => serializer.serialize_str(inner),
|
||||||
|
Value::Float(inner) => serializer.serialize_f64(*inner),
|
||||||
|
Value::Integer(inner) => serializer.serialize_i64(*inner),
|
||||||
|
Value::Boolean(inner) => serializer.serialize_bool(*inner),
|
||||||
|
Value::List(inner) => {
|
||||||
|
let mut tuple = serializer.serialize_tuple(inner.len())?;
|
||||||
|
|
||||||
|
for value in inner {
|
||||||
|
tuple.serialize_element(value)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
tuple.end()
|
||||||
|
}
|
||||||
|
Value::Empty => todo!(),
|
||||||
|
Value::Map(inner) => inner.serialize(serializer),
|
||||||
|
Value::Table(inner) => inner.serialize(serializer),
|
||||||
|
Value::Function(inner) => inner.serialize(serializer),
|
||||||
|
Value::Time(inner) => inner.serialize(serializer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Value {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Value::String(string) => write!(f, "{string}"),
|
||||||
|
Value::Float(float) => write!(f, "{}", float),
|
||||||
|
Value::Integer(int) => write!(f, "{}", int),
|
||||||
|
Value::Boolean(boolean) => write!(f, "{}", boolean),
|
||||||
|
Value::Empty => write!(f, "()"),
|
||||||
|
Value::List(list) => Table::from(list).fmt(f),
|
||||||
|
Value::Map(map) => write!(f, "{map}"),
|
||||||
|
Value::Table(table) => write!(f, "{table}"),
|
||||||
|
Value::Function(function) => write!(f, "{function}"),
|
||||||
|
Value::Time(time) => write!(f, "{time}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for Value {
|
||||||
|
fn from(string: String) -> Self {
|
||||||
|
Value::String(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for Value {
|
||||||
|
fn from(string: &str) -> Self {
|
||||||
|
Value::String(string.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<f64> for Value {
|
||||||
|
fn from(float: f64) -> Self {
|
||||||
|
Value::Float(float)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<i64> for Value {
|
||||||
|
fn from(int: i64) -> Self {
|
||||||
|
Value::Integer(int)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<bool> for Value {
|
||||||
|
fn from(boolean: bool) -> Self {
|
||||||
|
Value::Boolean(boolean)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<Value>> for Value {
|
||||||
|
fn from(tuple: Vec<Value>) -> Self {
|
||||||
|
Value::List(tuple)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Value> for Result<Value> {
|
||||||
|
fn from(value: Value) -> Self {
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<()> for Value {
|
||||||
|
fn from(_: ()) -> Self {
|
||||||
|
Value::Empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<JsonValue> for Value {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(json_value: JsonValue) -> Result<Self> {
|
||||||
|
use JsonValue::*;
|
||||||
|
|
||||||
|
match json_value {
|
||||||
|
Null => Ok(Value::Empty),
|
||||||
|
Short(short) => Ok(Value::String(short.to_string())),
|
||||||
|
String(string) => Ok(Value::String(string)),
|
||||||
|
Number(number) => Ok(Value::Float(f64::from(number))),
|
||||||
|
Boolean(boolean) => Ok(Value::Boolean(boolean)),
|
||||||
|
Object(object) => {
|
||||||
|
let mut map = VariableMap::new();
|
||||||
|
|
||||||
|
for (key, node_value) in object.iter() {
|
||||||
|
let value = Value::try_from(node_value)?;
|
||||||
|
|
||||||
|
map.set_value(key, value)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::Map(map))
|
||||||
|
}
|
||||||
|
Array(array) => {
|
||||||
|
let mut list = Vec::new();
|
||||||
|
|
||||||
|
for json_value in array {
|
||||||
|
let value = Value::try_from(json_value)?;
|
||||||
|
|
||||||
|
list.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::List(list))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&JsonValue> for Value {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(json_value: &JsonValue) -> Result<Self> {
|
||||||
|
use JsonValue::*;
|
||||||
|
|
||||||
|
match json_value {
|
||||||
|
Null => Ok(Value::Empty),
|
||||||
|
Short(short) => Ok(Value::String(short.to_string())),
|
||||||
|
String(string) => Ok(Value::String(string.clone())),
|
||||||
|
Number(number) => Ok(Value::Float(f64::from(*number))),
|
||||||
|
Boolean(boolean) => Ok(Value::Boolean(*boolean)),
|
||||||
|
Object(object) => {
|
||||||
|
let mut map = VariableMap::new();
|
||||||
|
|
||||||
|
for (key, node_value) in object.iter() {
|
||||||
|
let value = Value::try_from(node_value)?;
|
||||||
|
|
||||||
|
map.set_value(key, value)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::Map(map))
|
||||||
|
}
|
||||||
|
Array(array) => {
|
||||||
|
let mut list = Vec::new();
|
||||||
|
|
||||||
|
for json_value in array {
|
||||||
|
let value = Value::try_from(json_value)?;
|
||||||
|
|
||||||
|
list.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::List(list))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Value> for String {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(value: Value) -> std::result::Result<Self, Self::Error> {
|
||||||
|
if let Value::String(value) = value {
|
||||||
|
Ok(value)
|
||||||
|
} else {
|
||||||
|
Err(Error::ExpectedString { actual: value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Value> for f64 {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(value: Value) -> std::result::Result<Self, Self::Error> {
|
||||||
|
if let Value::Float(value) = value {
|
||||||
|
Ok(value)
|
||||||
|
} else {
|
||||||
|
Err(Error::ExpectedFloat { actual: value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Value> for i64 {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(value: Value) -> std::result::Result<Self, Self::Error> {
|
||||||
|
if let Value::Integer(value) = value {
|
||||||
|
Ok(value)
|
||||||
|
} else {
|
||||||
|
Err(Error::ExpectedInt { actual: value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Value> for bool {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(value: Value) -> std::result::Result<Self, Self::Error> {
|
||||||
|
if let Value::Boolean(value) = value {
|
||||||
|
Ok(value)
|
||||||
|
} else {
|
||||||
|
Err(Error::ExpectedBoolean { actual: value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ValueVisitor {
|
||||||
|
marker: PhantomData<fn() -> Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValueVisitor {
|
||||||
|
fn new() -> Self {
|
||||||
|
ValueVisitor {
|
||||||
|
marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for ValueVisitor {
|
||||||
|
type Value = Value;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("Any valid whale data.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_bool<E>(self, v: bool) -> std::result::Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
Ok(Value::Boolean(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_i8<E>(self, v: i8) -> std::result::Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
self.visit_i64(v as i64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_i16<E>(self, v: i16) -> std::result::Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
self.visit_i64(v as i64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_i32<E>(self, v: i32) -> std::result::Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
self.visit_i64(v as i64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_i64<E>(self, v: i64) -> std::result::Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
Ok(Value::Integer(v as i64))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_i128<E>(self, v: i128) -> std::result::Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
if v > i64::MAX as i128 {
|
||||||
|
Ok(Value::Integer(i64::MAX))
|
||||||
|
} else {
|
||||||
|
Ok(Value::Integer(v as i64))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_u8<E>(self, v: u8) -> std::result::Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
self.visit_u64(v as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_u16<E>(self, v: u16) -> std::result::Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
self.visit_u64(v as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_u32<E>(self, v: u32) -> std::result::Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
self.visit_u64(v as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_u64<E>(self, v: u64) -> std::result::Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
self.visit_i64(v as i64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_u128<E>(self, v: u128) -> std::result::Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
self.visit_i128(v as i128)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_f32<E>(self, v: f32) -> std::result::Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
self.visit_f64(v as f64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_f64<E>(self, v: f64) -> std::result::Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
Ok(Value::Float(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_char<E>(self, v: char) -> std::result::Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
self.visit_str(&v.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E>(self, v: &str) -> std::result::Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
Ok(Value::String(v.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_borrowed_str<E>(self, v: &'de str) -> std::result::Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
self.visit_str(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_string<E>(self, v: String) -> std::result::Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
Ok(Value::String(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_bytes<E>(self, v: &[u8]) -> std::result::Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
let _ = v;
|
||||||
|
Err(serde::de::Error::invalid_type(
|
||||||
|
serde::de::Unexpected::Bytes(v),
|
||||||
|
&self,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_borrowed_bytes<E>(self, v: &'de [u8]) -> std::result::Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
self.visit_bytes(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_byte_buf<E>(self, v: Vec<u8>) -> std::result::Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
self.visit_bytes(&v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_none<E>(self) -> std::result::Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
Err(serde::de::Error::invalid_type(
|
||||||
|
serde::de::Unexpected::Option,
|
||||||
|
&self,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_some<D>(self, deserializer: D) -> std::result::Result<Self::Value, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let _ = deserializer;
|
||||||
|
Err(serde::de::Error::invalid_type(
|
||||||
|
serde::de::Unexpected::Option,
|
||||||
|
&self,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_unit<E>(self) -> std::result::Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
Err(serde::de::Error::invalid_type(
|
||||||
|
serde::de::Unexpected::Unit,
|
||||||
|
&self,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_newtype_struct<D>(self, deserializer: D) -> std::result::Result<Self::Value, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let _ = deserializer;
|
||||||
|
Err(serde::de::Error::invalid_type(
|
||||||
|
serde::de::Unexpected::NewtypeStruct,
|
||||||
|
&self,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_seq<A>(self, mut access: A) -> std::result::Result<Self::Value, A::Error>
|
||||||
|
where
|
||||||
|
A: SeqAccess<'de>,
|
||||||
|
{
|
||||||
|
let mut list = Vec::new();
|
||||||
|
|
||||||
|
while let Some(value) = access.next_element()? {
|
||||||
|
list.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::List(list))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_map<M>(self, mut access: M) -> std::result::Result<Value, M::Error>
|
||||||
|
where
|
||||||
|
M: MapAccess<'de>,
|
||||||
|
{
|
||||||
|
let mut map = VariableMap::new();
|
||||||
|
|
||||||
|
while let Some((key, value)) = access.next_entry()? {
|
||||||
|
map.set_value(key, value)
|
||||||
|
.expect("Failed to deserialize Value. This is a no-op.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::Map(map))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_enum<A>(self, data: A) -> std::result::Result<Self::Value, A::Error>
|
||||||
|
where
|
||||||
|
A: serde::de::EnumAccess<'de>,
|
||||||
|
{
|
||||||
|
let _ = data;
|
||||||
|
Err(serde::de::Error::invalid_type(
|
||||||
|
serde::de::Unexpected::Enum,
|
||||||
|
&self,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn __private_visit_untagged_option<D>(self, _: D) -> std::result::Result<Self::Value, ()>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Value {
|
||||||
|
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
deserializer.deserialize_any(ValueVisitor::new())
|
||||||
|
}
|
||||||
|
}
|
353
src/value/table.rs
Normal file
353
src/value/table.rs
Normal file
@ -0,0 +1,353 @@
|
|||||||
|
use crate::{Error, Result, Value, VariableMap};
|
||||||
|
use comfy_table::{Cell, Color, ContentArrangement, Table as ComfyTable};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
cmp::Ordering,
|
||||||
|
fmt::{self, Display, Formatter},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Table {
|
||||||
|
header: Vec<String>,
|
||||||
|
rows: Vec<Vec<Value>>,
|
||||||
|
primary_key_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Table {
|
||||||
|
pub fn new(column_names: Vec<String>) -> Self {
|
||||||
|
Table {
|
||||||
|
header: column_names,
|
||||||
|
rows: Vec::new(),
|
||||||
|
primary_key_index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reserve(&mut self, additional: usize) {
|
||||||
|
self.rows.reserve(additional);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn column_names(&self) -> &Vec<String> {
|
||||||
|
&self.header
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rows(&self) -> &Vec<Vec<Value>> {
|
||||||
|
&self.rows
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.rows.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.rows.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sort(&mut self) {
|
||||||
|
self.rows.sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&mut self, row: Vec<Value>) -> Result<()> {
|
||||||
|
if row.len() != self.header.len() {
|
||||||
|
return Err(Error::WrongColumnAmount {
|
||||||
|
expected: self.header.len(),
|
||||||
|
actual: row.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.rows.push(row);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, index: usize) -> Result<()> {
|
||||||
|
self.rows.remove(index);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_row(&self, index: usize) -> Option<&Vec<Value>> {
|
||||||
|
self.rows.get(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, value: &Value) -> Option<&Vec<Value>> {
|
||||||
|
let primary_key = self.column_names().get(self.primary_key_index)?;
|
||||||
|
|
||||||
|
self.get_where(primary_key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select(&self, column_names: &[String]) -> Table {
|
||||||
|
let mut new_table = Table::new(column_names.to_vec());
|
||||||
|
|
||||||
|
for row in &self.rows {
|
||||||
|
let mut new_row = Vec::new();
|
||||||
|
|
||||||
|
for (i, value) in row.iter().enumerate() {
|
||||||
|
let column_name = self.header.get(i).unwrap();
|
||||||
|
let new_table_column_index =
|
||||||
|
new_table
|
||||||
|
.header
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find_map(|(index, new_column_name)| {
|
||||||
|
if new_column_name == column_name {
|
||||||
|
Some(index)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(index) = new_table_column_index {
|
||||||
|
while new_row.len() < index + 1 {
|
||||||
|
new_row.push(Value::Empty);
|
||||||
|
}
|
||||||
|
new_row[index] = value.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new_table.insert(new_row).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
new_table
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_where(&self, column_name: &str, expected: &Value) -> Option<&Vec<Value>> {
|
||||||
|
let column_index = self.get_column_index(column_name)?;
|
||||||
|
|
||||||
|
for row in &self.rows {
|
||||||
|
if let Some(actual) = row.get(column_index) {
|
||||||
|
if actual == expected {
|
||||||
|
return Some(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn filter(&self, column_name: &str, expected: &Value) -> Option<Table> {
|
||||||
|
let mut filtered = Table::new(self.header.clone());
|
||||||
|
let column_index = self.get_column_index(column_name)?;
|
||||||
|
|
||||||
|
for row in &self.rows {
|
||||||
|
let actual = row.get(column_index).unwrap();
|
||||||
|
|
||||||
|
if actual == expected {
|
||||||
|
let _ = filtered.insert(row.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_column_index(&self, column_name: &str) -> Option<usize> {
|
||||||
|
let column_names = &self.header;
|
||||||
|
for (i, column) in column_names.iter().enumerate() {
|
||||||
|
if column == column_name {
|
||||||
|
return Some(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Table {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
let mut table = ComfyTable::new();
|
||||||
|
|
||||||
|
table
|
||||||
|
.load_preset("││──├─┼┤│ ┬┴╭╮╰╯")
|
||||||
|
.set_content_arrangement(ContentArrangement::Dynamic)
|
||||||
|
.set_header(&self.header);
|
||||||
|
|
||||||
|
for row in &self.rows {
|
||||||
|
let row = row.iter().map(|value| {
|
||||||
|
let text = match value {
|
||||||
|
Value::List(list) => format!("{list:?}"),
|
||||||
|
Value::Map(map) => format!("Map ({} items)", map.len()),
|
||||||
|
Value::Table(table) => format!("Table ({} items)", table.len()),
|
||||||
|
Value::Function(_) => "Function".to_string(),
|
||||||
|
Value::Empty => "Empty".to_string(),
|
||||||
|
value => value.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cell = Cell::new(text).bg(Color::Rgb {
|
||||||
|
r: 40,
|
||||||
|
g: 40,
|
||||||
|
b: 40,
|
||||||
|
});
|
||||||
|
|
||||||
|
if value.is_string() {
|
||||||
|
cell = cell.fg(Color::Green);
|
||||||
|
}
|
||||||
|
if value.is_integer() {
|
||||||
|
cell = cell.fg(Color::Blue);
|
||||||
|
}
|
||||||
|
if value.is_boolean() {
|
||||||
|
cell = cell.fg(Color::Red);
|
||||||
|
}
|
||||||
|
if value.is_function() {
|
||||||
|
cell = cell.fg(Color::Cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
cell
|
||||||
|
});
|
||||||
|
|
||||||
|
table.add_row(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.header.is_empty() {
|
||||||
|
table.set_header(["empty"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(f, "{table}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Value> for Table {
|
||||||
|
fn from(value: &Value) -> Self {
|
||||||
|
match value {
|
||||||
|
Value::String(string) => {
|
||||||
|
let mut table = Table::new(vec!["string".to_string()]);
|
||||||
|
|
||||||
|
table
|
||||||
|
.insert(vec![Value::String(string.to_string())])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
table
|
||||||
|
}
|
||||||
|
Value::Float(float) => {
|
||||||
|
let mut table = Table::new(vec!["float".to_string()]);
|
||||||
|
|
||||||
|
table.insert(vec![Value::Float(*float)]).unwrap();
|
||||||
|
|
||||||
|
table
|
||||||
|
}
|
||||||
|
Value::Integer(integer) => {
|
||||||
|
let mut table = Table::new(vec!["integer".to_string()]);
|
||||||
|
|
||||||
|
table.insert(vec![Value::Integer(*integer)]).unwrap();
|
||||||
|
|
||||||
|
table
|
||||||
|
}
|
||||||
|
Value::Boolean(boolean) => {
|
||||||
|
let mut table = Table::new(vec!["boolean".to_string()]);
|
||||||
|
|
||||||
|
table.insert(vec![Value::Boolean(*boolean)]).unwrap();
|
||||||
|
|
||||||
|
table
|
||||||
|
}
|
||||||
|
Value::List(list) => Self::from(list),
|
||||||
|
Value::Empty => Table::new(Vec::with_capacity(0)),
|
||||||
|
Value::Map(map) => Self::from(map),
|
||||||
|
Value::Table(table) => table.clone(),
|
||||||
|
Value::Function(function) => {
|
||||||
|
let mut table = Table::new(vec!["function".to_string()]);
|
||||||
|
|
||||||
|
table
|
||||||
|
.insert(vec![Value::Function(function.clone())])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
table
|
||||||
|
}
|
||||||
|
Value::Time(_) => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Vec<Value>> for Table {
|
||||||
|
fn from(list: &Vec<Value>) -> Self {
|
||||||
|
let mut table = Table::new(vec!["index".to_string(), "item".to_string()]);
|
||||||
|
|
||||||
|
for (i, value) in list.iter().enumerate() {
|
||||||
|
table
|
||||||
|
.insert(vec![Value::Integer(i as i64), value.clone()])
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
table
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&mut Vec<Value>> for Table {
|
||||||
|
fn from(list: &mut Vec<Value>) -> Self {
|
||||||
|
let mut table = Table::new(vec!["index".to_string(), "item".to_string()]);
|
||||||
|
|
||||||
|
for (i, value) in list.iter().enumerate() {
|
||||||
|
if let Ok(list) = value.as_list() {
|
||||||
|
table.insert(list.clone()).unwrap();
|
||||||
|
} else {
|
||||||
|
table
|
||||||
|
.insert(vec![Value::Integer(i as i64), value.clone()])
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<VariableMap> for Table {
|
||||||
|
fn from(map: VariableMap) -> Self {
|
||||||
|
let keys = map.inner().keys().cloned().collect();
|
||||||
|
let values = map.inner().values().cloned().collect();
|
||||||
|
let mut table = Table::new(keys);
|
||||||
|
|
||||||
|
table
|
||||||
|
.insert(values)
|
||||||
|
.expect("Failed to create Table from Map. This is a no-op.");
|
||||||
|
|
||||||
|
table
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&VariableMap> for Table {
|
||||||
|
fn from(map: &VariableMap) -> Self {
|
||||||
|
let keys = map.inner().keys().cloned().collect();
|
||||||
|
let values = map.inner().values().cloned().collect();
|
||||||
|
let mut table = Table::new(keys);
|
||||||
|
|
||||||
|
table
|
||||||
|
.insert(values)
|
||||||
|
.expect("Failed to create Table from Map. This is a no-op.");
|
||||||
|
|
||||||
|
table
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&mut VariableMap> for Table {
|
||||||
|
fn from(map: &mut VariableMap) -> Self {
|
||||||
|
let keys = map.inner().keys().cloned().collect();
|
||||||
|
let values = map.inner().values().cloned().collect();
|
||||||
|
let mut table = Table::new(keys);
|
||||||
|
|
||||||
|
table
|
||||||
|
.insert(values)
|
||||||
|
.expect("Failed to create Table from Map. This is a no-op.");
|
||||||
|
|
||||||
|
table
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for Table {}
|
||||||
|
|
||||||
|
impl PartialEq for Table {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
if self.header != other.header {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.rows == other.rows
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Table {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
self.header.partial_cmp(&other.header)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for Table {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
self.header.cmp(&other.header)
|
||||||
|
}
|
||||||
|
}
|
88
src/value/time.rs
Normal file
88
src/value/time.rs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
use std::{
|
||||||
|
fmt::{self, Display, Formatter},
|
||||||
|
time::{Instant, SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
|
||||||
|
use chrono::{DateTime, FixedOffset, Local as LocalTime, NaiveDateTime};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum Time {
|
||||||
|
Utc(NaiveDateTime),
|
||||||
|
Local(DateTime<LocalTime>),
|
||||||
|
Monotonic(Instant),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Time {
|
||||||
|
pub fn utc(instant: Instant) -> Self {
|
||||||
|
let utc =
|
||||||
|
NaiveDateTime::from_timestamp_micros(instant.elapsed().as_micros() as i64).unwrap();
|
||||||
|
|
||||||
|
Time::Utc(utc)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_timestamp(microseconds: i64) -> Self {
|
||||||
|
let utc = NaiveDateTime::from_timestamp_micros(microseconds).unwrap();
|
||||||
|
|
||||||
|
Time::Utc(utc)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn local(instant: Instant) -> Self {
|
||||||
|
let local = DateTime::from_local(
|
||||||
|
NaiveDateTime::from_timestamp_micros(instant.elapsed().as_micros() as i64).unwrap(),
|
||||||
|
FixedOffset::west_opt(0).unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Time::Local(local)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn monotonic(instant: Instant) -> Self {
|
||||||
|
Time::Monotonic(instant)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_local(&self) -> String {
|
||||||
|
let date_time = match *self {
|
||||||
|
Time::Utc(utc) => DateTime::from_utc(utc, FixedOffset::west_opt(0).unwrap()),
|
||||||
|
Time::Local(local) => local,
|
||||||
|
Time::Monotonic(instant) => DateTime::from_utc(
|
||||||
|
NaiveDateTime::from_timestamp_micros(instant.elapsed().as_micros() as i64).unwrap(),
|
||||||
|
FixedOffset::west_opt(0).unwrap(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
date_time.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Time {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.as_local())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Time {
|
||||||
|
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Time {
|
||||||
|
fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SystemTime> for Time {
|
||||||
|
fn from(value: SystemTime) -> Self {
|
||||||
|
let timestamp = value.duration_since(UNIX_EPOCH).unwrap().as_micros();
|
||||||
|
let naive = NaiveDateTime::from_timestamp_micros(timestamp as i64).unwrap();
|
||||||
|
|
||||||
|
Time::Utc(naive)
|
||||||
|
}
|
||||||
|
}
|
86
src/value/value_type.rs
Normal file
86
src/value/value_type.rs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use crate::Value;
|
||||||
|
|
||||||
|
/// The type of a `Value`.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum ValueType {
|
||||||
|
Any,
|
||||||
|
String,
|
||||||
|
Float,
|
||||||
|
Int,
|
||||||
|
Boolean,
|
||||||
|
List(Vec<ValueType>),
|
||||||
|
Empty,
|
||||||
|
Map,
|
||||||
|
Table,
|
||||||
|
Function,
|
||||||
|
Time,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for ValueType {}
|
||||||
|
|
||||||
|
impl PartialEq for ValueType {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
match (self, other) {
|
||||||
|
(ValueType::Any, _) => true,
|
||||||
|
(_, ValueType::Any) => true,
|
||||||
|
(ValueType::String, ValueType::String) => true,
|
||||||
|
(ValueType::Float, ValueType::Float) => true,
|
||||||
|
(ValueType::Int, ValueType::Int) => true,
|
||||||
|
(ValueType::Boolean, ValueType::Boolean) => true,
|
||||||
|
(ValueType::List(left), ValueType::List(right)) => left == right,
|
||||||
|
(ValueType::Empty, ValueType::Empty) => true,
|
||||||
|
(ValueType::Map, ValueType::Map) => true,
|
||||||
|
(ValueType::Table, ValueType::Table) => true,
|
||||||
|
(ValueType::Function, ValueType::Function) => true,
|
||||||
|
(ValueType::Time, ValueType::Time) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ValueType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let text = match &self {
|
||||||
|
ValueType::Any => "any",
|
||||||
|
ValueType::String => "string",
|
||||||
|
ValueType::Float => "float",
|
||||||
|
ValueType::Int => "integer",
|
||||||
|
ValueType::Boolean => "boolean",
|
||||||
|
ValueType::List(_) => "list",
|
||||||
|
ValueType::Empty => "empty",
|
||||||
|
ValueType::Map => "map",
|
||||||
|
ValueType::Table => "table",
|
||||||
|
ValueType::Function => "function",
|
||||||
|
ValueType::Time => "time",
|
||||||
|
};
|
||||||
|
|
||||||
|
write!(f, "{text}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Value> for ValueType {
|
||||||
|
fn from(value: &Value) -> Self {
|
||||||
|
match value {
|
||||||
|
Value::String(_) => ValueType::String,
|
||||||
|
Value::Float(_) => ValueType::Float,
|
||||||
|
Value::Integer(_) => ValueType::Int,
|
||||||
|
Value::Boolean(_) => ValueType::Boolean,
|
||||||
|
Value::List(list) => {
|
||||||
|
ValueType::List(list.iter().map(|value| value.value_type()).collect())
|
||||||
|
}
|
||||||
|
Value::Empty => ValueType::Empty,
|
||||||
|
Value::Map(_) => ValueType::Map,
|
||||||
|
Value::Table { .. } => ValueType::Table,
|
||||||
|
Value::Function(_) => ValueType::Function,
|
||||||
|
Value::Time(_) => ValueType::Time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&mut Value> for ValueType {
|
||||||
|
fn from(value: &mut Value) -> Self {
|
||||||
|
From::<&Value>::from(value)
|
||||||
|
}
|
||||||
|
}
|
211
src/value/variable_map.rs
Normal file
211
src/value/variable_map.rs
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
collections::BTreeMap,
|
||||||
|
fmt::{self, Display, Formatter},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{value::Value, Error, Result, Table, MACRO_LIST};
|
||||||
|
|
||||||
|
/// A context that stores its mappings in hash maps.
|
||||||
|
#[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct VariableMap {
|
||||||
|
variables: BTreeMap<String, Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VariableMap {
|
||||||
|
/// Creates a new instace.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
VariableMap {
|
||||||
|
variables: BTreeMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn call_function(&self, identifier: &str, argument: &Value) -> Result<Value> {
|
||||||
|
for macro_item in MACRO_LIST {
|
||||||
|
let valid_input_types = macro_item.info().inputs;
|
||||||
|
|
||||||
|
if identifier == macro_item.info().identifier {
|
||||||
|
let input_type = argument.value_type();
|
||||||
|
|
||||||
|
if valid_input_types.contains(&input_type) {
|
||||||
|
return macro_item.run(argument);
|
||||||
|
} else {
|
||||||
|
return Err(Error::MacroArgumentType {
|
||||||
|
macro_info: macro_item.info(),
|
||||||
|
actual: argument.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (key, value) in &self.variables {
|
||||||
|
if identifier == key {
|
||||||
|
if let Ok(function) = value.as_function() {
|
||||||
|
let mut context = VariableMap::new();
|
||||||
|
|
||||||
|
context.set_value("input", argument.clone())?;
|
||||||
|
|
||||||
|
return function.run_with_context(&mut context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut split = identifier.split(':').rev();
|
||||||
|
|
||||||
|
if let (Some(function_identifier), Some(variable_identifier)) = (split.next(), split.next())
|
||||||
|
{
|
||||||
|
if function_identifier.contains(':') {
|
||||||
|
return self.call_function(function_identifier, argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
if variable_identifier.contains('.') {
|
||||||
|
let value = self.get_value(variable_identifier)?.unwrap_or(Value::Empty);
|
||||||
|
|
||||||
|
return self.call_function(function_identifier, &value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(value) = self.get_value(variable_identifier)? {
|
||||||
|
if argument.is_empty() {
|
||||||
|
return self.call_function(function_identifier, &value);
|
||||||
|
}
|
||||||
|
|
||||||
|
let value_type = value.value_type();
|
||||||
|
|
||||||
|
let list = Value::List(vec![value, argument.clone()]);
|
||||||
|
|
||||||
|
return self.call_function(function_identifier, &list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(Error::FunctionIdentifierNotFound(identifier.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_value(&self, identifier: &str) -> Result<Option<Value>> {
|
||||||
|
let split = identifier.split_once('.');
|
||||||
|
|
||||||
|
if let Some((identifier, next_identifier)) = split {
|
||||||
|
if let Some(value) = self.variables.get(identifier) {
|
||||||
|
if let Value::Map(map) = value {
|
||||||
|
map.get_value(next_identifier)
|
||||||
|
} else {
|
||||||
|
Err(Error::ExpectedMap {
|
||||||
|
actual: value.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let value = self.variables.get(identifier);
|
||||||
|
|
||||||
|
if let Some(value) = value {
|
||||||
|
Ok(Some(value.clone()))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_value(&mut self, identifier: &str, value: Value) -> Result<()> {
|
||||||
|
let split = identifier.split_once('.');
|
||||||
|
|
||||||
|
if let Some((map_name, next_identifier)) = split {
|
||||||
|
let get_value = self.variables.get_mut(map_name);
|
||||||
|
|
||||||
|
if let Some(map_value) = get_value {
|
||||||
|
if let Value::Map(map) = map_value {
|
||||||
|
map.set_value(next_identifier, value)
|
||||||
|
} else {
|
||||||
|
Err(Error::ExpectedMap {
|
||||||
|
actual: map_value.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let mut new_map = VariableMap::new();
|
||||||
|
|
||||||
|
new_map.set_value(next_identifier, value)?;
|
||||||
|
|
||||||
|
self.variables
|
||||||
|
.insert(map_name.to_string(), Value::Map(new_map));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.variables.insert(identifier.to_string(), value);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to the inner BTreeMap.
|
||||||
|
pub fn inner(&self) -> &BTreeMap<String, Value> {
|
||||||
|
&self.variables
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of stored variables.
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.variables.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the length is zero.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.variables.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for VariableMap {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for VariableMap {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
Table::from(self).fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Table> for VariableMap {
|
||||||
|
fn from(value: &Table) -> Self {
|
||||||
|
let mut map = VariableMap::new();
|
||||||
|
|
||||||
|
for (row_index, row) in value.rows().iter().enumerate() {
|
||||||
|
map.set_value(&row_index.to_string(), Value::List(row.clone()))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
map
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_and_set_simple_value() {
|
||||||
|
let mut map = VariableMap::new();
|
||||||
|
|
||||||
|
map.set_value("x", Value::Integer(1)).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(Value::Integer(1), map.get_value("x").unwrap().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_and_set_nested_maps() {
|
||||||
|
let mut map = VariableMap::new();
|
||||||
|
|
||||||
|
map.set_value("x", Value::Map(VariableMap::new())).unwrap();
|
||||||
|
map.set_value("x.x", Value::Map(VariableMap::new()))
|
||||||
|
.unwrap();
|
||||||
|
map.set_value("x.x.x", Value::Map(VariableMap::new()))
|
||||||
|
.unwrap();
|
||||||
|
map.set_value("x.x.x.x", Value::Map(VariableMap::new()))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Value::Map(VariableMap::new()),
|
||||||
|
map.get_value("x.x.x.x").unwrap().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user