From b1e64f29e52ce698958aedc1e466bc8f743e9a58 Mon Sep 17 00:00:00 2001 From: Joel Date: Thu, 30 Jul 2026 16:53:16 +0100 Subject: [PATCH 1/2] Drinks Info API Project --- Drinks info/.vscode/launch.json | 26 ++++ Drinks info/.vscode/tasks.json | 41 ++++++ Drinks info/Drinks/Drinks.csproj | 16 +++ Drinks info/Drinks/DrinksService.cs | 125 ++++++++++++++++++ Drinks info/Drinks/Models/Category.cs | 17 +++ Drinks info/Drinks/Models/DrinkDetail.cs | 65 +++++++++ Drinks info/Drinks/Models/Drinks.cs | 19 +++ Drinks info/Drinks/Program.cs | 12 ++ .../Drinks/TableVisualisationEngine.cs | 29 ++++ Drinks info/Drinks/UserInput.cs | 65 +++++++++ Drinks info/Drinks/Validation.cs | 43 ++++++ 11 files changed, 458 insertions(+) create mode 100644 Drinks info/.vscode/launch.json create mode 100644 Drinks info/.vscode/tasks.json create mode 100644 Drinks info/Drinks/Drinks.csproj create mode 100644 Drinks info/Drinks/DrinksService.cs create mode 100644 Drinks info/Drinks/Models/Category.cs create mode 100644 Drinks info/Drinks/Models/DrinkDetail.cs create mode 100644 Drinks info/Drinks/Models/Drinks.cs create mode 100644 Drinks info/Drinks/Program.cs create mode 100644 Drinks info/Drinks/TableVisualisationEngine.cs create mode 100644 Drinks info/Drinks/UserInput.cs create mode 100644 Drinks info/Drinks/Validation.cs diff --git a/Drinks info/.vscode/launch.json b/Drinks info/.vscode/launch.json new file mode 100644 index 00000000..d22d9697 --- /dev/null +++ b/Drinks info/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md + "name": ".drinks", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "${defaultBuildTask}", + // If you have changed target frameworks, make sure to update the program path. + "program": "C:/Users/j_men/Drinks info/Drinks/bin/Debug/net10.0/Drinks.dll", + "args": [], + "cwd": "${workspaceFolder}/Drinks", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "externalTerminal", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/Drinks info/.vscode/tasks.json b/Drinks info/.vscode/tasks.json new file mode 100644 index 00000000..86c17842 --- /dev/null +++ b/Drinks info/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "AppData/Roaming/Code/User/workspaceStorage/c68621f48698f6f8ca29549c1e8312f8/ms-dotnettools.csdevkit/Drinks info.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "AppData/Roaming/Code/User/workspaceStorage/c68621f48698f6f8ca29549c1e8312f8/ms-dotnettools.csdevkit/Drinks info.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "AppData/Roaming/Code/User/workspaceStorage/c68621f48698f6f8ca29549c1e8312f8/ms-dotnettools.csdevkit/Drinks info.sln" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Drinks info/Drinks/Drinks.csproj b/Drinks info/Drinks/Drinks.csproj new file mode 100644 index 00000000..6c70d107 --- /dev/null +++ b/Drinks info/Drinks/Drinks.csproj @@ -0,0 +1,16 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + diff --git a/Drinks info/Drinks/DrinksService.cs b/Drinks info/Drinks/DrinksService.cs new file mode 100644 index 00000000..90479a6a --- /dev/null +++ b/Drinks info/Drinks/DrinksService.cs @@ -0,0 +1,125 @@ +using System.Reflection; +using System.Web; +using drinks_info.Models; +using Newtonsoft.Json; +using RestSharp; + +public class DrinksService +{ + // resposible for interacting with the drinks API + + + public List GetCategories() + { + //similar to var connection = new SqlConnection(connectionString); + + var client = new RestClient("http://www.thecocktaildb.com/api/json/v1/1/"); + var request = new RestRequest("List.php?c=list"); + var response = client.ExecuteAsync(request); + + List categories = new(); + + if (response.Result.StatusCode == System.Net.HttpStatusCode.OK) + { + string rawResponse = response.Result.Content; + var serialize = JsonConvert.DeserializeObject(rawResponse); + // List returnedList = serialize.CatergoriesList; + categories = serialize.CatergoriesList; + + TableVisualisationEngine.ShowTable(categories, "Categories Menu"); + return categories; + } + return categories; + + } + + + + internal List GetDrinksByCategory(string category) + { + + var client = new RestClient("http://www.thecocktaildb.com/api/json/v1/1/"); + var request = new RestRequest($"filter.php?c={HttpUtility.UrlEncode(category)}"); + var response = client.ExecuteAsync(request); + + List drinks = new(); + + if (response.Result.StatusCode == System.Net.HttpStatusCode.OK) + { + string rawResponse = response.Result.Content; + + var serialize = JsonConvert.DeserializeObject(rawResponse); + // converts the rawResponse JSON string into a Drinks object + // Drinks is the class we wrote, and serialize is the actual object that gets created from it, + // filled with the real data from the API + + drinks = serialize.DrinksList; + //create a list that returns our converted rawResponse (serialize) serialize.DrinksList + // accesses a property on the object + + TableVisualisationEngine.ShowTable(drinks, "Drinks List"); + //print the list + + return drinks; + } + + return drinks; + + } + + + internal void GetDrink(string drink) + { + var client = new RestClient("http://www.thecocktaildb.com/api/json/v1/1/"); + var request = new RestRequest($"lookup.php?i={drink}"); + var response = client.ExecuteAsync(request); + + if (response.Result.StatusCode == System.Net.HttpStatusCode.OK) + { + string rawResponse = response.Result.Content; + + var serialize = JsonConvert.DeserializeObject(rawResponse); + //Takes the raw JSON string and converts it into a DrinkDetailObject + + List returnedList = serialize.DrinkDetailList; + //because DrinkDetailList lives inside DrinkDetailObject we can reach in and grab it + DrinkDetail drinkDetail = returnedList[0]; + // Get the first DrinkDetail object from the list + + List prepList = new(); + //Creates an empty list. + //The goal is to fill it with rows for the table. + + string formatedName = ""; + + foreach (PropertyInfo prop in drinkDetail.GetType().GetProperties()) + { + //drinkDetail.GetType() returns DrinkDetail + //.GetProperties() returns all properties in that class: strDrink strCategory etc + if (prop.Name.Contains("str")) + + { + formatedName = prop.Name.Substring(3); + }//remove str from each property + + + if (!string.IsNullOrEmpty(prop.GetValue(drinkDetail)?.ToString())) + {//if drinkdetail is not empty + prepList.Add(new + { + word = formatedName, + Value = prop.GetValue(drinkDetail) + }); + } + } + TableVisualisationEngine.ShowTable(prepList, "Drink Detail"); + + } + + + + } + + + +} \ No newline at end of file diff --git a/Drinks info/Drinks/Models/Category.cs b/Drinks info/Drinks/Models/Category.cs new file mode 100644 index 00000000..aa0972b0 --- /dev/null +++ b/Drinks info/Drinks/Models/Category.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; + +public class Categories +{ + + [JsonProperty("drinks")] + + public List CatergoriesList { get; set; } +} + + +public class Category +{ + public string strCategory { get; set; } + +} + diff --git a/Drinks info/Drinks/Models/DrinkDetail.cs b/Drinks info/Drinks/Models/DrinkDetail.cs new file mode 100644 index 00000000..d0744ad3 --- /dev/null +++ b/Drinks info/Drinks/Models/DrinkDetail.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace drinks_info.Models +{ + internal class DrinkDetailObject + { + [JsonProperty("drinks")] + public List DrinkDetailList { get; set; } + } + + internal class DrinkDetail + { + public string strDrink { get; set; } + public object strDrinkAlternate { get; set; } + public object strTags { get; set; } + public object strVideo { get; set; } + public string strCategory { get; set; } + public object strIBA { get; set; } + public string strAlcoholic { get; set; } + public string strGlass { get; set; } + public string strInstructions { get; set; } + public object strInstructionsES { get; set; } + public string strInstructionsDE { get; set; } + public object strInstructionsFR { get; set; } + public string strInstructionsIT { get; set; } + public object strInstructionsZHHANS { get; set; } + public object strInstructionsZHHANT { get; set; } + public string strDrinkThumb { get; set; } + public string strIngredient1 { get; set; } + public string strIngredient2 { get; set; } + public string strIngredient3 { get; set; } + public string strIngredient4 { get; set; } + public object strIngredient5 { get; set; } + public object strIngredient6 { get; set; } + public object strIngredient7 { get; set; } + public object strIngredient8 { get; set; } + public object strIngredient9 { get; set; } + public object strIngredient10 { get; set; } + public object strIngredient11 { get; set; } + public object strIngredient12 { get; set; } + public object strIngredient13 { get; set; } + public object strIngredient14 { get; set; } + public object strIngredient15 { get; set; } + public string strMeasure1 { get; set; } + public string strMeasure2 { get; set; } + public string strMeasure3 { get; set; } + public string strMeasure4 { get; set; } + public object strMeasure5 { get; set; } + public object strMeasure6 { get; set; } + public object strMeasure7 { get; set; } + public object strMeasure8 { get; set; } + public object strMeasure9 { get; set; } + public object strMeasure10 { get; set; } + public object strMeasure11 { get; set; } + public object strMeasure12 { get; set; } + public object strMeasure13 { get; set; } + public object strMeasure14 { get; set; } + public object strMeasure15 { get; set; } + public object strImageSource { get; set; } + public object strImageAttribution { get; set; } + public string strCreativeCommonsConfirmed { get; set; } + public string dateModified { get; set; } + } +} \ No newline at end of file diff --git a/Drinks info/Drinks/Models/Drinks.cs b/Drinks info/Drinks/Models/Drinks.cs new file mode 100644 index 00000000..20e1f40a --- /dev/null +++ b/Drinks info/Drinks/Models/Drinks.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +public class Drinks +{ + [JsonProperty("drinks")] + //this tells the compiler when you see a JSON field called drinks, put its contents into this property + + public List DrinksList { get; set; } + //the list is the drink because its using the fields in the drink model +} + +public class Drink +{ + + public string idDrink { get; set; } + + public string strDrink { get; set; } + +} diff --git a/Drinks info/Drinks/Program.cs b/Drinks info/Drinks/Program.cs new file mode 100644 index 00000000..cd12a151 --- /dev/null +++ b/Drinks info/Drinks/Program.cs @@ -0,0 +1,12 @@ +class Program +{ + + static void Main(string[] args) + { + UserInput Input = new(); + Input.GetCategoriesInput(); + Console.ReadKey(); + + } + +} \ No newline at end of file diff --git a/Drinks info/Drinks/TableVisualisationEngine.cs b/Drinks info/Drinks/TableVisualisationEngine.cs new file mode 100644 index 00000000..f648dbcb --- /dev/null +++ b/Drinks info/Drinks/TableVisualisationEngine.cs @@ -0,0 +1,29 @@ +using System.Diagnostics.CodeAnalysis; +using ConsoleTableExt; + +public class TableVisualisationEngine +{ + + // SHOWS CONSOLE DATA SPECTRE NEEDED + + public static void ShowTable(List tableData, [AllowNull] string tableName) where Tbl : + class + { + Console.Clear(); + + if (tableName == null) + tableName = ""; + + Console.WriteLine("\n\n"); + + ConsoleTableBuilder + .From(tableData) + .WithColumn(tableName) + .ExportAndWriteLine(TableAligntment.Center); + Console.WriteLine("\n\n"); + + + } + + +} diff --git a/Drinks info/Drinks/UserInput.cs b/Drinks info/Drinks/UserInput.cs new file mode 100644 index 00000000..2761d2a7 --- /dev/null +++ b/Drinks info/Drinks/UserInput.cs @@ -0,0 +1,65 @@ + +public class UserInput +{ + + DrinksService drinksService = new(); + + internal void GetCategoriesInput() + { + + var categories = drinksService.GetCategories(); + + Console.WriteLine("Choose Category:"); + + string category = Console.ReadLine(); + + + while (!Validation.IsStringValid(category)) + { + Console.WriteLine("\nInvalid category"); + category = Console.ReadLine(); + } + + if (!categories.Any(x => x.strCategory == category)) + { + Console.WriteLine("Category does not exist."); + GetCategoriesInput(); + } + + + GetDrinksInput(category); + + + } + + private void GetDrinksInput(string category) + { + var drinks = drinksService.GetDrinksByCategory(category); + Console.WriteLine("Choose a drink ID or go back to category menu by typing 0:"); + + //eeds to ask for drinkid not drink + + string drink = Console.ReadLine(); + + if (drink == "0") GetCategoriesInput(); + + while (!Validation.IsIdValid(drink)) + { + Console.WriteLine("\nInvalid Drink"); + drink = Console.ReadLine(); + } + + if (!drinks.Any(x => x.idDrink == drink)) + { + Console.WriteLine("Category does not exist."); + GetCategoriesInput(); + } + + drinksService.GetDrink(drink); + + Console.WriteLine("Press any key to go back to the categories menu."); + Console.ReadKey(); + if(!Console.KeyAvailable) GetCategoriesInput(); + + } +} \ No newline at end of file diff --git a/Drinks info/Drinks/Validation.cs b/Drinks info/Drinks/Validation.cs new file mode 100644 index 00000000..9c6477ba --- /dev/null +++ b/Drinks info/Drinks/Validation.cs @@ -0,0 +1,43 @@ +public class Validation +{ + + + internal static bool IsStringValid(string stringInput) + { + if (String.IsNullOrEmpty(stringInput)) + { + return false; + } + + + foreach (char letter in stringInput) + { + if (!char.IsLetter(letter) && letter != '/' && letter != ' ') + return false; + } + + return true; + } + + + + public static bool IsIdValid(string stringInput) + { + if (string.IsNullOrEmpty(stringInput)) + { + return false; + + } + + foreach (char c in stringInput) + { + if (!Char.IsDigit(c)) + { + return false; + }// if not digit return false + } + //otherwise + return true; + + } +} From abf915debe9c123c3956e92e00e3686a67227f08 Mon Sep 17 00:00:00 2001 From: Joel Date: Thu, 30 Jul 2026 19:55:21 +0100 Subject: [PATCH 2/2] Added README --- README.txt | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 README.txt diff --git a/README.txt b/README.txt new file mode 100644 index 00000000..8058e605 --- /dev/null +++ b/README.txt @@ -0,0 +1,75 @@ +drinks info app + +We are making a drinks info app that uses an API to get and display details of a drink by allowing users to choose a category of drink as well as the drink ID. + +to do this we need several classes: + + +Main + +Main or program.cs is where we would call our methods as always, we are calling one method inside user input that calls methods from drinkservice.cs passing along the information the user entered into those drinkservice methods where the API methods live. + + + +RestSharp and HHTPclient differences +RestSharp already breaks down the data and gives us the string directly in the line string rawResponse = response.Result.Content; + +HttpClient does not do that, Httpclient needs to read the data it is receiving before breaking it down here we need string rawResponse = await response.Content.ReadAsStringAsync(); this converts our data into a string that our deserializer can read. once it can be read it can break it down and map the data into the list. + +UserInput.cs + +UserInput Input = new(); needs to be called as none of the methods used in this program are static so instances must be created before we can access them. + +Only Input.GetCategoriesInput(); needs to be called so we call that before placing a console.readkey() after it so the terminal dosent close automatically. + +There are three model classes I need to make, one for each returned data set from the API. Model classes hold the shape of data and the core business logic of the application. + +Category.cs - Category class holds the returned property strCategory when the API is called. the Categories class holds the property "drinks" as well as the list CatergoriesList + +Catergorieslist is later access through the serialize variable to show the returned categories from the API. + +DrinkDetail.cs - DrinkDetailObject is the wrapper class holds the property "drinks" as well as the DrinkDetailList list which we use in our data. After deserialisation, the list is accessed via serialize.DrinkDetailList to show the returned drinks from the API. + +Drinks.cs - holds the property "drinks" which is what the set of data retuned from the api is named, public class Drink holds the variables returned by the API which are strDrink & idDrink + +TableVisualisation.cs +This class is responsible for showing the data as a table using the consoletablebuilder. + + +DrinkService.cs +The most important class of all which is our drinkservice class, this class is responsible for carrying out all three API requests one for getting the category of drinks available, one for getting the drinks that match the users entered category and lastly getting the details of the drink that they chose, the details include many variables that we hold inside the DrinkDetail model class, this will list information for all the variables in the model except for null values. + +Inside this class we create a "client" which is the website +A "request" which is responsible for the endpoint which fetches the EXACT request +and response which executes the client using request as the passed in information. + + +Validation.cs +This class is for creating validation for userinput two methods exist in this class, IsStringValid & IsIdValid are for validating user input. + +IsStringValid checks for null or empty input in the input (stringInput) return false. + +IsIdValid checks whether the input is null or empty, and whether every character in the string is a digit; if either check fails, it returns false. Otherwise, it returns true, allowing the program to proceed with that ID. + +There was a weird quirk with the formatted name, the formattedName only displayed the names correctly when I used the word key, when I tried to use "word" the table was misaligned. +All in all I think the project taught me about how to consume an API well, I learned about the request/response pipeline, deserialization, the two-class model pattern, why async/await exists, and how LINQ validates user input. + +I learned that when deserialising API data there are two separate classes in the model class, one wrapper class representing the outer JSON object and one inner class representing each item in the list +To deserialize correctly we need both classes. + +This class is responsible for defining the jsonproperty which is the instruction telling the deserialiser which JSON field maps to this property returned from the API. The wrapper class uses [JsonProperty("drinks")] to tell the deserialiser that the JSON field called drinks should be mapped into this property, since the C# property name (DrinksList) is different from the JSON field name. + +Directly above the property, we declare [JsonProperty("drinks")], and the property itself is public List DrinksList { get; set; } — together they say: take the JSON field drinks and map it into this list. + +The Drink class contains the properties named idDrink & strDrink this is why the list is named after this class, it uses the jsonproperty to locate the name of the dataset and uses the Drink list to define the returned values fields. + +[JsonProperty] tells the deserialiser which JSON field to read from. List tells it what shape to build for each item inside that field. + + + + + + + + +