-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
SES: Support templates with if/else (#6867)
- Loading branch information
Showing
8 changed files
with
279 additions
and
50 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,53 +1,180 @@ | ||
from typing import Any, Dict, Optional | ||
from typing import Any, Dict, Optional, Type | ||
|
||
from .exceptions import MissingRenderingAttributeException | ||
from moto.utilities.tokenizer import GenericTokenizer | ||
|
||
|
||
class BlockProcessor: | ||
def __init__( | ||
self, template: str, template_data: Dict[str, Any], tokenizer: GenericTokenizer | ||
): | ||
self.template = template | ||
self.template_data = template_data | ||
self.tokenizer = tokenizer | ||
|
||
def parse(self) -> str: | ||
# Added to make MyPy happy | ||
# Not all implementations have this method | ||
# It's up to the caller to know whether to call this method | ||
raise NotImplementedError | ||
|
||
|
||
class EachBlockProcessor(BlockProcessor): | ||
def __init__( | ||
self, template: str, template_data: Dict[str, Any], tokenizer: GenericTokenizer | ||
): | ||
self.template = template | ||
self.tokenizer = tokenizer | ||
|
||
self.tokenizer.skip_characters("#each") | ||
self.tokenizer.skip_white_space() | ||
var_name = self.tokenizer.read_until("}}").strip() | ||
self.tokenizer.skip_characters("}}") | ||
self.template_data = template_data.get(var_name, []) | ||
|
||
def parse(self) -> str: | ||
parsed = "" | ||
current_pos = self.tokenizer.token_pos | ||
|
||
for template_data in self.template_data: | ||
self.tokenizer.token_pos = current_pos | ||
for char in self.tokenizer: | ||
if char == "{" and self.tokenizer.peek() == "{": | ||
self.tokenizer.skip_characters("{") | ||
self.tokenizer.skip_white_space() | ||
|
||
_processor = get_processor(self.tokenizer)( | ||
self.template, template_data, self.tokenizer # type: ignore | ||
) | ||
# If we've reached the end, we should stop processing | ||
# Our parent will continue with whatever comes after {{/each}} | ||
if type(_processor) == EachEndBlockProcessor: | ||
break | ||
# If we've encountered another processor, they can continue | ||
parsed += _processor.parse() | ||
|
||
continue | ||
|
||
parsed += char | ||
|
||
return parsed | ||
|
||
|
||
class EachEndBlockProcessor(BlockProcessor): | ||
def __init__( | ||
self, template: str, template_data: Dict[str, Any], tokenizer: GenericTokenizer | ||
): | ||
super().__init__(template, template_data, tokenizer) | ||
|
||
self.tokenizer.skip_characters("/each") | ||
self.tokenizer.skip_white_space() | ||
self.tokenizer.skip_characters("}}") | ||
|
||
|
||
class IfBlockProcessor(BlockProcessor): | ||
def __init__( | ||
self, template: str, template_data: Dict[str, Any], tokenizer: GenericTokenizer | ||
): | ||
super().__init__(template, template_data, tokenizer) | ||
|
||
self.tokenizer.skip_characters("#if") | ||
self.tokenizer.skip_white_space() | ||
condition = self.tokenizer.read_until("}}").strip() | ||
self.tokenizer.skip_characters("}}") | ||
self.parse_contents = template_data.get(condition) | ||
|
||
def parse(self) -> str: | ||
parsed = "" | ||
|
||
for char in self.tokenizer: | ||
if char == "{" and self.tokenizer.peek() == "{": | ||
self.tokenizer.skip_characters("{") | ||
self.tokenizer.skip_white_space() | ||
|
||
_processor = get_processor(self.tokenizer)( | ||
self.template, self.template_data, self.tokenizer | ||
) | ||
if type(_processor) == IfEndBlockProcessor: | ||
break | ||
elif type(_processor) == ElseBlockProcessor: | ||
self.parse_contents = not self.parse_contents | ||
continue | ||
if self.parse_contents: | ||
parsed += _processor.parse() | ||
|
||
continue | ||
|
||
if self.parse_contents: | ||
parsed += char | ||
|
||
return parsed | ||
|
||
|
||
class IfEndBlockProcessor(BlockProcessor): | ||
def __init__( | ||
self, template: str, template_data: Dict[str, Any], tokenizer: GenericTokenizer | ||
): | ||
super().__init__(template, template_data, tokenizer) | ||
|
||
self.tokenizer.skip_characters("/if") | ||
self.tokenizer.skip_white_space() | ||
self.tokenizer.skip_characters("}}") | ||
|
||
|
||
class ElseBlockProcessor(BlockProcessor): | ||
def __init__( | ||
self, template: str, template_data: Dict[str, Any], tokenizer: GenericTokenizer | ||
): | ||
super().__init__(template, template_data, tokenizer) | ||
|
||
self.tokenizer.skip_characters("else") | ||
self.tokenizer.skip_white_space() | ||
self.tokenizer.skip_characters("}}") | ||
|
||
|
||
class VarBlockProcessor(BlockProcessor): | ||
def parse(self) -> str: | ||
var_name = self.tokenizer.read_until("}}").strip() | ||
if self.template_data.get(var_name) is None: | ||
raise MissingRenderingAttributeException(var_name) | ||
data: str = self.template_data.get(var_name) # type: ignore | ||
self.tokenizer.skip_white_space() | ||
self.tokenizer.skip_characters("}}") | ||
return data | ||
|
||
|
||
def get_processor(tokenizer: GenericTokenizer) -> Type[BlockProcessor]: | ||
if tokenizer.peek(5) == "#each": | ||
return EachBlockProcessor | ||
if tokenizer.peek(5) == "/each": | ||
return EachEndBlockProcessor | ||
if tokenizer.peek(3) == "#if": | ||
return IfBlockProcessor | ||
if tokenizer.peek(3) == "/if": | ||
return IfEndBlockProcessor | ||
if tokenizer.peek(4) == "else": | ||
return ElseBlockProcessor | ||
return VarBlockProcessor | ||
|
||
|
||
def parse_template( | ||
template: str, | ||
template_data: Dict[str, Any], | ||
tokenizer: Optional[GenericTokenizer] = None, | ||
until: Optional[str] = None, | ||
) -> str: | ||
tokenizer = tokenizer or GenericTokenizer(template) | ||
state = "" | ||
|
||
parsed = "" | ||
var_name = "" | ||
for char in tokenizer: | ||
if until is not None and (char + tokenizer.peek(len(until) - 1)) == until: | ||
return parsed | ||
if char == "{" and tokenizer.peek() == "{": | ||
# Two braces next to each other indicate a variable/language construct such as for-each | ||
# We have different processors handling different constructs | ||
tokenizer.skip_characters("{") | ||
tokenizer.skip_white_space() | ||
if tokenizer.peek() == "#": | ||
state = "LOOP" | ||
tokenizer.skip_characters("#each") | ||
tokenizer.skip_white_space() | ||
else: | ||
state = "VAR" | ||
continue | ||
if char == "}": | ||
if state in ["LOOP", "VAR"]: | ||
tokenizer.skip_characters("}") | ||
if state == "VAR": | ||
if template_data.get(var_name) is None: | ||
raise MissingRenderingAttributeException(var_name) | ||
parsed += template_data.get(var_name) # type: ignore | ||
else: | ||
current_position = tokenizer.token_pos | ||
for item in template_data.get(var_name, []): | ||
tokenizer.token_pos = current_position | ||
parsed += parse_template( | ||
template, item, tokenizer, until="{{/each}}" | ||
) | ||
tokenizer.skip_characters("{/each}}") | ||
var_name = "" | ||
state = "" | ||
continue | ||
if state in ["LOOP", "VAR"]: | ||
var_name += char | ||
|
||
_processor = get_processor(tokenizer)(template, template_data, tokenizer) | ||
parsed += _processor.parse() | ||
continue | ||
|
||
parsed += char | ||
return parsed |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,28 @@ | ||
# This file is intentionally left blank. | ||
import os | ||
from functools import wraps | ||
from moto import mock_ses | ||
|
||
|
||
def ses_aws_verified(func): | ||
""" | ||
Function that is verified to work against AWS. | ||
Can be run against AWS at any time by setting: | ||
MOTO_TEST_ALLOW_AWS_REQUEST=true | ||
If this environment variable is not set, the function runs in a `mock_ses` context. | ||
""" | ||
|
||
@wraps(func) | ||
def pagination_wrapper(): | ||
allow_aws_request = ( | ||
os.environ.get("MOTO_TEST_ALLOW_AWS_REQUEST", "false").lower() == "true" | ||
) | ||
|
||
if allow_aws_request: | ||
resp = func() | ||
else: | ||
with mock_ses(): | ||
resp = func() | ||
return resp | ||
|
||
return pagination_wrapper |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.