Skip to content

Commit

Permalink
add comments
Browse files Browse the repository at this point in the history
  • Loading branch information
rex committed Oct 26, 2024
1 parent 2a10d26 commit 901128c
Show file tree
Hide file tree
Showing 19 changed files with 283 additions and 57 deletions.
14 changes: 14 additions & 0 deletions mobile-client/ToDoList/DataModel/ToDoItem.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
import SwiftUI

/// A model representing a To-Do item, including details such as its ID, title, deadline,
/// status, tags, and timestamps for creation and last update.
///
/// Conforms to `Identifiable`, `Codable`, and `Equatable` for use in SwiftUI lists, encoding/decoding,
/// and equality checks.
///
/// - Properties:
/// - id: Unique identifier for the to-do item.
/// - title: Title or description of the to-do task.
/// - deadline: Optional deadline for the task, formatted as a string.
/// - status: Current status of the task (e.g., "pending", "completed").
/// - tags: Array of tags or categories associated with the task.
/// - createdAt: Timestamp representing the creation date, formatted as a string.
/// - updatedAt: Timestamp representing the last update, formatted as a string.
struct ToDoItem: Identifiable, Codable, Equatable {
var id: Int
var title: String
Expand Down
25 changes: 23 additions & 2 deletions mobile-client/ToDoList/DataModel/ToDoItemData.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import SwiftData

// Represents a tag for a todo item, used for encoding/decoding
// and managing associated tags in the app.
/// Represents a tag associated with a to-do item, allowing
/// for encoding/decoding of tags in the app.
struct Tag: Codable {
let name: String
}

/// A SwiftData model representing a to-do item, including properties such as
/// `id`, `title`, and `tags` for flexible management within the app.
@Model
class ToDoItemData: Identifiable {
@Attribute(.unique) var id: Int
Expand All @@ -15,6 +17,17 @@ class ToDoItemData: Identifiable {
var tags: [Tag]
var createdAt: String
var updatedAt: String

/// Initializes a new `ToDoItemData` instance with the provided properties.
///
/// - Parameters:
/// - id: Unique identifier of the to-do item.
/// - title: Title or description of the to-do item.
/// - deadline: Optional ISO 8601 formatted deadline as a string.
/// - status: Current status of the item (e.g., "pending", "completed").
/// - tags: Array of associated `Tag` instances for categorization.
/// - createdAt: ISO 8601 formatted creation timestamp.
/// - updatedAt: ISO 8601 formatted last updated timestamp.
init(id: Int,
title: String,
deadline: String?,
Expand All @@ -32,6 +45,10 @@ class ToDoItemData: Identifiable {
self.updatedAt = updatedAt
}

/// Convenience initializer to create `ToDoItemData` from a `ToDoItem`.
/// Maps `tags` from an array of strings to `Tag` objects.
///
/// - Parameter todoItem: The `ToDoItem` instance to be converted.
convenience init(from todoItem: ToDoItem) {
let tags = todoItem.tags.map { Tag(name: $0) }
self.init(
Expand All @@ -45,6 +62,10 @@ class ToDoItemData: Identifiable {
)
}

/// Converts the `ToDoItemData` instance back to a `ToDoItem` instance,
/// with `tags` converted from `Tag` objects to an array of tag names as strings.
///
/// - Returns: A `ToDoItem` with the properties copied from `ToDoItemData`.
func toDoItem() -> ToDoItem {
let tags = tags.map(\.name)
return ToDoItem(
Expand Down
10 changes: 8 additions & 2 deletions mobile-client/ToDoList/Helper/ToDoArrayExtensions.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import Foundation

// Extensions for ToDoItemData
// Extensions for sorting ToDoItemData array by deadline.
extension [ToDoItemData] {
/// Sorts the `ToDoItemData` array by the `deadline` property, placing items
/// with the earliest deadlines at the beginning of the array. Items without
/// a specified deadline are considered to have a deadline of `Date.distantFuture`.
mutating func sortedByDeadline() {
sort { first, second in
let firstDeadline = ToDoDateFormatter.isoDateFormatter.date(from: first.deadline ?? "") ?? Date.distantFuture
Expand All @@ -11,8 +14,11 @@ extension [ToDoItemData] {
}
}

// Extensions for ToDoItem
// Extensions for sorting ToDoItem array by deadline.
extension [ToDoItem] {
/// Sorts the `ToDoItem` array by the `deadline` property, placing items
/// with the earliest deadlines at the beginning of the array. Items without
/// a specified deadline are considered to have a deadline of `Date.distantFuture`.
mutating func sortedByDeadline() {
sort { first, second in
let firstDeadline = ToDoDateFormatter.isoDateFormatter.date(from: first.deadline ?? "") ?? Date.distantFuture
Expand Down
14 changes: 14 additions & 0 deletions mobile-client/ToDoList/Model/DataFetcher.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
import Foundation

/// Protocol defining a data fetching interface for making network requests.
protocol DataFetcherProtocol {
/// Performs a data request and returns the resulting data and response.
///
/// - Parameter request: The URL request to be executed.
/// - Returns: A tuple containing the retrieved `Data` and `URLResponse`.
/// - Throws: An error if the data request fails.
func dataRequest(from request: URLRequest) async throws -> (Data, URLResponse)
}

/// Concrete implementation of `DataFetcherProtocol` using `URLSession`
/// to perform network requests.
final class URLSessionDataFetcher: DataFetcherProtocol {
/// Executes a network request using `URLSession.shared` and returns
/// the data and response.
///
/// - Parameter request: The URL request to be executed.
/// - Returns: A tuple containing the retrieved `Data` and `URLResponse`.
/// - Throws: An error if the request fails.
func dataRequest(from request: URLRequest) async throws -> (Data, URLResponse) {
try await URLSession.shared.data(for: request)
}
Expand Down
9 changes: 9 additions & 0 deletions mobile-client/ToDoList/Model/RequestBuilder.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import Foundation

/// Utility for building URL requests with specified parameters.
enum RequestBuilder {
/// Constructs a `URLRequest` with the provided URL, HTTP method, headers, and body data.
///
/// - Parameters:
/// - url: The `URL` for the request.
/// - method: The HTTP method as a `String` (e.g., "GET", "POST").
/// - headers: An optional dictionary of HTTP headers, where keys are header fields and values are their respective values.
/// - body: An optional `Data` object containing the body of the request.
/// - Returns: A configured `URLRequest` with the specified properties.
static func build(url: URL, method: String, headers: [String: String]?, body: Data?) -> URLRequest {
var request = URLRequest(url: url)
request.httpMethod = method
Expand Down
15 changes: 11 additions & 4 deletions mobile-client/ToDoList/Model/ToDoError.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import Foundation

/// An enumeration representing errors that can occur in the ToDo application.
/// An enumeration representing various errors that can occur in the ToDo application.
enum ToDoError: Error, LocalizedError, Equatable {
/// Error occurring due to network-related issues, wrapping an underlying `Error`.
case networkError(Error)
/// Error occurring during the decoding process, wrapping a `DecodingError`.
case decodingError(Error)
/// Error for an invalid server response, capturing the status code.
case invalidResponse(Int)
/// Error representing a general local issue, such as an issue fetching local data.
case localError
/// Error for an unknown issue when the exact cause is indeterminate.
case unknownError

/// Provides a user-friendly description for each error type.
var errorDescription: String? {
switch self {
case let .networkError(error):
Expand All @@ -23,6 +29,8 @@ enum ToDoError: Error, LocalizedError, Equatable {
}
}

/// Equatable conformance to compare two `ToDoError` values for equality.
/// Custom logic is used to handle error comparison by comparing underlying error codes and descriptions.
static func == (lhs: ToDoError, rhs: ToDoError) -> Bool {
switch (lhs, rhs) {
case let (.networkError(error1), .networkError(error2)),
Expand All @@ -31,9 +39,8 @@ enum ToDoError: Error, LocalizedError, Equatable {
(error1.localizedDescription == error2.localizedDescription)
case let (.invalidResponse(statusCode1), .invalidResponse(statusCode2)):
statusCode1 == statusCode2
case (.localError, .localError):
true
case (.unknownError, .unknownError):
case (.localError, .localError),
(.unknownError, .unknownError):
true
default:
false
Expand Down
35 changes: 35 additions & 0 deletions mobile-client/ToDoList/Model/ToDoLocalService.swift
Original file line number Diff line number Diff line change
@@ -1,32 +1,64 @@
import Foundation
import SwiftData

/// Protocol defining the core local storage operations for ToDo items.
protocol ToDoLocalServiceProtocol {
/// Fetches all ToDo items from local storage.
/// - Returns: An array of `ToDoItemData` instances.
/// - Throws: An error if fetching fails.
func fetchTodos() throws -> [ToDoItemData]

/// Saves a new ToDo item to local storage.
/// - Parameter todo: The `ToDoItemData` instance to save.
/// - Throws: An error if saving fails.
func save(todo: ToDoItemData) throws

/// Updates an existing ToDo item in local storage by ID.
/// - Parameters:
/// - todoId: The unique identifier of the ToDo item to update.
/// - newToDo: The updated `ToDoItem` instance with new data.
/// - Throws: An error if updating fails.
func update(todoId: Int, newToDo: ToDoItem) throws

/// Deletes a ToDo item from local storage.
/// - Parameter todo: The `ToDoItemData` instance to delete.
/// - Throws: An error if deleting fails.
func delete(todo: ToDoItemData) throws
}

/// Service class implementing `ToDoLocalServiceProtocol` for managing local ToDo item storage.
class ToDoLocalService: ToDoLocalServiceProtocol {
private let context: ModelContext

/// Initializes the service with a provided data model context.
/// - Parameter context: The `ModelContext` for interacting with the local database.
init(context: ModelContext) {
self.context = context
}

/// Fetches all ToDo items from local storage, sorted by deadline.
/// - Returns: A sorted array of `ToDoItemData` instances.
/// - Throws: An error if fetching fails.
func fetchTodos() throws -> [ToDoItemData] {
let fetchDescriptor = FetchDescriptor<ToDoItemData>()
var todos = try context.fetch(fetchDescriptor)
todos.sortedByDeadline()
return todos
}

/// Saves a new ToDo item to local storage.
/// - Parameter todo: The `ToDoItemData` instance to save.
/// - Throws: An error if saving fails.
func save(todo: ToDoItemData) throws {
context.insert(todo)
try context.save()
}

/// Updates an existing ToDo item in local storage.
/// - Parameters:
/// - todoId: The unique identifier of the ToDo item to update.
/// - newToDo: The updated `ToDoItem` instance with new values.
/// - Throws: An error if the update operation fails.
func update(todoId: Int, newToDo: ToDoItem) throws {
var descriptor = FetchDescriptor<ToDoItemData>(
predicate: #Predicate<ToDoItemData> { $0.id == todoId }
Expand All @@ -46,6 +78,9 @@ class ToDoLocalService: ToDoLocalServiceProtocol {
}
}

/// Deletes a ToDo item from local storage.
/// - Parameter todo: The `ToDoItemData` instance to delete.
/// - Throws: An error if the delete operation fails.
func delete(todo: ToDoItemData) throws {
context.delete(todo)
try context.save()
Expand Down
6 changes: 6 additions & 0 deletions mobile-client/ToDoList/Model/ToDoRemoteResponse.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import Foundation

/// A response model representing the result of adding a new ToDo item.
struct AddToDoResponse: Codable {
/// Indicates whether the add operation was successful.
let success: Bool
/// The newly added `ToDoItem` data returned from the server.
let data: ToDoItem
}

/// A response model representing the result of fetching multiple ToDo items.
struct FetchToDoResponse: Codable {
/// Indicates whether the fetch operation was successful.
let success: Bool
/// An array of `ToDoItem` data returned from the server.
let data: [ToDoItem]
}
44 changes: 41 additions & 3 deletions mobile-client/ToDoList/Model/ToDoRemoteService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,48 @@ import ComposableArchitecture
import Foundation
import SwiftData

/// A protocol defining the contract for remote ToDo service operations.
protocol ToDoRemoteServiceProtocol {
/// Fetches the list of ToDo items from the remote server.
/// - Throws: An error of type `ToDoError` if the fetch operation fails.
/// - Returns: An array of `ToDoItem` objects.
func fetchToDos() async throws -> [ToDoItem]

/// Fetches cached ToDo items from local storage.
/// - Throws: An error of type `ToDoError` if the fetch operation fails.
/// - Returns: An array of `ToDoItem` objects from local storage.
func fetchCachedTodos() async throws -> [ToDoItem]

/// Posts a new ToDo item to the remote server.
/// - Parameter todo: The `ToDoItem` to be added.
/// - Throws: An error of type `ToDoError` if the post operation fails.
/// - Returns: The newly created `ToDoItem` object from the server.
func postToDo(_ todo: ToDoItem) async throws -> ToDoItem

/// Deletes a ToDo item from the remote server by its ID.
/// - Parameter id: The unique identifier of the ToDo item to be deleted.
/// - Throws: An error of type `ToDoError` if the delete operation fails.
func deleteToDo(id: Int) async throws
}

/// A concrete implementation of the `ToDoRemoteServiceProtocol`.
final class ToDoRemoteService: ToDoRemoteServiceProtocol {
private let endPoint = "http://localhost:5000/todos"
private let localService: ToDoLocalServiceProtocol
private let dataFetcher: DataFetcherProtocol

/// Initializes a new instance of `ToDoRemoteService`.
/// - Parameters:
/// - localService: An instance conforming to `ToDoLocalServiceProtocol` for local operations.
/// - dataFetcher: An instance conforming to `DataFetcherProtocol` for network requests.
init(localService: ToDoLocalServiceProtocol, dataFetcher: DataFetcherProtocol) {
self.localService = localService
self.dataFetcher = dataFetcher
}

/// Handles errors and converts them into ToDoError types.
/// Handles errors and converts them into `ToDoError` types.
/// - Parameter error: The original error encountered.
/// - Throws: A `ToDoError` based on the type of the original error.
private func handleError(_ error: Error) throws -> Never {
if let toDoError = error as? ToDoError {
throw toDoError
Expand All @@ -36,11 +60,13 @@ final class ToDoRemoteService: ToDoRemoteServiceProtocol {
}
}

/// Fetches the list of ToDo items from the remote server and synchronizes local data.
/// - Throws: An error of type `ToDoError` if the fetch operation fails.
/// - Returns: An array of `ToDoItem` objects from the remote server.
func fetchToDos() async throws -> [ToDoItem] {
do {
let request = RequestBuilder.build(url: URL(string: endPoint)!, method: "GET", headers: nil, body: nil)
let (data, response)
= try await dataFetcher.dataRequest(from: request)
let (data, response) = try await dataFetcher.dataRequest(from: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw ToDoError.invalidResponse(0)
}
Expand Down Expand Up @@ -82,10 +108,17 @@ final class ToDoRemoteService: ToDoRemoteServiceProtocol {
}
}

/// Fetches cached ToDo items from local storage.
/// - Throws: An error of type `ToDoError` if the fetch operation fails.
/// - Returns: An array of `ToDoItem` objects from local storage.
func fetchCachedTodos() async throws -> [ToDoItem] {
try localService.fetchTodos().map { $0.toDoItem() }
}

/// Posts a new ToDo item to the remote server.
/// - Parameter todo: The `ToDoItem` to be added.
/// - Throws: An error of type `ToDoError` if the post operation fails.
/// - Returns: The newly created `ToDoItem` object from the server.
func postToDo(_ todo: ToDoItem) async throws -> ToDoItem {
do {
let encodedData = try JSONEncoder().encode(todo)
Expand All @@ -109,6 +142,9 @@ final class ToDoRemoteService: ToDoRemoteServiceProtocol {
}
}

/// Deletes a ToDo item from the remote server by its ID.
/// - Parameter id: The unique identifier of the ToDo item to be deleted.
/// - Throws: An error of type `ToDoError` if the delete operation fails.
func deleteToDo(id: Int) async throws {
do {
let deleteURL = "\(endPoint)/\(id)"
Expand All @@ -129,13 +165,15 @@ final class ToDoRemoteService: ToDoRemoteServiceProtocol {
}
}

/// A key for dependency injection of the ToDo remote service.
struct ToDoServiceKey: DependencyKey {
static var liveValue: ToDoRemoteServiceProtocol
= ToDoRemoteService(localService: ToDoLocalService(context: ModelContext(try! ModelContainer(for: ToDoItemData.self))),
dataFetcher: URLSessionDataFetcher())
}

extension DependencyValues {
/// A computed property to access the ToDo remote service in the dependency container.
var toDoService: ToDoRemoteServiceProtocol {
get { self[ToDoServiceKey.self] }
set { self[ToDoServiceKey.self] = newValue }
Expand Down
Loading

0 comments on commit 901128c

Please sign in to comment.