Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,98 +1,35 @@
from __future__ import annotations

import json
from enum import Enum
from typing import Final

from localstack.services.stepfunctions.asl.antlr.runtime.ASLLexer import ASLLexer
from localstack.services.stepfunctions.asl.component.state.state_choice.comparison.comparison_operator_type import (
ComparisonOperatorType,
)
from localstack.services.stepfunctions.asl.component.state.state_choice.comparison.comparison_stmt import (
ComparisonStmt,
)
from localstack.services.stepfunctions.asl.component.state.state_wait.variable import NoSuchVariable
from localstack.services.stepfunctions.asl.component.state.state_choice.comparison.operator.factory import (
OperatorFactory,
)
from localstack.services.stepfunctions.asl.component.state.state_choice.comparison.operator.operator import (
Operator,
)
from localstack.services.stepfunctions.asl.eval.environment import Environment


class ComparisonFunc(ComparisonStmt):
class ComparisonOperator(Enum):
BooleanEqual = ASLLexer.BOOLEANEQUALS
BooleanEqualsPath = ASLLexer.BOOLEANQUALSPATH
IsBoolean = ASLLexer.ISBOOLEAN
IsNull = ASLLexer.ISNULL
IsNumeric = ASLLexer.ISNUMERIC
IsPresent = ASLLexer.ISPRESENT
IsString = ASLLexer.ISSTRING
IsTimestamp = ASLLexer.ISTIMESTAMP
NumericEquals = ASLLexer.NUMERICEQUALS
NumericEqualsPath = ASLLexer.NUMERICEQUALSPATH
NumericGreaterThan = ASLLexer.NUMERICGREATERTHAN
NumericGreaterThanPath = ASLLexer.NUMERICGREATERTHANPATH
NumericGreaterThanEquals = ASLLexer.NUMERICGREATERTHANEQUALS
NumericGreaterThanEqualsPath = ASLLexer.NUMERICGREATERTHANEQUALSPATH
NumericLessThan = ASLLexer.NUMERICLESSTHAN
NumericLessThanPath = ASLLexer.NUMERICLESSTHANPATH
NumericLessThanEquals = ASLLexer.NUMERICLESSTHANEQUALS
NumericLessThanEqualsPath = ASLLexer.NUMERICLESSTHANEQUALSPATH
StringEquals = ASLLexer.STRINGEQUALS
StringEqualsPath = ASLLexer.STRINGEQUALSPATH
StringGreaterThan = ASLLexer.STRINGGREATERTHAN
StringGreaterThanPath = ASLLexer.STRINGGREATERTHANPATH
StringGreaterThanEquals = ASLLexer.STRINGGREATERTHANEQUALS
StringGreaterThanEqualsPath = ASLLexer.STRINGGREATERTHANEQUALSPATH
StringLessThan = ASLLexer.STRINGLESSTHAN
StringLessThanPath = ASLLexer.STRINGLESSTHANPATH
StringLessThanEquals = ASLLexer.STRINGLESSTHANEQUALS
StringLessThanEqualsPath = ASLLexer.STRINGLESSTHANEQUALSPATH
StringMatches = ASLLexer.STRINGMATCHES
TimestampEquals = ASLLexer.TIMESTAMPEQUALS
TimestampEqualsPath = ASLLexer.TIMESTAMPEQUALSPATH
TimestampGreaterThan = ASLLexer.TIMESTAMPGREATERTHAN
TimestampGreaterThanPath = ASLLexer.TIMESTAMPGREATERTHANPATH
TimestampGreaterThanEquals = ASLLexer.TIMESTAMPGREATERTHANEQUALS
TimestampGreaterThanEqualsPath = ASLLexer.TIMESTAMPGREATERTHANEQUALSPATH
TimestampLessThan = ASLLexer.TIMESTAMPLESSTHAN
TimestampLessThanPath = ASLLexer.TIMESTAMPLESSTHANPATH
TimestampLessThanEquals = ASLLexer.TIMESTAMPLESSTHANEQUALS
TimestampLessThanEqualsPath = ASLLexer.TIMESTAMPLESSTHANEQUALSPATH

def __str__(self):
return f"({self.__class__.__name__}| {self})"

def __init__(self, operator: ComparisonFunc.ComparisonOperator, value: json):
self.operator: Final[ComparisonFunc.ComparisonOperator] = operator
def __init__(self, operator: ComparisonOperatorType, value: json):
self.operator_type: Final[ComparisonOperatorType] = operator
self.value: json = value

def _eval_body(self, env: Environment) -> None:
value = self.value
match self.operator:
case ComparisonFunc.ComparisonOperator.IsNull:
self._is_null(env, value)
case ComparisonFunc.ComparisonOperator.StringEquals:
self._string_equals(env, value)
case ComparisonFunc.ComparisonOperator.IsPresent:
self._is_present(env)
# TODO: add other operators.
case x:
raise NotImplementedError(f"ComparisonFunc '{x}' is not supported yet.") # noqa

@staticmethod
def _is_null(env: Environment, value: json) -> None:
if not isinstance(value, bool):
raise RuntimeError(f"Unexpected binding to IsNull: '{value}'.")
val = env.stack.pop()
is_null = val is None and not isinstance(
val, NoSuchVariable
) # TODO: what if input_state is empty, eg. "" or {}?
res = is_null == value
env.stack.append(res)
operator: Operator = OperatorFactory.get(self.operator_type)
operator.eval(env=env, value=value)

@staticmethod
def _string_equals(env: Environment, value: json) -> None:
val = env.stack.pop()
res = str(val) == value
env.stack.append(res)

@staticmethod
def _is_present(env: Environment) -> None:
val = env.stack.pop()
res = not isinstance(val, NoSuchVariable)
env.stack.append(res)
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from enum import Enum

from localstack.services.stepfunctions.asl.antlr.runtime.ASLLexer import ASLLexer


class ComparisonOperatorType(Enum):
BooleanEquals = ASLLexer.BOOLEANEQUALS
BooleanEqualsPath = ASLLexer.BOOLEANQUALSPATH
IsBoolean = ASLLexer.ISBOOLEAN
IsNull = ASLLexer.ISNULL
IsNumeric = ASLLexer.ISNUMERIC
IsPresent = ASLLexer.ISPRESENT
IsString = ASLLexer.ISSTRING
IsTimestamp = ASLLexer.ISTIMESTAMP
NumericEquals = ASLLexer.NUMERICEQUALS
NumericEqualsPath = ASLLexer.NUMERICEQUALSPATH
NumericGreaterThan = ASLLexer.NUMERICGREATERTHAN
NumericGreaterThanPath = ASLLexer.NUMERICGREATERTHANPATH
NumericGreaterThanEquals = ASLLexer.NUMERICGREATERTHANEQUALS
NumericGreaterThanEqualsPath = ASLLexer.NUMERICGREATERTHANEQUALSPATH
NumericLessThan = ASLLexer.NUMERICLESSTHAN
NumericLessThanPath = ASLLexer.NUMERICLESSTHANPATH
NumericLessThanEquals = ASLLexer.NUMERICLESSTHANEQUALS
NumericLessThanEqualsPath = ASLLexer.NUMERICLESSTHANEQUALSPATH
StringEquals = ASLLexer.STRINGEQUALS
StringEqualsPath = ASLLexer.STRINGEQUALSPATH
StringGreaterThan = ASLLexer.STRINGGREATERTHAN
StringGreaterThanPath = ASLLexer.STRINGGREATERTHANPATH
StringGreaterThanEquals = ASLLexer.STRINGGREATERTHANEQUALS
StringGreaterThanEqualsPath = ASLLexer.STRINGGREATERTHANEQUALSPATH
StringLessThan = ASLLexer.STRINGLESSTHAN
StringLessThanPath = ASLLexer.STRINGLESSTHANPATH
StringLessThanEquals = ASLLexer.STRINGLESSTHANEQUALS
StringLessThanEqualsPath = ASLLexer.STRINGLESSTHANEQUALSPATH
StringMatches = ASLLexer.STRINGMATCHES
TimestampEquals = ASLLexer.TIMESTAMPEQUALS
TimestampEqualsPath = ASLLexer.TIMESTAMPEQUALSPATH
TimestampGreaterThan = ASLLexer.TIMESTAMPGREATERTHAN
TimestampGreaterThanPath = ASLLexer.TIMESTAMPGREATERTHANPATH
TimestampGreaterThanEquals = ASLLexer.TIMESTAMPGREATERTHANEQUALS
TimestampGreaterThanEqualsPath = ASLLexer.TIMESTAMPGREATERTHANEQUALSPATH
TimestampLessThan = ASLLexer.TIMESTAMPLESSTHAN
TimestampLessThanPath = ASLLexer.TIMESTAMPLESSTHANPATH
TimestampLessThanEquals = ASLLexer.TIMESTAMPLESSTHANEQUALS
TimestampLessThanEqualsPath = ASLLexer.TIMESTAMPLESSTHANEQUALSPATH

# def __str__(self):
# return f"({self.__class__.__name__}| {self})"
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from localstack.services.stepfunctions.asl.component.state.state_choice.comparison.comparison_operator_type import (
ComparisonOperatorType,
)
from localstack.services.stepfunctions.asl.component.state.state_choice.comparison.operator.implementations.boolean_equals import * # noqa
from localstack.services.stepfunctions.asl.component.state.state_choice.comparison.operator.implementations.is_operator import * # noqa
from localstack.services.stepfunctions.asl.component.state.state_choice.comparison.operator.operator import (
Operator,
)


class OperatorFactory:
@staticmethod
def get(typ: ComparisonOperatorType) -> Operator:
op = Operator.get((str(typ)), raise_if_missing=False)
if op is None:
raise NotImplementedError(f"{typ} is not supported.")
return op
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from typing import Any

from localstack.services.stepfunctions.asl.component.state.state_choice.comparison.comparison_operator_type import (
ComparisonOperatorType,
)
from localstack.services.stepfunctions.asl.component.state.state_choice.comparison.operator.operator import (
Operator,
)
from localstack.services.stepfunctions.asl.eval.environment import Environment
from localstack.services.stepfunctions.asl.utils.json_path import JSONPathUtils


class BooleanEquals(Operator):
@staticmethod
def impl_name() -> str:
return str(ComparisonOperatorType.BooleanEquals)

@staticmethod
def eval(env: Environment, value: Any) -> None:
variable = env.stack.pop()
res = False
if isinstance(variable, bool):
res = variable is value
env.stack.append(res)


class BooleanEqualsPath(Operator):
@staticmethod
def impl_name() -> str:
return str(ComparisonOperatorType.BooleanEqualsPath)

@staticmethod
def eval(env: Environment, value: Any) -> None:
comp_value: bool = JSONPathUtils.extract_json(value, env.inp)
if not isinstance(comp_value, bool):
raise TypeError(f"Expected type bool, but got '{comp_value}' from path '{value}'.")

variable = env.stack.pop()

res = False
if isinstance(variable, bool):
res = bool(variable) is comp_value
env.stack.append(res)
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import datetime
import logging
from typing import Any, Final, Optional

from localstack.services.stepfunctions.asl.component.state.state_choice.comparison.comparison_operator_type import (
ComparisonOperatorType,
)
from localstack.services.stepfunctions.asl.component.state.state_choice.comparison.operator.operator import (
Operator,
)
from localstack.services.stepfunctions.asl.component.state.state_wait.variable import NoSuchVariable
from localstack.services.stepfunctions.asl.eval.environment import Environment

LOG = logging.getLogger(__name__)


class IsBoolean(Operator):
@staticmethod
def impl_name() -> str:
return str(ComparisonOperatorType.IsBoolean)

@staticmethod
def eval(env: Environment, value: Any) -> None:
variable = env.stack.pop()
res = isinstance(variable, bool) is value
env.stack.append(res)


class IsNull(Operator):
@staticmethod
def impl_name() -> str:
return str(ComparisonOperatorType.IsNull)

@staticmethod
def eval(env: Environment, value: Any) -> None:
variable = env.stack.pop()
is_null = variable is None and not isinstance(variable, NoSuchVariable)
res = is_null is value
env.stack.append(res)


class IsNumeric(Operator):
@staticmethod
def impl_name() -> str:
return str(ComparisonOperatorType.IsNumeric)

@staticmethod
def eval(env: Environment, value: Any) -> None:
variable = env.stack.pop()
res = (isinstance(variable, (int, float)) and not isinstance(variable, bool)) is value
env.stack.append(res)


class IsPresent(Operator):
@staticmethod
def impl_name() -> str:
return str(ComparisonOperatorType.IsPresent)

@staticmethod
def eval(env: Environment, value: Any) -> None:
variable = env.stack.pop()
res = not isinstance(variable, NoSuchVariable) is value
env.stack.append(res)


class IsString(Operator):
@staticmethod
def impl_name() -> str:
return str(ComparisonOperatorType.IsString)

@staticmethod
def eval(env: Environment, value: Any) -> None:
variable = env.stack.pop()
res = isinstance(variable, str) is value
env.stack.append(res)


class IsTimestamp(Operator):
# Timestamps are strings which MUST conform to the RFC3339 profile of ISO 8601, with the further restrictions that
# an uppercase "T" character MUST be used to separate date and time, and an uppercase "Z" character MUST be
# present in the absence of a numeric time zone offset, for example "2016-03-14T01:59:00Z".
TIMESTAMP_FORMAT: Final[str] = "%Y-%m-%dT%H:%M:%SZ"

@staticmethod
def impl_name() -> str:
return str(ComparisonOperatorType.IsTimestamp)

@staticmethod
def string_to_timestamp(string: str) -> Optional[datetime.datetime]:
try:
return datetime.datetime.strptime(string, IsTimestamp.TIMESTAMP_FORMAT)
except Exception:
return None

@staticmethod
def is_timestamp(inp: Any) -> bool:
return isinstance(inp, str) and IsTimestamp.string_to_timestamp(inp) is not None

@staticmethod
def eval(env: Environment, value: Any) -> None:
variable = env.stack.pop()
LOG.warning(
f"State Choice's 'IsTimestamp' operator is not fully supported for input '{variable}' and target '{value}'."
)
res = IsTimestamp.is_timestamp(variable) is value
env.stack.append(res)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import abc
from typing import Any

from localstack.services.stepfunctions.asl.eval.environment import Environment
from localstack.utils.objects import SubtypesInstanceManager


class Operator(abc.ABC, SubtypesInstanceManager):
@staticmethod
@abc.abstractmethod
def eval(env: Environment, value: Any) -> None:
pass
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
from typing import Final

from jsonpath_ng import parse

from localstack.services.stepfunctions.asl.component.state.state_choice.comparison.comparison_stmt import (
ComparisonStmt,
)
from localstack.services.stepfunctions.asl.eval.environment import Environment
from localstack.services.stepfunctions.asl.utils.json_path import JSONPathUtils


class NoSuchVariable:
Expand All @@ -18,9 +17,8 @@ def __init__(self, value: str):
self.value: Final[str] = value

def _eval_body(self, env: Environment) -> None:
variable_expr = parse(self.value)
try:
value = variable_expr.find(env.inp)
value = JSONPathUtils.extract_json(self.value, env.inp)
except Exception as ex:
value = NoSuchVariable(f"{self.value}, {ex}")
env.stack.append(value)
11 changes: 6 additions & 5 deletions localstack/services/stepfunctions/asl/parse/preprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@
from localstack.services.stepfunctions.asl.component.state.state_choice.comparison.comparison_func import (
ComparisonFunc,
)
from localstack.services.stepfunctions.asl.component.state.state_choice.comparison.comparison_operator_type import (
ComparisonOperatorType,
)
from localstack.services.stepfunctions.asl.component.state.state_choice.comparison.comparison_props import (
ComparisonProps,
)
Expand Down Expand Up @@ -325,17 +328,15 @@ def visitVariable_decl(self, ctx: ASLParser.Variable_declContext) -> Variable:
value: str = self._inner_string_of(parse_tree=ctx.keyword_or_string())
return Variable(value=value)

def visitComparison_op(
self, ctx: ASLParser.Comparison_opContext
) -> ComparisonFunc.ComparisonOperator:
def visitComparison_op(self, ctx: ASLParser.Comparison_opContext) -> ComparisonOperatorType:
try:
operator_type: int = ctx.children[0].symbol.type
return ComparisonFunc.ComparisonOperator(operator_type)
return ComparisonOperatorType(operator_type)
except Exception:
raise ValueError(f"Could not derive ComparisonOperator from context '{ctx.getText()}'.")

def visitComparison_func(self, ctx: ASLParser.Comparison_funcContext) -> ComparisonFunc:
comparison_op: ComparisonFunc.ComparisonOperator = self.visit(ctx.comparison_op())
comparison_op: ComparisonOperatorType = self.visit(ctx.comparison_op())

json_decl = ctx.json_value_decl()
json_str: str = json_decl.getText()
Expand Down
Loading