dev: Fix json2ts codegen (#4590)

This commit is contained in:
Michael Genson 2024-11-25 03:25:35 -06:00 committed by GitHub
parent 3fc120236d
commit 82cc9e11f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 161 additions and 244 deletions

View file

@ -1,3 +1,4 @@
import re
from pathlib import Path
from jinja2 import Template
@ -64,7 +65,112 @@ def generate_global_components_types() -> None:
# Pydantic To Typescript Generator
def generate_typescript_types() -> None:
def generate_typescript_types() -> None: # noqa: C901
def contains_number(s: str) -> bool:
return bool(re.search(r"\d", s))
def remove_numbers(s: str) -> str:
return re.sub(r"\d", "", s)
def extract_type_name(line: str) -> str:
# Looking for "export type EnumName = enumVal1 | enumVal2 | ..."
if not (line.startswith("export type") and "=" in line):
return ""
return line.split(" ")[2]
def extract_property_type_name(line: str) -> str:
# Looking for " fieldName: FieldType;" or " fieldName: FieldType & string;"
if not (line.startswith(" ") and ":" in line):
return ""
return line.split(":")[1].strip().split(";")[0]
def extract_interface_name(line: str) -> str:
# Looking for "export interface InterfaceName {"
if not (line.startswith("export interface") and "{" in line):
return ""
return line.split(" ")[2]
def is_comment_line(line: str) -> bool:
s = line.strip()
return s.startswith("/*") or s.startswith("*")
def clean_output_file(file: Path) -> None:
"""
json2ts generates duplicate types off of our enums and appends a number to the end of the type name.
Our Python code (hopefully) doesn't have any duplicate enum names, or types with numbers in them,
so we can safely remove the numbers.
To do this, we read the output line-by-line and replace any type names that contain numbers with
the same type name, but without the numbers.
Note: the issue arrises from the JSON package json2ts, not the Python package pydantic2ts,
otherwise we could just fix pydantic2ts.
"""
# First pass: build a map of type names to their numberless counterparts and lines to skip
replacement_map = {}
lines_to_skip = set()
wait_for_semicolon = False
wait_for_close_bracket = False
skip_comments = False
with open(file) as f:
for i, line in enumerate(f.readlines()):
if wait_for_semicolon:
if ";" in line:
wait_for_semicolon = False
lines_to_skip.add(i)
continue
if wait_for_close_bracket:
if "}" in line:
wait_for_close_bracket = False
lines_to_skip.add(i)
continue
if type_name := extract_type_name(line):
if not contains_number(type_name):
continue
replacement_map[type_name] = remove_numbers(type_name)
if ";" not in line:
wait_for_semicolon = True
lines_to_skip.add(i)
elif type_name := extract_interface_name(line):
if not contains_number(type_name):
continue
replacement_map[type_name] = remove_numbers(type_name)
if "}" not in line:
wait_for_close_bracket = True
lines_to_skip.add(i)
elif skip_comments and is_comment_line(line):
lines_to_skip.add(i)
# we've passed the opening comments and empty line at the header
elif not skip_comments and not line.strip():
skip_comments = True
# Second pass: rewrite or remove lines as needed.
# We have to do two passes here because definitions don't always appear in the same order as their usage.
lines = []
with open(file) as f:
for i, line in enumerate(f.readlines()):
if i in lines_to_skip:
continue
if type_name := extract_property_type_name(line):
if type_name in replacement_map:
line = line.replace(type_name, replacement_map[type_name])
lines.append(line)
with open(file, "w") as f:
f.writelines(lines)
def path_to_module(path: Path):
str_path: str = str(path)
@ -98,9 +204,10 @@ def generate_typescript_types() -> None:
try:
path_as_module = path_to_module(module)
generate_typescript_defs(path_as_module, str(out_path), exclude=("MealieModel")) # type: ignore
except Exception as e:
clean_output_file(out_path)
except Exception:
failed_modules.append(module)
log.error(f"Module Error: {e}")
log.exception(f"Module Error: {module}")
log.debug("\n📁 Skipped Directories:")
for skipped_dir in skipped_dirs:

View file

@ -162,6 +162,7 @@ export interface RecipeTool {
name: string;
slug: string;
onHand?: boolean;
[k: string]: unknown;
}
export interface CustomPageImport {
name: string;

View file

@ -15,7 +15,7 @@ export interface CreateCookBook {
slug?: string | null;
position?: number;
public?: boolean;
queryFilterString: string;
queryFilterString?: string;
}
export interface ReadCookBook {
name: string;
@ -23,11 +23,11 @@ export interface ReadCookBook {
slug?: string | null;
position?: number;
public?: boolean;
queryFilterString: string;
queryFilterString?: string;
groupId: string;
householdId: string;
id: string;
queryFilter: QueryFilterJSON;
queryFilter?: QueryFilterJSON;
}
export interface QueryFilterJSON {
parts?: QueryFilterJSONPart[];
@ -47,11 +47,11 @@ export interface RecipeCookBook {
slug?: string | null;
position?: number;
public?: boolean;
queryFilterString: string;
queryFilterString?: string;
groupId: string;
householdId: string;
id: string;
queryFilter: QueryFilterJSON;
queryFilter?: QueryFilterJSON;
recipes: RecipeSummary[];
}
export interface RecipeSummary {
@ -106,7 +106,7 @@ export interface SaveCookBook {
slug?: string | null;
position?: number;
public?: boolean;
queryFilterString: string;
queryFilterString?: string;
groupId: string;
householdId: string;
}
@ -116,7 +116,7 @@ export interface UpdateCookBook {
slug?: string | null;
position?: number;
public?: boolean;
queryFilterString: string;
queryFilterString?: string;
groupId: string;
householdId: string;
id: string;

View file

@ -26,12 +26,14 @@ export interface CreateHouseholdPreferences {
}
export interface CreateInviteToken {
uses: number;
groupId?: string | null;
householdId?: string | null;
}
export interface CreateWebhook {
enabled?: boolean;
name?: string;
url?: string;
webhookType?: WebhookType & string;
webhookType?: WebhookType;
scheduledTime: string;
}
export interface EmailInitationResponse {
@ -46,10 +48,6 @@ export interface GroupEventNotifierCreate {
name: string;
appriseUrl?: string | null;
}
/**
* These events are in-sync with the EventTypes found in the EventBusService.
* If you modify this, make sure to update the EventBusService as well.
*/
export interface GroupEventNotifierOptions {
testMessage?: boolean;
webhookTask?: boolean;
@ -204,7 +202,7 @@ export interface ReadWebhook {
enabled?: boolean;
name?: string;
url?: string;
webhookType?: WebhookType & string;
webhookType?: WebhookType;
scheduledTime: string;
groupId: string;
householdId: string;
@ -263,7 +261,7 @@ export interface SaveWebhook {
enabled?: boolean;
name?: string;
url?: string;
webhookType?: WebhookType & string;
webhookType?: WebhookType;
scheduledTime: string;
groupId: string;
householdId: string;
@ -486,9 +484,6 @@ export interface ShoppingListItemUpdate {
} | null;
recipeReferences?: (ShoppingListItemRecipeRefCreate | ShoppingListItemRecipeRefUpdate)[];
}
/**
* Only used for bulk update operations where the shopping list item id isn't already supplied
*/
export interface ShoppingListItemUpdateBulk {
quantity?: number;
unit?: IngredientUnit | CreateIngredientUnit | null;
@ -509,9 +504,6 @@ export interface ShoppingListItemUpdateBulk {
recipeReferences?: (ShoppingListItemRecipeRefCreate | ShoppingListItemRecipeRefUpdate)[];
id: string;
}
/**
* Container for bulk shopping list item changes
*/
export interface ShoppingListItemsCollectionOut {
createdItems?: ShoppingListItemOut[];
updatedItems?: ShoppingListItemOut[];
@ -565,6 +557,8 @@ export interface RecipeSummary {
name?: string | null;
slug?: string;
image?: unknown;
recipeServings?: number;
recipeYieldQuantity?: number;
recipeYield?: string | null;
totalTime?: string | null;
prepTime?: string | null;
@ -599,6 +593,7 @@ export interface RecipeTool {
name: string;
slug: string;
onHand?: boolean;
[k: string]: unknown;
}
export interface ShoppingListRemoveRecipeParams {
recipeDecrementQuantity?: number;

View file

@ -12,21 +12,16 @@ export type LogicalOperator = "AND" | "OR";
export type RelationalKeyword = "IS" | "IS NOT" | "IN" | "NOT IN" | "CONTAINS ALL" | "LIKE" | "NOT LIKE";
export type RelationalOperator = "=" | "<>" | ">" | "<" | ">=" | "<=";
export interface Category {
id: string;
name: string;
slug: string;
}
export interface CreatePlanEntry {
date: string;
entryType?: PlanEntryType & string;
entryType?: PlanEntryType;
title?: string;
text?: string;
recipeId?: string | null;
}
export interface CreateRandomEntry {
date: string;
entryType?: PlanEntryType & string;
entryType?: PlanEntryType;
}
export interface ListItem {
title?: string | null;
@ -35,18 +30,18 @@ export interface ListItem {
checked?: boolean;
}
export interface PlanRulesCreate {
day?: PlanRulesDay & string;
entryType?: PlanRulesType & string;
queryFilterString: string;
day?: PlanRulesDay;
entryType?: PlanRulesType;
queryFilterString?: string;
}
export interface PlanRulesOut {
day?: PlanRulesDay & string;
entryType?: PlanRulesType & string;
queryFilterString: string;
day?: PlanRulesDay;
entryType?: PlanRulesType;
queryFilterString?: string;
groupId: string;
householdId: string;
id: string;
queryFilter: QueryFilterJSON;
queryFilter?: QueryFilterJSON;
}
export interface QueryFilterJSON {
parts?: QueryFilterJSONPart[];
@ -61,21 +56,21 @@ export interface QueryFilterJSONPart {
[k: string]: unknown;
}
export interface PlanRulesSave {
day?: PlanRulesDay & string;
entryType?: PlanRulesType & string;
queryFilterString: string;
day?: PlanRulesDay;
entryType?: PlanRulesType;
queryFilterString?: string;
groupId: string;
householdId: string;
}
export interface ReadPlanEntry {
date: string;
entryType?: PlanEntryType & string;
entryType?: PlanEntryType;
title?: string;
text?: string;
recipeId?: string | null;
id: number;
groupId: string;
userId?: string | null;
userId: string;
householdId: string;
recipe?: RecipeSummary | null;
}
@ -127,12 +122,12 @@ export interface RecipeTool {
}
export interface SavePlanEntry {
date: string;
entryType?: PlanEntryType & string;
entryType?: PlanEntryType;
title?: string;
text?: string;
recipeId?: string | null;
groupId: string;
userId?: string | null;
userId: string;
}
export interface ShoppingListIn {
name: string;
@ -147,11 +142,11 @@ export interface ShoppingListOut {
}
export interface UpdatePlanEntry {
date: string;
entryType?: PlanEntryType & string;
entryType?: PlanEntryType;
title?: string;
text?: string;
recipeId?: string | null;
id: number;
groupId: string;
userId?: string | null;
userId: string;
}

View file

@ -6,215 +6,37 @@
*/
export interface OpenAIIngredient {
/**
*
* The input is simply the ingredient string you are processing as-is. It is forbidden to
* modify this at all, you must provide the input exactly as you received it.
*
*/
input: string;
/**
*
* This value is a float between 0 - 100, where 100 is full confidence that the result is correct,
* and 0 is no confidence that the result is correct. If you're unable to parse anything,
* and you put the entire string in the notes, you should return 0 confidence. If you can easily
* parse the string into each component, then you should return a confidence of 100. If you have to
* guess which part is the unit and which part is the food, your confidence should be lower, such as 60.
* Even if there is no unit or note, if you're able to determine the food, you may use a higher confidence.
* If the entire ingredient consists of only a food, you can use a confidence of 100.
*
*/
confidence?: number | null;
/**
*
* The numerical representation of how much of this ingredient. For instance, if you receive
* "3 1/2 grams of minced garlic", the quantity is "3 1/2". Quantity may be represented as a whole number
* (integer), a float or decimal, or a fraction. You should output quantity in only whole numbers or
* floats, converting fractions into floats. Floats longer than 10 decimal places should be
* rounded to 10 decimal places.
*
*/
quantity?: number | null;
/**
*
* The unit of measurement for this ingredient. For instance, if you receive
* "2 lbs chicken breast", the unit is "lbs" (short for "pounds").
*
*/
unit?: string | null;
/**
*
* The actual physical ingredient used in the recipe. For instance, if you receive
* "3 cups of onions, chopped", the food is "onions".
*
*/
food?: string | null;
/**
*
* The rest of the text that represents more detail on how to prepare the ingredient.
* Anything that is not one of the above should be the note. For instance, if you receive
* "one can of butter beans, drained" the note would be "drained". If you receive
* "3 cloves of garlic peeled and finely chopped", the note would be "peeled and finely chopped".
*
*/
note?: string | null;
}
export interface OpenAIIngredients {
ingredients?: OpenAIIngredient[];
}
export interface OpenAIRecipe {
/**
*
* The name or title of the recipe. If you're unable to determine the name of the recipe, you should
* make your best guess based upon the ingredients and instructions provided.
*
*/
name: string;
/**
*
* A long description of the recipe. This should be a string that describes the recipe in a few words
* or sentences. If the recipe doesn't have a description, you should return None.
*
*/
description: string | null;
/**
*
* The yield of the recipe. For instance, if the recipe makes 12 cookies, the yield is "12 cookies".
* If the recipe makes 2 servings, the yield is "2 servings". Typically yield consists of a number followed
* by the word "serving" or "servings", but it can be any string that describes the yield. If the yield
* isn't specified, you should return None.
*
*/
recipe_yield?: string | null;
/**
*
* The total time it takes to make the recipe. This should be a string that describes a duration of time,
* such as "1 hour and 30 minutes", "90 minutes", or "1.5 hours". If the recipe has multiple times, choose
* the longest time. If the recipe doesn't specify a total time or duration, or it specifies a prep time or
* perform time but not a total time, you should return None. Do not duplicate times between total time, prep
* time and perform time.
*
*/
total_time?: string | null;
/**
*
* The time it takes to prepare the recipe. This should be a string that describes a duration of time,
* such as "30 minutes", "1 hour", or "1.5 hours". If the recipe has a total time, the prep time should be
* less than the total time. If the recipe doesn't specify a prep time, you should return None. If the recipe
* supplies only one time, it should be the total time. Do not duplicate times between total time, prep
* time and coperformok time.
*
*/
prep_time?: string | null;
/**
*
* The time it takes to cook the recipe. This should be a string that describes a duration of time,
* such as "30 minutes", "1 hour", or "1.5 hours". If the recipe has a total time, the perform time should be
* less than the total time. If the recipe doesn't specify a perform time, you should return None. If the
* recipe specifies a cook time, active time, or other time besides total or prep, you should use that
* time as the perform time. If the recipe supplies only one time, it should be the total time, and not the
* perform time. Do not duplicate times between total time, prep time and perform time.
*
*/
perform_time?: string | null;
/**
*
* A list of ingredients used in the recipe. Ingredients should be inserted in the order they appear in the
* recipe. If the recipe has no ingredients, you should return an empty list.
*
* Often times, but not always, ingredients are separated by line breaks. Use these as a guide to
* separate ingredients.
*
*/
ingredients?: OpenAIRecipeIngredient[];
/**
*
* A list of ingredients used in the recipe. Ingredients should be inserted in the order they appear in the
* recipe. If the recipe has no ingredients, you should return an empty list.
*
* Often times, but not always, instructions are separated by line breaks and/or separated by paragraphs.
* Use these as a guide to separate instructions. They also may be separated by numbers or words, such as
* "1.", "2.", "Step 1", "Step 2", "First", "Second", etc.
*
*/
instructions?: OpenAIRecipeInstruction[];
/**
*
* A list of notes found in the recipe. Notes should be inserted in the order they appear in the recipe.
* They may appear anywhere on the recipe, though they are typically found under the instructions.
*
*/
notes?: OpenAIRecipeNotes[];
}
export interface OpenAIRecipeIngredient {
/**
*
* The title of the section of the recipe that the ingredient is found in. Recipes may not specify
* ingredient sections, in which case this should be left blank.
* Only the first item in the section should have this set,
* whereas subsuquent items should have their titles left blank (unless they start a new section).
*
*/
title?: string | null;
/**
*
* The text of the ingredient. This should represent the entire ingredient, such as "1 cup of flour" or
* "2 cups of onions, chopped". If the ingredient is completely blank, skip it and do not add the ingredient,
* since this field is required.
*
* If the ingredient has no text, but has a title, include the title on the
* next ingredient instead.
*
*/
text: string;
}
export interface OpenAIRecipeInstruction {
/**
*
* The title of the section of the recipe that the instruction is found in. Recipes may not specify
* instruction sections, in which case this should be left blank.
* Only the first instruction in the section should have this set,
* whereas subsuquent instructions should have their titles left blank (unless they start a new section).
*
*/
title?: string | null;
/**
*
* The text of the instruction. This represents one step in the recipe, such as "Preheat the oven to 350",
* or "Sauté the onions for 20 minutes". Sometimes steps can be longer, such as "Bring a large pot of lightly
* salted water to a boil. Add ditalini pasta and cook for 8 minutes or until al dente; drain.".
*
* Sometimes, but not always, recipes will include their number in front of the text, such as
* "1.", "2.", or "Step 1", "Step 2", or "First", "Second". In the case where they are directly numbered
* ("1.", "2.", "Step one", "Step 1", "Step two", "Step 2", etc.), you should not include the number in
* the text. However, if they use words ("First", "Second", etc.), then those should be included.
*
* If the instruction is completely blank, skip it and do not add the instruction, since this field is
* required. If the ingredient has no text, but has a title, include the title on the next
* instruction instead.
*
*/
text: string;
}
export interface OpenAIRecipeNotes {
/**
*
* The title of the note. Notes may not specify a title, and just have a body of text. In this case,
* title should be left blank, and all content should go in the note text. If the note title is just
* "note" or "info", you should ignore it and leave the title blank.
*
*/
title?: string | null;
/**
*
* The text of the note. This should represent the entire note, such as "This recipe is great for
* a summer picnic" or "This recipe is a family favorite". They may also include additional prep
* instructions such as "to make this recipe gluten free, use gluten free flour", or "you may prepare
* the dough the night before and refrigerate it until ready to bake".
*
* If the note is completely blank, skip it and do not add the note, since this field is required.
*
*/
text: string;
}
export interface OpenAIBase {}

View file

@ -116,7 +116,7 @@ export interface ExportBase {
}
export interface ExportRecipes {
recipes: string[];
exportType?: ExportTypes & string;
exportType?: ExportTypes;
}
export interface IngredientConfidence {
average?: number | null;
@ -150,14 +150,11 @@ export interface MultiPurposeLabelSummary {
groupId: string;
id: string;
}
/**
* A list of ingredient references.
*/
export interface IngredientReferences {
referenceId?: string | null;
}
export interface IngredientRequest {
parser?: RegisteredParser & string;
parser?: RegisteredParser;
ingredient: string;
}
export interface IngredientUnit {
@ -181,7 +178,7 @@ export interface IngredientUnitAlias {
name: string;
}
export interface IngredientsRequest {
parser?: RegisteredParser & string;
parser?: RegisteredParser;
ingredients: string[];
}
export interface MergeFood {
@ -268,9 +265,9 @@ export interface RecipeTool {
export interface RecipeStep {
id?: string | null;
title?: string | null;
summary?: string | null;
text: string;
ingredientReferences?: IngredientReferences[];
summary?: string | null;
}
export interface RecipeAsset {
name: string;
@ -495,7 +492,7 @@ export interface ScrapeRecipeTest {
url: string;
useOpenAI?: boolean;
}
export interface SlugResponse { }
export interface SlugResponse {}
export interface TagIn {
name: string;
}

View file

@ -13,7 +13,7 @@ export interface ReportCreate {
category: ReportCategory;
groupId: string;
name: string;
status?: ReportSummaryStatus & string;
status?: ReportSummaryStatus;
}
export interface ReportEntryCreate {
reportId: string;
@ -35,7 +35,7 @@ export interface ReportOut {
category: ReportCategory;
groupId: string;
name: string;
status?: ReportSummaryStatus & string;
status?: ReportSummaryStatus;
id: string;
entries?: ReportEntryOut[];
}
@ -44,6 +44,6 @@ export interface ReportSummary {
category: ReportCategory;
groupId: string;
name: string;
status?: ReportSummaryStatus & string;
status?: ReportSummaryStatus;
id: string;
}

View file

@ -24,7 +24,7 @@ export interface PaginationQuery {
perPage?: number;
orderBy?: string | null;
orderByNullPosition?: OrderByNullPosition | null;
orderDirection?: OrderDirection & string;
orderDirection?: OrderDirection;
queryFilter?: string | null;
paginationSeed?: string | null;
}

View file

@ -69,7 +69,7 @@ export interface ReadWebhook {
enabled?: boolean;
name?: string;
url?: string;
webhookType?: WebhookType & string;
webhookType?: WebhookType;
scheduledTime: string;
groupId: string;
householdId: string;
@ -110,7 +110,7 @@ export interface PrivateUser {
username?: string | null;
fullName?: string | null;
email: string;
authMethod?: AuthMethod & string;
authMethod?: AuthMethod;
admin?: boolean;
group: string;
household: string;
@ -175,7 +175,7 @@ export interface CreateWebhook {
enabled?: boolean;
name?: string;
url?: string;
webhookType?: WebhookType & string;
webhookType?: WebhookType;
scheduledTime: string;
}
export interface UserBase {
@ -183,7 +183,7 @@ export interface UserBase {
username?: string | null;
fullName?: string | null;
email: string;
authMethod?: AuthMethod & string;
authMethod?: AuthMethod;
admin?: boolean;
group?: string | null;
household?: string | null;
@ -195,10 +195,10 @@ export interface UserBase {
}
export interface UserIn {
id?: string | null;
username?: string | null;
fullName?: string | null;
username: string;
fullName: string;
email: string;
authMethod?: AuthMethod & string;
authMethod?: AuthMethod;
admin?: boolean;
group?: string | null;
household?: string | null;
@ -214,7 +214,7 @@ export interface UserOut {
username?: string | null;
fullName?: string | null;
email: string;
authMethod?: AuthMethod & string;
authMethod?: AuthMethod;
admin?: boolean;
group: string;
household: string;