Expression Config Notation
Econ is a superset of Json that allows the computation and composition of data using expressions and macros. Rust Api info here
Econ can parse standard Json
{
"a": 10.50,
"b": "Hello World!",
"c": true,
"d": null,
"e": {
"a": "Hello World...Again!",
"b": [
{
"aa": 20,
"bb": false
},
{
"aa": 30,
"bb": true
}
]
},
"f": [
1,
2,
3,
4,
5
],
}
numbers are represented by 64 bit floats
5
25.75
strings not enclosed with "
's must start with a letter but may contain special characters and digits after as long as they are not operators or keywords.
"I'm a string with spaces"
Im_a_string_without_spaces
You can also make multiline strings that will be formatted in a nice way using the \
operator.
"i": "I'm a multiline" \
"string with \"* + - / filter\"" \
"keywords and operators",
Object keys are parsed as strings so the same rules apply to them as with string values.
key_with_no_spaces: 1 //valid "key with spaces": 2 //valid another key containing spaces: 3 //not valid
true
false
null
or
nil
{
"a": 1,
"b": 2,
"c": {
"d": {
"e": "nested"
}
}
}
[
1,
true,
"Hello",
{
"name": "Suzie"
},
nil
]
Econ can parse expressions into values and will always output valid Json.
Input
{
a: ((3+2)*10)/5,
b: true or false,
c: "Hello" + " " + "World!",
d: Hi_Im_also_a_string
}
Output
{
"a": 10,
"b": true,
"c": "Hello World!",
"d": "Hi_Im_also_a_string"
}
or
and
not
true
false
nil
null
inf
- a constant for infinity. Divide by zero's will evaluate as infinity.- Functions
Econ supports
- Arithmetic
+
,-
,*
,/
,%
,\
- Logic
or
/||
,and
/&&
,not
/~
- Comparison
>
,>=
,<
,<=
,==
,~=
- Grouping
()
- Ternary
?:
-condition ? expr if true : expr if false
- Access
[index/key]
,.index/key
- Length
#
- Comment
//
- Reference
$
/!
- Macro
@
Econ will do its best to perform arithmetic on types but will not make large leaps. For example: "Hello" + " " + "World"
will yield "Hello World"
or "The Number Two ->" + 2
will yield "The Number Two -> 2"
but true + 2
will throw an error Error Parsing -> "Invalid addition of types."
Input
{
a: ((1+3)/2) * (5/3),
//The \ operator wil concat strings or strings and bools/numbers with a newline inebetween.
d: I'm a multiline \
"string with \"* + - / filter\"" \
"keywords and operators",
e: [
"multi_line" \
"string" \
"in" \
"array",
"single line",
"another multi_line" \
"string" \
"in" \
"array"
],
f: "another multi_line" \
"string" \
"in" \
"object",
}
Output
{
"a": 3.3333333333333335,
"b": [
1,
2,
3,
4,
5,
6
],
"c": {
"a": 1,
"b": 2
},
"d": "I'm a multiline
string with "* + - / filter"
keywords and operators",
"e": [
"multi_line
string
in
array",
"single line",
"another multi_line
string
in
array"
],
"f": "another multi_line
string
in
object"
}
Econ can concatenate Objects and Arrays.
Input
{
a: { a: 1 } + { b: 2 },
b: [1,2] + [3,4] + [5,6]
}
Output
{
"a": {
a: 1,
b: 2
},
"b": [
1,
2,
3,
4,
5,
6
]
}
Note:
nil + {} -> {}
andnil + [] -> []
this is important for function logic specificallyfold()
Logical operators are similar to other programming languages and you can use the keywords or symbols. We followed in Lua's footsteps opting to use ~
over !
for the not
operator
Input
{
a: false,
b: true,
c: $a && $b,
d: $a or $b,
e: not $d,
f: ~($c and $a) or $e
}
Output
{
"a": false,
"b": true,
"c": false,
"d": true,
"e": false,
"f": true
}
Input
{
a: 20 < 20,
b: 100 > 60,
c: 25 == 25,
d: 10 ~= 5,
e: "Hello" == "Not Hello",
f: 20 <= 20,
g: 30 >= 30
}
Output
{
"a": false,
"b": true,
"c": true,
"d": true,
"e": false,
"f": true,
"g": true
}
Input
{
a: "a" == "b" ? 1 : 2
}
Output
{
"a": 2
}
Access operators are used to get elements from arrays and values from objects. Arrays are 0 base indexed. when using []
you can use expressions as long as they evaluate into strings for objects and numbers for arrays. Additionally you can group expressions for the .
operator like this
arr: [1,2,3],
a: $arr.(1+1) //outputs -> 3
Input
{
a: [1,2,3,4,5],
a_1: $a[1],
a_0: $a[0],
a_4: $a.4,
b: { name: "Dill", age: 20 },
c: $b.name + " is " + $b[age] + " years old"
}
Output
{
"a": [1,2,3,4,5],
"a_1": 2,
"a_0": 1,
"a_4": 5,
"b": {
"name": "Dill",
"age": 20
},
"c": "Dill is 20 years old"
}
Note:
.(key/index)
and[key/index]
are equivalent
Gets the length of an Object or Array.
#[0,1,2,3,4] //outputs -> 5
#{ a: 1, b: 2, c: 3} // outputs -> 3
In Econ you can reference keys using the $
or !
operators. You cannot reference a key before it is declared.
Referenced keys must not contain whitespace or any other reserved operators. References are searched for in the current object depth but you may search up in depth by chaining together $
's. If a key is not found it will return Nil
Input
{
a: 10,
b: $a*2,
c: {
ca: $$a + $$b,
cb: $a
}
}
Output
{
"a": 10,
"b": 20,
"c": {
"ca": 30,
"cb": nil
}
}
Similar to $
but it will walk up the object depth until it finds the key.
Input
{
a: 10,
b: $a*2,
c: {
ca: !a + !b,
cb: !a
}
}
Output
{
"a": 10,
"b": 20,
"c": {
"ca": 30,
"cb": 10
}
}
Econ supports a set amount of predefined functions; they include:
- Filter
filter(obj/array, iter => condition) -> obj/array
- Map
map(obj/array, iter => expr) -> obj/array
- Chars
chars(string) -> array
- To String
to_string(any) -> string
- Keys
keys(obj) -> array
- Values
values(obj) -> array
- Fold
fold(obj/array, |iter, acc| => expr) -> literal
- Sort
sort(array, |x, y| => cond) -> array
- Zip
zip(array, array) -> array
Takes an Object or Array iterates through and returns a new Object or Array with only elements matching the condition.
Input
{
a: filter({a: 1, b: 2, c: 3, d: 4}, i => ($i.val%2 == 0) || ($i.key == "a"))
}
Output
{
"a": {
"a": 1
"b": 2,
"d": 4,
}
}
Input
{
a: filter([0,1,2,3,4,5], i => $i%2 == 0)
}
Output
{
"a": [0,2,4]
}
Takes an Object or Array iterates through and returns a new Object or Array with elements modified by the expression
Input
{
a: map({a: 1, b: 2, c: 3, d: 4}, i => $i + 1)
}
Output
{
"a": {
"a": 2
"b": 3,
"c": 4
"d": 5,
}
}
Input
{
a: map([0,1,2,3,4,5], i => $i+1)
}
Output
{
"a": [
1,
2,
3,
4,
5
]
}
Takes a string and returns an Array of chars.
Input
{
a: chars("Hello")
}
Output
{
"a": [
"H",
"e",
"l",
"l",
"o"
]
}
Takes any type and returns a string. Most of the types are straight forward but Objects will take all values and concatentate them in order.
Input
{
a: to_string(["H", "e", "l", "l", "o"]),
b: to_string({a: "hello", b: "world"}),
c: to_string(nil),
d: to_string(true),
e: to_string([1,2,3,[7,4],{a:nil}])
}
Output
{
"a": "Hello",
"b": "helloworld",
"c": "nil",
"d": "true",
"e": "12374nil"
}
Takes an Object and returns and Array of keys.
Input
{
a: keys({a: 1, b: 2, c: 3}}
}
Output
{
"a": [
"a",
"b",
"c"
]
}
Takes an Object and returns and Array of keys.
Input
{
a: values({a: 1, b: 2, c: 3}}
}
Output
{
"a": [
1,
2,
3
]
}
Takes an Object or Array and iterates through it while giving you access to an accumulator reference that it returns. The accumulator is initialized as Nil
.
Input
{
a: fold({a: 1, b: 2, c: 3}, |i, acc| => $acc + $i.key + "=" + $i.val + " ")
}
Output
{
"a": "a=1 b=2 c=3"
}
Input
{
a: fold([1,2,3,4,5], |i, acc| => $acc + $i)
}
Output
{
"a": 15
}
Takes an Array and returns an Array sorted. If you try to sort Arrays with differing types then you will most likely get an error Error Parsing -> "Invalid comparison of types."
Input
{
a: sort([200, 30, 500, 5, 60], |x, y| => $x < $y)
}
Output
{
"a": [
5,
30,
60,
200,
500
]
}
Input
{
a: sort(["Cucumber", "Broccoli", "Apple", "Banana", "Peach"], |x, y| => $x < $y)
}
Output
{
"a": [
"Apple",
"Banana",
"Broccoli",
"Cucumber",
"Peach"
]
}
Takes two Arrays and returns a new Array with elements of same index in sub-arrays. ``
Input
{
a: zip([1,2,3], [4,5,6])
}
Output
{
"a": [
[
1,
4
],
[
2,
5
],
[
3,
6
]
]
}
Input
{
a: {
aa: 1,
bb: 2,
cc: 3
},
b: zip(keys($a), values($a))
}
Output
{
"a": {
"aa": 1,
"bb": 2,
"cc": 3
},
"b": [
[
"aa",
1
],
[
"bb",
2
],
[
"cc",
3
]
]
}
Macros are C-styled and like References must be declared before calling.
identifier(args, ...) token_stream \
token_stream_on_newline
Input
{
@person(name, age) name: age
a: {
@person("Dave", 20),
@person("Mickey", 25),
@person("Suzie", 23),
@person("Keli", 28)
}
}
Output
{
"a": {
"Dave": 20,
"Mickey": 25,
"Suzie": 23,
"Keli": 28
}
}
Input
{
@person(id, name, age) \
id: {\
name: age\
}
a: {
@person("1", "Dave", 20),
@person("2", "Mickey", 25),
@person("3", "Suzie", 23),
@person("4", "Keli", 28)
}
}
Output
{
"a": {
"1": {
"Dave": 20
},
"2": {
"Mickey": 25
},
"3": {
"Suzie": 23
},
"4": {
"Keli": 28
}
}
}
Input
{
@sort_descending(obj) sort(obj, |x, y| => $x > $y)
a: [1,3,0,5],
b: @sort_descending($a)
}
Output
{
"a": [
1,
3,
0,
5
],
"b": [
5,
3,
1,
0
]
}
Input
{
@is_even(x) (x%2 == 0)
a: @is_even(2),
b: @is_even(3),
c: @is_even(7)
}
Output
{
"a": true,
"b": false,
"c": false
}
Econ allows for two other types of macros which works on types rather than in function style.
@{type, ref => condition, new_value}
A constraint macro will work on all values of a certain type that are in scope. Only atomic types are allowed number
, bool
, string
, nil
Input
{
@{number, x => $x%2 == 0, "Even"}
a: 2,
b: 3,
c: 4
}
Ouput
{
"a": "Even",
"b": 3,
"c": "Even"
}
Input
{
@{number, x => $x < 5, 5}
a: [
2,
4,
7,
5,
10
],
b: {
@{number, x => $x > 0, $x * -1}
a: [
2,
4,
7,
5,
10
]
}
}
Ouput
{
"a": [
5,
5,
7,
5,
10
],
"b": {
"a": [
5,
5,
5,
5,
5
]
}
}
Input
{
@{string, x => $x == $x, to_string(filter(chars($x), x => $x ~= "_"))}
a: "_Mike",
b: "Kat_e",
c: "_Lis_a",
d: "Bill"
}
Ouput
{
"a": "Mike",
"b": "Kate",
"c": "Lisa",
"d": "Bill"
}
Input
{
@{nil, x => $x, false}
a: nil
}
Ouput
{
"a": false
}
An error macro is similar to a constraint macro but it throws a parser error rather than changing the value. The source will not be parsed if the error is thrown and it will not panic the main thread unless you handle it that way.
@!{type, ref => condition, error_msg}
Input
{
@!{string, x => $x == "Hello World!", "No Hello Worlds!"}
a: "Hello World!"
}
Output
Line [0003] Error Parsing -> "No Hello Worlds!"
Line [0002] @!{string, x => $x == "Hello World!", "No Hello Worlds!"}
Line [0003] -> a: "Hello World!"
Line [0004] }
Input
{
@!{bool, x => $x == $x, "Use a string Yes/No rather than booleans."}
a: true
}
Output
Line [0003] Error Parsing -> "Use a string Yes/No rather than booleans."
Line [0002] @!{bool, x => $x == $x, "Use a string Yes/No rather than booleans."}
Line [0003] -> a: true
Line [0004] }
Input
{
@!{
number,
x => ($x == inf) || ($x == -inf),
"Divide by zero"
}
a: 1/0,
b: -1/0
}
Output
Line [0007] Error Parsing -> "Divide by zero"
The proof-of-concept Api for Econ is written in Rust
In your project add this line to your [dependecies] list in cargo.toml
econ = { git = "https://github.com/dollerama/econ_rs.git" }
And then use econ::Econ
in your Rust code.
Simply fork the repo and run cargo test after making changes.
You can use the create function to parse either a file or string. Create can output debug info and will return Result<EconObj, String>
where as Econ::from()
will return an empty EconObj
if it fails and will not output debug info other than errors.
Source
let obj = Econ::create(
r#"
{
a: 1,
b: 2,
c: 3
}
"#, true);
Source
let obj = Econ::from(
r#"
{
a: {
b: [
{
c: [1,2,3]
},
{
c: [4,5,6]
}
]
}
}
"#);
Source
let obj = Econ::from("file.econ");
Source
let obj = Econ::from(
r#"
{
a: {
b: {
c: [
1,
2,
3,
4
]
}
}
}
"#);
assert_eq!(3f64, obj["a"]["b"]["c"][2].value::<f64>());