VivJson

for Python

Overview

  • Deserialize/serialize JSON.
  • Tiny language
    • The embedded scripting language
    • Dynamically typing
    • Lightweight language
  • The extension of JSON (JSON's object is valid statements as script.)

Use cases

  • In Python Application
    • Read/Write JSON's value.
    • Change the behavior of Application with downloaded script.
    • Change the operation of data with embedded script within data.
  • In command line
    • Manipulate JSON's value with script.

Example

In Python,

from vivjson.viv import Viv, Json, Method

data = '{"foo": 10, "bar": 30, "baz": 20}'

# JSON's object will be dict.
value, error_message = Viv.run(data)
print(value) # {'foo': 10, 'bar': 30, 'baz': 20}
print(type(value)) # <class 'dict'>

# The specific variable's value
value, error_message = Viv.run(data, 'return(foo)')
print(value) # 10
print(type(value)) # <class 'int'>

# The undefined value is evaluated as null (None).
value, error_message = Viv.run(data, 'return(qux)')
print(value) # None
print(type(value)) # <class 'NoneType'>
# The existence can be checked with "in" operator.
# Note that "." of the right hand side represents this block
# (this local scope).
# "." as the operand is permitted only the right hand side of
# "in" operator.
# Carefully, it can't check about the variable whose prefix is "_".
value, error_message = Viv.run(data, 'return("qux" in .)')
print(value) # False
print(type(value)) # <class 'bool'>

# The calculated value
value, error_message = Viv.run(data, 'return(foo + bar + baz)')
print(value) # 60

# Find maximum value.
# 1. Assignment: pairs = {"foo": 10, "bar": 30, "baz": 20}
# 2. for-loop with iterator: for (pair in pairs) {...}
#    - pair[0] is the above key.
#    - pair[1] is the above value.
# 3. Update max.
# 4. Return it.
code = 'max=-1, for (pair in pairs) {if (max < pair[1]) {max = pair[1]}}, return(max)'
value, error_message = Viv.run('pairs = ', '+', data, code)
print(value) # 30
# Note that "+" of arguments is concatenation. Of course, the following
# representation can be accepted.
value, error_message = Viv.run('pairs = ' + data, code)
print(value) # 30
# This is realized as below. However, such code may generate the unexpected
# result because "." represents this block that has the variable "max".
code = 'max=-1, for (pair in .) {if (max < pair[1]) {max = pair[1]}}, return(max)'
value, error_message = Viv.run(data, code)
print(value) # 30
# When "_max" is used instead of "max", it is improved.
# Because "." does not treat the variable whose prefix is "_".
code = '_max=-1, for (pair in .) {if (_max < pair[1]) {_max = pair[1]}}, return(_max)'
value, error_message = Viv.run(data, code)
print(value) # 30

# In default, both of script's code and JSON value are accepted.
# Json class is useful if you want to accept as JSON value rather
# than script's code.
data = '{"foo": 3}'
code = 'return(foo)'
value, error_message = Viv.run(Json(data), code)
print(value) # 3

# Using class instance, implicit variable, and member's access
# - When the given value is not JSON object (key-value pairs),
#   it is assigned into implicit variable.
#   The implicit variable is "_" if there is one value. Otherwise,
#   "_[0]", "_[1]", ... are used.
# - A class member is selected like foo.bar and foo['bar'].
#   In the following example, "_.2" and "_[2]".
data = '["foo", 10, [{"bar": null, "baz": "test"}, false]]'
instance, error_message = Viv.make_instance(data)
value, error_message = Viv.run(instance, "return(_[2][0]['bar'])")
print(value) # None (null is represented as None in Python.)
value, error_message = Viv.run(instance, "return(_.2.-2.baz)")
print(value) # test
# Calling class method
# - Method object has 2 elements.
#     - 1st element is method name as str.
#     - 2nd element is its arguments as list/tuple.
#   In the following example, Method('add', [10, 20]).
#   It is equivalent to "return(add(10, 20))".
code = 'function add(a, b) {' \
       ' return(a + b)' \
       '}'
instance, error_message = Viv.make_instance(code)
value, error_message = Viv.run(instance, Method('add', [10, 20]))
print(value) # 30
value, error_message = Viv.run(instance, 'return(add(10, 20))')
print(value) # 30

In command-line,

# The specific variable's value
vivjson '{"foo": 10, "bar": 30, "baz": 20}' 'return(foo)' # 10
# or
python3 -m vivjson '{"foo": 10, "bar": 30, "baz": 20}' 'return(foo)' # 10

# Using PIPE (-i option)
echo '{"foo": 10, "bar": 30, "baz": 20}' | vivjson -i 'return(foo)' # 10

# The calculated value
echo '{"foo": 10, "bar": 30, "baz": 20}' | \
vivjson -i 'return(foo + bar + baz)' #60

# Find maximum value.
echo '{"foo": 10, "bar": 30, "baz": 20}' | vivjson -i=pairs \
'max=-1, for (pair in pairs) {if (max < pair[1]) {max = pair[1]}}, return(max)' # 30

# Find maximum value without PIPE.
vivjson "pairs=" + '{"foo": 10, "bar": 30, "baz": 20}' \
'max=-1, for (pair in pairs) {if (max < pair[1]) {max = pair[1]}}, return(max)' # 30
# Note that "+" of arguments is concatenation. Of course, the following
# representation can be accepted.
vivjson 'pairs={"foo": 10, "bar": 30, "baz": 20}' \
'max=-1, for (pair in pairs) {if (max < pair[1]) {max = pair[1]}}, return(max)' # 30

# Getting member's value
# "return(foo.bar)" and "return(foo['bar'])" are equivalent.
vivjson '{"foo": [1, {"bar": true}, "test"]}' 'return(foo[0])' # 1
vivjson '{"foo": [1, {"bar": true}, "test"]}' 'return(foo.1.bar)' # true
vivjson '{"foo": [1, {"bar": true}, "test"]}' 'return(foo.-1)' # test

# Using implicit variable
# When the given value is not JSON object (key-value pairs),
# it is assigned into implicit variable.
# The implicit variable is "_" if there is one value. Otherwise,
# "_[0]", "_[1]", ... are used.
vivjson 1.5 'return(_)' # 1.5
vivjson 1.5 2 'return(_[0] + _[1])' # 3.5
echo '[{"name": "dog", "number": 2}, {"name": "cat", "number": 3}]' | \
vivjson -i 'result = {}' \
'for (data in _) {result[data.name] = data.number}' \
'return(result)' # {"dog": 2, "cat": 3}

# Help
vivjson

Installation

Python version: >= 3.9

python3 -m pip install vivjson

License

Licensed under the Apache License, Version 2.0.

API

Pattern Consumed memory Next running speed
Direct running Low Slow
Parsing and Running Middle Middle
Making class instance and Running High Fast

When class instance is made, class method can be called and member's variable can be updated.

Direct running

It is suitable if running times is only one.

            +--------------------+
            |                    |
            | Viv                |
            |                    |
            |  +--------------+  |
Python's    |  |              |  |     Python's
value   ------>| run          |------> value
            |  |              |  |       or
JSON's      |  |              |  |     JSON's
value   ------>|              |  |     value
            |  |              |  |
Script      |  |              |  |
code    ------>|              |  |
            |  +--------------+  |
            |                    |
            +--------------------+
                

For example,

value, error_message = Viv.run('{"foo": 3, "bar": 2}', "return(foo + bar)")
print(value) # 5

Parsing and Running

It is suitable that same running is repeated.
Because parsing is done only one time.

            +--------------------+                +--------------------+
            |                    |                |                    |
            | Viv                |                | Viv                |
            |                    |                |                    |
            |  +--------------+  |  Parsed        |  +--------------+  |
Python's    |  |              |  |  value/code    |  |              |  |     Python's
value   ------>| parse,       |--------------------->| run          |------> value
            |  | parse_file,  |  |                |  |              |  |       or
JSON's      |  | parse_text   |  |  Additional    |  |              |  |     JSON's
value   ------>|              |  |  Python/JSON's |  |              |  |     value
            |  |              |  |  value   -------->|              |  |
Script      |  |              |  |                |  |              |  |
code    ------>|              |  |  Additional ----->|              |  |
            |  +--------------+  |  Script code   |  +--------------+  |
            |                    |                |                    |
            +--------------------+                +--------------------+
                

For example,

parsed, error_message = Viv.parse("return(foo + bar)")
value, error_message = Viv.run('{"foo":3, "bar": 2}', parsed)
print(value) # 5

Making class instance and Running

It is suitable that same running is repeated.
Because parsing and initialization are done only one time.

            +---------------------+                +--------------------+
            |                     |                |                    |
            | Viv                 |                | Viv                |
            |                     |                |                    |
            |  +---------------+  |                |  +--------------+  |
Python's    |  |               |  |  Instance      |  |              |  |     Python's
value   ------>| make_instance |--------------------->| run          |------> value
            |  |               |  |                |  |              |  |       or
JSON's      |  |               |  |  Additional    |  |              |  |     JSON's
value   ------>|               |  |  Python/JSON's |  |              |  |     value
            |  |               |  |  value   -------->|              |  |
Script      |  |               |  |                |  |              |  |
code    ------>|               |  |  Additional ----->|              |  |
            |  +---------------+  |  Script code   |  |              |  |
            |                     |                |  |              |  |
            +---------------------+  Calling -------->|              |  |
                                     Method        |  +--------------+  |
                                                   |                    |
                                                   +--------------------+
                

For example,

code = "function add(a, b) {" \
       " return(a + b)" \
       "}" \
       "c = [20, false]"
instance, error_message = Viv.make_instance(code)

value, error_message = Viv.run(instance, '{"foo":3, "bar": 2}', 'return(add(foo, bar))')
print(value) # 5
value, error_message = Viv.run(instance, Method("add", [3, 2]))
print(value) # 5

value, error_message = Viv.run(instance, "return(c[0])")
print(value) # 20

Viv class

The following methods are available.

  • Running/Deserialization function
    • run : Run VivJson's code or deserialize JSON objects.
    • parse : Parse VivJson's code and JSON object.
    • parse_file : Parse a file that contains VivJson's code or JSON object.
    • parse_text : Parse a text that is VivJson's code or JSON object.
    • make_instance : Makes a class instance.
  • String conversion
    • make_string : Convert into String. Serialize into JSON string.

The following arguments can be given into all methods except make_string.
Note that make_string's argument is Any value.

Argument type Python object type Example
A VivJson's code str 'foo = bar / 2', 'result = test(100)'
A JSON value str '{"foo": 10, "bar": true}', '[1, 2]', 'dog', 'null'
A JSON value Json Json('{"foo": 10, "bar": true}'), Json('[1, 2]'), Json('dog'), Json('null')
A file path str 'data/events.json', 'calc.viv'
Some VivJson's codes, JSON values, file paths, variables, Parsed objects list or tuple [Json('{"foo": 10, "bar": 1.5}'), 'baz = foo + bar', 'return(baz)']
Some variables (name/value pairs) dict {"foo": "alpha", "bar": true}
Some configurations Config Config(infinity="Infinity", nan="NaN")
Some parsed statements Parsed Viv.parse('return(a+b)')
A class instance Instance Viv.make_instance('{"a": 3, "b": 2}')
A calling class-method Method Method('add', [100, -10])
The 1st element is the method name as str. The following list is its arguments.

The calling class-method needs class instance in arguments.
code = 'function add(a, b) {' \
       ' return(a + b)' \
       '}'
instance, error_message = Viv.make_instance(code)
value, error_message = Viv.run(instance, Method('add', [100, -10]))
print(value) # 90

Multiple arguments can be given into all methods except make_string.
Furthermore, an array of arguments can be given too. For example, value, error_message = Viv.run('{a:3,b:2}', 'return(a+b)') is equivalent to value, error_message = Viv.run(['{a:3,b:2}', 'return(a+b)']).

There are two value as the returned value except make_string.
Note that make_string's returned value is str.

Method First value if succeeded First value if failed Second value if succeeded Second value if failed
run bool, int, float, str, list, dict, None None "" (empty) Error message
parse Parsed None "" (empty) Error message
parse_file Parsed None "" (empty) Error message
parse_text Parsed None "" (empty) Error message
make_instance Instance None "" (empty) Error message
dict's key is str. Its value is bool, int, float, str, list, dict, or None.
list's element is also bool, int, float, str, list, dict, or None.

Config class

The following configurations are available.

Name Object type Default value Description
enable_stderr bool False When True is given, error message is outputted into stderr.
enable_tag_detail bool False When True is given, error message's tag contains either of "Lexer", "Parser", or "Evaluator".
enable_only_json bool False When True is given, the given data is parsed as JSON. In other words, script is disabled.
infinity str or None None When string is given, Infinity is allowed in JSON. Then the given string is used to input/output Infinity from/to JSON. (Note that it is not surrounded with quotation mark.)
When None is given and Infinity is happen, error is occurred.
nan str or None None When string is given, NaN (Not a Number) is allowed in JSON. Then the given string is used to input/output NaN from/to JSON. (Note that it is not surrounded with quotation mark.)
When None is given and NaN is happen, error is occurred.
max_array_size int 1000 Maximum array/block size.
max_depth int 200 Maximum recursive called times of evaluate method.
max_loop_times int 1000 Maximum loop times of "for", "while", and so on.

Each configuration is set/gotten with the following method.
On the other hand, each configuration can be given as an argument of constructor.

Name get method set method
enable_stderr get_enable_stderr enable_stderr
enable_tag_detail get_enable_tag_detail enable_tag_detail
enable_only_json get_enable_only_json enable_only_json
infinity get_infinity set_infinity
nan get_nan set_nan
max_array_size get_max_array_size set_max_array_size
max_depth get_max_depth set_max_depth
max_loop_times get_max_loop_times set_max_loop_times

For example,
config = Config(max_array_size=10)
config.set_infinity("Infinity")
config.set_nan("NaN")
value, error_message = Viv.run(Json(text), code, config)

By the way, the following JSON object is invalid because JSON's number can't treat Infinity and NaN. (See RFC 8259 Section 6)
However, VivJson can treat it using Config#set_infinity and Config#set_nan.
{
    "normal": 1.5,
    "inf": Infinity,
    "negative_inf": -Infinity,
    "nan": NaN,
    "str": "1.5"
}