diff --git a/src/viur/core/bones/__init__.py b/src/viur/core/bones/__init__.py index c921cc5b5..a8a7df07d 100644 --- a/src/viur/core/bones/__init__.py +++ b/src/viur/core/bones/__init__.py @@ -13,6 +13,7 @@ UniqueValue, ) from .boolean import BooleanBone +from .code import CodeBone, JinjaBone, LogicsBone, PythonBone from .captcha import CaptchaBone from .color import ColorBone from .credential import CredentialBone @@ -51,6 +52,10 @@ "BaseBone", "BooleanBone", "CaptchaBone", + "CodeBone", + "JinjaBone", + "LogicsBone", + "PythonBone", "CloneBehavior", "CloneStrategy", "ColorBone", diff --git a/src/viur/core/bones/code.py b/src/viur/core/bones/code.py new file mode 100644 index 000000000..5293a32b1 --- /dev/null +++ b/src/viur/core/bones/code.py @@ -0,0 +1,95 @@ +import ast +import jinja2 +import logics +from viur.core.bones.raw import RawBone + + +class CodeBone(RawBone): + """ + Stores source code with optional language-specific syntax validation. + + The ``syntax`` parameter sets the type suffix used by the frontend for syntax + highlighting, e.g. ``syntax="python"`` yields ``type = "raw.code.python"``. + ``type_suffix`` can override this to an arbitrary suffix. + + Neither ``multiple`` nor ``languages`` are supported. + Setting ``validate=False`` disables any syntax validation in subclasses. + """ + + type = "raw.code" + + def __init__( + self, + *, + indexed: bool = False, + languages=None, + multiple: bool = False, + syntax: str | None = None, + type_suffix: str = "", + validate: bool = True, + **kwargs, + ): + assert not multiple, "CodeBone does not support multiple values" + assert not languages, "CodeBone does not support language variants" + self.syntax = syntax + self.validate = validate + if self.syntax: + type_suffix = type_suffix or syntax + super().__init__(indexed=indexed, type_suffix=type_suffix, **kwargs) + + +class LogicsBone(CodeBone): + """ + Validates its value as a Logics expression (https://github.com/viur-framework/logics). + Uses Python syntax highlighting in the frontend. + """ + + def __init__(self, *, syntax: str = "logics", **kwargs): + super().__init__(syntax=syntax, type_suffix="python", **kwargs) + + def singleValueFromClient(self, value, skel, bone_name, client_data): + if value := str(value or "").strip(): + value += "\n" + return super().singleValueFromClient(value, skel, bone_name, client_data) + + def isInvalid(self, value): + if self.validate and value: + try: + logics.Logics(value) + except logics.ParseException as e: + return str(e).replace("&eof", "end-of-expression") + + +class JinjaBone(CodeBone): + """ + Validates its value as a Jinja2 template. + """ + + def __init__(self, *, syntax: str = "jinja2", **kwargs): + super().__init__(syntax=syntax, **kwargs) + + def isInvalid(self, value): + if self.validate and value: + env = jinja2.Environment() + try: + env.parse(value) + except jinja2.TemplateSyntaxError as e: + return f"Syntax error in line {e.lineno}: {e.message}" + except jinja2.TemplateError as e: + return f"General error: {e}" + + +class PythonBone(CodeBone): + """ + Validates its value as Python source code. + """ + + def __init__(self, *, syntax: str = "python", **kwargs): + super().__init__(syntax=syntax, **kwargs) + + def isInvalid(self, value): + if self.validate and value: + try: + ast.parse(value) + except SyntaxError as e: + return f"Syntax error in line {e.lineno}: {e.msg}" diff --git a/src/viur/core/modules/script.py b/src/viur/core/modules/script.py index 5937a7d2c..fab551f18 100644 --- a/src/viur/core/modules/script.py +++ b/src/viur/core/modules/script.py @@ -66,10 +66,8 @@ class ScriptLeafSkel(BaseScriptAbstractSkel): else "Filename is invalid or doesn't have a '.py'-suffix", ) - script = RawBone( + script = PythonBone( descr="Code", - type_suffix="code.python", - indexed=False, ) access = SelectBone( diff --git a/src/viur/core/skeleton/tasks.py b/src/viur/core/skeleton/tasks.py index 9102e46c6..7b0c2586c 100644 --- a/src/viur/core/skeleton/tasks.py +++ b/src/viur/core/skeleton/tasks.py @@ -15,7 +15,7 @@ from .utils import skeletonByKind, listKnownSkeletons from .relskel import RelSkel -from ..bones.raw import RawBone +from ..bones.code import LogicsBone from ..bones.record import RecordBone from ..bones.relational import RelationalBone, RelationalConsistency, RelationalUpdateLevel from ..bones.select import SelectBone @@ -221,9 +221,8 @@ class FilterRowUsingSkel(RelSkel): format="$(name)$(op)=$(value)", ) - condition = RawBone( + condition = LogicsBone( descr="Condition", - type_suffix="code.python", # Logics expression required=True, defaultValue="False # fused: by default, doesn't affect anything.\n", params={ @@ -232,11 +231,6 @@ class FilterRowUsingSkel(RelSkel): ) def execute(self, task, kinds, filters, condition): - try: - logics.Logics(condition) - except logics.ParseException as e: - raise errors.BadRequest(f"Error parsing condition {e}") - notify = current.user.get()["name"] for kind in kinds: