From 615ec3497e4914c5268e2b271e2ecca901db365f Mon Sep 17 00:00:00 2001 From: cedric05 Date: Tue, 31 Dec 2024 08:07:50 +0000 Subject: [PATCH] var support json substituion and expression substition --- dothttp/http.tx | 4 +- dothttp/parse/__init__.py | 2 +- dothttp/parse/dsl_jsonparser.py | 23 +++++++- dothttp/parse/expression.py | 85 +++++++++++++++++++++++++++++ dothttp/utils/property_util.py | 1 - pyproject.toml | 2 +- test/core/test_expression.py | 95 +++++++++++++++++++++++++++++++++ test/core/var/test_var.py | 41 ++++++++++++-- test/core/var/var_json_sub.http | 32 +++++++++++ 9 files changed, 274 insertions(+), 11 deletions(-) create mode 100644 dothttp/parse/expression.py create mode 100644 test/core/test_expression.py create mode 100644 test/core/var/var_json_sub.http diff --git a/dothttp/http.tx b/dothttp/http.tx index 5de7934..0d9f8c7 100644 --- a/dothttp/http.tx +++ b/dothttp/http.tx @@ -213,7 +213,7 @@ TOFILE: ; JSON: - array=Array | object=Object + array=Array | object=Object | var = VarString ; Array: @@ -237,7 +237,7 @@ Factor: ; Number: - /\d+/ + /\d+(\.)?\d*/ | ID ; diff --git a/dothttp/parse/__init__.py b/dothttp/parse/__init__.py index b913bcd..500cac4 100644 --- a/dothttp/parse/__init__.py +++ b/dothttp/parse/__init__.py @@ -362,7 +362,7 @@ def load_properties_from_var(model:MultidefHttp, property_util: PropertyProvider ## but it will complicate the code for variable in model.variables: if variable.value: - var_value = jsonmodel_to_json(variable.value) + var_value = jsonmodel_to_json(variable.value, property_util=property_util) property_util.add_infile_property_from_var(variable.name, var_value) elif variable.func: func_name = f"${variable.func.name}" diff --git a/dothttp/parse/dsl_jsonparser.py b/dothttp/parse/dsl_jsonparser.py index 012721b..55a39d5 100644 --- a/dothttp/parse/dsl_jsonparser.py +++ b/dothttp/parse/dsl_jsonparser.py @@ -1,8 +1,11 @@ -import json +import logging from typing import Dict, List, Optional, Union from ..utils.property_util import PropertyProvider, get_no_replace_property_provider from ..utils.common import triple_or_double_tostring +from .expression import Token, TokenType + +base_logger = logging.getLogger("dothttp") class JsonParser: @@ -19,6 +22,8 @@ def json_or_array_to_json(self, model) -> Union[Dict, List]: self.get_key(member): self.jsonmodel_to_json(member.value) for member in json_object.members } + elif var_value := model.var: + return self.property_util.get_updated_obj_content(var_value) def get_key(self, member): if member.key: @@ -47,7 +52,21 @@ def jsonmodel_to_json(self, model): elif model == "null": return None elif expr := model.expr: - return eval(expr) + try: + expression = Token.parse_expr(expr) + new_expression = "" + for token in expression: + if token.token_type == TokenType.VAR: + value = self.property_util.resolve_property_string(token.value) + else: + value = token.value + if not isinstance(value, str): + value = str(value) + new_expression += value + return eval(new_expression) + except: + base_logger.error(f"error in evaluating expression {expr}, new expression {new_expression}") + return 0 # Supporting function diff --git a/dothttp/parse/expression.py b/dothttp/parse/expression.py new file mode 100644 index 0000000..39441bb --- /dev/null +++ b/dothttp/parse/expression.py @@ -0,0 +1,85 @@ + +from enum import Enum + + +class TokenType(Enum): + VAR = 1 + OPERATOR = 2 + NUMBER = 3 + PARENTHESES = 4 + +class Token(): + def __init__(self, token_type, value): + self.token_type = token_type + self.value = value + + def __repr__(self): + return f"{self.token_type}({self.value})" + + def __eq__(self, compr): + if self.value == compr.value and self.token_type == compr.token_type: + return True + return False + + @staticmethod + def var(value): + return Token(TokenType.VAR, value) + + @staticmethod + def operator(value): + return Token(TokenType.OPERATOR, value) + + @staticmethod + def number(value): + return Token(TokenType.NUMBER, value) + + @staticmethod + def parentheses(value): + return Token(TokenType.PARENTHESES, value) + + @staticmethod + def parse_expr(expr): + current_var = "" + numeric = "" + for i in expr: + if current_var: + if i in ['\t', '\n', ' ']: + continue + elif i in ['+', '-', '*', '/']: + yield Token.var(current_var) + yield Token.operator(i) + current_var = "" + continue + elif i in ['(', ')']: + yield Token.var(current_var) + yield Token.parentheses(i) + current_var = "" + else: + current_var += i + elif numeric: + if i in ['\t', '\n', ' ']: + continue + elif i in ['+', '-', '*', '/']: + yield Token.number(numeric) + yield Token.operator(i) + numeric = "" + continue + elif i in ['(', ')']: + yield Token.number(numeric) + yield Token.parentheses(i) + numeric = "" + else: + numeric += i + elif i in ['+', '-', '*', '/']: + yield Token.operator(i) + elif i in ['(', ')']: + yield Token.parentheses(i) + elif i.isalpha(): + current_var = i + elif i.isnumeric(): + numeric = i + if current_var: + yield Token.var(current_var) + if numeric: + yield Token.number(numeric) + diff --git a/dothttp/utils/property_util.py b/dothttp/utils/property_util.py index 7af1697..e3d31e4 100644 --- a/dothttp/utils/property_util.py +++ b/dothttp/utils/property_util.py @@ -205,7 +205,6 @@ def check_properties_for_content(self, content): ) return content_prop_needed, props_needed - @lru_cache def available_properties_list(self): return ( set(self.env_properties.keys()) diff --git a/pyproject.toml b/pyproject.toml index 58b9165..fcb1fb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "dothttp-req" -version = "0.0.44a2" +version = "0.0.44a3" description = "Dothttp is Simple http client for testing and development" authors = ["Prasanth "] license = "MIT" diff --git a/test/core/test_expression.py b/test/core/test_expression.py new file mode 100644 index 0000000..2103731 --- /dev/null +++ b/test/core/test_expression.py @@ -0,0 +1,95 @@ +import pytest +from dothttp.parse.expression import Token, TokenType + +def test_var_token(): + token = Token.var("x") + assert token.token_type == TokenType.VAR + assert token.value == "x" + +def test_operator_token(): + token = Token.operator("+") + assert token.token_type == TokenType.OPERATOR + assert token.value == "+" + +def test_number_token(): + token = Token.number("123") + assert token.token_type == TokenType.NUMBER + assert token.value == "123" + +def test_parentheses_token(): + token = Token.parentheses("(") + assert token.token_type == TokenType.PARENTHESES + assert token.value == "(" + +def test_parse_expr_simple(): + expr = "x + 1" + tokens = list(Token.parse_expr(expr)) + print(tokens) + assert tokens == [ + Token.var("x"), + Token.operator("+"), + Token.number("1") + ] + +def test_parse_expr_with_parentheses(): + expr = "(x + 1) * y" + tokens = list(Token.parse_expr(expr)) + assert tokens == [ + Token.parentheses("("), + Token.var("x"), + Token.operator("+"), + Token.number("1"), + Token.parentheses(")"), + Token.operator("*"), + Token.var("y") + ] + +def test_parse_expr_with_spaces(): + expr = " x + 1 " + tokens = list(Token.parse_expr(expr)) + assert tokens == [ + Token.var("x"), + Token.operator("+"), + Token.number("1") + ] + +def test_parse_expr_with_multiple_digits(): + expr = "x + 123" + tokens = list(Token.parse_expr(expr)) + assert tokens == [ + Token.var("x"), + Token.operator("+"), + Token.number("123") + ] + +def test_parse_expr_with_multiple_vars(): + expr = "x + y" + tokens = list(Token.parse_expr(expr)) + assert tokens == [ + Token.var("x"), + Token.operator("+"), + Token.var("y") + ] + +def test_parse_expr_edge_case_empty(): + expr = "" + tokens = list(Token.parse_expr(expr)) + assert tokens == [] + +def test_parse_expr_edge_case_only_operators(): + expr = "+-*/" + tokens = list(Token.parse_expr(expr)) + assert tokens == [ + Token.operator("+"), + Token.operator("-"), + Token.operator("*"), + Token.operator("/") + ] + +def test_parse_expr_edge_case_only_parentheses(): + expr = "()" + tokens = list(Token.parse_expr(expr)) + assert tokens == [ + Token.parentheses("("), + Token.parentheses(")") + ] \ No newline at end of file diff --git a/test/core/var/test_var.py b/test/core/var/test_var.py index 537d0ab..63fba4b 100644 --- a/test/core/var/test_var.py +++ b/test/core/var/test_var.py @@ -149,12 +149,11 @@ def test_override(self): "incorrect body", ) - def test_var_templating(self): req = self.get_request(f"{base_dir}/templating.http") self.assertEqual( { - "a":10, + "a": 10, "b": "10 + 10 = 20" }, json.loads(req.body), @@ -162,6 +161,40 @@ def test_var_templating(self): ) # parse url and get query c parsed = urlparse(req.url) - # c=randomstrs + # c=randomstrs # total 12 = 10 (generated) + 2 (c=) - self.assertEqual(len(parsed.query), 12) \ No newline at end of file + self.assertEqual(len(parsed.query), 12) + + def test_var_json_sub(self): + req = self.get_request(f"{base_dir}/var_json_sub.http") + expected = { + "a": { + "b": { + "d": { + "e": { + "a": 10, + "b": 11.1, + "c": 21.1, + "d": "hello world", + "e": "if is a=10, b=11.1 and a + b is 21.1", + "f": True, + "g": None + } + } + } + } + } + + self.assertEqual(expected, json.loads(req.body), "incorrect body") + + def test_var_expression(self): + req = self.get_request(f"{base_dir}/expression_var.http") + expected = { + 'difference': 4.8999999999999995, + 'product': 52.52, + 'quotient': 1.942307692307692, + 'sum': 15.3, + "test": "This is a test" + } + + self.assertEqual(expected, json.loads(req.body), "incorrect body") diff --git a/test/core/var/var_json_sub.http b/test/core/var/var_json_sub.http new file mode 100644 index 0000000..e9f13af --- /dev/null +++ b/test/core/var/var_json_sub.http @@ -0,0 +1,32 @@ +var a = 10; +var b = 11.1; +var c = (10 + 11.1); +var d = "hello world"; +var e = $"if is a={a}, b={b} and a + b is {c}"; +var f = true; +var g = null; +var h = { + "a": {{a}}, + "b": {{b}}, + "c": {{c}}, + "d": {{d}}, + "e": {{e}}, + "f": {{f}}, + "g": {{g}} +}; +var e = { + "a":{ + "b": { + "d": { + "e": {{h}} + } + } + } +}; + +POST "https://req.dothttp.dev/" +json( + {{e}} +) + +