diff --git a/Healthy.xcodeproj/project.pbxproj b/Healthy.xcodeproj/project.pbxproj index 71515ab4..72a92bb0 100644 --- a/Healthy.xcodeproj/project.pbxproj +++ b/Healthy.xcodeproj/project.pbxproj @@ -133,6 +133,7 @@ B85D26A72A3902DF000A463D /* EmailValidatorsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85D26A62A3902DF000A463D /* EmailValidatorsTests.swift */; }; B85D26A92A390331000A463D /* PasswordValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85D26A82A390331000A463D /* PasswordValidatorTests.swift */; }; C21F76E42A4AFBF700E0609C /* Collection+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C21F76E32A4AFBF700E0609C /* Collection+Helpers.swift */; }; + C2926D4D2A69442F00BD1F7B /* MealDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2926D4C2A69442F00BD1F7B /* MealDetails.swift */; }; C2FE12102A4128D8001A7BE3 /* UIButton+AnimatableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2FE120F2A4128D8001A7BE3 /* UIButton+AnimatableView.swift */; }; C2FE12122A412904001A7BE3 /* AnimatableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2FE12112A412904001A7BE3 /* AnimatableView.swift */; }; C9057FE62A48AC78002E4459 /* SliderDishesView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C9057FE42A48AC78002E4459 /* SliderDishesView.xib */; }; @@ -285,6 +286,7 @@ B85D26A62A3902DF000A463D /* EmailValidatorsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailValidatorsTests.swift; sourceTree = ""; }; B85D26A82A390331000A463D /* PasswordValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordValidatorTests.swift; sourceTree = ""; }; C21F76E32A4AFBF700E0609C /* Collection+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Helpers.swift"; sourceTree = ""; }; + C2926D4C2A69442F00BD1F7B /* MealDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealDetails.swift; sourceTree = ""; }; C2FE120F2A4128D8001A7BE3 /* UIButton+AnimatableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+AnimatableView.swift"; sourceTree = ""; }; C2FE12112A412904001A7BE3 /* AnimatableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatableView.swift; sourceTree = ""; }; C9057FE42A48AC78002E4459 /* SliderDishesView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SliderDishesView.xib; sourceTree = ""; }; @@ -400,10 +402,10 @@ 025511BC2A3C656300295B91 /* Recipe.swift */, 025511BA2A3C655200295B91 /* SavedRecipe.swift */, 798B59B12A5D15E600E4DCFF /* MealCategories.swift */, - 0286F2852A5EEB1800F20478 /* RandomMealEntity.swift */, C972DB2E2A5F6F91000041D1 /* FilterByMainIngredient.swift */, 6D6B89AC2A5DE99F00E52F4C /* FilterByArea.swift */, 0286F2852A5EEB1800F20478 /* RandomMealEntity.swift */, + C2926D4C2A69442F00BD1F7B /* MealDetails.swift */, ); path = Entities; sourceTree = ""; @@ -1343,6 +1345,7 @@ 025A47592A336C2D008BF85A /* HomeHeaderView.swift in Sources */, B21F81C02A41A05C00905E50 /* UICollectionView+Helpers.swift in Sources */, 022924302A33A170009290A8 /* UIColors.Generated.swift in Sources */, + C2926D4D2A69442F00BD1F7B /* MealDetails.swift in Sources */, B2F799E42A34459B002F1894 /* MainTabBar.swift in Sources */, 025511D62A3D0A8B00295B91 /* CreateAccountViewController.swift in Sources */, 025511B32A3C5F8900295B91 /* FilterSearchViewModel.swift in Sources */, diff --git a/Healthy/Classes/Entities/MealDetails.swift b/Healthy/Classes/Entities/MealDetails.swift new file mode 100644 index 00000000..ac05c615 --- /dev/null +++ b/Healthy/Classes/Entities/MealDetails.swift @@ -0,0 +1,6 @@ +// MARK: - Meal Details +struct MealDetails { + let id, name, drinkAlternate, category, area, instructions, thumbnail, tags: String + var ingredients: [String] = [] + var measures: [String] = [] +} diff --git a/Networking/Sources/Networking/Networking/DynamicCodingKeys.swift b/Networking/Sources/Networking/Networking/DynamicCodingKeys.swift new file mode 100644 index 00000000..58e4eb83 --- /dev/null +++ b/Networking/Sources/Networking/Networking/DynamicCodingKeys.swift @@ -0,0 +1,12 @@ +struct DynamicCodingKeys: CodingKey { + var intValue: Int? + init?(intValue: Int) { + self.intValue = intValue + self.stringValue = "\(intValue)" + } + + var stringValue: String + init(stringValue: String) { + self.stringValue = stringValue + } +} diff --git a/Networking/Sources/Networking/Requests/MealDetails/MealDetailsRequest.swift b/Networking/Sources/Networking/Requests/MealDetails/MealDetailsRequest.swift new file mode 100644 index 00000000..e6346d81 --- /dev/null +++ b/Networking/Sources/Networking/Requests/MealDetails/MealDetailsRequest.swift @@ -0,0 +1,17 @@ +import Foundation + +// MARK: - MealDetailsRequest +public struct MealDetailsRequest: RequestType { + public typealias ResponseType = MealDetailsResponse + + private let id: String + + public init(_ id: String) { + self.id = id + } + + public var baseUrl: URL { Constants.theMealDB } + public var path: String { "lookup.php" } + public var method: String { "GET" } + public var queryParameters: [String: String] { [ "i": id ] } +} diff --git a/Networking/Sources/Networking/Requests/MealDetails/MealDetailsResponse.swift b/Networking/Sources/Networking/Requests/MealDetails/MealDetailsResponse.swift new file mode 100644 index 00000000..a3133598 --- /dev/null +++ b/Networking/Sources/Networking/Requests/MealDetails/MealDetailsResponse.swift @@ -0,0 +1,57 @@ +// MARK: - MealDetailsResponse +public struct MealDetailsResponse: Decodable { + let meals: [MealDetails] +} + +// MARK: - Meal Details +public struct MealDetails: Decodable { + let id, name, drinkAlternate, category, area, instructions, thumbnail, tags: String? + + var ingredients: [String] = [] + var measures: [String] = [] + + enum CodingKeys: String, CodingKey { + case id = "idMeal" + case name = "strMeal" + case drinkAlternate = "strDrinkAlternate" + case category = "strCategory" + case area = "strArea" + case instructions = "strInstructions" + case thumbnail = "strMealThumb" + case tags = "strTags" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + id = try container.decode(String.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + drinkAlternate = try container.decodeIfPresent(String.self, forKey: .drinkAlternate) + category = try container.decode(String.self, forKey: .category) + area = try container.decode(String.self, forKey: .area) + instructions = try container.decode(String.self, forKey: .instructions) + thumbnail = try container.decode(String.self, forKey: .thumbnail) + tags = try container.decode(String.self, forKey: .tags) + + ingredients = try dynamicValuesFor(dynamicKey: "strIngredient", with: decoder) + .compactMap { $0.isEmpty ? nil : $0} + measures = try dynamicValuesFor(dynamicKey: "strMeasure", with: decoder) + .compactMap { $0.isEmpty ? nil : $0} + } + + private func dynamicValuesFor(dynamicKey: String, with decoder: Decoder) throws -> [String] { + let dynamicContainer = try decoder.container(keyedBy: DynamicCodingKeys.self) + var dictionary: [String: String] = [:] + + dynamicContainer.allKeys.forEach { key in + if key.stringValue.hasPrefix(dynamicKey), + let value = try? dynamicContainer.decode(String.self, forKey: key) { + dictionary[key.stringValue] = value + } + } + + return dictionary + .sorted { $0.key < $1.key } + .compactMap { $0.value } + } +} diff --git a/Networking/Tests/NetworkingTests/Requests/MealCategoriesRequestTests.swift b/Networking/Tests/NetworkingTests/Requests/MealCategoriesRequestTests.swift index 0792dd93..4c628c22 100644 --- a/Networking/Tests/NetworkingTests/Requests/MealCategoriesRequestTests.swift +++ b/Networking/Tests/NetworkingTests/Requests/MealCategoriesRequestTests.swift @@ -4,43 +4,45 @@ import XCTest final class MealCategoriesRequestTests: XCTestCase { // MARK: Properties - + private var sut: MealCategoriesRequest! - + // MARK: - Lifecycle - + override func setUp() { sut = MealCategoriesRequest() } - + // MARK: - Tests - + func testMealCategoriesRequestProperties() { // Then XCTAssertEqual(sut.baseUrl, Constants.theMealDB) XCTAssertEqual(sut.path, "categories.php") XCTAssertEqual(sut.method, "GET") } - + func testMealCategoriesResponseDecoder() throws { // Given let mealCategoriesResponseAsString = """ - { - "categories": [ - { - "idCategory": "1", - "strCategory": "Beef", - "strCategoryThumb": "https://www.themealdb.com/images/category/beef.png", - "strCategoryDescription": "Beef is the culinary name for meat from cattle, particularly skeletal muscle. Humans have been eating beef since prehistoric times.[1] Beef is a source of high-quality protein and essential nutrients.[2]" - } - ] - } - """ - + { + "categories": [ + { + "idCategory": "1", + "strCategory": "Beef", + "strCategoryThumb": "https://www.themealdb.com/images/category/beef.png", + "strCategoryDescription": "Beef is the culinary name for meat from cattle, \ + particularly skeletal muscle. Humans have been eating beef since prehistoric times.[1] \ + Beef is a source of high-quality protein and essential nutrients.[2]" + } + ] + } + """ + // When let mealCategoriesResponseData = try XCTUnwrap(mealCategoriesResponseAsString.data(using: .utf8)) let mealCategoriesResponse = try? sut.responseDecoder(mealCategoriesResponseData) - + // Then XCTAssertNotNil(mealCategoriesResponse) XCTAssertEqual(mealCategoriesResponse?.categories.count, 1) diff --git a/Networking/Tests/NetworkingTests/Requests/MealDetailsRequestTests.swift b/Networking/Tests/NetworkingTests/Requests/MealDetailsRequestTests.swift new file mode 100644 index 00000000..4a2c547f --- /dev/null +++ b/Networking/Tests/NetworkingTests/Requests/MealDetailsRequestTests.swift @@ -0,0 +1,81 @@ +import XCTest +@testable import Networking + +final class MealDetailsRequestTests: XCTestCase { + var sut: MealDetailsRequest! + + let stub = """ + { + "meals": [ + { + "idMeal": "52772", + "strMeal": "Teriyaki Chicken Casserole", + "strDrinkAlternate": null, + "strCategory": "Chicken", + "strArea": "Japanese", + "strInstructions": "Preheat oven to 350° F.", + "strMealThumb": "https://www.themealdb.com/images/media/meals/wvpsxx1468256321.jpg", + "strTags": "Meat,Casserole", + "strIngredient1": "soy sauce", + "strIngredient2": "water", + "strIngredient3": "brown sugar", + "strIngredient4": "ground ginger", + "strIngredient5": "minced garlic", + "strIngredient6": "cornstarch", + "strIngredient7": "chicken breasts", + "strIngredient8": "stir-fry vegetables", + "strIngredient9": "brown rice", + "strMeasure1": "3/4 cup", + "strMeasure2": "1/2 cup", + "strMeasure3": "1/4 cup", + "strMeasure4": "1/2 teaspoon", + "strMeasure5": "1/2 teaspoon", + "strMeasure6": "4 Tablespoons", + "strMeasure7": "2", + "strMeasure8": "1 (12 oz.)", + "strMeasure9": "3 cups", + } + ] + } + """ + + // MARK: - Lifecycle + + override func setUp() { + sut = MealDetailsRequest("52772") + } + + // MARK: - Tests + + func testMealDetailsRequestProperties() { + // Then + XCTAssertEqual(sut.baseUrl, Constants.theMealDB) + XCTAssertEqual(sut.path, "lookup.php") + XCTAssertEqual(sut.method, "GET") + } + + func testMealDetailsResponseDecoder() throws { + // Given + let mealDetailsRequest = MealDetailsRequest("52772") + + // When + let mealDetailsResponseData = try XCTUnwrap(stub.data(using: .utf8)) + let mealDetailsResponse = try? mealDetailsRequest.responseDecoder(mealDetailsResponseData) + + // Then + let meal = try XCTUnwrap(mealDetailsResponse?.meals[0]) + XCTAssertNotNil(mealDetailsResponse) + XCTAssertEqual(mealDetailsResponse?.meals.count, 1) + XCTAssertEqual(meal.id, "52772") + XCTAssertEqual(meal.name, "Teriyaki Chicken Casserole") + XCTAssertEqual(meal.category, "Chicken") + XCTAssertEqual(meal.area, "Japanese") + XCTAssertEqual(meal.instructions, "Preheat oven to 350° F.") + XCTAssertEqual(meal.thumbnail, "https://www.themealdb.com/images/media/meals/wvpsxx1468256321.jpg") + XCTAssertEqual(meal.ingredients, ["soy sauce", "water", "brown sugar", "ground ginger", + "minced garlic", "cornstarch", "chicken breasts", + "stir-fry vegetables", "brown rice"]) + XCTAssertEqual(meal.measures, ["3/4 cup", "1/2 cup", "1/4 cup", "1/2 teaspoon", "1/2 teaspoon", + "4 Tablespoons", "2", "1 (12 oz.)", "3 cups"]) + } +}