Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Healthy.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -285,6 +286,7 @@
B85D26A62A3902DF000A463D /* EmailValidatorsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailValidatorsTests.swift; sourceTree = "<group>"; };
B85D26A82A390331000A463D /* PasswordValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordValidatorTests.swift; sourceTree = "<group>"; };
C21F76E32A4AFBF700E0609C /* Collection+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Helpers.swift"; sourceTree = "<group>"; };
C2926D4C2A69442F00BD1F7B /* MealDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealDetails.swift; sourceTree = "<group>"; };
C2FE120F2A4128D8001A7BE3 /* UIButton+AnimatableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+AnimatableView.swift"; sourceTree = "<group>"; };
C2FE12112A412904001A7BE3 /* AnimatableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatableView.swift; sourceTree = "<group>"; };
C9057FE42A48AC78002E4459 /* SliderDishesView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SliderDishesView.xib; sourceTree = "<group>"; };
Expand Down Expand Up @@ -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 = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down
6 changes: 6 additions & 0 deletions Healthy/Classes/Entities/MealDetails.swift
Original file line number Diff line number Diff line change
@@ -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] = []
Comment on lines +4 to +5
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var ingredients: [String] = []
var measures: [String] = []
let ingredients: [String] = []
let measures: [String] = []

Mutability is a big source of bugs. Avoid it.

}
12 changes: 12 additions & 0 deletions Networking/Sources/Networking/Networking/DynamicCodingKeys.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
struct DynamicCodingKeys: CodingKey {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding it, I like the idea 🙏🏼

var intValue: Int?
init?(intValue: Int) {
self.intValue = intValue
self.stringValue = "\(intValue)"
}

var stringValue: String
init(stringValue: String) {
self.stringValue = stringValue
}
Comment on lines +2 to +11
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var intValue: Int?
init?(intValue: Int) {
self.intValue = intValue
self.stringValue = "\(intValue)"
}
var stringValue: String
init(stringValue: String) {
self.stringValue = stringValue
}
let intValue: Int?
init?(intValue: Int) {
self.intValue = intValue
self.stringValue = "\(intValue)"
}
let stringValue: String
init(stringValue: String) {
self.stringValue = stringValue
}

To avoid duplication, you might consider making it generic, or at least have two versions for both Int and String.

}
Original file line number Diff line number Diff line change
@@ -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 ] }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// MARK: - MealDetailsResponse
public struct MealDetailsResponse: Decodable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public struct MealDetailsResponse: Decodable {
struct MealDetailsResponse: Decodable {

No need to make this public, just return a list of meals.

let meals: [MealDetails]
}

// MARK: - Meal Details
public struct MealDetails: Decodable {
let id, name, drinkAlternate, category, area, instructions, thumbnail, tags: String?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Id should never be optional


var ingredients: [String] = []
var measures: [String] = []
Comment on lines +10 to +11
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var ingredients: [String] = []
var measures: [String] = []
let ingredients: [String] = []
let measures: [String] = []


enum CodingKeys: String, CodingKey {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
enum CodingKeys: String, CodingKey {
private 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] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding this. A good practice when adding a code that the others in the might might not be aware of is to write comments. Documentation helps your future self understand why ou added this in the first place.

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 }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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"])
}
}