From b88db1385248ccd8fda93fbb3cbf6bdba759944f Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Wed, 20 Mar 2024 16:19:33 +0200 Subject: [PATCH 01/22] Update StatsTagsAndCategoriesInsight parsing to have a more flexible parsing to avoid unnecessary failures --- WordPressKit/Insights/StatsTagsAndCategoriesInsight.swift | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/WordPressKit/Insights/StatsTagsAndCategoriesInsight.swift b/WordPressKit/Insights/StatsTagsAndCategoriesInsight.swift index b283a18cc..5725fb581 100644 --- a/WordPressKit/Insights/StatsTagsAndCategoriesInsight.swift +++ b/WordPressKit/Insights/StatsTagsAndCategoriesInsight.swift @@ -12,13 +12,7 @@ extension StatsTagsAndCategoriesInsight: StatsInsightData { } public init?(jsonDictionary: [String: AnyObject]) { - guard - let outerTags = jsonDictionary["tags"] as? [[String: AnyObject]] - // The shape of the API response here leaves... something to be desired. - else { - return nil - } - + let outerTags = jsonDictionary["tags"] as? [[String: AnyObject]] ?? [] let tags = outerTags.compactMap { StatsTagAndCategory(tagsGroup: $0)} self.topTagsAndCategories = tags From 1bd23f409d15464d922be599e4d467cd81b57083 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Wed, 20 Mar 2024 22:27:53 +0200 Subject: [PATCH 02/22] Update StatsAnnualAndMostPopularTimeInsight to use correct data format and Codable --- ...StatsAnnualAndMostPopularTimeInsight.swift | 130 +++++++----------- 1 file changed, 53 insertions(+), 77 deletions(-) diff --git a/WordPressKit/Insights/StatsAnnualAndMostPopularTimeInsight.swift b/WordPressKit/Insights/StatsAnnualAndMostPopularTimeInsight.swift index 062986616..1983cdc09 100644 --- a/WordPressKit/Insights/StatsAnnualAndMostPopularTimeInsight.swift +++ b/WordPressKit/Insights/StatsAnnualAndMostPopularTimeInsight.swift @@ -1,5 +1,4 @@ -public struct StatsAnnualAndMostPopularTimeInsight { - +public struct StatsAnnualAndMostPopularTimeInsight: Codable { /// - A `DateComponents` object with one field populated: `weekday`. public let mostPopularDayOfWeek: DateComponents public let mostPopularDayOfWeekPercentage: Int @@ -7,77 +6,68 @@ public struct StatsAnnualAndMostPopularTimeInsight { /// - A `DateComponents` object with one field populated: `hour`. public let mostPopularHour: DateComponents public let mostPopularHourPercentage: Int + public let years: [Year]? + + enum CodingKeys: String, CodingKey { + case mostPopularHour = "highest_hour" + case mostPopularHourPercentage = "highest_hour_percent" + case mostPopularDayOfWeek = "highest_day_of_week" + case mostPopularDayOfWeekPercentage = "highest_day_percent" + case years + } - public let annualInsightsYear: Int - - public let annualInsightsTotalPostsCount: Int - public let annualInsightsTotalWordsCount: Int - public let annualInsightsAverageWordsCount: Double - - public let annualInsightsTotalLikesCount: Int - public let annualInsightsAverageLikesCount: Double - - public let annualInsightsTotalCommentsCount: Int - public let annualInsightsAverageCommentsCount: Double - - public let annualInsightsTotalImagesCount: Int - public let annualInsightsAverageImagesCount: Double - - public init(mostPopularDayOfWeek: DateComponents, - mostPopularDayOfWeekPercentage: Int, - mostPopularHour: DateComponents, - mostPopularHourPercentage: Int, - annualInsightsYear: Int, - annualInsightsTotalPostsCount: Int, - annualInsightsTotalWordsCount: Int, - annualInsightsAverageWordsCount: Double, - annualInsightsTotalLikesCount: Int, - annualInsightsAverageLikesCount: Double, - annualInsightsTotalCommentsCount: Int, - annualInsightsAverageCommentsCount: Double, - annualInsightsTotalImagesCount: Int, - annualInsightsAverageImagesCount: Double) { - self.mostPopularDayOfWeek = mostPopularDayOfWeek - self.mostPopularDayOfWeekPercentage = mostPopularDayOfWeekPercentage - - self.mostPopularHour = mostPopularHour - self.mostPopularHourPercentage = mostPopularHourPercentage - - self.annualInsightsYear = annualInsightsYear - - self.annualInsightsTotalPostsCount = annualInsightsTotalPostsCount - self.annualInsightsTotalWordsCount = annualInsightsTotalWordsCount - self.annualInsightsAverageWordsCount = annualInsightsAverageWordsCount - - self.annualInsightsTotalLikesCount = annualInsightsTotalLikesCount - self.annualInsightsAverageLikesCount = annualInsightsAverageLikesCount - - self.annualInsightsTotalCommentsCount = annualInsightsTotalCommentsCount - self.annualInsightsAverageCommentsCount = annualInsightsAverageCommentsCount - - self.annualInsightsTotalImagesCount = annualInsightsTotalImagesCount - self.annualInsightsAverageImagesCount = annualInsightsAverageImagesCount + public struct Year: Codable { + public let year: String + public let totalPosts: Int + public let totalWords: Int + public let averageWords: Double + public let totalLikes: Int + public let averageLikes: Double + public let totalComments: Int + public let averageComments: Double + public let totalImages: Int + public let averageImages: Double + + enum CodingKeys: String, CodingKey { + case year + case totalPosts = "total_posts" + case totalWords = "total_words" + case averageWords = "avg_words" + case totalLikes = "total_likes" + case averageLikes = "avg_likes" + case totalComments = "total_comments" + case averageComments = "avg_comments" + case totalImages = "total_images" + case averageImages = "avg_images" + } } } + extension StatsAnnualAndMostPopularTimeInsight: StatsInsightData { public static var pathComponent: String { return "stats/insights" } public init?(jsonDictionary: [String: AnyObject]) { - guard - let highestHour = jsonDictionary["highest_hour"] as? Int, - let highestHourPercentageValue = jsonDictionary["highest_hour_percent"] as? Double, - let highestDayOfWeek = jsonDictionary["highest_day_of_week"] as? Int, - let highestDayOfWeekPercentageValue = jsonDictionary["highest_day_percent"] as? Double, - let yearlyInsights = jsonDictionary["years"] as? [[String: AnyObject]], - let latestYearlyInsight = yearlyInsights.last, - let yearString = latestYearlyInsight["year"] as? String, - let currentYear = Int(yearString) - else { - return nil + do { + let jsonData = try JSONSerialization.data(withJSONObject: jsonDictionary, options: []) + let decoder = JSONDecoder() + self = try decoder.decode(StatsAnnualAndMostPopularTimeInsight.self, from: jsonData) + } catch { + return nil } + } +} + +extension StatsAnnualAndMostPopularTimeInsight { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let years = try container.decodeIfPresent([Year].self, forKey: .years) + let highestHour = try container.decode(Int.self, forKey: .mostPopularHour) + let highestHourPercentageValue = try container.decode(Double.self, forKey: .mostPopularHourPercentage) + let highestDayOfWeek = try container.decode(Int.self, forKey: .mostPopularDayOfWeek) + let highestDayOfWeekPercentageValue = try container.decode(Double.self, forKey: .mostPopularDayOfWeekPercentage) let mappedWeekday: ((Int) -> Int) = { // iOS Calendar system is `1-based` and uses Sunday as the first day of the week. @@ -93,20 +83,6 @@ extension StatsAnnualAndMostPopularTimeInsight: StatsInsightData { self.mostPopularDayOfWeekPercentage = Int(highestDayOfWeekPercentageValue.rounded()) self.mostPopularHour = hourComponents self.mostPopularHourPercentage = Int(highestHourPercentageValue.rounded()) - - self.annualInsightsYear = currentYear - - self.annualInsightsTotalPostsCount = latestYearlyInsight["total_posts"] as? Int ?? 0 - self.annualInsightsTotalWordsCount = latestYearlyInsight["total_words"] as? Int ?? 0 - self.annualInsightsAverageWordsCount = latestYearlyInsight["avg_words"] as? Double ?? 0 - - self.annualInsightsTotalLikesCount = latestYearlyInsight["total_likes"] as? Int ?? 0 - self.annualInsightsAverageLikesCount = latestYearlyInsight["avg_likes"] as? Double ?? 0 - - self.annualInsightsTotalCommentsCount = latestYearlyInsight["total_comments"] as? Int ?? 0 - self.annualInsightsAverageCommentsCount = latestYearlyInsight["avg_comments"] as? Double ?? 0 - - self.annualInsightsTotalImagesCount = latestYearlyInsight["total_images"] as? Int ?? 0 - self.annualInsightsAverageImagesCount = latestYearlyInsight["avg_images"] as? Double ?? 0 + self.years = years } } From 23e97a4c2fd7716fcbaeee11835c5a9d4c4713f1 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Thu, 21 Mar 2024 10:59:07 +0200 Subject: [PATCH 03/22] Update StatsTagsAndCategoriesInsight to use Codable --- .../StatsTagsAndCategoriesInsight.swift | 89 +++++++++---------- 1 file changed, 44 insertions(+), 45 deletions(-) diff --git a/WordPressKit/Insights/StatsTagsAndCategoriesInsight.swift b/WordPressKit/Insights/StatsTagsAndCategoriesInsight.swift index 5725fb581..882883d2a 100644 --- a/WordPressKit/Insights/StatsTagsAndCategoriesInsight.swift +++ b/WordPressKit/Insights/StatsTagsAndCategoriesInsight.swift @@ -1,8 +1,8 @@ -public struct StatsTagsAndCategoriesInsight { +public struct StatsTagsAndCategoriesInsight: Codable { public let topTagsAndCategories: [StatsTagAndCategory] - public init(topTagsAndCategories: [StatsTagAndCategory]) { - self.topTagsAndCategories = topTagsAndCategories + private enum CodingKeys: String, CodingKey { + case topTagsAndCategories = "tags" } } @@ -12,16 +12,18 @@ extension StatsTagsAndCategoriesInsight: StatsInsightData { } public init?(jsonDictionary: [String: AnyObject]) { - let outerTags = jsonDictionary["tags"] as? [[String: AnyObject]] ?? [] - let tags = outerTags.compactMap { StatsTagAndCategory(tagsGroup: $0)} - - self.topTagsAndCategories = tags + do { + let jsonData = try JSONSerialization.data(withJSONObject: jsonDictionary, options: []) + let decoder = JSONDecoder() + self = try decoder.decode(StatsTagsAndCategoriesInsight.self, from: jsonData) + } catch { + return nil + } } } -public struct StatsTagAndCategory { - - public enum Kind { +public struct StatsTagAndCategory: Codable { + public enum Kind: String, Codable { case tag case category case folder @@ -33,6 +35,14 @@ public struct StatsTagAndCategory { public let viewsCount: Int? public let children: [StatsTagAndCategory] + private enum CodingKeys: String, CodingKey { + case name + case kind = "type" + case url = "link" + case viewsCount = "views" + case children = "tags" + } + public init(name: String, kind: Kind, url: URL?, viewsCount: Int?, children: [StatsTagAndCategory]) { self.name = name self.kind = kind @@ -40,57 +50,46 @@ public struct StatsTagAndCategory { self.viewsCount = viewsCount self.children = children } - } extension StatsTagAndCategory { - init?(tagsGroup: [String: AnyObject]) { - guard - let innerTags = tagsGroup["tags"] as? [[String: AnyObject]] - else { - return nil - } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let innerTags = try container.decodeIfPresent([StatsTagAndCategory].self, forKey: .children) ?? [] + let viewsCount = try container.decodeIfPresent(Int.self, forKey: .viewsCount) // This gets kinda complicated. The API collects some tags/categories // into groups, and we have to handle that. - if innerTags.count == 1 { - let tag = innerTags.first! - let views = tagsGroup["views"] as? Int - - self.init(singleTag: tag, viewsCount: views) - return - } - - guard let views = tagsGroup["views"] as? Int else { - return nil + if innerTags.isEmpty { + self.init( + name: try container.decode(String.self, forKey: .name), + kind: try container.decode(Kind.self, forKey: .kind), + url: try container.decodeIfPresent(URL.self, forKey: .url), + viewsCount: nil, + children: [] + ) + } else if innerTags.count == 1, let tag = innerTags.first { + self.init(singleTag: tag, viewsCount: viewsCount) + } else { + let mappedChildren = innerTags.compactMap { StatsTagAndCategory(singleTag: $0) } + let label = mappedChildren.map { $0.name }.joined(separator: ", ") + self.init(name: label, kind: .folder, url: nil, viewsCount: viewsCount, children: mappedChildren) } - - let mappedChildren = innerTags.compactMap { StatsTagAndCategory(singleTag: $0) } - let label = mappedChildren.map { $0.name }.joined(separator: ", ") - - self.init(name: label, kind: .folder, url: nil, viewsCount: views, children: mappedChildren) } - init?(singleTag tag: [String: AnyObject], viewsCount: Int? = 0) { - guard - let name = tag["name"] as? String, - let type = tag["type"] as? String, - let url = tag["link"] as? String - else { - return nil - } - + init(singleTag tag: StatsTagAndCategory, viewsCount: Int? = 0) { let kind: Kind - switch type { - case "category": + switch tag.kind { + case .category: kind = .category - case "tag": + case .tag: kind = .tag default: kind = .category } - self.init(name: name, kind: kind, url: URL(string: url), viewsCount: viewsCount, children: []) + self.init(name: tag.name, kind: kind, url: tag.url, viewsCount: viewsCount, children: []) } } From 5ead79cecb0710660cc95c7c90580fa945608f59 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Thu, 21 Mar 2024 11:22:53 +0200 Subject: [PATCH 04/22] Update StatsAllTimesInsight to use Codable --- .../Insights/StatsAllTimesInsight.swift | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/WordPressKit/Insights/StatsAllTimesInsight.swift b/WordPressKit/Insights/StatsAllTimesInsight.swift index 16c372c98..086c4b770 100644 --- a/WordPressKit/Insights/StatsAllTimesInsight.swift +++ b/WordPressKit/Insights/StatsAllTimesInsight.swift @@ -1,4 +1,4 @@ -public struct StatsAllTimesInsight { +public struct StatsAllTimesInsight: Codable { public let postsCount: Int public let viewsCount: Int public let bestViewsDay: Date @@ -16,23 +16,43 @@ public struct StatsAllTimesInsight { self.visitorsCount = visitorsCount self.bestViewsPerDayCount = bestViewsPerDayCount } + + private enum CodingKeys: String, CodingKey { + case postsCount = "posts" + case viewsCount = "views" + case bestViewsDay = "views_best_day" + case visitorsCount = "visitors" + case bestViewsPerDayCount = "views_best_day_total" + } + + private enum RootKeys: String, CodingKey { + case stats + } } extension StatsAllTimesInsight: StatsInsightData { // MARK: - StatsInsightData Conformance public init?(jsonDictionary: [String: AnyObject]) { - guard - let statsDict = jsonDictionary["stats"] as? [String: AnyObject], - let bestViewsDayString = statsDict["views_best_day"] as? String - else { - return nil + do { + let jsonData = try JSONSerialization.data(withJSONObject: jsonDictionary, options: []) + let decoder = JSONDecoder() + self = try decoder.decode(StatsAllTimesInsight.self, from: jsonData) + } catch { + return nil } + } + + public init (from decoder: Decoder) throws { + let rootContainer = try decoder.container(keyedBy: RootKeys.self) + let container = try rootContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: .stats) + + self.postsCount = try container.decodeIfPresent(Int.self, forKey: .postsCount) ?? 0 + self.bestViewsPerDayCount = try container.decode(Int.self, forKey: .bestViewsPerDayCount) + self.visitorsCount = try container.decodeIfPresent(Int.self, forKey: .visitorsCount) ?? 0 - self.postsCount = statsDict["posts"] as? Int ?? 0 - self.bestViewsPerDayCount = statsDict["views_best_day_total"] as? Int ?? 0 - self.visitorsCount = statsDict["visitors"] as? Int ?? 0 - self.viewsCount = statsDict["views"] as? Int ?? 0 + self.viewsCount = try container.decodeIfPresent(Int.self, forKey: .viewsCount) ?? 0 + let bestViewsDayString = try container.decodeIfPresent(String.self, forKey: .bestViewsDay) ?? "" self.bestViewsDay = StatsAllTimesInsight.dateFormatter.date(from: bestViewsDayString) ?? Date() } From 40fcdacd11e383f114c905ebc28cb4415c32d5cd Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Thu, 21 Mar 2024 15:14:40 +0200 Subject: [PATCH 05/22] Update StatsPostingStreakInsight to use Codable --- .../Insights/StatsPostingStreakInsight.swift | 176 +++++++++++------- 1 file changed, 104 insertions(+), 72 deletions(-) diff --git a/WordPressKit/Insights/StatsPostingStreakInsight.swift b/WordPressKit/Insights/StatsPostingStreakInsight.swift index 5d8625296..85b7bea04 100644 --- a/WordPressKit/Insights/StatsPostingStreakInsight.swift +++ b/WordPressKit/Insights/StatsPostingStreakInsight.swift @@ -1,33 +1,70 @@ -public struct StatsPostingStreakInsight { - public let currentStreakStart: Date - public let currentStreakEnd: Date - public let currentStreakLength: Int - public let longestStreakStart: Date - public let longestStreakEnd: Date - public let longestStreakLength: Int - +public struct StatsPostingStreakInsight: Codable { + public let streaks: PostingStreaks public let postingEvents: [PostingStreakEvent] - public init(currentStreakStart: Date, - currentStreakEnd: Date, - currentStreakLength: Int, - longestStreakStart: Date, - longestStreakEnd: Date, - longestStreakLength: Int, - postingEvents: [PostingStreakEvent]) { - self.currentStreakStart = currentStreakStart - self.currentStreakEnd = currentStreakEnd - self.currentStreakLength = currentStreakLength + public var currentStreakStart: Date? { + streaks.current?.start + } + + public var currentStreakEnd: Date? { + streaks.current?.end + } + public var currentStreakLength: Int? { + streaks.current?.length + } - self.longestStreakStart = longestStreakStart - self.longestStreakEnd = longestStreakEnd - self.longestStreakLength = longestStreakLength + public var longestStreakStart: Date? { + streaks.long?.start ?? currentStreakStart + } + public var longestStreakEnd: Date? { + streaks.long?.end ?? currentStreakEnd + } - self.postingEvents = postingEvents + public var longestStreakLength: Int? { + streaks.long?.length ?? currentStreakLength + } + + private enum CodingKeys: String, CodingKey { + case streaks = "streak" + case postingEvents = "data" + } + + public init?(jsonDictionary: [String: AnyObject]) { + do { + let jsonData = try JSONSerialization.data(withJSONObject: jsonDictionary, options: []) + let decoder = JSONDecoder() + self = try decoder.decode(StatsPostingStreakInsight.self, from: jsonData) + } catch { + return nil + } + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.streaks = try container.decode(PostingStreaks.self, forKey: .streaks) + let postsData = (try? container.decodeIfPresent([String: Int].self, forKey: .postingEvents)) ?? [:] + + let postingDates = postsData.keys + .compactMap { Double($0) } + .map { Date(timeIntervalSince1970: $0) } + .map { Calendar.autoupdatingCurrent.startOfDay(for: $0) } + + if postingDates.isEmpty { + self.postingEvents = [] + } else { + let countedPosts = NSCountedSet(array: postingDates) + self.postingEvents = countedPosts.compactMap { value in + if let date = value as? Date { + return PostingStreakEvent(date: date, postCount: countedPosts.count(for: value)) + } else { + return nil + } + } + } } } -public struct PostingStreakEvent { +public struct PostingStreakEvent: Codable { public let date: Date public let postCount: Int @@ -37,6 +74,49 @@ public struct PostingStreakEvent { } } +public struct PostingStreaks: Codable { + public let long: PostingStreak? + public let current: PostingStreak? + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.long = try? container.decodeIfPresent(PostingStreak.self, forKey: .long) + self.current = try? container.decodeIfPresent(PostingStreak.self, forKey: .current) + } +} + +public struct PostingStreak: Codable { + public let start: Date + public let end: Date + public let length: Int + + private enum CodingKeys: String, CodingKey { + case start + case end + case length + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let startValue = try container.decode(String.self, forKey: .start) + if let start = StatsPostingStreakInsight.dateFormatter.date(from: startValue) { + self.start = start + } else { + throw DecodingError.dataCorruptedError(forKey: .start, in: container, debugDescription: "Start date string doesn't match expected format") + } + + let endValue = try container.decode(String.self, forKey: .end) + if let end = StatsPostingStreakInsight.dateFormatter.date(from: endValue) { + self.end = end + } else { + throw DecodingError.dataCorruptedError(forKey: .end, in: container, debugDescription: "End date string doesn't match expected format") + } + + length = try container.decodeIfPresent(Int.self, forKey: .length) ?? 0 + } +} + extension StatsPostingStreakInsight: StatsInsightData { // MARK: - StatsInsightData Conformance @@ -70,57 +150,9 @@ extension StatsPostingStreakInsight: StatsInsightData { "max": "5000"] } - public init?(jsonDictionary: [String: AnyObject]) { - guard - let postsData = jsonDictionary["data"] as? [String: AnyObject], - let streaks = jsonDictionary["streak"] as? [String: AnyObject], - let longestData = streaks["long"] as? [String: AnyObject], - let currentData = streaks["current"] as? [String: AnyObject], - let currentStart = currentData["start"] as? String, - let currentStartDate = StatsPostingStreakInsight.dateFormatter.date(from: currentStart), - let currentEnd = currentData["end"] as? String, - let currentEndDate = StatsPostingStreakInsight.dateFormatter.date(from: currentEnd), - let currentLength = currentData["length"] as? Int - else { - return nil - } - - let postingDates = postsData.keys - .compactMap { Double($0) } - .map { Date(timeIntervalSince1970: $0) } - .map { Calendar.autoupdatingCurrent.startOfDay(for: $0) } - - let countedPosts = NSCountedSet(array: postingDates) - - let postingEvents = countedPosts.map { - PostingStreakEvent(date: $0 as! Date, postCount: countedPosts.count(for: $0)) - } - - self.postingEvents = postingEvents - self.currentStreakStart = currentStartDate - self.currentStreakEnd = currentEndDate - self.currentStreakLength = currentLength - - // If there is no longest streak, use the current. - if let longestStart = longestData["start"] as? String, - let longestStartDate = StatsPostingStreakInsight.dateFormatter.date(from: longestStart), - let longestEnd = longestData["end"] as? String, - let longestEndDate = StatsPostingStreakInsight.dateFormatter.date(from: longestEnd), - let longestLength = longestData["length"] as? Int { - self.longestStreakStart = longestStartDate - self.longestStreakEnd = longestEndDate - self.longestStreakLength = longestLength - } else { - self.longestStreakStart = currentStartDate - self.longestStreakEnd = currentEndDate - self.longestStreakLength = currentLength - } - } - - private static var dateFormatter: DateFormatter { + fileprivate static var dateFormatter: DateFormatter { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd" return formatter } - } From 685ca74f4f7ebe9c9b1caa498458d460dcc44f8a Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Fri, 22 Mar 2024 16:45:06 +0200 Subject: [PATCH 06/22] Update StatsLastPostInsight to use Codable --- .../Insights/StatsLastPostInsight.swift | 67 +++++++++++-------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/WordPressKit/Insights/StatsLastPostInsight.swift b/WordPressKit/Insights/StatsLastPostInsight.swift index 36cc9590e..ac6c3edd3 100644 --- a/WordPressKit/Insights/StatsLastPostInsight.swift +++ b/WordPressKit/Insights/StatsLastPostInsight.swift @@ -1,10 +1,10 @@ -public struct StatsLastPostInsight { +public struct StatsLastPostInsight: Decodable { public let title: String public let url: URL public let publishedDate: Date public let likesCount: Int public let commentsCount: Int - public let viewsCount: Int + public private(set) var viewsCount: Int = 0 public let postID: Int public let featuredImageURL: URL? @@ -50,34 +50,47 @@ extension StatsLastPostInsight: StatsInsightData { private static let dateFormatter = ISO8601DateFormatter() public init?(jsonDictionary: [String: AnyObject], views: Int) { - - guard - let title = jsonDictionary["title"] as? String, - let dateString = jsonDictionary["date"] as? String, - let date = StatsLastPostInsight.dateFormatter.date(from: dateString), - let urlString = jsonDictionary["URL"] as? String, - let url = URL(string: urlString), - let likesCount = jsonDictionary["like_count"] as? Int, - let postID = jsonDictionary["ID"] as? Int, - let discussionDict = jsonDictionary["discussion"] as? [String: Any], - let commentsCount = discussionDict["comment_count"] as? Int - else { - return nil + do { + let jsonData = try JSONSerialization.data(withJSONObject: jsonDictionary, options: []) + let decoder = JSONDecoder() + self = try decoder.decode(StatsLastPostInsight.self, from: jsonData) + self.viewsCount = views + } catch { + return nil } + } +} - self.title = title.trimmingCharacters(in: CharacterSet.whitespaces).stringByDecodingXMLCharacters() - self.url = url - self.publishedDate = date - self.likesCount = likesCount - self.commentsCount = commentsCount - self.viewsCount = views - self.postID = postID +extension StatsLastPostInsight { + private enum CodingKeys: String, CodingKey { + case title + case url = "URL" + case publishedDate = "date" + case likesCount = "like_count" + case commentsCount + case postID = "ID" + case featuredImageURL = "featured_image" + case discussion + } - if let featuredImage = jsonDictionary["featured_image"] as? String, - let featuredURL = URL(string: featuredImage) { - self.featuredImageURL = featuredURL - } else { - self.featuredImageURL = nil + private enum DiscussionKeys: String, CodingKey { + case commentsCount = "comment_count" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + title = try container.decode(String.self, forKey: .title).trimmingCharacters(in: .whitespaces).stringByDecodingXMLCharacters() + url = try container.decode(URL.self, forKey: .url) + let dateString = try container.decode(String.self, forKey: .publishedDate) + guard let date = StatsLastPostInsight.dateFormatter.date(from: dateString) else { + throw DecodingError.dataCorruptedError(forKey: .publishedDate, in: container, debugDescription: "Date string does not match format expected by formatter.") } + publishedDate = date + likesCount = try container.decodeIfPresent(Int.self, forKey: .likesCount) ?? 0 + postID = try container.decode(Int.self, forKey: .postID) + featuredImageURL = try? container.decodeIfPresent(URL.self, forKey: .featuredImageURL) + + let discussionContainer = try container.nestedContainer(keyedBy: DiscussionKeys.self, forKey: .discussion) + commentsCount = try discussionContainer.decode(Int.self, forKey: .commentsCount) } } From d64f49025fd55d9fb42e2b11ba63d481e44e8b17 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Fri, 22 Mar 2024 18:26:06 +0200 Subject: [PATCH 07/22] Update StatsTodayInsight to use Codable --- WordPressKit/Insights/StatsTodayInsight.swift | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/WordPressKit/Insights/StatsTodayInsight.swift b/WordPressKit/Insights/StatsTodayInsight.swift index 31085c19f..5fe6b470c 100644 --- a/WordPressKit/Insights/StatsTodayInsight.swift +++ b/WordPressKit/Insights/StatsTodayInsight.swift @@ -1,4 +1,4 @@ -public struct StatsTodayInsight { +public struct StatsTodayInsight: Codable { public let viewsCount: Int public let visitorsCount: Int public let likesCount: Int @@ -23,9 +23,27 @@ extension StatsTodayInsight: StatsInsightData { } public init?(jsonDictionary: [String: AnyObject]) { - self.visitorsCount = jsonDictionary["visitors"] as? Int ?? 0 - self.viewsCount = jsonDictionary["views"] as? Int ?? 0 - self.likesCount = jsonDictionary["likes"] as? Int ?? 0 - self.commentsCount = jsonDictionary["comments"] as? Int ?? 0 + do { + let jsonData = try JSONSerialization.data(withJSONObject: jsonDictionary, options: []) + let decoder = JSONDecoder() + self = try decoder.decode(StatsTodayInsight.self, from: jsonData) + } catch { + return nil + } + } + + private enum CodingKeys: String, CodingKey { + case viewsCount = "views" + case visitorsCount = "visitors" + case likesCount = "likes" + case commentsCount = "comments" + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + viewsCount = (try? container.decodeIfPresent(Int.self, forKey: .viewsCount)) ?? 0 + visitorsCount = (try? container.decodeIfPresent(Int.self, forKey: .visitorsCount)) ?? 0 + likesCount = (try? container.decodeIfPresent(Int.self, forKey: .likesCount)) ?? 0 + commentsCount = (try? container.decodeIfPresent(Int.self, forKey: .commentsCount)) ?? 0 } } From 793b821df9ada6b71c4d8d6e78a77da1fb2c98d3 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Fri, 22 Mar 2024 18:38:26 +0200 Subject: [PATCH 08/22] Update StatsAnnualInsight to use Codable --- .../Insights/StatsAllAnnualInsight.swift | 66 ++++++++++++------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/WordPressKit/Insights/StatsAllAnnualInsight.swift b/WordPressKit/Insights/StatsAllAnnualInsight.swift index 95fa0b8ec..8463aafe3 100644 --- a/WordPressKit/Insights/StatsAllAnnualInsight.swift +++ b/WordPressKit/Insights/StatsAllAnnualInsight.swift @@ -1,12 +1,16 @@ -public struct StatsAllAnnualInsight { +public struct StatsAllAnnualInsight: Codable { public let allAnnualInsights: [StatsAnnualInsight] public init(allAnnualInsights: [StatsAnnualInsight]) { self.allAnnualInsights = allAnnualInsights } + + enum CodingKeys: String, CodingKey { + case allAnnualInsights = "years" + } } -public struct StatsAnnualInsight { +public struct StatsAnnualInsight: Codable { public let year: Int public let totalPostsCount: Int public let totalWordsCount: Int @@ -39,6 +43,38 @@ public struct StatsAnnualInsight { self.totalImagesCount = totalImagesCount self.averageImagesCount = averageImagesCount } + + private enum CodingKeys: String, CodingKey { + case year + case totalPostsCount = "total_posts" + case totalWordsCount = "total_words" + case averageWordsCount = "avg_words" + case totalLikesCount = "total_likes" + case averageLikesCount = "avg_likes" + case totalCommentsCount = "total_comments" + case averageCommentsCount = "avg_comments" + case totalImagesCount = "total_images" + case averageImagesCount = "avg_images" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if let year = Int(try container.decode(String.self, forKey: .year)) { + self.year = year + } else { + throw DecodingError.dataCorruptedError(forKey: .year, in: container, debugDescription: "Year cannot be parsed into number.") + } + totalPostsCount = (try? container.decodeIfPresent(Int.self, forKey: .totalPostsCount)) ?? 0 + totalWordsCount = (try? container.decodeIfPresent(Int.self, forKey: .totalWordsCount)) ?? 0 + averageWordsCount = (try? container.decodeIfPresent(Double.self, forKey: .averageWordsCount)) ?? 0 + totalLikesCount = (try? container.decodeIfPresent(Int.self, forKey: .totalLikesCount)) ?? 0 + averageLikesCount = (try? container.decodeIfPresent(Double.self, forKey: .averageLikesCount)) ?? 0 + totalCommentsCount = (try? container.decodeIfPresent(Int.self, forKey: .totalCommentsCount)) ?? 0 + averageCommentsCount = (try? container.decodeIfPresent(Double.self, forKey: .averageCommentsCount)) ?? 0 + totalImagesCount = (try? container.decodeIfPresent(Int.self, forKey: .totalImagesCount)) ?? 0 + averageImagesCount = (try? container.decodeIfPresent(Double.self, forKey: .averageImagesCount)) ?? 0 + } } extension StatsAllAnnualInsight: StatsInsightData { @@ -47,28 +83,12 @@ extension StatsAllAnnualInsight: StatsInsightData { } public init?(jsonDictionary: [String: AnyObject]) { - guard let yearlyInsights = jsonDictionary["years"] as? [[String: AnyObject]] else { + do { + let jsonData = try JSONSerialization.data(withJSONObject: jsonDictionary, options: []) + let decoder = JSONDecoder() + self = try decoder.decode(StatsAllAnnualInsight.self, from: jsonData) + } catch { return nil } - - let allAnnualInsights: [StatsAnnualInsight] = yearlyInsights.compactMap { - guard let yearString = $0["year"] as? String, - let year = Int(yearString) else { - return nil - } - - return StatsAnnualInsight(year: year, - totalPostsCount: $0["total_posts"] as? Int ?? 0, - totalWordsCount: $0["total_words"] as? Int ?? 0, - averageWordsCount: $0["avg_words"] as? Double ?? 0, - totalLikesCount: $0["total_likes"] as? Int ?? 0, - averageLikesCount: $0["avg_likes"] as? Double ?? 0, - totalCommentsCount: $0["total_comments"] as? Int ?? 0, - averageCommentsCount: $0["avg_comments"] as? Double ?? 0, - totalImagesCount: $0["total_images"] as? Int ?? 0, - averageImagesCount: $0["avg_images"] as? Double ?? 0) - } - - self.allAnnualInsights = allAnnualInsights } } From 09163c8dbe3627d62014e929b2ea88d27a97743f Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 25 Mar 2024 10:49:33 +0200 Subject: [PATCH 09/22] Update StatsPublicizeInsight to use Codable --- .../Insights/StatsPublicizeInsight.swift | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/WordPressKit/Insights/StatsPublicizeInsight.swift b/WordPressKit/Insights/StatsPublicizeInsight.swift index 974e5c99b..1466cdee6 100644 --- a/WordPressKit/Insights/StatsPublicizeInsight.swift +++ b/WordPressKit/Insights/StatsPublicizeInsight.swift @@ -1,9 +1,13 @@ -public struct StatsPublicizeInsight { +public struct StatsPublicizeInsight: Codable { public let publicizeServices: [StatsPublicizeService] public init(publicizeServices: [StatsPublicizeService]) { self.publicizeServices = publicizeServices } + + private enum CodingKeys: String, CodingKey { + case publicizeServices = "services" + } } extension StatsPublicizeInsight: StatsInsightData { @@ -14,20 +18,17 @@ extension StatsPublicizeInsight: StatsInsightData { } public init?(jsonDictionary: [String: AnyObject]) { - guard - let subscribers = jsonDictionary["services"] as? [[String: AnyObject]] - else { - return nil + do { + let jsonData = try JSONSerialization.data(withJSONObject: jsonDictionary, options: []) + let decoder = JSONDecoder() + self = try decoder.decode(StatsPublicizeInsight.self, from: jsonData) + } catch { + return nil } - - let followers = subscribers.compactMap { StatsPublicizeService(publicizeServiceDictionary: $0) } - - self.publicizeServices = followers } - } -public struct StatsPublicizeService { +public struct StatsPublicizeService: Codable { public let name: String public let followers: Int public let iconURL: URL? @@ -39,18 +40,20 @@ public struct StatsPublicizeService { self.followers = followers self.iconURL = iconURL } + + private enum CodingKeys: String, CodingKey { + case name = "service" + case followers + } } private extension StatsPublicizeService { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let name = try container.decode(String.self, forKey: .name) + let followers = (try? container.decodeIfPresent(Int.self, forKey: .followers)) ?? 0 - init?(publicizeServiceDictionary dictionary: [String: AnyObject]) { - guard - let name = dictionary["service"] as? String, - let followersCount = dictionary["followers"] as? Int else { - return nil - } - - self.init(name: name, followers: followersCount) + self.init(name: name, followers: followers) } init(name: String, followers: Int) { From 060bd5490426d2ca3f3fdb87652858ca5205a61c Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 25 Mar 2024 11:10:38 +0200 Subject: [PATCH 10/22] Update StatsCommentsInsight to use Codable --- .../Insights/StatsCommentsInsight.swift | 89 +++++++++++-------- 1 file changed, 50 insertions(+), 39 deletions(-) diff --git a/WordPressKit/Insights/StatsCommentsInsight.swift b/WordPressKit/Insights/StatsCommentsInsight.swift index cf123aacb..c4d41666b 100644 --- a/WordPressKit/Insights/StatsCommentsInsight.swift +++ b/WordPressKit/Insights/StatsCommentsInsight.swift @@ -1,4 +1,4 @@ -public struct StatsCommentsInsight { +public struct StatsCommentsInsight: Codable { public let topPosts: [StatsTopCommentsPost] public let topAuthors: [StatsTopCommentsAuthor] @@ -7,6 +7,11 @@ public struct StatsCommentsInsight { self.topPosts = topPosts self.topAuthors = topAuthors } + + private enum CodingKeys: String, CodingKey { + case topPosts = "posts" + case topAuthors = "authors" + } } extension StatsCommentsInsight: StatsInsightData { @@ -17,23 +22,17 @@ extension StatsCommentsInsight: StatsInsightData { } public init?(jsonDictionary: [String: AnyObject]) { - guard - let posts = jsonDictionary["posts"] as? [[String: AnyObject]], - let authors = jsonDictionary["authors"] as? [[String: AnyObject]] - else { - return nil + do { + let jsonData = try JSONSerialization.data(withJSONObject: jsonDictionary, options: []) + let decoder = JSONDecoder() + self = try decoder.decode(StatsCommentsInsight.self, from: jsonData) + } catch { + return nil } - - let topPosts = posts.compactMap { StatsTopCommentsPost(jsonDictionary: $0) } - let topAuthors = authors.compactMap { StatsTopCommentsAuthor(jsonDictionary: $0) } - - self.topPosts = topPosts - self.topAuthors = topAuthors } - } -public struct StatsTopCommentsAuthor { +public struct StatsTopCommentsAuthor: Codable { public let name: String public let commentCount: Int public let iconURL: URL? @@ -45,9 +44,15 @@ public struct StatsTopCommentsAuthor { self.commentCount = commentCount self.iconURL = iconURL } + + private enum CodingKeys: String, CodingKey { + case name + case commentCount = "comments" + case iconURL = "gravatar" + } } -public struct StatsTopCommentsPost { +public struct StatsTopCommentsPost: Codable { public let name: String public let postID: String public let commentCount: Int @@ -62,26 +67,34 @@ public struct StatsTopCommentsPost { self.commentCount = commentCount self.postURL = postURL } + + private enum CodingKeys: String, CodingKey { + case name + case postID = "id" + case commentCount = "comments" + case postURL = "link" + } } private extension StatsTopCommentsAuthor { - init?(jsonDictionary: [String: AnyObject]) { - guard - let name = jsonDictionary["name"] as? String, - let avatar = jsonDictionary["gravatar"] as? String, - let comments = jsonDictionary["comments"] as? String, - let commentCount = Int(comments) - else { - return nil + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let name = try container.decode(String.self, forKey: .name) + let commentCount: Int + if let comments = try? container.decodeIfPresent(String.self, forKey: .commentCount) { + commentCount = Int(comments) ?? 0 + } else { + commentCount = 0 } + let iconURL = try container.decodeIfPresent(String.self, forKey: .iconURL) - self.init(name: name, avatar: avatar, commentCount: commentCount) + self.init(name: name, avatar: iconURL, commentCount: commentCount) } - init?(name: String, avatar: String, commentCount: Int) { + init(name: String, avatar: String?, commentCount: Int) { let url: URL? - if var components = URLComponents(string: avatar) { + if let avatar, var components = URLComponents(string: avatar) { components.query = "d=mm&s=60" // to get a properly-sized avatar. url = components.url } else { @@ -95,20 +108,18 @@ private extension StatsTopCommentsAuthor { } private extension StatsTopCommentsPost { - init?(jsonDictionary: [String: AnyObject]) { - guard - let name = jsonDictionary["name"] as? String, - let postID = jsonDictionary["id"] as? String, - let commentString = jsonDictionary["comments"] as? String, - let commentCount = Int(commentString), - let postURL = jsonDictionary["link"] as? String - else { - return nil + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let name = try container.decode(String.self, forKey: .name) + let postID = try container.decode(String.self, forKey: .postID) + let commentCount: Int + if let comments = try? container.decodeIfPresent(String.self, forKey: .commentCount) { + commentCount = Int(comments) ?? 0 + } else { + commentCount = 0 } + let postURL = try container.decodeIfPresent(URL.self, forKey: .postURL) - self.init(name: name, - postID: postID, - commentCount: commentCount, - postURL: URL(string: postURL)) + self.init(name: name, postID: postID, commentCount: commentCount, postURL: postURL) } } From 061b616da9946d8f50d4f8b7e8b2abfdfb11cfe5 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 25 Mar 2024 11:45:12 +0200 Subject: [PATCH 11/22] Update StatsDotComFollowersInsight to use Codable --- .../StatsDotComFollowersInsight.swift | 85 +++++++++++-------- 1 file changed, 48 insertions(+), 37 deletions(-) diff --git a/WordPressKit/Insights/StatsDotComFollowersInsight.swift b/WordPressKit/Insights/StatsDotComFollowersInsight.swift index ae657d62b..a9d4f0aa3 100644 --- a/WordPressKit/Insights/StatsDotComFollowersInsight.swift +++ b/WordPressKit/Insights/StatsDotComFollowersInsight.swift @@ -1,4 +1,4 @@ -public struct StatsDotComFollowersInsight { +public struct StatsDotComFollowersInsight: Codable { public let dotComFollowersCount: Int public let topDotComFollowers: [StatsFollower] @@ -7,6 +7,11 @@ public struct StatsDotComFollowersInsight { self.dotComFollowersCount = dotComFollowersCount self.topDotComFollowers = topDotComFollowers } + + private enum CodingKeys: String, CodingKey { + case dotComFollowersCount = "total_wpcom" + case topDotComFollowers = "subscribers" + } } extension StatsDotComFollowersInsight: StatsInsightData { @@ -22,24 +27,19 @@ extension StatsDotComFollowersInsight: StatsInsightData { } public init?(jsonDictionary: [String: AnyObject]) { - guard - let subscribersCount = jsonDictionary["total_wpcom"] as? Int, - let subscribers = jsonDictionary["subscribers"] as? [[String: AnyObject]] - else { - return nil + do { + let jsonData = try JSONSerialization.data(withJSONObject: jsonDictionary, options: []) + let decoder = JSONDecoder() + self = try decoder.decode(StatsDotComFollowersInsight.self, from: jsonData) + } catch { + return nil } - - let followers = subscribers.compactMap { StatsFollower(jsonDictionary: $0) } - - self.dotComFollowersCount = subscribersCount - self.topDotComFollowers = followers } - // MARK: - fileprivate static let dateFormatter = ISO8601DateFormatter() } -public struct StatsFollower { +public struct StatsFollower: Codable { public let id: String? public let name: String public let subscribedDate: Date @@ -54,39 +54,50 @@ public struct StatsFollower { self.avatarURL = avatarURL self.id = id } + + private enum CodingKeys: String, CodingKey { + case id = "ID" + case name = "label" + case subscribedDate = "date_subscribed" + case avatarURL = "avatar" + } } extension StatsFollower { - - init?(jsonDictionary: [String: AnyObject]) { - guard - let name = jsonDictionary["label"] as? String, - let avatar = jsonDictionary["avatar"] as? String, - let dateString = jsonDictionary["date_subscribed"] as? String - else { - return nil + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.name = try container.decode(String.self, forKey: .name) + if let id = try? container.decodeIfPresent(Int.self, forKey: .id) { + self.id = "\(id)" + } else if let id = try? container.decodeIfPresent(String.self, forKey: .id) { + self.id = id + } else { + self.id = nil } - let id = jsonDictionary["ID"] as? String - self.init(name: name, avatar: avatar, date: dateString, id: id) - } - init?(name: String, avatar: String, date: String, id: String? = nil) { - guard let date = StatsDotComFollowersInsight.dateFormatter.date(from: date) else { - return nil + let avatar = try? container.decodeIfPresent(String.self, forKey: .avatarURL) + if let avatar, var components = URLComponents(string: avatar) { + components.query = "d=mm&s=60" // to get a properly-sized avatar. + self.avatarURL = components.url + } else { + self.avatarURL = nil } - let url: URL? - - if var components = URLComponents(string: avatar) { - components.query = "d=mm&s=60" // to get a properly-sized avatar. - url = components.url + let dateString = try container.decode(String.self, forKey: .subscribedDate) + if let date = StatsDotComFollowersInsight.dateFormatter.date(from: dateString) { + self.subscribedDate = date } else { - url = nil + throw DecodingError.dataCorruptedError(forKey: .subscribedDate, in: container, debugDescription: "Date string does not match format expected by formatter.") } + } - self.name = name - self.subscribedDate = date - self.avatarURL = url - self.id = id + init?(jsonDictionary: [String: AnyObject]) { + do { + let jsonData = try JSONSerialization.data(withJSONObject: jsonDictionary, options: []) + let decoder = JSONDecoder() + self = try decoder.decode(StatsFollower.self, from: jsonData) + } catch { + return nil + } } } From be9948e4a84113df98180972817cbcce901ebdbe Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 25 Mar 2024 11:45:23 +0200 Subject: [PATCH 12/22] Update StatsEmailFollowersInsight to use Codable --- .../Insights/StatsEmailFollowersInsight.swift | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/WordPressKit/Insights/StatsEmailFollowersInsight.swift b/WordPressKit/Insights/StatsEmailFollowersInsight.swift index 846cefb68..3136de275 100644 --- a/WordPressKit/Insights/StatsEmailFollowersInsight.swift +++ b/WordPressKit/Insights/StatsEmailFollowersInsight.swift @@ -1,4 +1,4 @@ -public struct StatsEmailFollowersInsight { +public struct StatsEmailFollowersInsight: Codable { public let emailFollowersCount: Int public let topEmailFollowers: [StatsFollower] @@ -7,6 +7,11 @@ public struct StatsEmailFollowersInsight { self.emailFollowersCount = emailFollowersCount self.topEmailFollowers = topEmailFollowers } + + private enum CodingKeys: String, CodingKey { + case emailFollowersCount = "total_email" + case topEmailFollowers = "subscribers" + } } extension StatsEmailFollowersInsight: StatsInsightData { @@ -22,17 +27,12 @@ extension StatsEmailFollowersInsight: StatsInsightData { } public init?(jsonDictionary: [String: AnyObject]) { - guard - let subscribersCount = jsonDictionary["total_email"] as? Int, - let subscribers = jsonDictionary["subscribers"] as? [[String: AnyObject]] - else { - return nil + do { + let jsonData = try JSONSerialization.data(withJSONObject: jsonDictionary, options: []) + let decoder = JSONDecoder() + self = try decoder.decode(StatsEmailFollowersInsight.self, from: jsonData) + } catch { + return nil } - - let followers = subscribers.compactMap { StatsFollower(jsonDictionary: $0) } - - self.emailFollowersCount = subscribersCount - self.topEmailFollowers = followers } - } From 09a00e2eaa81e4147e2c33762bff1a8b05f83485 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 25 Mar 2024 11:53:15 +0200 Subject: [PATCH 13/22] Make JSON decoding logic reusable for StatsInsightData type --- WordPressKit/Insights/StatsAllAnnualInsight.swift | 10 ---------- WordPressKit/Insights/StatsAllTimesInsight.swift | 12 ------------ .../StatsAnnualAndMostPopularTimeInsight.swift | 10 ---------- WordPressKit/Insights/StatsCommentsInsight.swift | 10 ---------- .../Insights/StatsDotComFollowersInsight.swift | 10 ---------- .../Insights/StatsEmailFollowersInsight.swift | 10 ---------- .../Insights/StatsPostingStreakInsight.swift | 10 ---------- WordPressKit/Insights/StatsPublicizeInsight.swift | 10 ---------- .../Insights/StatsTagsAndCategoriesInsight.swift | 10 ---------- WordPressKit/Insights/StatsTodayInsight.swift | 10 ---------- WordPressKit/StatsServiceRemoteV2.swift | 12 ++++++++++++ 11 files changed, 12 insertions(+), 102 deletions(-) diff --git a/WordPressKit/Insights/StatsAllAnnualInsight.swift b/WordPressKit/Insights/StatsAllAnnualInsight.swift index 8463aafe3..af4a749a6 100644 --- a/WordPressKit/Insights/StatsAllAnnualInsight.swift +++ b/WordPressKit/Insights/StatsAllAnnualInsight.swift @@ -81,14 +81,4 @@ extension StatsAllAnnualInsight: StatsInsightData { public static var pathComponent: String { return "stats/insights" } - - public init?(jsonDictionary: [String: AnyObject]) { - do { - let jsonData = try JSONSerialization.data(withJSONObject: jsonDictionary, options: []) - let decoder = JSONDecoder() - self = try decoder.decode(StatsAllAnnualInsight.self, from: jsonData) - } catch { - return nil - } - } } diff --git a/WordPressKit/Insights/StatsAllTimesInsight.swift b/WordPressKit/Insights/StatsAllTimesInsight.swift index 086c4b770..10e1c2790 100644 --- a/WordPressKit/Insights/StatsAllTimesInsight.swift +++ b/WordPressKit/Insights/StatsAllTimesInsight.swift @@ -31,18 +31,6 @@ public struct StatsAllTimesInsight: Codable { } extension StatsAllTimesInsight: StatsInsightData { - - // MARK: - StatsInsightData Conformance - public init?(jsonDictionary: [String: AnyObject]) { - do { - let jsonData = try JSONSerialization.data(withJSONObject: jsonDictionary, options: []) - let decoder = JSONDecoder() - self = try decoder.decode(StatsAllTimesInsight.self, from: jsonData) - } catch { - return nil - } - } - public init (from decoder: Decoder) throws { let rootContainer = try decoder.container(keyedBy: RootKeys.self) let container = try rootContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: .stats) diff --git a/WordPressKit/Insights/StatsAnnualAndMostPopularTimeInsight.swift b/WordPressKit/Insights/StatsAnnualAndMostPopularTimeInsight.swift index 1983cdc09..db170c2ab 100644 --- a/WordPressKit/Insights/StatsAnnualAndMostPopularTimeInsight.swift +++ b/WordPressKit/Insights/StatsAnnualAndMostPopularTimeInsight.swift @@ -48,16 +48,6 @@ extension StatsAnnualAndMostPopularTimeInsight: StatsInsightData { public static var pathComponent: String { return "stats/insights" } - - public init?(jsonDictionary: [String: AnyObject]) { - do { - let jsonData = try JSONSerialization.data(withJSONObject: jsonDictionary, options: []) - let decoder = JSONDecoder() - self = try decoder.decode(StatsAnnualAndMostPopularTimeInsight.self, from: jsonData) - } catch { - return nil - } - } } extension StatsAnnualAndMostPopularTimeInsight { diff --git a/WordPressKit/Insights/StatsCommentsInsight.swift b/WordPressKit/Insights/StatsCommentsInsight.swift index c4d41666b..f3703e9a2 100644 --- a/WordPressKit/Insights/StatsCommentsInsight.swift +++ b/WordPressKit/Insights/StatsCommentsInsight.swift @@ -20,16 +20,6 @@ extension StatsCommentsInsight: StatsInsightData { public static var pathComponent: String { return "stats/comments" } - - public init?(jsonDictionary: [String: AnyObject]) { - do { - let jsonData = try JSONSerialization.data(withJSONObject: jsonDictionary, options: []) - let decoder = JSONDecoder() - self = try decoder.decode(StatsCommentsInsight.self, from: jsonData) - } catch { - return nil - } - } } public struct StatsTopCommentsAuthor: Codable { diff --git a/WordPressKit/Insights/StatsDotComFollowersInsight.swift b/WordPressKit/Insights/StatsDotComFollowersInsight.swift index a9d4f0aa3..6340282de 100644 --- a/WordPressKit/Insights/StatsDotComFollowersInsight.swift +++ b/WordPressKit/Insights/StatsDotComFollowersInsight.swift @@ -26,16 +26,6 @@ extension StatsDotComFollowersInsight: StatsInsightData { return "stats/followers" } - public init?(jsonDictionary: [String: AnyObject]) { - do { - let jsonData = try JSONSerialization.data(withJSONObject: jsonDictionary, options: []) - let decoder = JSONDecoder() - self = try decoder.decode(StatsDotComFollowersInsight.self, from: jsonData) - } catch { - return nil - } - } - fileprivate static let dateFormatter = ISO8601DateFormatter() } diff --git a/WordPressKit/Insights/StatsEmailFollowersInsight.swift b/WordPressKit/Insights/StatsEmailFollowersInsight.swift index 3136de275..49dc7be2d 100644 --- a/WordPressKit/Insights/StatsEmailFollowersInsight.swift +++ b/WordPressKit/Insights/StatsEmailFollowersInsight.swift @@ -25,14 +25,4 @@ extension StatsEmailFollowersInsight: StatsInsightData { public static var pathComponent: String { return "stats/followers" } - - public init?(jsonDictionary: [String: AnyObject]) { - do { - let jsonData = try JSONSerialization.data(withJSONObject: jsonDictionary, options: []) - let decoder = JSONDecoder() - self = try decoder.decode(StatsEmailFollowersInsight.self, from: jsonData) - } catch { - return nil - } - } } diff --git a/WordPressKit/Insights/StatsPostingStreakInsight.swift b/WordPressKit/Insights/StatsPostingStreakInsight.swift index 85b7bea04..ec197f600 100644 --- a/WordPressKit/Insights/StatsPostingStreakInsight.swift +++ b/WordPressKit/Insights/StatsPostingStreakInsight.swift @@ -29,16 +29,6 @@ public struct StatsPostingStreakInsight: Codable { case postingEvents = "data" } - public init?(jsonDictionary: [String: AnyObject]) { - do { - let jsonData = try JSONSerialization.data(withJSONObject: jsonDictionary, options: []) - let decoder = JSONDecoder() - self = try decoder.decode(StatsPostingStreakInsight.self, from: jsonData) - } catch { - return nil - } - } - public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.streaks = try container.decode(PostingStreaks.self, forKey: .streaks) diff --git a/WordPressKit/Insights/StatsPublicizeInsight.swift b/WordPressKit/Insights/StatsPublicizeInsight.swift index 1466cdee6..e4e5752d6 100644 --- a/WordPressKit/Insights/StatsPublicizeInsight.swift +++ b/WordPressKit/Insights/StatsPublicizeInsight.swift @@ -16,16 +16,6 @@ extension StatsPublicizeInsight: StatsInsightData { public static var pathComponent: String { return "stats/publicize" } - - public init?(jsonDictionary: [String: AnyObject]) { - do { - let jsonData = try JSONSerialization.data(withJSONObject: jsonDictionary, options: []) - let decoder = JSONDecoder() - self = try decoder.decode(StatsPublicizeInsight.self, from: jsonData) - } catch { - return nil - } - } } public struct StatsPublicizeService: Codable { diff --git a/WordPressKit/Insights/StatsTagsAndCategoriesInsight.swift b/WordPressKit/Insights/StatsTagsAndCategoriesInsight.swift index 882883d2a..b83e76281 100644 --- a/WordPressKit/Insights/StatsTagsAndCategoriesInsight.swift +++ b/WordPressKit/Insights/StatsTagsAndCategoriesInsight.swift @@ -10,16 +10,6 @@ extension StatsTagsAndCategoriesInsight: StatsInsightData { public static var pathComponent: String { return "stats/tags" } - - public init?(jsonDictionary: [String: AnyObject]) { - do { - let jsonData = try JSONSerialization.data(withJSONObject: jsonDictionary, options: []) - let decoder = JSONDecoder() - self = try decoder.decode(StatsTagsAndCategoriesInsight.self, from: jsonData) - } catch { - return nil - } - } } public struct StatsTagAndCategory: Codable { diff --git a/WordPressKit/Insights/StatsTodayInsight.swift b/WordPressKit/Insights/StatsTodayInsight.swift index 5fe6b470c..2a271b62d 100644 --- a/WordPressKit/Insights/StatsTodayInsight.swift +++ b/WordPressKit/Insights/StatsTodayInsight.swift @@ -22,16 +22,6 @@ extension StatsTodayInsight: StatsInsightData { return "stats/summary" } - public init?(jsonDictionary: [String: AnyObject]) { - do { - let jsonData = try JSONSerialization.data(withJSONObject: jsonDictionary, options: []) - let decoder = JSONDecoder() - self = try decoder.decode(StatsTodayInsight.self, from: jsonData) - } catch { - return nil - } - } - private enum CodingKeys: String, CodingKey { case viewsCount = "views" case visitorsCount = "visitors" diff --git a/WordPressKit/StatsServiceRemoteV2.swift b/WordPressKit/StatsServiceRemoteV2.swift index f0567bfac..3c1542536 100644 --- a/WordPressKit/StatsServiceRemoteV2.swift +++ b/WordPressKit/StatsServiceRemoteV2.swift @@ -411,3 +411,15 @@ extension StatsInsightData { return "stats/" } } + +public extension StatsInsightData where Self: Codable { + init?(jsonDictionary: [String: AnyObject]) { + do { + let jsonData = try JSONSerialization.data(withJSONObject: jsonDictionary, options: []) + let decoder = JSONDecoder() + self = try decoder.decode(Self.self, from: jsonData) + } catch { + return nil + } + } +} From 1ecf67fe84cf57dbf16beb281b80aa67bd0ae224 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 25 Mar 2024 11:59:29 +0200 Subject: [PATCH 14/22] Make CodingKeys private --- WordPressKit/Insights/StatsAllAnnualInsight.swift | 2 +- .../Insights/StatsAnnualAndMostPopularTimeInsight.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/WordPressKit/Insights/StatsAllAnnualInsight.swift b/WordPressKit/Insights/StatsAllAnnualInsight.swift index af4a749a6..4c40baff7 100644 --- a/WordPressKit/Insights/StatsAllAnnualInsight.swift +++ b/WordPressKit/Insights/StatsAllAnnualInsight.swift @@ -5,7 +5,7 @@ public struct StatsAllAnnualInsight: Codable { self.allAnnualInsights = allAnnualInsights } - enum CodingKeys: String, CodingKey { + private enum CodingKeys: String, CodingKey { case allAnnualInsights = "years" } } diff --git a/WordPressKit/Insights/StatsAnnualAndMostPopularTimeInsight.swift b/WordPressKit/Insights/StatsAnnualAndMostPopularTimeInsight.swift index db170c2ab..380947910 100644 --- a/WordPressKit/Insights/StatsAnnualAndMostPopularTimeInsight.swift +++ b/WordPressKit/Insights/StatsAnnualAndMostPopularTimeInsight.swift @@ -8,7 +8,7 @@ public struct StatsAnnualAndMostPopularTimeInsight: Codable { public let mostPopularHourPercentage: Int public let years: [Year]? - enum CodingKeys: String, CodingKey { + private enum CodingKeys: String, CodingKey { case mostPopularHour = "highest_hour" case mostPopularHourPercentage = "highest_hour_percent" case mostPopularDayOfWeek = "highest_day_of_week" @@ -28,7 +28,7 @@ public struct StatsAnnualAndMostPopularTimeInsight: Codable { public let totalImages: Int public let averageImages: Double - enum CodingKeys: String, CodingKey { + private enum CodingKeys: String, CodingKey { case year case totalPosts = "total_posts" case totalWords = "total_words" From 4dd5a77c6db52b6f34d99c258e5f8bb2dc4e32b1 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 25 Mar 2024 13:48:20 +0200 Subject: [PATCH 15/22] Add unit tests for decoding Stats Insight entities --- WordPressKit.xcodeproj/project.pbxproj | 48 +++ ...StatsAnnualAndMostPopularTimeInsight.swift | 1 - .../Insights/StatsLastPostInsight.swift | 6 +- .../MockData/stats-insight-comments.json | 71 +++++ .../MockData/stats-insight-followers.json | 138 +++++++++ .../MockData/stats-insight-last-post.json | 203 +++++++++++++ .../MockData/stats-insight-publicize.json | 8 + .../MockData/stats-insight-streak.json | 157 ++++++++++ .../MockData/stats-insight-summary.json | 10 + .../stats-insight-tag-and-category.json | 105 +++++++ .../V2/Insights/MockData/stats-insight.json | 94 ++++++ .../Stats/V2/Insights/MockData/stats.json | 277 ++++++++++++++++++ .../Insights/StatsInsightDecodingTests.swift | 38 +++ 13 files changed, 1152 insertions(+), 4 deletions(-) create mode 100644 WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-comments.json create mode 100644 WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-followers.json create mode 100644 WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-last-post.json create mode 100644 WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-publicize.json create mode 100644 WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-streak.json create mode 100644 WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-summary.json create mode 100644 WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-tag-and-category.json create mode 100644 WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight.json create mode 100644 WordPressKitTests/Models/Stats/V2/Insights/MockData/stats.json create mode 100644 WordPressKitTests/Models/Stats/V2/Insights/StatsInsightDecodingTests.swift diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index d6d1b331f..bbe35a78a 100644 --- a/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit.xcodeproj/project.pbxproj @@ -13,6 +13,16 @@ 01438D392B6A361B0097D60A /* stats-summary.json in Resources */ = {isa = PBXBuildFile; fileRef = 01438D372B6A35FB0097D60A /* stats-summary.json */; }; 01438D3B2B6A36BF0097D60A /* StatsTotalsSummaryData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01438D3A2B6A36BF0097D60A /* StatsTotalsSummaryData.swift */; }; 0152100C28EDA9E400DD6783 /* StatsAnnualAndMostPopularTimeInsightDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0152100B28EDA9E400DD6783 /* StatsAnnualAndMostPopularTimeInsightDecodingTests.swift */; }; + 01D251942BB1834A006349C0 /* StatsInsightDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D251932BB1834A006349C0 /* StatsInsightDecodingTests.swift */; }; + 01D251972BB18F2A006349C0 /* stats-insight-last-post.json in Resources */ = {isa = PBXBuildFile; fileRef = 01D251962BB18F2A006349C0 /* stats-insight-last-post.json */; }; + 01D251992BB1961D006349C0 /* stats-insight-followers.json in Resources */ = {isa = PBXBuildFile; fileRef = 01D251982BB1961D006349C0 /* stats-insight-followers.json */; }; + 01D2519B2BB19723006349C0 /* stats-insight.json in Resources */ = {isa = PBXBuildFile; fileRef = 01D2519A2BB19723006349C0 /* stats-insight.json */; }; + 01D2519D2BB19845006349C0 /* stats.json in Resources */ = {isa = PBXBuildFile; fileRef = 01D2519C2BB19845006349C0 /* stats.json */; }; + 01D2519F2BB19957006349C0 /* stats-insight-publicize.json in Resources */ = {isa = PBXBuildFile; fileRef = 01D2519E2BB19957006349C0 /* stats-insight-publicize.json */; }; + 01D251A12BB19997006349C0 /* stats-insight-summary.json in Resources */ = {isa = PBXBuildFile; fileRef = 01D251A02BB19997006349C0 /* stats-insight-summary.json */; }; + 01D251A32BB19A22006349C0 /* stats-insight-comments.json in Resources */ = {isa = PBXBuildFile; fileRef = 01D251A22BB19A22006349C0 /* stats-insight-comments.json */; }; + 01D251A52BB19ABE006349C0 /* stats-insight-tag-and-category.json in Resources */ = {isa = PBXBuildFile; fileRef = 01D251A42BB19ABE006349C0 /* stats-insight-tag-and-category.json */; }; + 01D251A72BB19AE1006349C0 /* stats-insight-streak.json in Resources */ = {isa = PBXBuildFile; fileRef = 01D251A62BB19AE1006349C0 /* stats-insight-streak.json */; }; 0847B92C2A4442730044D32F /* IPLocationRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0847B92B2A4442730044D32F /* IPLocationRemote.swift */; }; 08C7493E2A45EA11000DA0E2 /* IPLocationRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08C7493D2A45EA11000DA0E2 /* IPLocationRemoteTests.swift */; }; 0C1C08412B9CD79900E52F8C /* PostServiceRemoteExtended.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1C08402B9CD79900E52F8C /* PostServiceRemoteExtended.swift */; }; @@ -739,6 +749,16 @@ 01438D372B6A35FB0097D60A /* stats-summary.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "stats-summary.json"; sourceTree = ""; }; 01438D3A2B6A36BF0097D60A /* StatsTotalsSummaryData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatsTotalsSummaryData.swift; sourceTree = ""; }; 0152100B28EDA9E400DD6783 /* StatsAnnualAndMostPopularTimeInsightDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsAnnualAndMostPopularTimeInsightDecodingTests.swift; sourceTree = ""; }; + 01D251932BB1834A006349C0 /* StatsInsightDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsInsightDecodingTests.swift; sourceTree = ""; }; + 01D251962BB18F2A006349C0 /* stats-insight-last-post.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "stats-insight-last-post.json"; sourceTree = ""; }; + 01D251982BB1961D006349C0 /* stats-insight-followers.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "stats-insight-followers.json"; sourceTree = ""; }; + 01D2519A2BB19723006349C0 /* stats-insight.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "stats-insight.json"; sourceTree = ""; }; + 01D2519C2BB19845006349C0 /* stats.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = stats.json; sourceTree = ""; }; + 01D2519E2BB19957006349C0 /* stats-insight-publicize.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "stats-insight-publicize.json"; sourceTree = ""; }; + 01D251A02BB19997006349C0 /* stats-insight-summary.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "stats-insight-summary.json"; sourceTree = ""; }; + 01D251A22BB19A22006349C0 /* stats-insight-comments.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "stats-insight-comments.json"; sourceTree = ""; }; + 01D251A42BB19ABE006349C0 /* stats-insight-tag-and-category.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "stats-insight-tag-and-category.json"; sourceTree = ""; }; + 01D251A62BB19AE1006349C0 /* stats-insight-streak.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "stats-insight-streak.json"; sourceTree = ""; }; 0847B92B2A4442730044D32F /* IPLocationRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPLocationRemote.swift; sourceTree = ""; }; 08C7493D2A45EA11000DA0E2 /* IPLocationRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPLocationRemoteTests.swift; sourceTree = ""; }; 0C1C08402B9CD79900E52F8C /* PostServiceRemoteExtended.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostServiceRemoteExtended.swift; sourceTree = ""; }; @@ -1485,6 +1505,22 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 01D251952BB18351006349C0 /* MockData */ = { + isa = PBXGroup; + children = ( + 01D251962BB18F2A006349C0 /* stats-insight-last-post.json */, + 01D251982BB1961D006349C0 /* stats-insight-followers.json */, + 01D2519A2BB19723006349C0 /* stats-insight.json */, + 01D2519C2BB19845006349C0 /* stats.json */, + 01D2519E2BB19957006349C0 /* stats-insight-publicize.json */, + 01D251A02BB19997006349C0 /* stats-insight-summary.json */, + 01D251A22BB19A22006349C0 /* stats-insight-comments.json */, + 01D251A42BB19ABE006349C0 /* stats-insight-tag-and-category.json */, + 01D251A62BB19AE1006349C0 /* stats-insight-streak.json */, + ); + path = MockData; + sourceTree = ""; + }; 08C7493C2A45E9E3000DA0E2 /* Privacy */ = { isa = PBXGroup; children = ( @@ -2709,7 +2745,9 @@ F3FF8A1D279C86FE00E5C90F /* Insights */ = { isa = PBXGroup; children = ( + 01D251952BB18351006349C0 /* MockData */, F3FF8A1E279C871A00E5C90F /* StatsDotComFollowersInsightTests.swift */, + 01D251932BB1834A006349C0 /* StatsInsightDecodingTests.swift */, ); path = Insights; sourceTree = ""; @@ -2957,6 +2995,7 @@ 3297E28D25647E0300287D21 /* jetpack-scan-enqueue-success.json in Resources */, FE50966A2A30A4F900DDD071 /* jetpack-social-403.json in Resources */, 7403A2F71EF06FEB00DED7DC /* me-settings-change-display-name-bad-json-failure.json in Resources */, + 01D251992BB1961D006349C0 /* stats-insight-followers.json in Resources */, 74B335E41F06F6B30053A184 /* WordPressComRestApiMultipleErrors.json in Resources */, 740B23E61F17FB4200067A2A /* xmlrpc-metaweblog-newpost-invalid-posttype-failure.xml in Resources */, 73D592FC21E550D300E4CF84 /* site-verticals-single.json in Resources */, @@ -3011,6 +3050,7 @@ 9AB6D64E218731AB0008F274 /* post-revisions-mapping-success.json in Resources */, 404057C7221B36070060250C /* stats-search-term-result.json in Resources */, 7403A2F41EF06FEB00DED7DC /* me-settings-auth-failure.json in Resources */, + 01D251972BB18F2A006349C0 /* stats-insight-last-post.json in Resources */, FA87FE0B24EB4419003FBEE3 /* reader-post-comments-subscribe-success.json in Resources */, 930999581F16598A00F006A1 /* get-single-theme-v1.1.json in Resources */, 74B335E61F06F6E90053A184 /* WordPressComRestApiMedia.json in Resources */, @@ -3023,6 +3063,7 @@ FFE247B420C891E6002DF3A2 /* WordPressComOAuthSuccess.json in Resources */, 404057D4221C5FC40060250C /* stats-countries-data.json in Resources */, 74D67F1F1F15C3240010C5ED /* people-send-invitation-success.json in Resources */, + 01D251A52BB19ABE006349C0 /* stats-insight-tag-and-category.json in Resources */, FFE247B020C891E6002DF3A2 /* WordPressComAuthenticateWithIDToken2FANeededSuccess.json in Resources */, 8B2F4BEB24ABCA700056C08A /* reader-cards-success.json in Resources */, 436D563E2118E34D00CEAA33 /* supported-states-success.json in Resources */, @@ -3071,6 +3112,7 @@ E1E89C681FD6B2E9006E7A33 /* plugin-directory-jetpack.json in Resources */, 74D67F201F15C3240010C5ED /* people-validate-invitation-failure.json in Resources */, FEEFD8B4280DD60200A3E261 /* blogging-prompts-success.json in Resources */, + 01D2519F2BB19957006349C0 /* stats-insight-publicize.json in Resources */, 74D67F161F15C2D70010C5ED /* site-users-update-role-bad-json-failure.json in Resources */, 93BD27661EE73442002BB00B /* me-success.json in Resources */, 404057DC221C9FD80060250C /* stats-referrer-data.json in Resources */, @@ -3096,9 +3138,11 @@ 984E34F422EF9465005C3F92 /* stats-file-downloads.json in Resources */, 93F50A3C1F226C0100B5BEBA /* WordPressComRestApiFailThrottled.json in Resources */, 740B23ED1F17FB7E00067A2A /* xmlrpc-bad-username-password-error.xml in Resources */, + 01D2519B2BB19723006349C0 /* stats-insight.json in Resources */, FA4261722570CC91003A01E2 /* activity-groups-bad-json-failure.json in Resources */, 3297E28C25647E0300287D21 /* jetpack-scan-enqueue-failure.json in Resources */, 9A881754223C01E400A3AB20 /* jetpack-service-error-activation-install.json in Resources */, + 01D251A12BB19997006349C0 /* stats-insight-summary.json in Resources */, 93BD275B1EE73442002BB00B /* is-available-username-failure.json in Resources */, FFE247B320C891E6002DF3A2 /* WordPressComOAuthNeeds2FAFail.json in Resources */, E13EE1491F332B8500C15787 /* site-plugins-success.json in Resources */, @@ -3122,6 +3166,7 @@ 9A2D0B2F225E1245009E585F /* jetpack-service-check-site-success.json in Resources */, 829BA4301FACF187003ADEEA /* activity-rewind-status-restore-failure.json in Resources */, 8B16CE962525045F007BE5A9 /* reader-posts-success.json in Resources */, + 01D251A32BB19A22006349C0 /* stats-insight-comments.json in Resources */, C92EFF7325E7444400E0308D /* common-starter-site-designs-empty-designs.json in Resources */, 74D67F391F15C3740010C5ED /* site-viewers-delete-bad-json.json in Resources */, BA8EA71524A0610200D5CC9F /* plugin-service-remote-featured-plugins-invalid.json in Resources */, @@ -3213,6 +3258,7 @@ FFE247B220C891E6002DF3A2 /* WordPressComAuthenticateWithIDTokenBearerTokenSuccess.json in Resources */, 74D67F371F15C3740010C5ED /* site-users-delete-success.json in Resources */, 74D67F341F15C3740010C5ED /* site-users-delete-bad-json-failure.json in Resources */, + 01D251A72BB19AE1006349C0 /* stats-insight-streak.json in Resources */, 7403A2FF1EF06FEB00DED7DC /* me-settings-revert-email-success.json in Resources */, FEB7A88F271873BD00A8CF85 /* reader-post-comments-update-notification-success.json in Resources */, 74B335E21F06F6730053A184 /* WordPressComRestApiFailRequestInvalidToken.json in Resources */, @@ -3246,6 +3292,7 @@ FA79F1872591730D00D235A9 /* backup-get-backup-status-complete-success.json in Resources */, 74D67F381F15C3740010C5ED /* site-viewers-delete-auth-failure.json in Resources */, 826016FA1F9FAF6300533B6C /* activity-log-success-1.json in Resources */, + 01D2519D2BB19845006349C0 /* stats.json in Resources */, 9A881752223C01E400A3AB20 /* jetpack-service-error-login-failure.json in Resources */, FEE4EF5F2730334D003CDA3C /* comments-v2-view-context-success.json in Resources */, 740B23E41F17FB4200067A2A /* xmlrpc-metaweblog-editpost-success.xml in Resources */, @@ -3615,6 +3662,7 @@ 9AB6D64A218727D60008F274 /* PostServiceRemoteRESTRevisionsTest.swift in Sources */, 01438D382B6A35FB0097D60A /* stats-summary.json in Sources */, 7430C9BD1F192C0F0051B8E6 /* ReaderPostServiceRemoteTests.m in Sources */, + 01D251942BB1834A006349C0 /* StatsInsightDecodingTests.swift in Sources */, 1DC837C229B9F04F009DCD4B /* RemoteVideoPressVideoTests.swift in Sources */, FAD1345125909DEA00A8FEB1 /* JetpackBackupServiceRemoteTests.swift in Sources */, 8B2F4BE924ABC9DC0056C08A /* ReaderPostServiceRemote+CardsTests.swift in Sources */, diff --git a/WordPressKit/Insights/StatsAnnualAndMostPopularTimeInsight.swift b/WordPressKit/Insights/StatsAnnualAndMostPopularTimeInsight.swift index 380947910..f1fa407e3 100644 --- a/WordPressKit/Insights/StatsAnnualAndMostPopularTimeInsight.swift +++ b/WordPressKit/Insights/StatsAnnualAndMostPopularTimeInsight.swift @@ -43,7 +43,6 @@ public struct StatsAnnualAndMostPopularTimeInsight: Codable { } } - extension StatsAnnualAndMostPopularTimeInsight: StatsInsightData { public static var pathComponent: String { return "stats/insights" diff --git a/WordPressKit/Insights/StatsLastPostInsight.swift b/WordPressKit/Insights/StatsLastPostInsight.swift index ac6c3edd3..c13845e0c 100644 --- a/WordPressKit/Insights/StatsLastPostInsight.swift +++ b/WordPressKit/Insights/StatsLastPostInsight.swift @@ -42,7 +42,7 @@ extension StatsLastPostInsight: StatsInsightData { } public init?(jsonDictionary: [String: AnyObject]) { - fatalError("This shouldn't be ever called, instead init?(jsonDictionary:_ views:_) be called instead.") + self.init(jsonDictionary: jsonDictionary, views: 0) } // MARK: - @@ -86,11 +86,11 @@ extension StatsLastPostInsight { throw DecodingError.dataCorruptedError(forKey: .publishedDate, in: container, debugDescription: "Date string does not match format expected by formatter.") } publishedDate = date - likesCount = try container.decodeIfPresent(Int.self, forKey: .likesCount) ?? 0 + likesCount = (try? container.decodeIfPresent(Int.self, forKey: .likesCount)) ?? 0 postID = try container.decode(Int.self, forKey: .postID) featuredImageURL = try? container.decodeIfPresent(URL.self, forKey: .featuredImageURL) let discussionContainer = try container.nestedContainer(keyedBy: DiscussionKeys.self, forKey: .discussion) - commentsCount = try discussionContainer.decode(Int.self, forKey: .commentsCount) + commentsCount = (try? discussionContainer.decodeIfPresent(Int.self, forKey: .commentsCount)) ?? 0 } } diff --git a/WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-comments.json b/WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-comments.json new file mode 100644 index 000000000..d5eed7054 --- /dev/null +++ b/WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-comments.json @@ -0,0 +1,71 @@ +{ + "date": "2024-03-26", + "authors": [ + { + "name": "novasupernova", + "link": "?user_id=3217654", + "gravatar": "https://1.gravatar.com/avatar/12345abcdef12345abcdef12345abcdef?s=64&d=https%3A%2F%2F1.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64", + "comments": "400", + "follow_data": { + "params": { + "stat-source": "stats_comments", + "follow-text": "Follow", + "following-text": "Following", + "following-hover-text": "Unfollow", + "blog_domain": "supernovanova.wordpress.com", + "blog_url": "http://supernovanova.wordpress.com", + "blog_id": 101010101, + "site_id": 101010101, + "blog_title": "Supernova's Space", + "is_following": true + }, + "type": "follow" + } + }, + { + "name": "Bob the Builder", + "link": "?user_id=98765432", + "gravatar": "https://2.gravatar.com/avatar/abcdef123456abcdef123456abcdef1234?s=64&d=https%3A%2F%2F2.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64", + "comments": "150", + "follow_data": { + "params": { + "stat-source": "stats_comments", + "follow-text": "Follow", + "following-text": "Following", + "following-hover-text": "Unfollow", + "blog_domain": "bobthebuilder.wordpress.com", + "blog_url": "http://bobthebuilder.wordpress.com", + "blog_id": 202020202, + "site_id": 202020202, + "blog_title": "Building Blocks", + "is_following": false + }, + "type": "follow" + } + } + ], + "posts": [ + { + "name": "Exploring the Depths of Space", + "link": "http://novasupernova.wordpress.com/2024/03/26/exploring-the-depths-of-space/", + "id": "98765", + "comments": "200" + }, + { + "name": "Building the Future", + "link": "http://bobthebuilder.wordpress.com/2024/03/26/building-the-future/", + "id": "54321", + "comments": "120" + } + ], + "monthly_comments": 320, + "total_comments": 150000, + "most_active_day": "2024-03-25 18:00:00", + "most_active_time": "17:00", + "most_commented_post": { + "name": "Exploring the Depths of Space", + "link": "http://novasupernova.wordpress.com/2024/03/26/exploring-the-depths-of-space/", + "id": "98765", + "comments": "200" + } +} diff --git a/WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-followers.json b/WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-followers.json new file mode 100644 index 000000000..e7e091aff --- /dev/null +++ b/WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-followers.json @@ -0,0 +1,138 @@ +{ + "page": 2, + "pages": 1, + "total": 15, + "total_email": 2156000, + "total_wpcom": 12, + "subscribers": [ + { + "avatar": "https://0.gravatar.com/avatar/abcd1234abcd1234abcd1234abcd1234?s=64&d=https%3A%2F%2F0.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64", + "label": "asdasd", + "login": "asdasdasd", + "ID": 789123456, + "url": "http://jd12123.wordpress.com", + "follow_data": { + "params": { + "stat-source": "stats_comments", + "follow-text": "Follow", + "following-text": "Following", + "following-hover-text": "Unfollow", + "blog_domain": "janedoe2024.wordpress.com", + "blog_url": "http://janedoe2024.wordpress.com", + "blog_id": 789123, + "site_id": 789123, + "blog_title": "Jane's World", + "is_following": true + }, + "type": "follow" + }, + "date_subscribed": "2024-03-26T08:15:00+00:00" + }, + { + "avatar": "https://1.gravatar.com/avatar/56785678567856785678567856785678?s=64&d=https%3A%2F%2F1.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64", + "label": "alexthegreat", + "login": "alexthegreat", + "ID": 987654321, + "url": null, + "follow_data": null, + "date_subscribed": "2024-03-26T07:45:20+00:00" + }, + { + "avatar": "https://2.gravatar.com/avatar/23452345234523452345234523452345?s=64&d=https%3A%2F%2F2.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64", + "label": "techwizard", + "login": "techwizard", + "ID": 456789123, + "url": "https://techwizard.dailyh.shop", + "follow_data": { + "params": { + "stat-source": "stats_comments", + "follow-text": "Follow", + "following-text": "Following", + "following-hover-text": "Unfollow", + "blog_domain": "techwizard.dailyh.shop", + "blog_url": "https://techwizard.dailyh.shop", + "blog_id": 456789, + "site_id": 456789, + "blog_title": "Tech Wizardry", + "is_following": false + }, + "type": "follow" + }, + "date_subscribed": "2024-03-26T09:30:45+00:00" + }, + { + "avatar": "https://3.gravatar.com/avatar/dfghdfghdfghdfghdfghdfghdfghdfgh?s=64&d=https%3A%2F%2F3.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64", + "label": "creative_mind", + "login": "creative_mind", + "ID": 123456789, + "url": null, + "follow_data": null, + "date_subscribed": "2024-03-26T10:02:17+00:00" + }, + { + "avatar": "https://0.gravatar.com/avatar/09f8d2c10abfde456789abcd1234efab?s=64&d=https%3A%2F%2F0.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64", + "label": "nature_lover", + "login": "nature_lover", + "ID": 987654789, + "url": "http://naturelover.wordpress.com", + "follow_data": { + "params": { + "stat-source": "stats_comments", + "follow-text": "Follow", + "following-text": "Following", + "following-hover-text": "Unfollow", + "blog_domain": "naturelover.wordpress.com", + "blog_url": "http://naturelover.wordpress.com", + "blog_id": 987654, + "site_id": 987654, + "blog_title": "Love For Nature", + "is_following": false + }, + "type": "follow" + }, + "date_subscribed": "2024-03-26T11:55:00+00:00" + }, + { + "avatar": "https://1.gravatar.com/avatar/abcdefabcdefabcdefabcdefabcdefab?s=64&d=https%3A%2F%2F1.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64", + "label": "world_traveler", + "login": "world_traveler", + "ID": 123123123, + "url": null, + "follow_data": null, + "date_subscribed": "2024-03-26T12:30:25+00:00" + }, + { + "avatar": "https://2.gravatar.com/avatar/bc12bc12bc12bc12bc12bc12bc12bc12?s=64&d=https%3A%2F%2F2.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64", + "label": "foodie_adventures", + "login": "foodie_adventures", + "ID": 321321321, + "url": "https://foodieadventures.dailyh.shop", + "follow_data": { + "params": { + "stat-source": "stats_comments", + "follow-text": "Follow", + "following-text": "Following", + "following-hover-text": "Unfollow", + "blog_domain": "foodieadventures.dailyh.shop", + "blog_url": "https://foodieadventures.dailyh.shop", + "blog_id": 321321, + "site_id": 321321, + "blog_title": "Adventures in Food", + "is_following": true + }, + "type": "follow" + }, + "date_subscribed": "2024-03-26T13:47:31+00:00" + }, + { + "avatar": "https://3.gravatar.com/avatar/1234abcd5678efgh9101112i13141516?s=64&d=https%3A%2F%2F3.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64", + "label": "bookworm_betty", + "login": "bookworm_betty", + "ID": 456456456, + "url": null, + "follow_data": null, + "date_subscribed": "2024-03-26T14:59:02+00:00" + } + ], + "is_owner_subscribing": false +} diff --git a/WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-last-post.json b/WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-last-post.json new file mode 100644 index 000000000..d7af866c7 --- /dev/null +++ b/WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-last-post.json @@ -0,0 +1,203 @@ +{ + "ID": 24601, + "site_ID": 987654, + "author": { + "ID": 654321, + "login": "janedoe", + "email": false, + "name": "Jane Doe", + "first_name": "Jane", + "last_name": "Doe", + "nice_name": "janedoeblog", + "URL": "", + "avatar_URL": "https://example.com/avatar/12345abcdef?s=96&d=identicon", + "profile_URL": "https://example.com/janedoe", + "site_ID": 123456 + }, + "date": "2024-07-04T12:00:00+00:00", + "modified": "2024-07-05T12:00:00+00:00", + "title": "Exploring the New Web in 60 Minutes", + "URL": "http://exampleblog.com/2024/07/04/exploring-the-new-web/", + "short_URL": "https://exmpl.co/abc123", + "content": "

Discover the latest web technologies and how they can enhance your online presence. Join us as we dive deep into the future of the web.

", + "excerpt": "

An insightful look into the future of web technologies.

", + "slug": "new-web-exploration", + "guid": "http://exampleblog.com/?p=24601", + "status": "publish", + "sticky": false, + "password": "", + "parent": false, + "type": "post", + "discussion": { + "comments_open": true, + "comment_status": "open", + "pings_open": true, + "ping_status": "open", + "comment_count": 42 + }, + "likes_enabled": true, + "sharing_enabled": true, + "like_count": 128, + "i_like": false, + "is_reblogged": false, + "is_following": false, + "global_ID": "def123456abc7890ghijkl", + "featured_image": "https://exampleblog.com/files/2024/07/new-web.jpg", + "post_thumbnail": { + "ID": 78910, + "URL": "https://exampleblog.com/files/2024/07/new-web-thumbnail.jpg", + "guid": "http://exampleblog.com/files/2024/07/new-web-thumbnail.jpg", + "mime_type": "image/jpeg", + "width": 1200, + "height": 628 + }, + "format": "standard", + "geo": false, + "menu_order": 0, + "page_template": "", + "publicize_URLs": [], + "terms": { + "category": { + "technology": { + "ID": 101, + "name": "Technology", + "slug": "technology", + "description": "", + "post_count": 10, + "parent": 0, + "meta": { + "links": { + "self": "https://api.example.com/sites/987654/categories/slug:technology", + "help": "https://api.example.com/sites/987654/categories/slug:technology/help", + "site": "https://api.example.com/sites/987654" + } + } + }, + "tutorials": { + "ID": 102, + "name": "Tutorials", + "slug": "tutorials", + "description": "", + "post_count": 5, + "parent": 0, + "meta": { + "links": { + "self": "https://api.example.com/sites/987654/categories/slug:tutorials", + "help": "https://api.example.com/sites/987654/categories/slug:tutorials/help", + "site": "https://api.example.com/sites/987654" + } + } + } + }, + "post_tag": { + "Web Development": { + "ID": 201, + "name": "Web Development", + "slug": "web-development", + "description": "", + "post_count": 15, + "meta": { + "links": { + "self": "https://api.example.com/sites/987654/tags/slug:web-development", + "help": "https://api.example.com/sites/987654/tags/slug:web-development/help", + "site": "https://api.example.com/sites/987654" + } + } + } + }, + "post_format": {}, + "mentions": {} + }, + "tags": { + "Web Development": { + "ID": 201, + "name": "Web Development", + "slug": "web-development", + "description": "", + "post_count": 15, + "meta": { + "links": { + "self": "https://api.example.com/sites/987654/tags/slug:web-development", + "help": "https://api.example.com/sites/987654/tags/slug:web-development/help", + "site": "https://api.example.com/sites/987654" + } + } + } + }, + "categories": { + "Tech News": { + "ID": 303, + "name": "Tech News", + "slug": "tech-news", + "description": "", + "post_count": 8, + "parent": 0, + "meta": { + "links": { + "self": "https://api.example.com/sites/987654/categories/slug:tech-news", + "help": "https://api.example.com/sites/987654/categories/slug:tech-news/help", + "site": "https://api.example.com/sites/987654" + } + } + } + }, + "attachments": { + "78910": { + "ID": 78910, + "URL": "https://exampleblog.com/files/2024/07/new-web-thumbnail.jpg", + "guid": "http://exampleblog.com/files/2024/07/new-web-thumbnail.jpg", + "date": "2024-07-04T10:30:00+00:00", + "post_ID": 24601, + "author_ID": 654321, + "file": "new-web-thumbnail.jpg", + "mime_type": "image/jpeg", + "title": "new-web-exploration-thumbnail", + "caption": "", + "description": "", + "alt": "Exploring the new web", + "thumbnails": {}, + "height": 628, + "width": 1200, + "exif": { + "aperture": "2.8", + "credit": "Jane Doe", + "camera": "ExampleCam 2000", + "caption": "", + "created_timestamp": "1593628800", + "copyright": "Jane Doe", + "focal_length": "35mm", + "iso": "100", + "shutter_speed": "1/500", + "title": "Exploring The New Web", + "orientation": "1", + "keywords": [] + }, + "meta": { + "links": { + "self": "https://api.example.com/sites/987654/media/78910", + "help": "https://api.example.com/sites/987654/media/78910/help", + "site": "https://api.example.com/sites/987654", + "parent": "https://api.example.com/sites/987654/posts/24601" + } + } + } + }, + "attachment_count": 1, + "metadata": [], + "meta": { + "links": { + "self": "https://api.example.com/sites/987654/posts/24601", + "help": "https://api.example.com/sites/987654/posts/24601/help", + "site": "https://api.example.com/sites/987654", + "replies": "https://api.example.com/sites/987654/posts/24601/replies/", + "likes": "https://api.example.com/sites/987654/posts/24601/likes/" + } + }, + "capabilities": { + "publish_post": true, + "delete_post": true, + "edit_post": true + }, + "other_URLs": {} +} + diff --git a/WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-publicize.json b/WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-publicize.json new file mode 100644 index 000000000..3fdc2fb8c --- /dev/null +++ b/WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-publicize.json @@ -0,0 +1,8 @@ +{ + "services": [ + { + "service": "twitter", + "followers": 3210876 + } + ] +} diff --git a/WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-streak.json b/WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-streak.json new file mode 100644 index 000000000..4d04d1c00 --- /dev/null +++ b/WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-streak.json @@ -0,0 +1,157 @@ +{ + "streak": { + "long": { + "start": "2024-03-15", + "end": "2024-03-17", + "length": 3 + }, + "current": { + "start": "2024-03-26", + "end": "2024-03-26", + "length": 1 + } + }, + "data": { + "1679926417": 1, + "1679970356": 1, + "1680627618": 1, + "1680648454": 1, + "1681156015": 1, + "1681746437": 1, + "1682020880": 1, + "1682802244": 1, + "1683027437": 1, + "1683558000": 1, + "1683730800": 1, + "1684516488": 1, + "1684871359": 1, + "1685112306": 1, + "1685458800": 1, + "1685538600": 1, + "1685625000": 1, + "1686077933": 1, + "1686234791": 1, + "1686585998": 1, + "1686923991": 1, + "1687372234": 1, + "1687443658": 1, + "1687893312": 1, + "1688059750": 1, + "1688757257": 1, + "1689093792": 1, + "1689277226": 1, + "1689968825": 1, + "1690313258": 1, + "1690492403": 1, + "1690899495": 1, + "1690988400": 1, + "1691424882": 1, + "1691593200": 1, + "1692028149": 1, + "1692213743": 1, + "1692384772": 1, + "1692638209": 1, + "1692729299": 1, + "1692901008": 1, + "1693002671": 1, + "1693325502": 1, + "1693500655": 1, + "1693938297": 1, + "1694109552": 1, + "1694454487": 1, + "1694709879": 1, + "1695049200": 1, + "1695309223": 1, + "1695838388": 1, + "1696013313": 1, + "1696517726": 1, + "1696893017": 1, + "1696967117": 1, + "1697039771": 1, + "1697485371": 1, + "1697649185": 1, + "1698141600": 1, + "1698257329": 1, + "1698681243": 1, + "1698938827": 1, + "1699293108": 1, + "1699388287": 1, + "1700084318": 1, + "1700581896": 1, + "1700784300": 1, + "1701380426": 1, + "1701705339": 1, + "1701804150": 1, + "1701972153": 1, + "1702414024": 1, + "1702667260": 1, + "1702914430": 1, + "1703009224": 1, + "1703090194": 1, + "1703692800": 1, + "1703865774": 1, + "1704470931": 1, + "1704735826": 1, + "1705003534": 1, + "1705353578": 1, + "1705942721": 1, + "1706120834": 1, + "1706724768": 1, + "1707424376": 1, + "1707757703": 1, + "1708450594": 1, + "1708625022": 1, + "1709037585": 1, + "1709066321": 1, + "1709240103": 1, + "1709583824": 1, + "1709759188": 1, + "1709823600": 1, + "1710272286": 1, + "1710347440": 1, + "1710441795": 1, + "1710763200": 1, + "1710854077": 1, + "1711030387": 1, + "1711100000": 1, + "1711200000": 1, + "1711300000": 1, + "1711400000": 1, + "1711500000": 1, + "1711600000": 1, + "1711700000": 1, + "1711800000": 1, + "1711900000": 1, + "1712000000": 1, + "1712100000": 1, + "1712200000": 1, + "1712300000": 1, + "1712400000": 1, + "1712500000": 1, + "1712600000": 1, + "1712700000": 1, + "1712800000": 1, + "1712900000": 1, + "1713000000": 1, + "1713100000": 1, + "1713200000": 1, + "1713300000": 1, + "1713400000": 1, + "1713500000": 1, + "1713600000": 1, + "1713700000": 1, + "1713800000": 1, + "1713900000": 1, + "1714000000": 1, + "1714100000": 1, + "1714200000": 1, + "1714300000": 1, + "1714400000": 1, + "1714500000": 1, + "1714600000": 1, + "1714700000": 1, + "1714800000": 1, + "1714900000": 1, + "1715000000": 1 + } +} diff --git a/WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-summary.json b/WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-summary.json new file mode 100644 index 000000000..b5f915d53 --- /dev/null +++ b/WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-summary.json @@ -0,0 +1,10 @@ +{ + "date": "2024-03-26", + "period": "day", + "views": 1234, + "visitors": 910, + "likes": 65, + "reblogs": 2, + "comments": 3, + "followers": 104796150 +} diff --git a/WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-tag-and-category.json b/WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-tag-and-category.json new file mode 100644 index 000000000..838985fff --- /dev/null +++ b/WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight-tag-and-category.json @@ -0,0 +1,105 @@ +{ + "date": "2024-03-26", + "tags": [ + { + "tags": [ + { + "type": "category", + "name": "Innovations", + "link": "http://test.site.com/category/innovations/" + } + ], + "views": 20500 + }, + { + "tags": [ + { + "type": "category", + "name": "Tech Updates", + "link": "http://test.site.com/category/tech-updates/" + } + ], + "views": 19200 + }, + { + "tags": [ + { + "type": "category", + "name": "Product News", + "link": "http://test.site.com/category/product-news/" + } + ], + "views": 18200 + }, + { + "tags": [ + { + "type": "tag", + "name": "Web Design", + "link": "http://test.site.com/tag/web-design/" + } + ], + "views": 8000 + }, + { + "tags": [ + { + "type": "tag", + "name": "SEO", + "link": "http://test.site.com/tag/seo/" + } + ], + "views": 3900 + }, + { + "tags": [ + { + "type": "category", + "name": "Tutorials", + "link": "http://test.site.com/category/tutorials/" + } + ], + "views": 3750 + }, + { + "tags": [ + { + "type": "category", + "name": "User Guides", + "link": "http://test.site.com/category/user-guides/" + } + ], + "views": 3300 + }, + { + "tags": [ + { + "type": "category", + "name": "Design Inspiration", + "link": "http://test.site.com/category/design-inspiration/" + } + ], + "views": 2450 + }, + { + "tags": [ + { + "type": "tag", + "name": "E-commerce", + "link": "http://test.site.com/tag/e-commerce/" + } + ], + "views": 2290 + }, + { + "tags": [ + { + "type": "category", + "name": "Success Stories", + "link": "http://test.site.com/category/success-stories/" + } + ], + "views": 2180 + } + ] +} diff --git a/WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight.json b/WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight.json new file mode 100644 index 000000000..13fb630b2 --- /dev/null +++ b/WordPressKitTests/Models/Stats/V2/Insights/MockData/stats-insight.json @@ -0,0 +1,94 @@ +{ + "highest_hour": 14, + "highest_hour_percent": 6.4, + "highest_day_of_week": 3, + "highest_day_percent": 23.7, + "days": { + "0": 21000, + "1": 40000, + "2": 35000, + "3": 45000, + "4": 30000, + "5": 20000, + "6": 25000 + }, + "hours": { + "10": 5000, + "11": 5200, + "12": 5300, + "13": 5400, + "14": 6000, + "15": 6100, + "16": 5800, + "17": 5700, + "18": 5900, + "19": 5600, + "20": 5550, + "21": 5450, + "22": 5350, + "23": 5250, + "06": 5150, + "09": 5050, + "07": 4950, + "08": 4850, + "00": 4750, + "01": 4650, + "02": 4550, + "05": 4450, + "04": 4350, + "03": 4250 + }, + "hourly_views": { + "2024-03-24 12:00:00": 80, + "2024-03-24 13:00:00": 90, + "2024-03-24 14:00:00": 100, + "2024-03-24 15:00:00": 110, + "2024-03-24 16:00:00": 120, + "2024-03-24 17:00:00": 130, + "2024-03-24 18:00:00": 140, + "2024-03-24 19:00:00": 150, + "2024-03-24 20:00:00": 160, + "2024-03-24 21:00:00": 170, + "2024-03-24 22:00:00": 180, + "2024-03-24 23:00:00": 190, + "2024-03-25 00:00:00": 200, + "2024-03-25 01:00:00": 210, + "2024-03-25 02:00:00": 220, + "2024-03-25 03:00:00": 230, + "2024-03-25 04:00:00": 240, + "2024-03-25 05:00:00": 250, + "2024-03-25 06:00:00": 260, + "2024-03-25 07:00:00": 270, + "2024-03-25 08:00:00": 280, + "2024-03-25 09:00:00": 290, + "2024-03-25 10:00:00": 300, + "2024-03-25 11:00:00": 310 + }, + "years": [ + { + "year": "2023", + "total_posts": 75, + "total_words": 75000, + "avg_words": 1000, + "total_likes": 1500, + "avg_likes": 20, + "total_comments": 300, + "avg_comments": 4, + "total_images": 150, + "avg_images": 2 + }, + { + "year": "2024", + "total_posts": 80, + "total_words": 80000, + "avg_words": 1000, + "total_likes": 1600, + "avg_likes": 20, + "total_comments": 320, + "avg_comments": 4, + "total_images": 160, + "avg_images": 2 + } + ] +} + diff --git a/WordPressKitTests/Models/Stats/V2/Insights/MockData/stats.json b/WordPressKitTests/Models/Stats/V2/Insights/MockData/stats.json new file mode 100644 index 000000000..85ad0e66a --- /dev/null +++ b/WordPressKitTests/Models/Stats/V2/Insights/MockData/stats.json @@ -0,0 +1,277 @@ +{ + "date": "2024-03-26", + "stats": { + "visitors_today": 950, + "visitors_yesterday": 1200, + "visitors": 19000000, + "views_today": 1200, + "views_yesterday": 2500, + "views_best_day": "2012-05-20", + "views_best_day_total": 110000, + "views": 63000000, + "comments": 130000, + "posts": 1800, + "followers_blog": 108000000, + "followers_comments": 18000, + "comments_per_month": 240, + "comments_most_active_recent_day": "2024-01-01 00:00:00", + "comments_most_active_time": "17:00", + "comments_spam": 400000, + "categories": 105, + "tags": 1600, + "shares": 4900000, + "shares_jetpack-whatsapp": 610000, + "shares_twitter": 600000, + "shares_facebook": 470000, + "shares_reddit": 470000, + "shares_tumblr": 465000, + "shares_pocket": 450000, + "shares_pinterest": 435000, + "shares_linkedin": 427000, + "shares_press-this": 12600, + "shares_email": 6300, + "shares_mastodon": 50, + "shares_print": 1 + }, + "visits": { + "date": "2024-03-26", + "unit": "day", + "fields": [ + "period", + "views", + "visitors" + ], + "data": [ + [ + "2024-02-26", + 1800, + 1300 + ], + [ + "2024-02-27", + 3000, + 2000 + ], + [ + "2024-02-28", + 5500, + 3500 + ], + [ + "2024-03-01", + 5000, + 3400 + ], + [ + "2024-03-02", + 4200, + 3100 + ], + [ + "2024-03-03", + 3200, + 2300 + ], + [ + "2024-03-04", + 2200, + 1600 + ], + [ + "2024-03-05", + 2500, + 1400 + ], + [ + "2024-03-06", + 4900, + 3200 + ], + [ + "2024-03-07", + 10500, + 7700 + ], + [ + "2024-03-08", + 7000, + 5300 + ], + [ + "2024-03-09", + 9500, + 6700 + ], + [ + "2024-03-10", + 3600, + 2500 + ], + [ + "2024-03-11", + 2900, + 1800 + ], + [ + "2024-03-12", + 2000, + 1500 + ], + [ + "2024-03-13", + 2800, + 1900 + ], + [ + "2024-03-14", + 4100, + 2800 + ], + [ + "2024-03-15", + 4000, + 2100 + ], + [ + "2024-03-16", + 3200, + 2100 + ], + [ + "2024-03-17", + 2900, + 2200 + ], + [ + "2024-03-18", + 2400, + 1600 + ], + [ + "2024-03-19", + 1900, + 1350 + ], + [ + "2024-03-20", + 5700, + 4500 + ], + [ + "2024-03-21", + 13500, + 11700 + ], + [ + "2024-03-22", + 6600, + 5400 + ], + [ + "2024-03-23", + 4500, + 3200 + ], + [ + "2024-03-24", + 3400, + 1900 + ], + [ + "2024-03-25", + 2600, + 1300 + ], + [ + "2024-03-26", + 2800, + 1200 + ], + [ + "2024-03-27", + 3000, + 1400 + ], + [ + "2024-03-28", + 3200, + 1500 + ], + [ + "2024-03-29", + 3400, + 1600 + ], + [ + "2024-03-30", + 3600, + 1700 + ], + [ + "2024-03-31", + 3800, + 1800 + ], + [ + "2024-04-01", + 4000, + 1900 + ], + [ + "2024-04-02", + 4200, + 2000 + ], + [ + "2024-04-03", + 4400, + 2100 + ], + [ + "2024-04-04", + 4600, + 2200 + ], + [ + "2024-04-05", + 4800, + 2300 + ], + [ + "2024-04-06", + 5000, + 2400 + ], + [ + "2024-04-07", + 5200, + 2500 + ], + [ + "2024-04-08", + 5400, + 2600 + ], + [ + "2024-04-09", + 5600, + 2700 + ], + [ + "2024-04-10", + 5800, + 2800 + ], + [ + "2024-04-11", + 6000, + 2900 + ], + [ + "2024-04-12", + 6200, + 3000 + ] + ] + } +} diff --git a/WordPressKitTests/Models/Stats/V2/Insights/StatsInsightDecodingTests.swift b/WordPressKitTests/Models/Stats/V2/Insights/StatsInsightDecodingTests.swift new file mode 100644 index 000000000..266955143 --- /dev/null +++ b/WordPressKitTests/Models/Stats/V2/Insights/StatsInsightDecodingTests.swift @@ -0,0 +1,38 @@ +import XCTest +import WordPressKit + +final class StatsInsightDecodingTests: XCTestCase { + private struct StatsInsightEntity { + let type: StatsInsightData.Type + let fileName: String + } + + private let testEntities: [StatsInsightEntity] = [ + .init(type: StatsLastPostInsight.self, fileName: "stats-insight-last-post"), + .init(type: StatsDotComFollowersInsight.self, fileName: "stats-insight-followers"), + .init(type: StatsEmailFollowersInsight.self, fileName: "stats-insight-followers"), + .init(type: StatsAllTimesInsight.self, fileName: "stats"), + .init(type: StatsAllAnnualInsight.self, fileName: "stats-insight"), + .init(type: StatsAnnualAndMostPopularTimeInsight.self, fileName: "stats-insight"), + .init(type: StatsPublicizeInsight.self, fileName: "stats-insight-publicize"), + .init(type: StatsTodayInsight.self, fileName: "stats-insight-summary"), + .init(type: StatsCommentsInsight.self, fileName: "stats-insight-comments"), + .init(type: StatsTagsAndCategoriesInsight.self, fileName: "stats-insight-tag-and-category"), + .init(type: StatsPostingStreakInsight.self, fileName: "stats-insight-streak"), + ] + + func testStatsInsightEntitiesDecoding() throws { + for entitity in testEntities { + let json = getJSON(entitity.fileName) + XCTAssertNotNil(entitity.type.init(jsonDictionary: json), "Entity \(entitity.type) cannot be decoded from \(entitity.fileName)") + } + } +} + +private extension StatsInsightDecodingTests { + func getJSON(_ fileName: String) -> [String: AnyObject] { + let path = Bundle(for: type(of: self)).path(forResource: fileName, ofType: "json")! + let data = try! Data(contentsOf: URL(fileURLWithPath: path)) + return try! JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String: AnyObject] + } +} From 62b5c638223e4b308a595e1e2a51ff1d6b15df0c Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 25 Mar 2024 13:54:20 +0200 Subject: [PATCH 16/22] Update CHANGELOG.md --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dd443dc7..33c425b96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,7 @@ _None._ ### Breaking Changes -_None._ +- Changes the structure of `StatsAnnualAndMostPopularTimeInsight` to more accurately reflect JSON response. [#763] ### New Features @@ -46,7 +46,7 @@ _None._ ### Internal Changes -_None._ +- Improved parsing using Codable for Stats Insight entities. [#763] ## 14.1.0 From 2ac4c46135badddae8bbfb384fb19e0c52f4ec9e Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:24:26 +0200 Subject: [PATCH 17/22] Update StatsAnnualAndMostPopularTimeInsight decoding to be more permitting --- .../StatsAnnualAndMostPopularTimeInsight.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/WordPressKit/Insights/StatsAnnualAndMostPopularTimeInsight.swift b/WordPressKit/Insights/StatsAnnualAndMostPopularTimeInsight.swift index f1fa407e3..628104fd8 100644 --- a/WordPressKit/Insights/StatsAnnualAndMostPopularTimeInsight.swift +++ b/WordPressKit/Insights/StatsAnnualAndMostPopularTimeInsight.swift @@ -40,6 +40,20 @@ public struct StatsAnnualAndMostPopularTimeInsight: Codable { case totalImages = "total_images" case averageImages = "avg_images" } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + year = try container.decode(String.self, forKey: .year) + totalPosts = (try? container.decodeIfPresent(Int.self, forKey: .totalPosts)) ?? 0 + totalWords = (try? container.decode(Int.self, forKey: .totalWords)) ?? 0 + averageWords = (try? container.decode(Double.self, forKey: .averageWords)) ?? 0 + totalLikes = (try? container.decode(Int.self, forKey: .totalLikes)) ?? 0 + averageLikes = (try? container.decode(Double.self, forKey: .averageLikes)) ?? 0 + totalComments = (try? container.decode(Int.self, forKey: .totalComments)) ?? 0 + averageComments = (try? container.decode(Double.self, forKey: .averageComments)) ?? 0 + totalImages = (try? container.decode(Int.self, forKey: .totalImages)) ?? 0 + averageImages = (try? container.decode(Double.self, forKey: .averageImages)) ?? 0 + } } } From 2cc6127f235f6efd7e7a3ccef7d2d0b4f6e31260 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:24:52 +0200 Subject: [PATCH 18/22] Update StatsDotComFollowersInsightTests to reflect more permissive decoding stratregy --- .../Stats/V2/Insights/StatsDotComFollowersInsightTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPressKitTests/Models/Stats/V2/Insights/StatsDotComFollowersInsightTests.swift b/WordPressKitTests/Models/Stats/V2/Insights/StatsDotComFollowersInsightTests.swift index a0825bb0f..fe9a1847d 100644 --- a/WordPressKitTests/Models/Stats/V2/Insights/StatsDotComFollowersInsightTests.swift +++ b/WordPressKitTests/Models/Stats/V2/Insights/StatsDotComFollowersInsightTests.swift @@ -86,7 +86,7 @@ class StatsDotComFollowersInsightTests: XCTestCase { let follower = StatsFollower(jsonDictionary: jsonDictionary) // Then - XCTAssertNil(follower) + XCTAssertNotNil(follower) } } From caa52b9c24bbe8f272f0e26f682bb29324c5ad22 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:25:04 +0200 Subject: [PATCH 19/22] Update StatsAnnualAndMostPopularTimeInsightDecodingTests to reflect more permissive decoding stratregy --- .../StatsAnnualAndMostPopularTimeInsightDecodingTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPressKitTests/StatsAnnualAndMostPopularTimeInsightDecodingTests.swift b/WordPressKitTests/StatsAnnualAndMostPopularTimeInsightDecodingTests.swift index 6e4d6470a..34b7f661b 100644 --- a/WordPressKitTests/StatsAnnualAndMostPopularTimeInsightDecodingTests.swift +++ b/WordPressKitTests/StatsAnnualAndMostPopularTimeInsightDecodingTests.swift @@ -33,7 +33,7 @@ final class StatsAnnualAndMostPopularTimeInsightDecodingTests: XCTestCase { let insight = StatsAnnualAndMostPopularTimeInsight(jsonDictionary: json as [String: AnyObject]) // Then - XCTAssertNil(insight) + XCTAssertNotNil(insight) } func testDecodingDecimalPercentagesRoundsSuccessful() { From 9ddb4673d5c292c507a5a552815d220113df3dc3 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:30:59 +0200 Subject: [PATCH 20/22] Swiftlint updates --- .../Insights/StatsCommentsInsight.swift | 34 ++++++++++--------- .../StatsDotComFollowersInsight.swift | 2 +- .../Insights/StatsPublicizeInsight.swift | 18 +++++----- 3 files changed, 29 insertions(+), 25 deletions(-) diff --git a/WordPressKit/Insights/StatsCommentsInsight.swift b/WordPressKit/Insights/StatsCommentsInsight.swift index f3703e9a2..d18f58563 100644 --- a/WordPressKit/Insights/StatsCommentsInsight.swift +++ b/WordPressKit/Insights/StatsCommentsInsight.swift @@ -67,20 +67,6 @@ public struct StatsTopCommentsPost: Codable { } private extension StatsTopCommentsAuthor { - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let name = try container.decode(String.self, forKey: .name) - let commentCount: Int - if let comments = try? container.decodeIfPresent(String.self, forKey: .commentCount) { - commentCount = Int(comments) ?? 0 - } else { - commentCount = 0 - } - let iconURL = try container.decodeIfPresent(String.self, forKey: .iconURL) - - self.init(name: name, avatar: iconURL, commentCount: commentCount) - } - init(name: String, avatar: String?, commentCount: Int) { let url: URL? @@ -97,8 +83,24 @@ private extension StatsTopCommentsAuthor { } } -private extension StatsTopCommentsPost { - public init(from decoder: Decoder) throws { +public extension StatsTopCommentsAuthor { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let name = try container.decode(String.self, forKey: .name) + let commentCount: Int + if let comments = try? container.decodeIfPresent(String.self, forKey: .commentCount) { + commentCount = Int(comments) ?? 0 + } else { + commentCount = 0 + } + let iconURL = try container.decodeIfPresent(String.self, forKey: .iconURL) + + self.init(name: name, avatar: iconURL, commentCount: commentCount) + } +} + +public extension StatsTopCommentsPost { + init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let name = try container.decode(String.self, forKey: .name) let postID = try container.decode(String.self, forKey: .postID) diff --git a/WordPressKit/Insights/StatsDotComFollowersInsight.swift b/WordPressKit/Insights/StatsDotComFollowersInsight.swift index 6340282de..32d6e55f7 100644 --- a/WordPressKit/Insights/StatsDotComFollowersInsight.swift +++ b/WordPressKit/Insights/StatsDotComFollowersInsight.swift @@ -59,7 +59,7 @@ extension StatsFollower { self.name = try container.decode(String.self, forKey: .name) if let id = try? container.decodeIfPresent(Int.self, forKey: .id) { self.id = "\(id)" - } else if let id = try? container.decodeIfPresent(String.self, forKey: .id) { + } else if let id = try? container.decodeIfPresent(String.self, forKey: .id) { self.id = id } else { self.id = nil diff --git a/WordPressKit/Insights/StatsPublicizeInsight.swift b/WordPressKit/Insights/StatsPublicizeInsight.swift index e4e5752d6..6db08b0c3 100644 --- a/WordPressKit/Insights/StatsPublicizeInsight.swift +++ b/WordPressKit/Insights/StatsPublicizeInsight.swift @@ -38,14 +38,6 @@ public struct StatsPublicizeService: Codable { } private extension StatsPublicizeService { - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let name = try container.decode(String.self, forKey: .name) - let followers = (try? container.decodeIfPresent(Int.self, forKey: .followers)) ?? 0 - - self.init(name: name, followers: followers) - } - init(name: String, followers: Int) { let niceName: String let icon: URL? @@ -79,3 +71,13 @@ private extension StatsPublicizeService { self.iconURL = icon } } + +public extension StatsPublicizeService { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let name = try container.decode(String.self, forKey: .name) + let followers = (try? container.decodeIfPresent(Int.self, forKey: .followers)) ?? 0 + + self.init(name: name, followers: followers) + } +} From d617a412132fd12be4b82a71fb0577fa9256eb42 Mon Sep 17 00:00:00 2001 From: Paul Von Schrottky Date: Fri, 29 Mar 2024 22:00:10 -0400 Subject: [PATCH 21/22] Add back tests and mocks --- WordPressKit.xcodeproj/project.pbxproj | 48 ++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index 27429216c..a32c79033 100644 --- a/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit.xcodeproj/project.pbxproj @@ -557,6 +557,16 @@ AB49D0B325D1B4D80084905B /* post-likes-failure.json in Resources */ = {isa = PBXBuildFile; fileRef = AB49D0B225D1B4D80084905B /* post-likes-failure.json */; }; ABD95B7F25DD6C4B00735BEE /* CommentServiceRemoteRESTLikesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD95B7E25DD6C4B00735BEE /* CommentServiceRemoteRESTLikesTests.swift */; }; ABD95B8525DD6DA200735BEE /* comment-likes-success.json in Resources */ = {isa = PBXBuildFile; fileRef = ABD95B8425DD6DA200735BEE /* comment-likes-success.json */; }; + B04D8C042BB7895A002717A2 /* stats-insight-comments.json in Resources */ = {isa = PBXBuildFile; fileRef = B04D8BF92BB7895A002717A2 /* stats-insight-comments.json */; }; + B04D8C052BB7895A002717A2 /* stats-insight-followers.json in Resources */ = {isa = PBXBuildFile; fileRef = B04D8BFA2BB7895A002717A2 /* stats-insight-followers.json */; }; + B04D8C062BB7895A002717A2 /* stats-insight-last-post.json in Resources */ = {isa = PBXBuildFile; fileRef = B04D8BFB2BB7895A002717A2 /* stats-insight-last-post.json */; }; + B04D8C072BB7895A002717A2 /* stats-insight-publicize.json in Resources */ = {isa = PBXBuildFile; fileRef = B04D8BFC2BB7895A002717A2 /* stats-insight-publicize.json */; }; + B04D8C082BB7895A002717A2 /* stats-insight-streak.json in Resources */ = {isa = PBXBuildFile; fileRef = B04D8BFD2BB7895A002717A2 /* stats-insight-streak.json */; }; + B04D8C092BB7895A002717A2 /* stats-insight-summary.json in Resources */ = {isa = PBXBuildFile; fileRef = B04D8BFE2BB7895A002717A2 /* stats-insight-summary.json */; }; + B04D8C0A2BB7895A002717A2 /* stats-insight-tag-and-category.json in Resources */ = {isa = PBXBuildFile; fileRef = B04D8BFF2BB7895A002717A2 /* stats-insight-tag-and-category.json */; }; + B04D8C0B2BB7895A002717A2 /* stats-insight.json in Resources */ = {isa = PBXBuildFile; fileRef = B04D8C002BB7895A002717A2 /* stats-insight.json */; }; + B04D8C0C2BB7895A002717A2 /* stats.json in Resources */ = {isa = PBXBuildFile; fileRef = B04D8C012BB7895A002717A2 /* stats.json */; }; + B04D8C0D2BB7895A002717A2 /* StatsInsightDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B04D8C032BB7895A002717A2 /* StatsInsightDecodingTests.swift */; }; B5969E1C20A49AC4005E9DF1 /* NSString+MD5.h in Headers */ = {isa = PBXBuildFile; fileRef = B5969E1920A49AC4005E9DF1 /* NSString+MD5.h */; settings = {ATTRIBUTES = (Public, ); }; }; B5969E1D20A49AC4005E9DF1 /* NSString+MD5.m in Sources */ = {isa = PBXBuildFile; fileRef = B5969E1A20A49AC4005E9DF1 /* NSString+MD5.m */; }; B5A4822B20AC6C0B009D95F6 /* WPKitLogging.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A4822A20AC6C0B009D95F6 /* WPKitLogging.swift */; }; @@ -1296,6 +1306,16 @@ AB49D0B225D1B4D80084905B /* post-likes-failure.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "post-likes-failure.json"; sourceTree = ""; }; ABD95B7E25DD6C4B00735BEE /* CommentServiceRemoteRESTLikesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentServiceRemoteRESTLikesTests.swift; sourceTree = ""; }; ABD95B8425DD6DA200735BEE /* comment-likes-success.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "comment-likes-success.json"; sourceTree = ""; }; + B04D8BF92BB7895A002717A2 /* stats-insight-comments.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-insight-comments.json"; sourceTree = ""; }; + B04D8BFA2BB7895A002717A2 /* stats-insight-followers.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-insight-followers.json"; sourceTree = ""; }; + B04D8BFB2BB7895A002717A2 /* stats-insight-last-post.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-insight-last-post.json"; sourceTree = ""; }; + B04D8BFC2BB7895A002717A2 /* stats-insight-publicize.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-insight-publicize.json"; sourceTree = ""; }; + B04D8BFD2BB7895A002717A2 /* stats-insight-streak.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-insight-streak.json"; sourceTree = ""; }; + B04D8BFE2BB7895A002717A2 /* stats-insight-summary.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-insight-summary.json"; sourceTree = ""; }; + B04D8BFF2BB7895A002717A2 /* stats-insight-tag-and-category.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-insight-tag-and-category.json"; sourceTree = ""; }; + B04D8C002BB7895A002717A2 /* stats-insight.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-insight.json"; sourceTree = ""; }; + B04D8C012BB7895A002717A2 /* stats.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = stats.json; sourceTree = ""; }; + B04D8C032BB7895A002717A2 /* StatsInsightDecodingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatsInsightDecodingTests.swift; sourceTree = ""; }; B5969E1920A49AC4005E9DF1 /* NSString+MD5.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+MD5.h"; sourceTree = ""; }; B5969E1A20A49AC4005E9DF1 /* NSString+MD5.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+MD5.m"; sourceTree = ""; }; B5A4822A20AC6C0B009D95F6 /* WPKitLogging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WPKitLogging.swift; sourceTree = ""; }; @@ -2419,6 +2439,22 @@ path = "Mock Data"; sourceTree = ""; }; + B04D8C022BB7895A002717A2 /* MockData */ = { + isa = PBXGroup; + children = ( + B04D8BF92BB7895A002717A2 /* stats-insight-comments.json */, + B04D8BFA2BB7895A002717A2 /* stats-insight-followers.json */, + B04D8BFB2BB7895A002717A2 /* stats-insight-last-post.json */, + B04D8BFC2BB7895A002717A2 /* stats-insight-publicize.json */, + B04D8BFD2BB7895A002717A2 /* stats-insight-streak.json */, + B04D8BFE2BB7895A002717A2 /* stats-insight-summary.json */, + B04D8BFF2BB7895A002717A2 /* stats-insight-tag-and-category.json */, + B04D8C002BB7895A002717A2 /* stats-insight.json */, + B04D8C012BB7895A002717A2 /* stats.json */, + ); + path = MockData; + sourceTree = ""; + }; B5A4821F20AC6B5C009D95F6 /* Private */ = { isa = PBXGroup; children = ( @@ -2490,6 +2526,8 @@ F3FF8A1D279C86FE00E5C90F /* Insights */ = { isa = PBXGroup; children = ( + B04D8C022BB7895A002717A2 /* MockData */, + B04D8C032BB7895A002717A2 /* StatsInsightDecodingTests.swift */, F3FF8A1E279C871A00E5C90F /* StatsDotComFollowersInsightTests.swift */, ); path = Insights; @@ -2755,6 +2793,7 @@ 9A881756223C01E400A3AB20 /* jetpack-service-error-activation-failure.json in Resources */, 740B23E71F17FB4200067A2A /* xmlrpc-metaweblog-newpost-success.xml in Resources */, 74FC6F411F191C1D00112505 /* notifications-last-seen.json in Resources */, + B04D8C092BB7895A002717A2 /* stats-insight-summary.json in Resources */, 74C473BD1EF329CA009918F2 /* site-export-auth-failure.json in Resources */, E6C1E84A1EF21FC100D139D9 /* is-passwordless-account-success.json in Resources */, BA3F139224A0AB54006367A3 /* plugin-install-already-installed.json in Resources */, @@ -2794,6 +2833,7 @@ FFE247B420C891E6002DF3A2 /* WordPressComOAuthSuccess.json in Resources */, 404057D4221C5FC40060250C /* stats-countries-data.json in Resources */, 74D67F1F1F15C3240010C5ED /* people-send-invitation-success.json in Resources */, + B04D8C0A2BB7895A002717A2 /* stats-insight-tag-and-category.json in Resources */, FFE247B020C891E6002DF3A2 /* WordPressComAuthenticateWithIDToken2FANeededSuccess.json in Resources */, 8B2F4BEB24ABCA700056C08A /* reader-cards-success.json in Resources */, 436D563E2118E34D00CEAA33 /* supported-states-success.json in Resources */, @@ -2818,16 +2858,20 @@ 74D67F141F15C2D70010C5ED /* site-roles-bad-json-failure.json in Resources */, 9A2D0B31225E1245009E585F /* jetpack-service-check-site-failure-data.json in Resources */, 40E4698B2017C2840030DB5F /* plugin-directory-popular.json in Resources */, + B04D8C0B2BB7895A002717A2 /* stats-insight.json in Resources */, 74C473B91EF325F6009918F2 /* site-delete-missing-status-failure.json in Resources */, 7403A2FD1EF06FEB00DED7DC /* me-settings-change-primary-site-success.json in Resources */, + B04D8C082BB7895A002717A2 /* stats-insight-streak.json in Resources */, 74C473B71EF3229B009918F2 /* site-delete-unexpected-json-failure.json in Resources */, 3297E2852564746800287D21 /* jetpack-scan-unavailable.json in Resources */, + B04D8C072BB7895A002717A2 /* stats-insight-publicize.json in Resources */, AB49D0B325D1B4D80084905B /* post-likes-failure.json in Resources */, 9A2D0B30225E1245009E585F /* jetpack-service-check-site-success-no-jetpack.json in Resources */, FA79F1882591730D00D235A9 /* backup-prepare-backup-success.json in Resources */, E6B0461225E5B6F500DF6F4F /* sites-invites.json in Resources */, 9A88174E223C01E400A3AB20 /* jetpack-service-error-unknown.json in Resources */, 40819771221DFDB700A298E4 /* stats-posts-data.json in Resources */, + B04D8C062BB7895A002717A2 /* stats-insight-last-post.json in Resources */, 740B23EE1F17FB7E00067A2A /* xmlrpc-malformed-request-xml-error.xml in Resources */, 826016F91F9FAF6300533B6C /* activity-log-success-3.json in Resources */, FE20A6A8282BC83A0025E975 /* blogging-prompts-settings-update-with-response.json in Resources */, @@ -2899,6 +2943,7 @@ E6B0461525E5B6F500DF6F4F /* sites-invites-links-disable-empty.json in Resources */, 93BD27631EE73442002BB00B /* me-sites-visibility-bad-json-failure.json in Resources */, 17BF9A7220C7E18200BF57D2 /* reader-site-search-success-hasmore.json in Resources */, + B04D8C0C2BB7895A002717A2 /* stats.json in Resources */, FE5096632A309DE000DDD071 /* jetpack-social-no-publicize.json in Resources */, 9AB6D64B21872A0D0008F274 /* post-revisions-success.json in Resources */, 1DF972C029B107E7007A72BC /* videopress-public-video.json in Resources */, @@ -2908,6 +2953,7 @@ 3297E1E42564683600287D21 /* jetpack-scan-in-progress.json in Resources */, E689431E21B0A1A800C5E4A7 /* plans-mobile-success.json in Resources */, BA8EA71724A0763900D5CC9F /* site-plugins-malformed.json in Resources */, + B04D8C052BB7895A002717A2 /* stats-insight-followers.json in Resources */, 74D67F3B1F15C3740010C5ED /* site-viewers-delete-success.json in Resources */, 93BD275F1EE73442002BB00B /* me-sites-auth-failure.json in Resources */, 74585BA11F0D6F5300E7E667 /* domain-service-empty.json in Resources */, @@ -2920,6 +2966,7 @@ 74D67F151F15C2D70010C5ED /* site-roles-success.json in Resources */, 8BFB4E6625B07905004D026E /* jetpack-capabilities-34197361-success.json in Resources */, 9A881753223C01E400A3AB20 /* jetpack-service-error-site-is-jetpack.json in Resources */, + B04D8C042BB7895A002717A2 /* stats-insight-comments.json in Resources */, D8DB404221EF22B500B8238E /* site-segments-multiple.json in Resources */, 740B23E11F17FB4200067A2A /* xmlrpc-metaweblog-editpost-bad-xml-failure.xml in Resources */, BA5AEF6D24923DDD007D8E49 /* plugin-state-contact-form-7.json in Resources */, @@ -3376,6 +3423,7 @@ 46ABD0EA262EEE0400C7FF24 /* AppTransportSecuritySettingsTests.swift in Sources */, 74D67F0A1F15C24C0010C5ED /* PeopleServiceRemoteTests.swift in Sources */, 4A1123A22B19690C004690CF /* MultipartFormTests.swift in Sources */, + B04D8C0D2BB7895A002717A2 /* StatsInsightDecodingTests.swift in Sources */, 9F3E0BAE20873836009CB5BA /* ReaderTopicServiceRemoteTest+Subscriptions.swift in Sources */, 4A05E79E2B30F3C500C25E3B /* HTTPRequestHelpers.swift in Sources */, BA0637ED2492382200AF8419 /* PluginStateTests.swift in Sources */, From e57b288b55d1053b8586828b39ea4a9f372fbe91 Mon Sep 17 00:00:00 2001 From: Paul Von Schrottky Date: Fri, 29 Mar 2024 22:00:44 -0400 Subject: [PATCH 22/22] Fix test to check required properties --- ...tatsAnnualAndMostPopularTimeInsightDecodingTests.swift | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Tests/WordPressKitTests/Tests/StatsAnnualAndMostPopularTimeInsightDecodingTests.swift b/Tests/WordPressKitTests/Tests/StatsAnnualAndMostPopularTimeInsightDecodingTests.swift index 34b7f661b..025368461 100644 --- a/Tests/WordPressKitTests/Tests/StatsAnnualAndMostPopularTimeInsightDecodingTests.swift +++ b/Tests/WordPressKitTests/Tests/StatsAnnualAndMostPopularTimeInsightDecodingTests.swift @@ -9,8 +9,7 @@ final class StatsAnnualAndMostPopularTimeInsightDecodingTests: XCTestCase { "highest_hour": 1, "highest_hour_percent": 1, "highest_day_of_week": 1, - "highest_day_percent": 1, - "years": [["year": "2022"]] + "highest_day_percent": 1 ] // When @@ -25,15 +24,14 @@ final class StatsAnnualAndMostPopularTimeInsightDecodingTests: XCTestCase { let json: [String: Any] = [ "highest_hour": 1, "highest_hour_percent": 1, - "highest_day_of_week": 1, - "highest_day_percent": 1 + "highest_day_of_week": 1 ] // When let insight = StatsAnnualAndMostPopularTimeInsight(jsonDictionary: json as [String: AnyObject]) // Then - XCTAssertNotNil(insight) + XCTAssertNil(insight) } func testDecodingDecimalPercentagesRoundsSuccessful() {