2020-01-26 00:04:29 +01:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
from glob import glob
|
2020-01-26 01:45:20 +01:00
|
|
|
from os import path
|
2020-01-26 00:04:29 +01:00
|
|
|
import itertools
|
2020-02-24 00:58:35 +01:00
|
|
|
from copy import deepcopy
|
2020-01-26 15:09:22 +01:00
|
|
|
from typing import Iterable, List, Union, Tuple
|
2020-01-26 01:45:20 +01:00
|
|
|
from tatsu import parse as tatsu_parse
|
2020-01-26 15:09:22 +01:00
|
|
|
from tatsu.ast import AST
|
2020-01-27 19:15:24 +01:00
|
|
|
from tatsu.exceptions import SemanticError
|
2020-01-26 01:29:19 +01:00
|
|
|
from .models import Location, Choice, Option, Dish
|
2020-02-24 00:31:14 +01:00
|
|
|
from utils import first
|
2020-01-26 00:04:29 +01:00
|
|
|
|
|
|
|
|
|
|
|
# TODO Use proper way to get resources, see https://stackoverflow.com/a/10935674
|
|
|
|
with open(path.join(path.dirname(__file__), "hlds.tatsu")) as fh:
|
|
|
|
GRAMMAR = fh.read()
|
|
|
|
|
|
|
|
|
2020-01-26 01:29:19 +01:00
|
|
|
def filter_instance(cls, iterable):
|
|
|
|
return [item for item in iterable if isinstance(item, cls)]
|
|
|
|
|
|
|
|
|
2020-09-26 01:10:37 +02:00
|
|
|
class ChoiceReference:
|
|
|
|
def __init__(self, identifier, price):
|
|
|
|
self.identifier = identifier
|
|
|
|
self.price = price
|
|
|
|
|
|
|
|
|
2020-01-26 01:45:20 +01:00
|
|
|
# pylint: disable=no-self-use
|
2020-01-26 01:29:19 +01:00
|
|
|
class HldsSemanticActions:
|
2020-01-26 15:09:22 +01:00
|
|
|
def location(self, ast) -> Location:
|
2020-07-17 11:40:15 +02:00
|
|
|
choices = {
|
|
|
|
choice.id: choice for choice in filter_instance(Choice, ast["items_"])
|
|
|
|
}
|
2020-03-04 22:56:45 +01:00
|
|
|
dishes: Iterable[Dish] = filter_instance(Dish, ast["items_"])
|
2020-01-26 01:40:50 +01:00
|
|
|
for dish in dishes:
|
|
|
|
for i, choice in enumerate(dish.choices):
|
2020-01-26 02:09:53 +01:00
|
|
|
if not isinstance(choice[1], Choice):
|
2020-09-26 01:10:37 +02:00
|
|
|
choiceId, choiceRef = choice
|
|
|
|
assert isinstance(choiceRef, ChoiceReference)
|
|
|
|
# We must replace the ChoiceReference with the Choice it refers to. A deep copy
|
|
|
|
# allows us to modify the individual Options of the Choice.
|
|
|
|
choiceObject = deepcopy(choices[choiceRef.identifier])
|
|
|
|
|
|
|
|
for option in choiceObject.options:
|
|
|
|
option.price += choiceRef.price
|
|
|
|
|
|
|
|
dish.choices[i] = (choiceId, choiceObject)
|
2020-01-26 01:40:50 +01:00
|
|
|
|
2020-08-15 17:58:02 +02:00
|
|
|
# Move the base price to the first single_choice if the dish doesn't have a fixed price
|
2020-07-17 11:40:15 +02:00
|
|
|
first_single_choice = first(
|
|
|
|
c[1] for c in dish.choices if c[0] == "single_choice"
|
|
|
|
)
|
2020-03-04 22:56:45 +01:00
|
|
|
price_range = dish.price_range()
|
|
|
|
if dish.price and price_range[0] != price_range[1] and first_single_choice:
|
2020-02-24 00:31:14 +01:00
|
|
|
for option in first_single_choice.options:
|
|
|
|
option.price += dish.price
|
|
|
|
dish.price = 0
|
|
|
|
|
2020-01-27 00:46:29 +01:00
|
|
|
attributes = {att["key"]: att["value"] for att in ast["attributes"]}
|
|
|
|
|
2020-01-26 01:29:19 +01:00
|
|
|
return Location(
|
|
|
|
ast["id"],
|
|
|
|
name=ast["name"],
|
2020-01-26 01:40:50 +01:00
|
|
|
dishes=dishes,
|
2020-01-27 00:46:29 +01:00
|
|
|
osm=attributes.get("osm"),
|
|
|
|
address=attributes.get("address"),
|
2020-02-24 21:04:57 +01:00
|
|
|
telephone=attributes.get("phone"),
|
2020-01-27 00:46:29 +01:00
|
|
|
website=attributes.get("website"),
|
2020-01-26 01:29:19 +01:00
|
|
|
)
|
|
|
|
|
2020-01-26 23:51:29 +01:00
|
|
|
def dish_block(self, ast) -> Dish:
|
2020-01-26 01:29:19 +01:00
|
|
|
return Dish(
|
|
|
|
ast["id"],
|
|
|
|
name=ast["name"],
|
|
|
|
description=ast["description"],
|
2020-02-24 00:31:14 +01:00
|
|
|
price=ast["price"] or 0,
|
|
|
|
tags=ast["tags"] or [],
|
2020-01-26 01:29:19 +01:00
|
|
|
choices=ast["choices"],
|
|
|
|
)
|
|
|
|
|
2020-01-26 15:09:22 +01:00
|
|
|
def choice_block(self, ast) -> Choice:
|
2020-01-27 19:15:24 +01:00
|
|
|
if ast["price"] or ast["tags"]:
|
2020-07-17 11:40:15 +02:00
|
|
|
raise SemanticError(
|
2020-09-26 01:10:37 +02:00
|
|
|
"Choice block definitions cannot have price or tags, put them on each of its options instead"
|
2020-07-17 11:40:15 +02:00
|
|
|
)
|
2020-01-27 19:15:24 +01:00
|
|
|
|
2020-01-26 01:29:19 +01:00
|
|
|
return Choice(
|
|
|
|
ast["id"],
|
|
|
|
name=ast["name"],
|
|
|
|
description=ast["description"],
|
|
|
|
options=ast["entries"],
|
|
|
|
)
|
|
|
|
|
2020-01-26 15:09:22 +01:00
|
|
|
def indent_choice_block(self, ast) -> Tuple[str, Union[Choice, AST]]:
|
2020-09-26 01:10:37 +02:00
|
|
|
if ast["kind"] == "declaration":
|
|
|
|
return (ast["type"], self.choice_block(ast))
|
|
|
|
else:
|
|
|
|
if ast["type"] == "single_choice" and ast["price"]:
|
|
|
|
raise SemanticError(
|
|
|
|
"Single_choice choices can't have a price, because it would always be triggered"
|
|
|
|
)
|
|
|
|
return (ast["type"], ChoiceReference(ast["id"], ast["price"] or 0))
|
2020-01-26 01:29:19 +01:00
|
|
|
|
2020-01-26 15:09:22 +01:00
|
|
|
def indent_choice_entry(self, ast) -> Option:
|
2020-01-26 01:29:19 +01:00
|
|
|
return Option(
|
|
|
|
ast["id"],
|
|
|
|
name=ast["name"],
|
|
|
|
description=ast["description"],
|
2020-02-24 00:31:14 +01:00
|
|
|
price=ast["price"] or 0,
|
2020-02-25 19:05:21 +01:00
|
|
|
tags=ast["tags"] or [],
|
2020-01-26 01:29:19 +01:00
|
|
|
)
|
|
|
|
|
2020-01-26 01:40:50 +01:00
|
|
|
noindent_choice_entry = indent_choice_entry
|
|
|
|
|
2020-01-26 15:09:22 +01:00
|
|
|
def price(self, ast) -> int:
|
2020-07-17 11:40:15 +02:00
|
|
|
return 100 * int(ast["value_unit"]) + (
|
|
|
|
0
|
|
|
|
if not ast["value_cents"]
|
|
|
|
else 10 * int(ast["value_cents"])
|
|
|
|
if len(ast["value_cents"]) == 1
|
|
|
|
else int(ast["value_cents"])
|
2020-01-26 15:09:22 +01:00
|
|
|
)
|
2020-01-26 01:29:19 +01:00
|
|
|
|
|
|
|
def _default(self, ast):
|
|
|
|
return ast
|
|
|
|
|
2020-07-17 11:40:15 +02:00
|
|
|
|
2020-01-26 01:29:19 +01:00
|
|
|
SEMANTICS = HldsSemanticActions()
|
2020-01-26 00:04:29 +01:00
|
|
|
|
|
|
|
|
2020-01-26 15:09:22 +01:00
|
|
|
def parse(menu: str) -> List[Location]:
|
2020-01-26 01:29:19 +01:00
|
|
|
parsed = tatsu_parse(GRAMMAR, menu, semantics=SEMANTICS)
|
2020-01-26 00:04:29 +01:00
|
|
|
return parsed
|
|
|
|
|
|
|
|
|
2020-01-26 15:09:22 +01:00
|
|
|
def parse_file(filename: str) -> List[Location]:
|
2020-01-26 01:45:20 +01:00
|
|
|
with open(filename, "r") as file_handle:
|
|
|
|
return parse(file_handle.read())
|
2020-01-26 00:04:29 +01:00
|
|
|
|
|
|
|
|
2020-01-26 15:09:22 +01:00
|
|
|
def parse_files(files: Iterable[str]) -> List[Location]:
|
2020-01-26 00:04:29 +01:00
|
|
|
menus = map(parse_file, files)
|
|
|
|
return list(itertools.chain.from_iterable(menus))
|
|
|
|
|
|
|
|
|
2020-01-26 15:09:22 +01:00
|
|
|
def parse_all_directory(directory: str) -> List[Location]:
|
2020-01-26 00:04:29 +01:00
|
|
|
# TODO Use proper way to get resources, see https://stackoverflow.com/a/10935674
|
|
|
|
files = glob(path.join(directory, "**.hlds"), recursive=True)
|
|
|
|
return parse_files(files)
|