From 2f183c758f02d1e29a8a8ae350799c88c3fcaef8 Mon Sep 17 00:00:00 2001 From: Emi Vasilek Date: Mon, 6 Nov 2023 05:18:39 +0100 Subject: [PATCH] validate using jsonschema, not manually --- recipes.py | 82 ++++++---------------------------------- schemas/ingredients.json | 46 ++++++++++++++++++++++ schemas/recipe.json | 36 ++++++++++++++++++ schemas/units.json | 31 +++++++++++++++ 4 files changed, 125 insertions(+), 70 deletions(-) create mode 100644 schemas/ingredients.json create mode 100644 schemas/recipe.json create mode 100644 schemas/units.json diff --git a/recipes.py b/recipes.py index 3b00fbf..8246a38 100644 --- a/recipes.py +++ b/recipes.py @@ -1,8 +1,10 @@ from typing import Dict, List, Any, Optional, Set import os +import json import yaml import jinja2 +import jsonschema class Context: @@ -18,29 +20,20 @@ class Conversion: return unit return None - assert_dict(dct, ["from", "to", "ratio"], []) - - assert_type(dct, "from", str) fromunit = find_unit(dct["from"]) if fromunit is None: raise RuntimeError(f"unit {dct['from']} doesn't exist") self.fromunit = fromunit - assert_type(dct, "to", str) tounit = find_unit(dct["to"]) if tounit is None: raise RuntimeError(f"unit {tounit} doesn't exist") self.tounit = tounit - # TODO: or float - assert_type(dct, "ratio", int) self.ratio = dct["ratio"] class Unit: 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"] unitsx = units[:] @@ -48,7 +41,6 @@ class Unit: self.conversions: List[Conversion] = [] if "conversions" in dct: - assert_list(dct["conversions"]) for convdct 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") @@ -59,9 +51,7 @@ class Unit: self.aliases: List[str] = [] if "aliases" in dct: - assert_list(dct["aliases"]) for alias in dct["aliases"]: - assert_type(alias, "", str) self.aliases.append(alias) @@ -70,7 +60,6 @@ class Units: self.units: List[Unit] = [] def load(self, lst: List[Any]) -> List[str]: - assert_list(lst) for unitdct in lst: unit = Unit() unit.load(self.units, unitdct) @@ -90,32 +79,24 @@ class Ingredient: def load(self, dct: Dict[str, Any]) -> List[str]: issues = [] - assert_dict(dct, ["name"], ["wdid", "pricedb", "aliases", "conversions"]) - - assert_type(dct, "name", str) self.name = dct["name"] self.wdid = -1 if "wdid" in dct: - assert_type(dct, "wdid", int) self.wdid = dct["wdid"] self.pricedb = None if "pricedb" in dct: - assert_list(dct["pricedb"]) self.pricedb = PriceDBs(self.ctx) issues += self.pricedb.load(dct["pricedb"]) self.aliases = [] if "aliases" in dct: - assert_list(dct["aliases"]) for elem in dct["aliases"]: - assert_type(elem, "", str) self.aliases.append(elem) self.conversions = [] if "conversions" in dct: - assert_list(dct["conversions"]) for dct in dct["conversions"]: conversion = Conversion() conversion.load(self.ctx.units.units, dct) @@ -130,7 +111,6 @@ class Ingredients: def load(self, lst: List[Any]) -> List[str]: issues = [] - assert_list(lst) for ingdct in lst: ing = Ingredient(self.ctx) issues += ing.load(ingdct) @@ -151,7 +131,6 @@ class PriceDBs: def load(self, lst: List[Any]) -> List[str]: issues = [] - assert_list(lst) for elem in lst: pricedb = PriceDB(self.ctx) issues += pricedb.load(elem) @@ -165,8 +144,6 @@ class PriceDB: def load(self, dct: Dict[str, Any]) -> List[str]: issues = [] - assert_dict(dct, ["price"], ["amount", "unit"]) - if isinstance(dct["price"], float): self.price = dct["price"] elif isinstance(dct["price"], int): @@ -185,7 +162,6 @@ class PriceDB: self.unit = None if "unit" in dct: - assert_type(dct, "unit", str) try: self.unit = self.ctx.units.get(dct["unit"]) except RuntimeError as e: @@ -203,9 +179,6 @@ class IngredientInstance: def load(self, dct: Dict[str, Any]) -> List[str]: issues = [] - assert_dict(dct, ["name"], ["amount", "unit", "note", "or"]) - - assert_type(dct, "name", str) self.name = dct["name"] try: @@ -224,19 +197,16 @@ class IngredientInstance: self.unit = self.defaultunit if "unit" in dct: - assert_type(dct, "unit", str) try: self.unit = self.ctx.units.get(dct["unit"]) except RuntimeError as e: issues.append(str(e)) self.note = "" if "note" in dct: - assert_type(dct, "note", str) self.note = dct["note"] self.alternatives = [] if "or" in dct: - assert_list(dct["or"]) for ingdct in dct["or"]: ingredient = IngredientInstance( self.ctx, defaultamount=self.amount, defaultunit=self.unit @@ -253,21 +223,14 @@ class RecipePart: def load(self, dct: Dict[str, Any]) -> List[str]: issues = [] - assert_dict(dct, ["title", "ingredients", "steps"], []) - - assert_type(dct, "title", str) self.title = dct["title"] - assert_type(dct, "ingredients", list) self.ingredients: List[IngredientInstance] = [] for ing in dct["ingredients"]: ingredient = IngredientInstance(self.ctx) issues += ingredient.load(ing) self.ingredients.append(ingredient) - assert_type(dct, "steps", list) - for elem in dct["steps"]: - assert_type(elem, "", str) self.steps: List[str] = dct["steps"] return issues @@ -284,11 +247,8 @@ class Recipe: def load(self, dct: Dict[str, Any]) -> List[str]: issues: List[str] = [] if "parts" in dct: - assert_dict(dct, ["title"], ["parts"]) - assert_type(dct, "title", str) self.title = dct["title"] - assert_list(dct["parts"]) for partdct in dct["parts"]: rp = RecipePart(self.ctx) issues += rp.load(partdct) @@ -301,32 +261,6 @@ class Recipe: 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: def __init__(self) -> None: self.jinjaenv = jinja2.Environment( @@ -353,8 +287,10 @@ class Builder: def load_file(self, file: str) -> Any: print(f"loading {file}") with open(file, encoding="utf-8") as f: - ymltxt = f.read() - return yaml.safe_load(ymltxt) + txt = f.read() + if file.endswith(".json"): + return json.loads(txt) + return yaml.safe_load(txt) def rendertemplate( self, templatepath: str, format: str, file: str, args: Any @@ -373,9 +309,13 @@ class Builder: issues: List[str] = [] 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) 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) if len(issues) != 0: @@ -391,12 +331,14 @@ class Builder: files.sort() recipes: List[Recipe] = [] + recipeschema = self.load_file("schemas/recipe.json") for file in files: if not file.endswith(".yaml"): print(f"unknown extension of {file}") continue recipe = Recipe(self.ctx) recipedct = self.load_file("recipes/recipes/" + file) + jsonschema.validate(instance=recipedct, schema=recipeschema) issues += recipe.load(recipedct) recipe.srcpath = file recipe.outpath = file[:-5] + ".html" diff --git a/schemas/ingredients.json b/schemas/ingredients.json new file mode 100644 index 0000000..c4f637c --- /dev/null +++ b/schemas/ingredients.json @@ -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" } + } + } + } +} \ No newline at end of file diff --git a/schemas/recipe.json b/schemas/recipe.json new file mode 100644 index 0000000..5793003 --- /dev/null +++ b/schemas/recipe.json @@ -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" } + } + } +} \ No newline at end of file diff --git a/schemas/units.json b/schemas/units.json new file mode 100644 index 0000000..bd756e1 --- /dev/null +++ b/schemas/units.json @@ -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" } + } + } + } +} \ No newline at end of file