validate using jsonschema, not manually
This commit is contained in:
parent
a2cbbb2068
commit
2f183c758f
4 changed files with 125 additions and 70 deletions
82
recipes.py
82
recipes.py
|
@ -1,8 +1,10 @@
|
||||||
from typing import Dict, List, Any, Optional, Set
|
from typing import Dict, List, Any, Optional, Set
|
||||||
import os
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
import jinja2
|
import jinja2
|
||||||
|
import jsonschema
|
||||||
|
|
||||||
|
|
||||||
class Context:
|
class Context:
|
||||||
|
@ -18,29 +20,20 @@ class Conversion:
|
||||||
return unit
|
return unit
|
||||||
return None
|
return None
|
||||||
|
|
||||||
assert_dict(dct, ["from", "to", "ratio"], [])
|
|
||||||
|
|
||||||
assert_type(dct, "from", str)
|
|
||||||
fromunit = find_unit(dct["from"])
|
fromunit = find_unit(dct["from"])
|
||||||
if fromunit is None:
|
if fromunit is None:
|
||||||
raise RuntimeError(f"unit {dct['from']} doesn't exist")
|
raise RuntimeError(f"unit {dct['from']} doesn't exist")
|
||||||
self.fromunit = fromunit
|
self.fromunit = fromunit
|
||||||
|
|
||||||
assert_type(dct, "to", str)
|
|
||||||
tounit = find_unit(dct["to"])
|
tounit = find_unit(dct["to"])
|
||||||
if tounit is None:
|
if tounit is None:
|
||||||
raise RuntimeError(f"unit {tounit} doesn't exist")
|
raise RuntimeError(f"unit {tounit} doesn't exist")
|
||||||
self.tounit = tounit
|
self.tounit = tounit
|
||||||
|
|
||||||
# TODO: or float
|
|
||||||
assert_type(dct, "ratio", int)
|
|
||||||
self.ratio = dct["ratio"]
|
self.ratio = dct["ratio"]
|
||||||
|
|
||||||
class Unit:
|
class Unit:
|
||||||
def load(self, units: List["Unit"], dct: Dict[str, Any]) -> None:
|
def load(self, units: List["Unit"], dct: Dict[str, Any]) -> None:
|
||||||
assert_dict(dct, ["name"], ["conversions", "aliases"])
|
|
||||||
|
|
||||||
assert_type(dct, "name", str)
|
|
||||||
self.name = dct["name"]
|
self.name = dct["name"]
|
||||||
|
|
||||||
unitsx = units[:]
|
unitsx = units[:]
|
||||||
|
@ -48,7 +41,6 @@ class Unit:
|
||||||
|
|
||||||
self.conversions: List[Conversion] = []
|
self.conversions: List[Conversion] = []
|
||||||
if "conversions" in dct:
|
if "conversions" in dct:
|
||||||
assert_list(dct["conversions"])
|
|
||||||
for convdct in dct["conversions"]:
|
for convdct in dct["conversions"]:
|
||||||
if "from" in dct["conversions"]:
|
if "from" in dct["conversions"]:
|
||||||
raise RuntimeError(f"conversions in units.yaml cannot have a from field, it is automatically assigned from the unit name")
|
raise RuntimeError(f"conversions in units.yaml cannot have a from field, it is automatically assigned from the unit name")
|
||||||
|
@ -59,9 +51,7 @@ class Unit:
|
||||||
|
|
||||||
self.aliases: List[str] = []
|
self.aliases: List[str] = []
|
||||||
if "aliases" in dct:
|
if "aliases" in dct:
|
||||||
assert_list(dct["aliases"])
|
|
||||||
for alias in dct["aliases"]:
|
for alias in dct["aliases"]:
|
||||||
assert_type(alias, "", str)
|
|
||||||
self.aliases.append(alias)
|
self.aliases.append(alias)
|
||||||
|
|
||||||
|
|
||||||
|
@ -70,7 +60,6 @@ class Units:
|
||||||
self.units: List[Unit] = []
|
self.units: List[Unit] = []
|
||||||
|
|
||||||
def load(self, lst: List[Any]) -> List[str]:
|
def load(self, lst: List[Any]) -> List[str]:
|
||||||
assert_list(lst)
|
|
||||||
for unitdct in lst:
|
for unitdct in lst:
|
||||||
unit = Unit()
|
unit = Unit()
|
||||||
unit.load(self.units, unitdct)
|
unit.load(self.units, unitdct)
|
||||||
|
@ -90,32 +79,24 @@ class Ingredient:
|
||||||
|
|
||||||
def load(self, dct: Dict[str, Any]) -> List[str]:
|
def load(self, dct: Dict[str, Any]) -> List[str]:
|
||||||
issues = []
|
issues = []
|
||||||
assert_dict(dct, ["name"], ["wdid", "pricedb", "aliases", "conversions"])
|
|
||||||
|
|
||||||
assert_type(dct, "name", str)
|
|
||||||
self.name = dct["name"]
|
self.name = dct["name"]
|
||||||
|
|
||||||
self.wdid = -1
|
self.wdid = -1
|
||||||
if "wdid" in dct:
|
if "wdid" in dct:
|
||||||
assert_type(dct, "wdid", int)
|
|
||||||
self.wdid = dct["wdid"]
|
self.wdid = dct["wdid"]
|
||||||
|
|
||||||
self.pricedb = None
|
self.pricedb = None
|
||||||
if "pricedb" in dct:
|
if "pricedb" in dct:
|
||||||
assert_list(dct["pricedb"])
|
|
||||||
self.pricedb = PriceDBs(self.ctx)
|
self.pricedb = PriceDBs(self.ctx)
|
||||||
issues += self.pricedb.load(dct["pricedb"])
|
issues += self.pricedb.load(dct["pricedb"])
|
||||||
|
|
||||||
self.aliases = []
|
self.aliases = []
|
||||||
if "aliases" in dct:
|
if "aliases" in dct:
|
||||||
assert_list(dct["aliases"])
|
|
||||||
for elem in dct["aliases"]:
|
for elem in dct["aliases"]:
|
||||||
assert_type(elem, "", str)
|
|
||||||
self.aliases.append(elem)
|
self.aliases.append(elem)
|
||||||
|
|
||||||
self.conversions = []
|
self.conversions = []
|
||||||
if "conversions" in dct:
|
if "conversions" in dct:
|
||||||
assert_list(dct["conversions"])
|
|
||||||
for dct in dct["conversions"]:
|
for dct in dct["conversions"]:
|
||||||
conversion = Conversion()
|
conversion = Conversion()
|
||||||
conversion.load(self.ctx.units.units, dct)
|
conversion.load(self.ctx.units.units, dct)
|
||||||
|
@ -130,7 +111,6 @@ class Ingredients:
|
||||||
|
|
||||||
def load(self, lst: List[Any]) -> List[str]:
|
def load(self, lst: List[Any]) -> List[str]:
|
||||||
issues = []
|
issues = []
|
||||||
assert_list(lst)
|
|
||||||
for ingdct in lst:
|
for ingdct in lst:
|
||||||
ing = Ingredient(self.ctx)
|
ing = Ingredient(self.ctx)
|
||||||
issues += ing.load(ingdct)
|
issues += ing.load(ingdct)
|
||||||
|
@ -151,7 +131,6 @@ class PriceDBs:
|
||||||
|
|
||||||
def load(self, lst: List[Any]) -> List[str]:
|
def load(self, lst: List[Any]) -> List[str]:
|
||||||
issues = []
|
issues = []
|
||||||
assert_list(lst)
|
|
||||||
for elem in lst:
|
for elem in lst:
|
||||||
pricedb = PriceDB(self.ctx)
|
pricedb = PriceDB(self.ctx)
|
||||||
issues += pricedb.load(elem)
|
issues += pricedb.load(elem)
|
||||||
|
@ -165,8 +144,6 @@ class PriceDB:
|
||||||
|
|
||||||
def load(self, dct: Dict[str, Any]) -> List[str]:
|
def load(self, dct: Dict[str, Any]) -> List[str]:
|
||||||
issues = []
|
issues = []
|
||||||
assert_dict(dct, ["price"], ["amount", "unit"])
|
|
||||||
|
|
||||||
if isinstance(dct["price"], float):
|
if isinstance(dct["price"], float):
|
||||||
self.price = dct["price"]
|
self.price = dct["price"]
|
||||||
elif isinstance(dct["price"], int):
|
elif isinstance(dct["price"], int):
|
||||||
|
@ -185,7 +162,6 @@ class PriceDB:
|
||||||
|
|
||||||
self.unit = None
|
self.unit = None
|
||||||
if "unit" in dct:
|
if "unit" in dct:
|
||||||
assert_type(dct, "unit", str)
|
|
||||||
try:
|
try:
|
||||||
self.unit = self.ctx.units.get(dct["unit"])
|
self.unit = self.ctx.units.get(dct["unit"])
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
|
@ -203,9 +179,6 @@ class IngredientInstance:
|
||||||
|
|
||||||
def load(self, dct: Dict[str, Any]) -> List[str]:
|
def load(self, dct: Dict[str, Any]) -> List[str]:
|
||||||
issues = []
|
issues = []
|
||||||
assert_dict(dct, ["name"], ["amount", "unit", "note", "or"])
|
|
||||||
|
|
||||||
assert_type(dct, "name", str)
|
|
||||||
self.name = dct["name"]
|
self.name = dct["name"]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -224,19 +197,16 @@ class IngredientInstance:
|
||||||
|
|
||||||
self.unit = self.defaultunit
|
self.unit = self.defaultunit
|
||||||
if "unit" in dct:
|
if "unit" in dct:
|
||||||
assert_type(dct, "unit", str)
|
|
||||||
try:
|
try:
|
||||||
self.unit = self.ctx.units.get(dct["unit"])
|
self.unit = self.ctx.units.get(dct["unit"])
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
issues.append(str(e))
|
issues.append(str(e))
|
||||||
self.note = ""
|
self.note = ""
|
||||||
if "note" in dct:
|
if "note" in dct:
|
||||||
assert_type(dct, "note", str)
|
|
||||||
self.note = dct["note"]
|
self.note = dct["note"]
|
||||||
|
|
||||||
self.alternatives = []
|
self.alternatives = []
|
||||||
if "or" in dct:
|
if "or" in dct:
|
||||||
assert_list(dct["or"])
|
|
||||||
for ingdct in dct["or"]:
|
for ingdct in dct["or"]:
|
||||||
ingredient = IngredientInstance(
|
ingredient = IngredientInstance(
|
||||||
self.ctx, defaultamount=self.amount, defaultunit=self.unit
|
self.ctx, defaultamount=self.amount, defaultunit=self.unit
|
||||||
|
@ -253,21 +223,14 @@ class RecipePart:
|
||||||
|
|
||||||
def load(self, dct: Dict[str, Any]) -> List[str]:
|
def load(self, dct: Dict[str, Any]) -> List[str]:
|
||||||
issues = []
|
issues = []
|
||||||
assert_dict(dct, ["title", "ingredients", "steps"], [])
|
|
||||||
|
|
||||||
assert_type(dct, "title", str)
|
|
||||||
self.title = dct["title"]
|
self.title = dct["title"]
|
||||||
|
|
||||||
assert_type(dct, "ingredients", list)
|
|
||||||
self.ingredients: List[IngredientInstance] = []
|
self.ingredients: List[IngredientInstance] = []
|
||||||
for ing in dct["ingredients"]:
|
for ing in dct["ingredients"]:
|
||||||
ingredient = IngredientInstance(self.ctx)
|
ingredient = IngredientInstance(self.ctx)
|
||||||
issues += ingredient.load(ing)
|
issues += ingredient.load(ing)
|
||||||
self.ingredients.append(ingredient)
|
self.ingredients.append(ingredient)
|
||||||
|
|
||||||
assert_type(dct, "steps", list)
|
|
||||||
for elem in dct["steps"]:
|
|
||||||
assert_type(elem, "", str)
|
|
||||||
self.steps: List[str] = dct["steps"]
|
self.steps: List[str] = dct["steps"]
|
||||||
|
|
||||||
return issues
|
return issues
|
||||||
|
@ -284,11 +247,8 @@ class Recipe:
|
||||||
def load(self, dct: Dict[str, Any]) -> List[str]:
|
def load(self, dct: Dict[str, Any]) -> List[str]:
|
||||||
issues: List[str] = []
|
issues: List[str] = []
|
||||||
if "parts" in dct:
|
if "parts" in dct:
|
||||||
assert_dict(dct, ["title"], ["parts"])
|
|
||||||
assert_type(dct, "title", str)
|
|
||||||
self.title = dct["title"]
|
self.title = dct["title"]
|
||||||
|
|
||||||
assert_list(dct["parts"])
|
|
||||||
for partdct in dct["parts"]:
|
for partdct in dct["parts"]:
|
||||||
rp = RecipePart(self.ctx)
|
rp = RecipePart(self.ctx)
|
||||||
issues += rp.load(partdct)
|
issues += rp.load(partdct)
|
||||||
|
@ -301,32 +261,6 @@ class Recipe:
|
||||||
return issues
|
return issues
|
||||||
|
|
||||||
|
|
||||||
def assert_dict(
|
|
||||||
dct: Dict[str, Any], required_keys: List[str], optional_keys: List[str]
|
|
||||||
) -> None:
|
|
||||||
if not isinstance(dct, dict):
|
|
||||||
raise RuntimeError(f"{dct} has to be a dict")
|
|
||||||
for reqkey in required_keys:
|
|
||||||
if reqkey not in dct:
|
|
||||||
raise RuntimeError(f"{reqkey} is required")
|
|
||||||
extraelems = [x for x in dct.keys() if x not in required_keys + optional_keys]
|
|
||||||
if len(extraelems) != 0:
|
|
||||||
raise RuntimeError(f"{extraelems} not allowed")
|
|
||||||
|
|
||||||
|
|
||||||
def assert_type(dct: Dict[str, Any], key: str, type: type) -> None:
|
|
||||||
if key == "":
|
|
||||||
if not isinstance(dct, type):
|
|
||||||
raise RuntimeError(f"{key} has to be a {type}")
|
|
||||||
elif key in dct and not isinstance(dct[key], type):
|
|
||||||
raise RuntimeError(f"{key} has to be a {type}")
|
|
||||||
|
|
||||||
|
|
||||||
def assert_list(lst: List[Any]) -> None:
|
|
||||||
if not isinstance(lst, list):
|
|
||||||
raise RuntimeError(f"{lst} has to be a {list}")
|
|
||||||
|
|
||||||
|
|
||||||
class Builder:
|
class Builder:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.jinjaenv = jinja2.Environment(
|
self.jinjaenv = jinja2.Environment(
|
||||||
|
@ -353,8 +287,10 @@ class Builder:
|
||||||
def load_file(self, file: str) -> Any:
|
def load_file(self, file: str) -> Any:
|
||||||
print(f"loading {file}")
|
print(f"loading {file}")
|
||||||
with open(file, encoding="utf-8") as f:
|
with open(file, encoding="utf-8") as f:
|
||||||
ymltxt = f.read()
|
txt = f.read()
|
||||||
return yaml.safe_load(ymltxt)
|
if file.endswith(".json"):
|
||||||
|
return json.loads(txt)
|
||||||
|
return yaml.safe_load(txt)
|
||||||
|
|
||||||
def rendertemplate(
|
def rendertemplate(
|
||||||
self, templatepath: str, format: str, file: str, args: Any
|
self, templatepath: str, format: str, file: str, args: Any
|
||||||
|
@ -373,9 +309,13 @@ class Builder:
|
||||||
issues: List[str] = []
|
issues: List[str] = []
|
||||||
|
|
||||||
unitsdct = self.load_file("recipes/units.yaml")
|
unitsdct = self.load_file("recipes/units.yaml")
|
||||||
|
unitsschema = self.load_file("schemas/units.json")
|
||||||
|
jsonschema.validate(instance=unitsdct, schema=unitsschema)
|
||||||
issues += self.ctx.units.load(unitsdct)
|
issues += self.ctx.units.load(unitsdct)
|
||||||
|
|
||||||
ingredientsdct = self.load_file("recipes/ingredients.yaml")
|
ingredientsdct = self.load_file("recipes/ingredients.yaml")
|
||||||
|
ingredientsschema = self.load_file("schemas/ingredients.json")
|
||||||
|
jsonschema.validate(instance=ingredientsdct, schema=ingredientsschema)
|
||||||
issues += self.ctx.ingredients.load(ingredientsdct)
|
issues += self.ctx.ingredients.load(ingredientsdct)
|
||||||
|
|
||||||
if len(issues) != 0:
|
if len(issues) != 0:
|
||||||
|
@ -391,12 +331,14 @@ class Builder:
|
||||||
files.sort()
|
files.sort()
|
||||||
|
|
||||||
recipes: List[Recipe] = []
|
recipes: List[Recipe] = []
|
||||||
|
recipeschema = self.load_file("schemas/recipe.json")
|
||||||
for file in files:
|
for file in files:
|
||||||
if not file.endswith(".yaml"):
|
if not file.endswith(".yaml"):
|
||||||
print(f"unknown extension of {file}")
|
print(f"unknown extension of {file}")
|
||||||
continue
|
continue
|
||||||
recipe = Recipe(self.ctx)
|
recipe = Recipe(self.ctx)
|
||||||
recipedct = self.load_file("recipes/recipes/" + file)
|
recipedct = self.load_file("recipes/recipes/" + file)
|
||||||
|
jsonschema.validate(instance=recipedct, schema=recipeschema)
|
||||||
issues += recipe.load(recipedct)
|
issues += recipe.load(recipedct)
|
||||||
recipe.srcpath = file
|
recipe.srcpath = file
|
||||||
recipe.outpath = file[:-5] + ".html"
|
recipe.outpath = file[:-5] + ".html"
|
||||||
|
|
46
schemas/ingredients.json
Normal file
46
schemas/ingredients.json
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://example.com/ingredients.json",
|
||||||
|
"title": "Ingredients",
|
||||||
|
"description": "Ingredients",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [ "name" ],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"wdid": { "type": "integer" },
|
||||||
|
"pricedb": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [ "price", "amount", "unit" ],
|
||||||
|
"properties": {
|
||||||
|
"price": { "type": "integer" },
|
||||||
|
"amount": { "type": "integer" },
|
||||||
|
"unit": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"conversions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [ "from", "to", "ratio" ],
|
||||||
|
"properties": {
|
||||||
|
"from": { "type": "string" },
|
||||||
|
"to": { "type": "string" },
|
||||||
|
"ratio": { "type": "integer" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
schemas/recipe.json
Normal file
36
schemas/recipe.json
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://example.com/recipe.json",
|
||||||
|
"title": "Recipe",
|
||||||
|
"description": "Recipe",
|
||||||
|
"type": "object",
|
||||||
|
"required": [ "title" ],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"title": { "type": "string" },
|
||||||
|
"ingredients": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$id": "https://example.com/ingredient.json",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [ "name" ],
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"amount": { "type": "number" },
|
||||||
|
"unit": { "type": "string" },
|
||||||
|
"or": { "items": { "$ref": "/ingredient.json" } },
|
||||||
|
"note": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
},
|
||||||
|
"parts": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "/recipe.json" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
schemas/units.json
Normal file
31
schemas/units.json
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://example.com/units.json",
|
||||||
|
"title": "Units",
|
||||||
|
"description": "Units",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [ "name" ],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"conversions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [ "to", "ratio" ],
|
||||||
|
"properties": {
|
||||||
|
"to": { "type": "string" },
|
||||||
|
"ratio": { "type": "integer" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue