feat: Introduce comprehensive API debugging tools, enhance decoding error handling, and simplify API model definitions.
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// APIResponse.swift
|
||||
// AppClientBaseSwift
|
||||
//
|
||||
// API response wrapper models
|
||||
// Các model wrapper cho API response
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - API Response Wrapper
|
||||
// Wrapper Response API
|
||||
|
||||
/// Generic API response wrapper
|
||||
/// Wrapper response API chung
|
||||
struct APIResponse<T: Decodable>: Decodable {
|
||||
/// Success status
|
||||
/// Trạng thái thành công
|
||||
let success: Bool
|
||||
|
||||
/// Response data
|
||||
/// Dữ liệu response
|
||||
let data: T?
|
||||
|
||||
/// Error message if any
|
||||
/// Thông báo lỗi nếu có
|
||||
let error: String?
|
||||
|
||||
/// Pagination info if any
|
||||
/// Thông tin phân trang nếu có
|
||||
let pagination: Pagination?
|
||||
}
|
||||
|
||||
// MARK: - Pagination
|
||||
// Phân trang
|
||||
|
||||
/// Pagination information
|
||||
/// Thông tin phân trang
|
||||
struct Pagination: Decodable {
|
||||
let currentPage: Int?
|
||||
let pageSize: Int?
|
||||
let totalPages: Int?
|
||||
let totalItems: Int?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case currentPage
|
||||
case pageSize
|
||||
case totalPages
|
||||
case totalItems
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Simple Response
|
||||
// Response đơn giản
|
||||
|
||||
/// Simple success/error response
|
||||
/// Response thành công/lỗi đơn giản
|
||||
struct SimpleResponse: Decodable {
|
||||
let success: Bool
|
||||
let message: String?
|
||||
let error: String?
|
||||
}
|
||||
|
||||
// MARK: - List Response
|
||||
// Response danh sách
|
||||
|
||||
/// Generic list response wrapper
|
||||
/// Wrapper response danh sách
|
||||
struct ListResponse<T: Decodable>: Decodable {
|
||||
let success: Bool
|
||||
let data: [T]?
|
||||
let error: String?
|
||||
let pagination: Pagination?
|
||||
}
|
||||
306
apps/app-client-base-swift/AppClientBaseSwift/APITestView.swift
Normal file
306
apps/app-client-base-swift/AppClientBaseSwift/APITestView.swift
Normal file
@@ -0,0 +1,306 @@
|
||||
//
|
||||
// APITestView.swift
|
||||
// AppClientBaseSwift
|
||||
//
|
||||
// Debug view for testing API responses
|
||||
// View debug để test API responses
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - API Test View
|
||||
// View test API
|
||||
|
||||
/// Debug view to test API endpoints
|
||||
/// View debug để test các API endpoints
|
||||
struct APITestView: View {
|
||||
|
||||
@State private var testResult: String = "Chưa có kết quả"
|
||||
@State private var isLoading: Bool = false
|
||||
@State private var rawJSON: String = ""
|
||||
@State private var accessToken: String = ""
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ScrollView {
|
||||
VStack(spacing: 20) {
|
||||
|
||||
// Access Token Input
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Access Token")
|
||||
.font(.headline)
|
||||
|
||||
TextEditor(text: $accessToken)
|
||||
.frame(height: 100)
|
||||
.padding(8)
|
||||
.background(Color.gray.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
|
||||
Text("Paste access token từ Keychain hoặc sau khi login thành công")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding()
|
||||
|
||||
// Test Buttons
|
||||
VStack(spacing: 12) {
|
||||
Button("Test /users/me Endpoint") {
|
||||
Task {
|
||||
await testUsersMeEndpoint()
|
||||
}
|
||||
}
|
||||
.buttonStyle(TestButtonStyle(color: .blue))
|
||||
|
||||
Button("Test với Mock Token") {
|
||||
Task {
|
||||
await testWithMockToken()
|
||||
}
|
||||
}
|
||||
.buttonStyle(TestButtonStyle(color: .orange))
|
||||
|
||||
Button("Clear Results") {
|
||||
testResult = "Đã xóa kết quả"
|
||||
rawJSON = ""
|
||||
}
|
||||
.buttonStyle(TestButtonStyle(color: .red))
|
||||
}
|
||||
.padding()
|
||||
.disabled(isLoading)
|
||||
|
||||
// Loading Indicator
|
||||
if isLoading {
|
||||
ProgressView("Đang test API...")
|
||||
.padding()
|
||||
}
|
||||
|
||||
// Test Result
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Kết quả:")
|
||||
.font(.headline)
|
||||
|
||||
ScrollView {
|
||||
Text(testResult)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding()
|
||||
.background(Color.gray.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
.frame(maxHeight: 200)
|
||||
}
|
||||
.padding()
|
||||
|
||||
// Raw JSON
|
||||
if !rawJSON.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Raw JSON Response:")
|
||||
.font(.headline)
|
||||
|
||||
ScrollView {
|
||||
Text(rawJSON)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding()
|
||||
.background(Color.green.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
.frame(maxHeight: 300)
|
||||
|
||||
Button("Copy JSON") {
|
||||
UIPasteboard.general.string = rawJSON
|
||||
testResult = "✅ Đã copy JSON vào clipboard!"
|
||||
}
|
||||
.buttonStyle(TestButtonStyle(color: .green))
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
// Instructions
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Hướng dẫn:")
|
||||
.font(.headline)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("1. Login thành công trước")
|
||||
Text("2. Access token sẽ tự động được lấy từ Keychain")
|
||||
Text("3. Nhấn 'Test /users/me Endpoint'")
|
||||
Text("4. Xem Raw JSON để biết server trả về gì")
|
||||
Text("5. So sánh với User model trong code")
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.blue.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.navigationTitle("API Debugger")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
.onAppear {
|
||||
// Load current access token
|
||||
if let token = AuthManager.shared.accessToken {
|
||||
accessToken = token
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test Methods
|
||||
|
||||
/// Test /users/me endpoint
|
||||
private func testUsersMeEndpoint() async {
|
||||
isLoading = true
|
||||
testResult = "🔄 Đang gọi API..."
|
||||
|
||||
do {
|
||||
// Build URL
|
||||
guard let url = URL(string: APIConfig.baseURL + APIConfig.apiVersion + "/users/me") else {
|
||||
testResult = "❌ Invalid URL"
|
||||
isLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
// Create request
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
|
||||
// Use token from input or Keychain
|
||||
let token = accessToken.isEmpty ? AuthManager.shared.accessToken : accessToken
|
||||
if let token = token, !token.isEmpty {
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
|
||||
testResult = """
|
||||
📤 Request Details:
|
||||
URL: \(url.absoluteString)
|
||||
Method: GET
|
||||
Token: \(token?.prefix(20) ?? "None")...
|
||||
|
||||
⏳ Waiting for response...
|
||||
"""
|
||||
|
||||
// Perform request
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
testResult = "❌ Invalid response"
|
||||
isLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
// Get raw JSON
|
||||
if let jsonString = String(data: data, encoding: .utf8) {
|
||||
rawJSON = jsonString.prettyPrintedJSON ?? jsonString
|
||||
}
|
||||
|
||||
// Parse result
|
||||
testResult = """
|
||||
📥 Response Details:
|
||||
Status Code: \(httpResponse.statusCode)
|
||||
|
||||
"""
|
||||
|
||||
if httpResponse.statusCode == 200 {
|
||||
testResult += "✅ Status: SUCCESS\n\n"
|
||||
|
||||
// Try to decode as User
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
decoder.dateDecodingStrategy = .iso8601
|
||||
|
||||
do {
|
||||
let user = try decoder.decode(User.self, from: data)
|
||||
testResult += """
|
||||
✅ Decoded User Successfully:
|
||||
ID: \(user.id)
|
||||
Email: \(user.email)
|
||||
Name: \(user.name)
|
||||
Avatar: \(user.avatarUrl ?? "nil")
|
||||
Phone: \(user.phoneNumber ?? "nil")
|
||||
Email Verified: \(user.isEmailVerified)
|
||||
"""
|
||||
} catch let decodingError {
|
||||
testResult += """
|
||||
❌ Decoding Failed:
|
||||
\(decodingError.localizedDescription)
|
||||
|
||||
💡 Check Raw JSON below để xem field nào bị thiếu hoặc sai tên
|
||||
"""
|
||||
}
|
||||
} else if httpResponse.statusCode == 401 {
|
||||
testResult += "❌ Status: UNAUTHORIZED (Token invalid or expired)\n"
|
||||
} else {
|
||||
testResult += "❌ Status: ERROR\n"
|
||||
if let errorMessage = String(data: data, encoding: .utf8) {
|
||||
testResult += "Error: \(errorMessage)\n"
|
||||
}
|
||||
}
|
||||
|
||||
} catch {
|
||||
testResult = """
|
||||
❌ Request Failed:
|
||||
\(error.localizedDescription)
|
||||
"""
|
||||
rawJSON = ""
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
/// Test with mock token
|
||||
private func testWithMockToken() async {
|
||||
testResult = """
|
||||
⚠️ Testing với mock token...
|
||||
|
||||
Lưu ý: Request này sẽ fail với 401 Unauthorized
|
||||
Mục đích: Kiểm tra server response format khi unauthorized
|
||||
"""
|
||||
|
||||
accessToken = "mock_token_for_testing"
|
||||
|
||||
await testUsersMeEndpoint()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test Button Style
|
||||
|
||||
struct TestButtonStyle: ButtonStyle {
|
||||
let color: Color
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(
|
||||
color.opacity(configuration.isPressed ? 0.7 : 1.0)
|
||||
)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - String Extension for Pretty JSON
|
||||
|
||||
extension String {
|
||||
var prettyPrintedJSON: String? {
|
||||
guard let data = self.data(using: .utf8),
|
||||
let jsonObject = try? JSONSerialization.jsonObject(with: data),
|
||||
let prettyData = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted),
|
||||
let prettyString = String(data: prettyData, encoding: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
return prettyString
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview {
|
||||
APITestView()
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
//
|
||||
// APIUsageExamples.swift
|
||||
// AppClientBaseSwift
|
||||
//
|
||||
// Examples of using APIResponse wrapper
|
||||
// Ví dụ sử dụng APIResponse wrapper
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/*
|
||||
|
||||
HƯỚNG DẪN SỬ DỤNG API RESPONSE WRAPPER
|
||||
======================================
|
||||
|
||||
Server của bạn trả về response với format:
|
||||
{
|
||||
"success": true,
|
||||
"data": { ... } hoặc [ ... ],
|
||||
"error": null,
|
||||
"pagination": null
|
||||
}
|
||||
|
||||
Tất cả API calls phải sử dụng wrapper tương ứng.
|
||||
|
||||
*/
|
||||
|
||||
// MARK: - Example 1: Single Object Response
|
||||
// Ví dụ 1: Response trả về 1 object
|
||||
|
||||
/*
|
||||
GET /api/v1/users/me
|
||||
Response:
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "...",
|
||||
"email": "...",
|
||||
"name": "..."
|
||||
},
|
||||
"error": null,
|
||||
"pagination": null
|
||||
}
|
||||
*/
|
||||
|
||||
func exampleFetchUser() async throws -> User {
|
||||
// Cách 1: Dùng unwrap() helper (recommended)
|
||||
let response: APIResponse<User> = try await APIService.shared.get(
|
||||
endpoint: "/users/me"
|
||||
)
|
||||
let user = try response.unwrap()
|
||||
return user
|
||||
|
||||
// Cách 2: Manual check
|
||||
// let response: APIResponse<User> = try await APIService.shared.get(endpoint: "/users/me")
|
||||
// guard response.success, let user = response.data else {
|
||||
// throw APIError.serverError(statusCode: 400, message: response.error ?? "Failed")
|
||||
// }
|
||||
// return user
|
||||
}
|
||||
|
||||
// MARK: - Example 2: List/Array Response
|
||||
// Ví dụ 2: Response trả về danh sách
|
||||
|
||||
/*
|
||||
GET /api/v1/users?page=1
|
||||
Response:
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{ "id": "1", "email": "...", "name": "..." },
|
||||
{ "id": "2", "email": "...", "name": "..." }
|
||||
],
|
||||
"error": null,
|
||||
"pagination": {
|
||||
"currentPage": 1,
|
||||
"pageSize": 20,
|
||||
"totalPages": 5,
|
||||
"totalItems": 100
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
func exampleFetchUsers() async throws -> [User] {
|
||||
// Dùng ListResponse wrapper
|
||||
let response: ListResponse<User> = try await APIService.shared.get(
|
||||
endpoint: "/users?page=1"
|
||||
)
|
||||
|
||||
// Unwrap để lấy array
|
||||
let users = try response.unwrap()
|
||||
|
||||
// Có thể access pagination info
|
||||
if let pagination = response.pagination {
|
||||
print("Page \(pagination.currentPage ?? 0) of \(pagination.totalPages ?? 0)")
|
||||
print("Total items: \(pagination.totalItems ?? 0)")
|
||||
}
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
// MARK: - Example 3: POST Request with Response Data
|
||||
// Ví dụ 3: POST request có response data
|
||||
|
||||
/*
|
||||
POST /api/v1/posts
|
||||
Body: { "title": "...", "content": "..." }
|
||||
Response:
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "post_123",
|
||||
"title": "...",
|
||||
"content": "...",
|
||||
"createdAt": "..."
|
||||
},
|
||||
"error": null,
|
||||
"pagination": null
|
||||
}
|
||||
*/
|
||||
|
||||
struct Post: Codable {
|
||||
let id: String
|
||||
let title: String
|
||||
let content: String
|
||||
let createdAt: Date?
|
||||
}
|
||||
|
||||
struct CreatePostRequest: Encodable {
|
||||
let title: String
|
||||
let content: String
|
||||
}
|
||||
|
||||
func exampleCreatePost(title: String, content: String) async throws -> Post {
|
||||
let request = CreatePostRequest(title: title, content: content)
|
||||
|
||||
let response: APIResponse<Post> = try await APIService.shared.post(
|
||||
endpoint: "/posts",
|
||||
body: request
|
||||
)
|
||||
|
||||
return try response.unwrap()
|
||||
}
|
||||
|
||||
// MARK: - Example 4: DELETE/UPDATE Requests
|
||||
// Ví dụ 4: DELETE/UPDATE requests
|
||||
|
||||
/*
|
||||
DELETE /api/v1/posts/123
|
||||
Response:
|
||||
{
|
||||
"success": true,
|
||||
"data": null,
|
||||
"error": null,
|
||||
"pagination": null
|
||||
}
|
||||
*/
|
||||
|
||||
func exampleDeletePost(id: String) async throws {
|
||||
// Dùng SimpleResponse khi không cần data
|
||||
let response: SimpleResponse = try await APIService.shared.delete(
|
||||
endpoint: "/posts/\(id)"
|
||||
)
|
||||
|
||||
if !response.success {
|
||||
throw APIError.serverError(
|
||||
statusCode: 400,
|
||||
message: response.error ?? response.message ?? "Delete failed"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Example 5: PUT/PATCH Request
|
||||
// Ví dụ 5: PUT/PATCH request
|
||||
|
||||
/*
|
||||
PUT /api/v1/users/profile
|
||||
Body: { "name": "New Name", "phoneNumber": "..." }
|
||||
Response:
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "...",
|
||||
"email": "...",
|
||||
"name": "New Name",
|
||||
...
|
||||
},
|
||||
"error": null,
|
||||
"pagination": null
|
||||
}
|
||||
*/
|
||||
|
||||
struct UpdateProfileRequest: Encodable {
|
||||
let name: String?
|
||||
let phoneNumber: String?
|
||||
}
|
||||
|
||||
func exampleUpdateProfile(name: String?, phoneNumber: String?) async throws -> User {
|
||||
let request = UpdateProfileRequest(name: name, phoneNumber: phoneNumber)
|
||||
|
||||
let response: APIResponse<User> = try await APIService.shared.put(
|
||||
endpoint: "/users/profile",
|
||||
body: request
|
||||
)
|
||||
|
||||
return try response.unwrap()
|
||||
}
|
||||
|
||||
// MARK: - Example 6: Error Handling
|
||||
// Ví dụ 6: Xử lý lỗi
|
||||
|
||||
func exampleWithErrorHandling() async {
|
||||
do {
|
||||
let response: APIResponse<User> = try await APIService.shared.get(
|
||||
endpoint: "/users/me"
|
||||
)
|
||||
let user = try response.unwrap()
|
||||
print("Success: \(user.email)")
|
||||
|
||||
} catch let error as APIError {
|
||||
switch error {
|
||||
case .decodingError(let decodingError):
|
||||
print("❌ Decoding error: \(decodingError)")
|
||||
// Server response structure doesn't match expected model
|
||||
|
||||
case .unauthorized:
|
||||
print("❌ Unauthorized - token invalid or expired")
|
||||
// Need to login again
|
||||
|
||||
case .networkError(let networkError):
|
||||
print("❌ Network error: \(networkError)")
|
||||
// No internet connection or server unreachable
|
||||
|
||||
case .serverError(let statusCode, let message):
|
||||
print("❌ Server error \(statusCode): \(message ?? "Unknown")")
|
||||
// Server returned error response
|
||||
|
||||
case .noData:
|
||||
print("❌ No data received")
|
||||
|
||||
default:
|
||||
print("❌ Unknown error: \(error)")
|
||||
}
|
||||
|
||||
} catch {
|
||||
print("❌ Unexpected error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Example 7: Nested Objects
|
||||
// Ví dụ 7: Objects lồng nhau
|
||||
|
||||
/*
|
||||
GET /api/v1/posts/123/comments
|
||||
Response:
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": "comment_1",
|
||||
"content": "Great post!",
|
||||
"author": {
|
||||
"id": "user_1",
|
||||
"name": "John Doe",
|
||||
"avatarUrl": "..."
|
||||
},
|
||||
"createdAt": "..."
|
||||
}
|
||||
],
|
||||
"error": null,
|
||||
"pagination": { ... }
|
||||
}
|
||||
*/
|
||||
|
||||
struct Comment: Codable {
|
||||
let id: String
|
||||
let content: String
|
||||
let author: CommentAuthor
|
||||
let createdAt: Date?
|
||||
}
|
||||
|
||||
struct CommentAuthor: Codable {
|
||||
let id: String
|
||||
let name: String
|
||||
let avatarUrl: String?
|
||||
}
|
||||
|
||||
func exampleFetchComments(postId: String) async throws -> [Comment] {
|
||||
let response: ListResponse<Comment> = try await APIService.shared.get(
|
||||
endpoint: "/posts/\(postId)/comments"
|
||||
)
|
||||
return try response.unwrap()
|
||||
}
|
||||
|
||||
// MARK: - Example 8: Search/Filter with Query Parameters
|
||||
// Ví dụ 8: Search/Filter với query parameters
|
||||
|
||||
/*
|
||||
GET /api/v1/posts?search=swift&category=tutorial&page=1&pageSize=20
|
||||
*/
|
||||
|
||||
func exampleSearchPosts(
|
||||
search: String?,
|
||||
category: String?,
|
||||
page: Int = 1,
|
||||
pageSize: Int = 20
|
||||
) async throws -> [Post] {
|
||||
var queryItems: [String] = []
|
||||
|
||||
if let search = search {
|
||||
queryItems.append("search=\(search)")
|
||||
}
|
||||
if let category = category {
|
||||
queryItems.append("category=\(category)")
|
||||
}
|
||||
queryItems.append("page=\(page)")
|
||||
queryItems.append("pageSize=\(pageSize)")
|
||||
|
||||
let queryString = queryItems.joined(separator: "&")
|
||||
let endpoint = "/posts?\(queryString)"
|
||||
|
||||
let response: ListResponse<Post> = try await APIService.shared.get(
|
||||
endpoint: endpoint
|
||||
)
|
||||
|
||||
return try response.unwrap()
|
||||
}
|
||||
|
||||
// MARK: - Example 9: Upload File (if needed)
|
||||
// Ví dụ 9: Upload file (nếu cần)
|
||||
|
||||
/*
|
||||
POST /api/v1/users/avatar
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
Note: Cần implement multipart form data nếu cần upload file
|
||||
Hiện tại APIService chỉ hỗ trợ JSON
|
||||
*/
|
||||
|
||||
// MARK: - Example 10: Custom Headers
|
||||
// Ví dụ 10: Custom headers
|
||||
|
||||
func exampleWithCustomHeaders() async throws -> User {
|
||||
// Nếu cần thêm headers đặc biệt
|
||||
let response: APIResponse<User> = try await APIService.shared.request(
|
||||
endpoint: "/users/me",
|
||||
method: .get,
|
||||
body: nil as String?,
|
||||
headers: [
|
||||
"X-Custom-Header": "custom-value",
|
||||
"X-Request-ID": UUID().uuidString
|
||||
]
|
||||
)
|
||||
|
||||
return try response.unwrap()
|
||||
}
|
||||
|
||||
// MARK: - Best Practices
|
||||
// Các best practices
|
||||
|
||||
/*
|
||||
|
||||
✅ DO - Nên làm:
|
||||
|
||||
1. Luôn dùng APIResponse<T> hoặc ListResponse<T>
|
||||
2. Dùng unwrap() helper để code ngắn gọn
|
||||
3. Handle errors properly với do-catch
|
||||
4. Log errors trong development
|
||||
5. Validate request data trước khi gọi API
|
||||
6. Cache data khi cần (như User data)
|
||||
|
||||
❌ DON'T - Không nên làm:
|
||||
|
||||
1. Không decode trực tiếp thành Model mà không wrapper
|
||||
2. Không ignore errors
|
||||
3. Không hardcode URLs, dùng Constants
|
||||
4. Không lưu sensitive data vào UserDefaults (dùng Keychain)
|
||||
5. Không block main thread với synchronous calls
|
||||
|
||||
*/
|
||||
|
||||
// MARK: - Testing Tips
|
||||
// Tips test API
|
||||
|
||||
/*
|
||||
|
||||
Test APIs trong development:
|
||||
|
||||
1. Dùng APITestView để test từng endpoint
|
||||
2. Check Console logs với DebugLogger
|
||||
3. Verify JSON response structure
|
||||
4. Test error cases (401, 404, 500, etc.)
|
||||
5. Test với slow network / offline
|
||||
|
||||
Mock data cho UI testing:
|
||||
|
||||
#if DEBUG
|
||||
func mockFetchUser() async -> User {
|
||||
return User.sample
|
||||
}
|
||||
#endif
|
||||
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,389 @@
|
||||
//
|
||||
// APIDebugView.swift
|
||||
// AppClientBaseSwift
|
||||
//
|
||||
// Comprehensive API debugging view
|
||||
// View debug API toàn diện
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - API Debug View
|
||||
// View Debug API
|
||||
|
||||
struct APIDebugView: View {
|
||||
@State private var endpoint: String = "/users/me"
|
||||
@State private var method: HTTPMethod = .get
|
||||
@State private var requestBody: String = ""
|
||||
@State private var customHeaders: String = ""
|
||||
@State private var useAccessToken: Bool = true
|
||||
|
||||
@State private var responseStatus: String = "Chưa gửi request"
|
||||
@State private var responseJSON: String = ""
|
||||
@State private var decodingResult: String = ""
|
||||
@State private var isLoading: Bool = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Form {
|
||||
// Endpoint Section
|
||||
Section(header: Text("Request Configuration")) {
|
||||
TextField("Endpoint (vd: /users/me)", text: $endpoint)
|
||||
.autocapitalization(.none)
|
||||
|
||||
Picker("Method", selection: $method) {
|
||||
Text("GET").tag(HTTPMethod.get)
|
||||
Text("POST").tag(HTTPMethod.post)
|
||||
Text("PUT").tag(HTTPMethod.put)
|
||||
Text("DELETE").tag(HTTPMethod.delete)
|
||||
}
|
||||
|
||||
Toggle("Use Access Token", isOn: $useAccessToken)
|
||||
}
|
||||
|
||||
// Request Body Section
|
||||
if method == .post || method == .put {
|
||||
Section(header: Text("Request Body (JSON)")) {
|
||||
TextEditor(text: $requestBody)
|
||||
.frame(height: 150)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
|
||||
Button("Load Sample Body") {
|
||||
requestBody = sampleRequestBody
|
||||
}
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
// Custom Headers Section
|
||||
Section(header: Text("Custom Headers (optional)")) {
|
||||
TextEditor(text: $customHeaders)
|
||||
.frame(height: 100)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
|
||||
Text("Format: Key: Value (mỗi header trên một dòng)")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
// Action Button
|
||||
Section {
|
||||
Button(action: {
|
||||
Task {
|
||||
await sendRequest()
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
}
|
||||
Text(isLoading ? "Đang gửi..." : "Gửi Request")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
.disabled(isLoading || endpoint.isEmpty)
|
||||
|
||||
Button("Clear Results") {
|
||||
clearResults()
|
||||
}
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
|
||||
// Response Status Section
|
||||
Section(header: Text("Response Status")) {
|
||||
Text(responseStatus)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.foregroundColor(responseStatusColor)
|
||||
}
|
||||
|
||||
// Raw JSON Section
|
||||
if !responseJSON.isEmpty {
|
||||
Section(header: HStack {
|
||||
Text("Raw JSON Response")
|
||||
Spacer()
|
||||
Button("Copy") {
|
||||
UIPasteboard.general.string = responseJSON
|
||||
}
|
||||
.font(.caption)
|
||||
}) {
|
||||
ScrollView {
|
||||
Text(responseJSON)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(8)
|
||||
}
|
||||
.frame(height: 200)
|
||||
}
|
||||
}
|
||||
|
||||
// Decoding Result Section
|
||||
if !decodingResult.isEmpty {
|
||||
Section(header: Text("Decoding Analysis")) {
|
||||
ScrollView {
|
||||
Text(decodingResult)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(8)
|
||||
}
|
||||
.frame(height: 200)
|
||||
}
|
||||
}
|
||||
|
||||
// Quick Actions
|
||||
Section(header: Text("Quick Actions")) {
|
||||
Button("Test /users/me") {
|
||||
endpoint = "/users/me"
|
||||
method = .get
|
||||
useAccessToken = true
|
||||
}
|
||||
|
||||
Button("Test /auth/register") {
|
||||
endpoint = "/auth/register"
|
||||
method = .post
|
||||
useAccessToken = false
|
||||
requestBody = """
|
||||
{
|
||||
"firstName": "Test",
|
||||
"lastName": "User",
|
||||
"email": "test@example.com",
|
||||
"password": "Test123!"
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
// Debugging Tips
|
||||
Section(header: Text("Debugging Tips")) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
tipText("1. Kiểm tra Raw JSON để xem server trả về gì")
|
||||
tipText("2. Xem Decoding Analysis để biết field nào bị sai")
|
||||
tipText("3. So sánh tên field trong JSON với model")
|
||||
tipText("4. Kiểm tra có wrapper {success, data} không")
|
||||
tipText("5. Verify snake_case/camelCase conversion")
|
||||
}
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
.navigationTitle("API Debugger")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private var responseStatusColor: Color {
|
||||
if responseStatus.contains("✅") || responseStatus.contains("200") {
|
||||
return .green
|
||||
} else if responseStatus.contains("❌") || responseStatus.contains("Error") {
|
||||
return .red
|
||||
} else {
|
||||
return .primary
|
||||
}
|
||||
}
|
||||
|
||||
private func tipText(_ text: String) -> some View {
|
||||
HStack(alignment: .top, spacing: 4) {
|
||||
Text("💡")
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
|
||||
private var sampleRequestBody: String {
|
||||
"""
|
||||
{
|
||||
"name": "Sample Value",
|
||||
"email": "test@example.com"
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
private func clearResults() {
|
||||
responseStatus = "Đã xóa kết quả"
|
||||
responseJSON = ""
|
||||
decodingResult = ""
|
||||
}
|
||||
|
||||
// MARK: - Network Request
|
||||
|
||||
private func sendRequest() async {
|
||||
isLoading = true
|
||||
responseStatus = "🔄 Đang gửi request..."
|
||||
responseJSON = ""
|
||||
decodingResult = ""
|
||||
|
||||
do {
|
||||
// Build URL
|
||||
guard let url = URL(string: APIConfig.baseURL + APIConfig.apiVersion + endpoint) else {
|
||||
responseStatus = "❌ Invalid URL"
|
||||
isLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
// Create request
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = method.rawValue
|
||||
request.timeoutInterval = APIConfig.timeout
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
|
||||
// Add access token if needed
|
||||
if useAccessToken, let token = AuthManager.shared.accessToken {
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
|
||||
// Parse and add custom headers
|
||||
let headerLines = customHeaders.components(separatedBy: "\n")
|
||||
for line in headerLines {
|
||||
let parts = line.components(separatedBy: ":")
|
||||
if parts.count == 2 {
|
||||
let key = parts[0].trimmingCharacters(in: .whitespaces)
|
||||
let value = parts[1].trimmingCharacters(in: .whitespaces)
|
||||
request.setValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
}
|
||||
|
||||
// Add request body
|
||||
if !requestBody.isEmpty, let bodyData = requestBody.data(using: .utf8) {
|
||||
request.httpBody = bodyData
|
||||
}
|
||||
|
||||
// Send request
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
responseStatus = "❌ Invalid response"
|
||||
isLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
// Store raw JSON
|
||||
if let jsonString = String(data: data, encoding: .utf8) {
|
||||
responseJSON = jsonString.prettyPrintedJSON ?? jsonString
|
||||
}
|
||||
|
||||
// Update status
|
||||
let statusEmoji = httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 ? "✅" : "❌"
|
||||
responseStatus = """
|
||||
\(statusEmoji) Status Code: \(httpResponse.statusCode)
|
||||
URL: \(url.absoluteString)
|
||||
Method: \(method.rawValue)
|
||||
"""
|
||||
|
||||
// Analyze decoding
|
||||
if httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 {
|
||||
analyzeResponse(data)
|
||||
}
|
||||
|
||||
} catch {
|
||||
responseStatus = "❌ Error: \(error.localizedDescription)"
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
private func analyzeResponse(_ data: Data) {
|
||||
var analysis = "🔍 Response Analysis:\n\n"
|
||||
|
||||
// Try to parse as JSON
|
||||
guard let jsonObject = try? JSONSerialization.jsonObject(with: data) else {
|
||||
analysis += "❌ Cannot parse as JSON"
|
||||
decodingResult = analysis
|
||||
return
|
||||
}
|
||||
|
||||
// Check if it's a dictionary
|
||||
if let dict = jsonObject as? [String: Any] {
|
||||
analysis += "✅ Response is a Dictionary\n"
|
||||
analysis += "📊 Keys: \(dict.keys.count)\n\n"
|
||||
|
||||
// List all keys with types
|
||||
analysis += "🔑 Available Fields:\n"
|
||||
for (key, value) in dict.sorted(by: { $0.key < $1.key }) {
|
||||
let valueType = type(of: value)
|
||||
let preview = previewValue(value)
|
||||
analysis += " • \(key): \(valueType)\(preview)\n"
|
||||
}
|
||||
|
||||
// Check for wrapper format
|
||||
let hasSuccess = dict["success"] != nil
|
||||
let hasData = dict["data"] != nil
|
||||
let hasError = dict["error"] != nil
|
||||
|
||||
analysis += "\n📦 Wrapper Detection:\n"
|
||||
analysis += " • Has 'success': \(hasSuccess ? "✅" : "❌")\n"
|
||||
analysis += " • Has 'data': \(hasData ? "✅" : "❌")\n"
|
||||
analysis += " • Has 'error': \(hasError ? "✅" : "❌")\n"
|
||||
|
||||
if hasData, let dataValue = dict["data"] {
|
||||
analysis += "\n📊 Data Content:\n"
|
||||
if let dataDict = dataValue as? [String: Any] {
|
||||
analysis += " Type: Dictionary\n"
|
||||
analysis += " Keys: \(dataDict.keys.joined(separator: ", "))\n"
|
||||
} else if let dataArray = dataValue as? [Any] {
|
||||
analysis += " Type: Array\n"
|
||||
analysis += " Count: \(dataArray.count)\n"
|
||||
} else {
|
||||
analysis += " Type: \(type(of: dataValue))\n"
|
||||
}
|
||||
}
|
||||
|
||||
// Suggestions for User model
|
||||
if endpoint.contains("/users") {
|
||||
analysis += "\n💡 User Model Mapping:\n"
|
||||
let userFields = ["id", "email", "name", "firstName", "lastName",
|
||||
"avatarUrl", "phoneNumber", "isEmailVerified",
|
||||
"emailConfirmed", "createdAt", "updatedAt", "roles"]
|
||||
|
||||
for field in userFields {
|
||||
if dict[field] != nil {
|
||||
analysis += " ✅ \(field)\n"
|
||||
} else {
|
||||
// Try snake_case version
|
||||
let snakeCase = field.toSnakeCase()
|
||||
if dict[snakeCase] != nil {
|
||||
analysis += " ⚠️ \(field) → found as '\(snakeCase)'\n"
|
||||
} else {
|
||||
analysis += " ❌ \(field) → not found\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if let array = jsonObject as? [[String: Any]] {
|
||||
analysis += "✅ Response is an Array\n"
|
||||
analysis += "📊 Count: \(array.count)\n"
|
||||
if let firstItem = array.first {
|
||||
analysis += "🔑 First Item Keys:\n"
|
||||
for key in firstItem.keys.sorted() {
|
||||
analysis += " • \(key)\n"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
analysis += "⚠️ Unknown response format\n"
|
||||
}
|
||||
|
||||
decodingResult = analysis
|
||||
}
|
||||
|
||||
private func previewValue(_ value: Any) -> String {
|
||||
switch value {
|
||||
case let string as String:
|
||||
let preview = string.prefix(30)
|
||||
return preview.count < string.count ? " = \"\(preview)...\"" : " = \"\(preview)\""
|
||||
case let number as NSNumber:
|
||||
return " = \(number)"
|
||||
case let bool as Bool:
|
||||
return " = \(bool)"
|
||||
case is NSNull:
|
||||
return " = null"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview {
|
||||
APIDebugView()
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
//
|
||||
// APIResponseDebugger.swift
|
||||
// AppClientBaseSwift
|
||||
//
|
||||
// Helper for debugging API response issues
|
||||
// Helper để debug các vấn đề API response
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - API Response Debugger
|
||||
// Debugger cho API Response
|
||||
|
||||
/// Helper class for debugging API responses
|
||||
/// Class helper để debug API responses
|
||||
struct APIResponseDebugger {
|
||||
|
||||
/// Print detailed information about raw JSON data
|
||||
/// In thông tin chi tiết về raw JSON data
|
||||
/// - Parameters:
|
||||
/// - data: Raw JSON data / Dữ liệu JSON thô
|
||||
/// - expectedType: Expected type name / Tên type mong đợi
|
||||
static func debugJSONData(_ data: Data, expectedType: String) {
|
||||
#if DEBUG
|
||||
print("\n" + String(repeating: "=", count: 60))
|
||||
print("🔍 API RESPONSE DEBUG")
|
||||
print(String(repeating: "=", count: 60))
|
||||
|
||||
// Print raw JSON
|
||||
if let jsonString = String(data: data, encoding: .utf8) {
|
||||
print("\n📄 Raw JSON Response:")
|
||||
print(jsonString.prettyPrintedJSON ?? jsonString)
|
||||
}
|
||||
|
||||
// Try to parse as generic JSON object
|
||||
if let jsonObject = try? JSONSerialization.jsonObject(with: data),
|
||||
let jsonData = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted),
|
||||
let prettyJSON = String(data: jsonData, encoding: .utf8) {
|
||||
|
||||
print("\n🎨 Pretty Printed JSON:")
|
||||
print(prettyJSON)
|
||||
|
||||
// Analyze structure
|
||||
if let dict = jsonObject as? [String: Any] {
|
||||
print("\n🔑 Available Keys:")
|
||||
for (key, value) in dict {
|
||||
let valueType = type(of: value)
|
||||
print(" - \(key): \(valueType)")
|
||||
}
|
||||
|
||||
// Check for wrapper format
|
||||
let hasSuccess = dict["success"] != nil
|
||||
let hasData = dict["data"] != nil
|
||||
let hasError = dict["error"] != nil
|
||||
|
||||
if hasSuccess || hasData {
|
||||
print("\n📦 Detected Wrapper Format:")
|
||||
print(" - Has 'success': \(hasSuccess)")
|
||||
print(" - Has 'data': \(hasData)")
|
||||
print(" - Has 'error': \(hasError)")
|
||||
|
||||
if hasData, let dataValue = dict["data"] {
|
||||
print("\n📊 Data Type: \(type(of: dataValue))")
|
||||
if let dataDict = dataValue as? [String: Any] {
|
||||
print(" Data Keys: \(dataDict.keys.joined(separator: ", "))")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print("\n⚠️ No wrapper detected - direct object response")
|
||||
}
|
||||
} else if let array = jsonObject as? [[String: Any]] {
|
||||
print("\n📊 Array Response with \(array.count) items")
|
||||
if let firstItem = array.first {
|
||||
print(" First item keys: \(firstItem.keys.joined(separator: ", "))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print("\n🎯 Expected Type: \(expectedType)")
|
||||
print(String(repeating: "=", count: 60) + "\n")
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Compare expected model fields with actual JSON keys
|
||||
/// So sánh các field mong đợi của model với key thực tế trong JSON
|
||||
/// - Parameters:
|
||||
/// - data: Raw JSON data / Dữ liệu JSON thô
|
||||
/// - expectedKeys: Expected keys / Các key mong đợi
|
||||
static func compareFields(data: Data, expectedKeys: [String]) {
|
||||
#if DEBUG
|
||||
guard let jsonObject = try? JSONSerialization.jsonObject(with: data),
|
||||
let dict = jsonObject as? [String: Any] else {
|
||||
print("❌ Cannot parse JSON as dictionary")
|
||||
return
|
||||
}
|
||||
|
||||
print("\n🔍 Field Comparison:")
|
||||
print("Expected vs Actual\n")
|
||||
|
||||
let actualKeys = Set(dict.keys)
|
||||
let expectedSet = Set(expectedKeys)
|
||||
|
||||
// Missing keys
|
||||
let missing = expectedSet.subtracting(actualKeys)
|
||||
if !missing.isEmpty {
|
||||
print("❌ Missing Keys (in model but not in JSON):")
|
||||
missing.forEach { print(" - \($0)") }
|
||||
}
|
||||
|
||||
// Extra keys
|
||||
let extra = actualKeys.subtracting(expectedSet)
|
||||
if !extra.isEmpty {
|
||||
print("\n➕ Extra Keys (in JSON but not in model):")
|
||||
extra.forEach { print(" - \($0)") }
|
||||
}
|
||||
|
||||
// Matching keys
|
||||
let matching = actualKeys.intersection(expectedSet)
|
||||
if !matching.isEmpty {
|
||||
print("\n✅ Matching Keys:")
|
||||
matching.forEach { print(" - \($0)") }
|
||||
}
|
||||
|
||||
// Suggest CodingKeys mapping
|
||||
if !extra.isEmpty || !missing.isEmpty {
|
||||
print("\n💡 Suggestion: Add custom CodingKeys mapping:")
|
||||
print("enum CodingKeys: String, CodingKey {")
|
||||
for key in expectedKeys {
|
||||
if missing.contains(key) {
|
||||
// Try to find similar key in actual
|
||||
let snakeCase = key.toSnakeCase()
|
||||
if actualKeys.contains(snakeCase) {
|
||||
print(" case \(key) = \"\(snakeCase)\"")
|
||||
} else {
|
||||
print(" case \(key) // ⚠️ Not found in response")
|
||||
}
|
||||
} else {
|
||||
print(" case \(key)")
|
||||
}
|
||||
}
|
||||
print("}")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Generate Swift model code from JSON data
|
||||
/// Tạo code Swift model từ JSON data
|
||||
/// - Parameters:
|
||||
/// - data: Raw JSON data / Dữ liệu JSON thô
|
||||
/// - modelName: Name for the model / Tên cho model
|
||||
static func generateModelCode(from data: Data, modelName: String = "GeneratedModel") {
|
||||
#if DEBUG
|
||||
guard let jsonObject = try? JSONSerialization.jsonObject(with: data),
|
||||
let dict = jsonObject as? [String: Any] else {
|
||||
print("❌ Cannot parse JSON as dictionary")
|
||||
return
|
||||
}
|
||||
|
||||
print("\n🏗️ Generated Model Code:\n")
|
||||
print("struct \(modelName): Codable {")
|
||||
|
||||
for (key, value) in dict.sorted(by: { $0.key < $1.key }) {
|
||||
let camelKey = key.toCamelCase()
|
||||
let swiftType = swiftType(for: value)
|
||||
let isOptional = value is NSNull
|
||||
|
||||
print(" let \(camelKey): \(swiftType)\(isOptional ? "?" : "")")
|
||||
|
||||
if key != camelKey {
|
||||
print(" // API key: \"\(key)\"")
|
||||
}
|
||||
}
|
||||
|
||||
print("}")
|
||||
print("\n")
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Determine Swift type from JSON value
|
||||
/// Xác định Swift type từ giá trị JSON
|
||||
private static func swiftType(for value: Any) -> String {
|
||||
switch value {
|
||||
case is String:
|
||||
return "String"
|
||||
case is Int:
|
||||
return "Int"
|
||||
case is Double, is Float:
|
||||
return "Double"
|
||||
case is Bool:
|
||||
return "Bool"
|
||||
case is [Any]:
|
||||
return "[Any]" // Could be more specific
|
||||
case is [String: Any]:
|
||||
return "[String: Any]" // Could be more specific
|
||||
case is NSNull:
|
||||
return "String" // Default to String for null values
|
||||
default:
|
||||
return "Any"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - String Extensions for Case Conversion
|
||||
// Extensions String cho chuyển đổi case
|
||||
|
||||
extension String {
|
||||
/// Convert string to snake_case
|
||||
/// Chuyển string sang snake_case
|
||||
func toSnakeCase() -> String {
|
||||
let pattern = "([a-z0-9])([A-Z])"
|
||||
|
||||
let regex = try? NSRegularExpression(pattern: pattern, options: [])
|
||||
let range = NSRange(location: 0, length: count)
|
||||
let modString = regex?.stringByReplacingMatches(
|
||||
in: self,
|
||||
options: [],
|
||||
range: range,
|
||||
withTemplate: "$1_$2"
|
||||
)
|
||||
|
||||
return (modString ?? self).lowercased()
|
||||
}
|
||||
|
||||
/// Convert string to camelCase
|
||||
/// Chuyển string sang camelCase
|
||||
func toCamelCase() -> String {
|
||||
let components = self.components(separatedBy: "_")
|
||||
guard components.count > 1 else { return self }
|
||||
|
||||
let first = components[0].lowercased()
|
||||
let rest = components.dropFirst().map { $0.capitalized }.joined()
|
||||
|
||||
return first + rest
|
||||
}
|
||||
|
||||
/// Pretty print JSON string
|
||||
/// In đẹp JSON string
|
||||
var prettyPrintedJSON: String? {
|
||||
guard let data = self.data(using: .utf8),
|
||||
let jsonObject = try? JSONSerialization.jsonObject(with: data),
|
||||
let prettyData = try? JSONSerialization.data(
|
||||
withJSONObject: jsonObject,
|
||||
options: .prettyPrinted
|
||||
),
|
||||
let prettyString = String(data: prettyData, encoding: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
return prettyString
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Example Usage
|
||||
// Ví dụ sử dụng
|
||||
|
||||
#if DEBUG
|
||||
extension APIResponseDebugger {
|
||||
/// Example of how to use the debugger
|
||||
/// Ví dụ cách sử dụng debugger
|
||||
static func exampleUsage() {
|
||||
let sampleJSON = """
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "123",
|
||||
"email": "test@example.com",
|
||||
"first_name": "John",
|
||||
"last_name": "Doe",
|
||||
"email_confirmed": true,
|
||||
"created_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
guard let data = sampleJSON.data(using: .utf8) else { return }
|
||||
|
||||
// Debug full response
|
||||
debugJSONData(data, expectedType: "APIResponse<User>")
|
||||
|
||||
// Compare fields
|
||||
let expectedUserFields = [
|
||||
"id", "email", "name", "firstName", "lastName",
|
||||
"avatarUrl", "phoneNumber", "isEmailVerified",
|
||||
"createdAt", "updatedAt", "roles"
|
||||
]
|
||||
compareFields(data: data, expectedKeys: expectedUserFields)
|
||||
|
||||
// Generate model
|
||||
generateModelCode(from: data, modelName: "User")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,94 @@
|
||||
//
|
||||
// SimulatorWarningsSuppressor.swift
|
||||
// AppClientBaseSwift
|
||||
//
|
||||
// Suppress common iOS Simulator warnings
|
||||
// Tắt các warnings phổ biến của iOS Simulator
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
// MARK: - Simulator Warnings Suppressor
|
||||
// Suppressor cho Simulator Warnings
|
||||
|
||||
/// Helper to suppress simulator-specific warnings
|
||||
/// Helper để tắt warnings đặc thù của simulator
|
||||
struct SimulatorWarningsSuppressor {
|
||||
|
||||
/// Suppress common iOS Simulator warnings
|
||||
/// Tắt các warnings phổ biến của iOS Simulator
|
||||
static func suppressSimulatorWarnings() {
|
||||
#if DEBUG && targetEnvironment(simulator)
|
||||
|
||||
// Suppress Auto Layout constraint warnings
|
||||
// Tắt warnings về Auto Layout constraints
|
||||
UserDefaults.standard.setValue(false, forKey: "_UIConstraintBasedLayoutLogUnsatisfiable")
|
||||
|
||||
// Note: Haptic and keyboard warnings cannot be suppressed programmatically
|
||||
// These are internal iOS system logs
|
||||
// Lưu ý: Haptic và keyboard warnings không thể tắt bằng code
|
||||
// Đây là system logs nội bộ của iOS
|
||||
|
||||
print("ℹ️ Simulator warnings suppression enabled")
|
||||
print(" - Auto Layout constraint warnings: suppressed")
|
||||
print(" - Haptic feedback warnings: cannot suppress (simulator limitation)")
|
||||
print(" - Keyboard warnings: cannot suppress (system logs)")
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Check if running on simulator
|
||||
/// Kiểm tra có đang chạy trên simulator không
|
||||
static var isSimulator: Bool {
|
||||
#if targetEnvironment(simulator)
|
||||
return true
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Print simulator-friendly message about known issues
|
||||
/// In thông báo về các issues đã biết trên simulator
|
||||
static func printSimulatorInfo() {
|
||||
#if DEBUG && targetEnvironment(simulator)
|
||||
print("""
|
||||
|
||||
⚠️ Running on iOS Simulator - Known Issues:
|
||||
|
||||
1. Haptic Feedback Warnings:
|
||||
- hapticpatternlibrary.plist not found
|
||||
- This is normal on simulator
|
||||
- Will work fine on real devices
|
||||
|
||||
2. Auto Layout Warnings:
|
||||
- Keyboard constraint conflicts
|
||||
- iOS automatically resolves these
|
||||
- No visual impact on app
|
||||
|
||||
3. Keyboard Cache Warnings:
|
||||
- Candidate multiplexer warnings
|
||||
- Simulator-specific issue
|
||||
- Does not affect functionality
|
||||
|
||||
💡 These warnings do NOT indicate bugs in your code.
|
||||
|
||||
""")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - App Delegate Extension
|
||||
// Extension cho App Delegate
|
||||
|
||||
#if DEBUG && targetEnvironment(simulator)
|
||||
extension SimulatorWarningsSuppressor {
|
||||
|
||||
/// Call this in AppDelegate or App init
|
||||
/// Gọi hàm này trong AppDelegate hoặc App init
|
||||
static func configure() {
|
||||
suppressSimulatorWarnings()
|
||||
printSimulatorInfo()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
173
apps/app-client-base-swift/AppClientBaseSwift/DebugLogger.swift
Normal file
173
apps/app-client-base-swift/AppClientBaseSwift/DebugLogger.swift
Normal file
@@ -0,0 +1,173 @@
|
||||
//
|
||||
// DebugLogger.swift
|
||||
// AppClientBaseSwift
|
||||
//
|
||||
// Debug logging utilities
|
||||
// Tiện ích ghi log debug
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Debug Logger
|
||||
// Logger Debug
|
||||
|
||||
/// Debug logging utility
|
||||
/// Tiện ích ghi log debug
|
||||
enum DebugLogger {
|
||||
|
||||
/// Log level
|
||||
/// Mức độ log
|
||||
enum Level: String {
|
||||
case verbose = "📝"
|
||||
case info = "ℹ️"
|
||||
case warning = "⚠️"
|
||||
case error = "❌"
|
||||
case success = "✅"
|
||||
}
|
||||
|
||||
/// Log message with level
|
||||
/// Ghi log message với level
|
||||
/// - Parameters:
|
||||
/// - level: Log level / Mức độ log
|
||||
/// - message: Log message / Thông điệp log
|
||||
/// - file: Source file / File nguồn
|
||||
/// - function: Function name / Tên function
|
||||
/// - line: Line number / Số dòng
|
||||
static func log(
|
||||
_ level: Level,
|
||||
_ message: String,
|
||||
file: String = #file,
|
||||
function: String = #function,
|
||||
line: Int = #line
|
||||
) {
|
||||
#if DEBUG
|
||||
let fileName = (file as NSString).lastPathComponent
|
||||
let timestamp = DateFormatter.localizedString(from: Date(), dateStyle: .none, timeStyle: .medium)
|
||||
print("\(level.rawValue) [\(timestamp)] \(fileName):\(line) - \(function)")
|
||||
print(" \(message)")
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Log verbose message
|
||||
/// Ghi log verbose
|
||||
static func verbose(_ message: String, file: String = #file, function: String = #function, line: Int = #line) {
|
||||
log(.verbose, message, file: file, function: function, line: line)
|
||||
}
|
||||
|
||||
/// Log info message
|
||||
/// Ghi log info
|
||||
static func info(_ message: String, file: String = #file, function: String = #function, line: Int = #line) {
|
||||
log(.info, message, file: file, function: function, line: line)
|
||||
}
|
||||
|
||||
/// Log warning message
|
||||
/// Ghi log warning
|
||||
static func warning(_ message: String, file: String = #file, function: String = #function, line: Int = #line) {
|
||||
log(.warning, message, file: file, function: function, line: line)
|
||||
}
|
||||
|
||||
/// Log error message
|
||||
/// Ghi log error
|
||||
static func error(_ message: String, file: String = #file, function: String = #function, line: Int = #line) {
|
||||
log(.error, message, file: file, function: function, line: line)
|
||||
}
|
||||
|
||||
/// Log success message
|
||||
/// Ghi log success
|
||||
static func success(_ message: String, file: String = #file, function: String = #function, line: Int = #line) {
|
||||
log(.success, message, file: file, function: function, line: line)
|
||||
}
|
||||
|
||||
/// Log JSON data
|
||||
/// Ghi log dữ liệu JSON
|
||||
/// - Parameters:
|
||||
/// - data: JSON data / Dữ liệu JSON
|
||||
/// - title: Optional title / Tiêu đề tùy chọn
|
||||
static func logJSON(_ data: Data, title: String = "JSON Response") {
|
||||
#if DEBUG
|
||||
guard let jsonString = String(data: data, encoding: .utf8) else {
|
||||
error("Unable to convert data to string")
|
||||
return
|
||||
}
|
||||
|
||||
// Try to pretty print JSON
|
||||
// Thử format JSON đẹp
|
||||
if let jsonData = jsonString.data(using: .utf8),
|
||||
let jsonObject = try? JSONSerialization.jsonObject(with: jsonData),
|
||||
let prettyData = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted),
|
||||
let prettyString = String(data: prettyData, encoding: .utf8) {
|
||||
print("📄 \(title):")
|
||||
print(prettyString)
|
||||
} else {
|
||||
print("📄 \(title):")
|
||||
print(jsonString)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Log API request
|
||||
/// Ghi log API request
|
||||
/// - Parameters:
|
||||
/// - method: HTTP method / Phương thức HTTP
|
||||
/// - url: Request URL / URL request
|
||||
/// - headers: Request headers / Headers request
|
||||
/// - body: Request body / Body request
|
||||
static func logRequest(
|
||||
method: String,
|
||||
url: String,
|
||||
headers: [String: String]? = nil,
|
||||
body: Data? = nil
|
||||
) {
|
||||
#if DEBUG
|
||||
print("🚀 API Request:")
|
||||
print(" Method: \(method)")
|
||||
print(" URL: \(url)")
|
||||
|
||||
if let headers = headers, !headers.isEmpty {
|
||||
print(" Headers:")
|
||||
headers.forEach { key, value in
|
||||
// Hide sensitive data
|
||||
// Ẩn dữ liệu nhạy cảm
|
||||
if key.lowercased() == "authorization" {
|
||||
print(" \(key): Bearer ***")
|
||||
} else {
|
||||
print(" \(key): \(value)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let body = body {
|
||||
print(" Body:")
|
||||
logJSON(body, title: "Request Body")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Log API response
|
||||
/// Ghi log API response
|
||||
/// - Parameters:
|
||||
/// - statusCode: HTTP status code / Mã trạng thái HTTP
|
||||
/// - data: Response data / Dữ liệu response
|
||||
/// - error: Error if any / Lỗi nếu có
|
||||
static func logResponse(
|
||||
statusCode: Int,
|
||||
data: Data?,
|
||||
error: Error? = nil
|
||||
) {
|
||||
#if DEBUG
|
||||
if let error = error {
|
||||
print("❌ API Response Error:")
|
||||
print(" Status Code: \(statusCode)")
|
||||
print(" Error: \(error.localizedDescription)")
|
||||
} else {
|
||||
let emoji = statusCode >= 200 && statusCode < 300 ? "✅" : "⚠️"
|
||||
print("\(emoji) API Response:")
|
||||
print(" Status Code: \(statusCode)")
|
||||
}
|
||||
|
||||
if let data = data {
|
||||
logJSON(data, title: "Response Data")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user