From 82cc9e11f7421d5a63ff329ac556c331bea5439d Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Mon, 25 Nov 2024 03:25:35 -0600 Subject: [PATCH] dev: Fix json2ts codegen (#4590) --- dev/code-generation/gen_ts_types.py | 113 +++++++++++++++++- frontend/lib/api/types/admin.ts | 1 + frontend/lib/api/types/cookbook.ts | 14 +-- frontend/lib/api/types/household.ts | 21 ++-- frontend/lib/api/types/meal-plan.ts | 41 +++---- frontend/lib/api/types/openai.ts | 178 ---------------------------- frontend/lib/api/types/recipe.ts | 13 +- frontend/lib/api/types/reports.ts | 6 +- frontend/lib/api/types/response.ts | 2 +- frontend/lib/api/types/user.ts | 16 +-- 10 files changed, 161 insertions(+), 244 deletions(-) diff --git a/dev/code-generation/gen_ts_types.py b/dev/code-generation/gen_ts_types.py index b15f7ee9d..408262f25 100644 --- a/dev/code-generation/gen_ts_types.py +++ b/dev/code-generation/gen_ts_types.py @@ -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: diff --git a/frontend/lib/api/types/admin.ts b/frontend/lib/api/types/admin.ts index 79678aed5..34553897d 100644 --- a/frontend/lib/api/types/admin.ts +++ b/frontend/lib/api/types/admin.ts @@ -162,6 +162,7 @@ export interface RecipeTool { name: string; slug: string; onHand?: boolean; + [k: string]: unknown; } export interface CustomPageImport { name: string; diff --git a/frontend/lib/api/types/cookbook.ts b/frontend/lib/api/types/cookbook.ts index 721e6320f..22b5079a2 100644 --- a/frontend/lib/api/types/cookbook.ts +++ b/frontend/lib/api/types/cookbook.ts @@ -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; diff --git a/frontend/lib/api/types/household.ts b/frontend/lib/api/types/household.ts index 79b1a7ffa..0e0072391 100644 --- a/frontend/lib/api/types/household.ts +++ b/frontend/lib/api/types/household.ts @@ -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; diff --git a/frontend/lib/api/types/meal-plan.ts b/frontend/lib/api/types/meal-plan.ts index f11ec1feb..4869c7c59 100644 --- a/frontend/lib/api/types/meal-plan.ts +++ b/frontend/lib/api/types/meal-plan.ts @@ -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; } diff --git a/frontend/lib/api/types/openai.ts b/frontend/lib/api/types/openai.ts index f1a358ae0..be28b87be 100644 --- a/frontend/lib/api/types/openai.ts +++ b/frontend/lib/api/types/openai.ts @@ -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 {} diff --git a/frontend/lib/api/types/recipe.ts b/frontend/lib/api/types/recipe.ts index ed0c7ab00..84440f19b 100644 --- a/frontend/lib/api/types/recipe.ts +++ b/frontend/lib/api/types/recipe.ts @@ -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; } diff --git a/frontend/lib/api/types/reports.ts b/frontend/lib/api/types/reports.ts index 39de61648..2b763275e 100644 --- a/frontend/lib/api/types/reports.ts +++ b/frontend/lib/api/types/reports.ts @@ -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; } diff --git a/frontend/lib/api/types/response.ts b/frontend/lib/api/types/response.ts index 2eef08546..30480aba6 100644 --- a/frontend/lib/api/types/response.ts +++ b/frontend/lib/api/types/response.ts @@ -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; } diff --git a/frontend/lib/api/types/user.ts b/frontend/lib/api/types/user.ts index 27c5d794f..855f64e7b 100644 --- a/frontend/lib/api/types/user.ts +++ b/frontend/lib/api/types/user.ts @@ -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;