diff --git a/Sources/ToucanSDK/ContentType/ContentType.swift b/Sources/ToucanSDK/ContentType/ContentType.swift index a86c164f..07a8e1ea 100644 --- a/Sources/ToucanSDK/ContentType/ContentType.swift +++ b/Sources/ToucanSDK/ContentType/ContentType.swift @@ -33,8 +33,6 @@ struct ContentType: Codable { case double case bool case date - case array - case object } let type: DataType diff --git a/Sources/ToucanSDK/ContextStore/ContextStore.swift b/Sources/ToucanSDK/ContextStore/ContextStore.swift index 6a959b51..102f3adb 100644 --- a/Sources/ToucanSDK/ContextStore/ContextStore.swift +++ b/Sources/ToucanSDK/ContextStore/ContextStore.swift @@ -8,85 +8,6 @@ import Foundation import Logging -// TODO: better sort algorithm using data types -extension [PageBundle] { - - func sorted( - key: String?, - order: ContentType.Order? - ) -> [PageBundle] { - guard let key, let order else { - return self - } - switch key { - case "publication": - return sorted { lhs, rhs in - switch order { - case .asc: - return lhs.publication < rhs.publication - case .desc: - return lhs.publication > rhs.publication - } - } - default: - return sorted { lhs, rhs in - guard - let l = lhs.frontMatter[key] as? String, - let r = rhs.frontMatter[key] as? String - else { - guard - let l = lhs.frontMatter[key] as? Int, - let r = rhs.frontMatter[key] as? Int - else { - return false - } - switch order { - case .asc: - return l < r - case .desc: - return l > r - } - } - // TODO: proper case insensitive compare - switch order { - case .asc: - // switch l.caseInsensitiveCompare(r) { - // case .orderedAscending: - // return true - // case .orderedDescending: - // return false - // case .orderedSame: - // return false - // } - return l.lowercased() < r.lowercased() - case .desc: - return l.lowercased() > r.lowercased() - } - } - } - } - - func limited(_ value: Int?) -> [PageBundle] { - Array(prefix(value ?? Int.max)) - } - - func filtered(_ filter: ContentType.Filter?) -> [PageBundle] { - guard let filter else { - return self - } - return self.filter { pageBundle in - guard let field = pageBundle.frontMatter[filter.field] else { - return false - } - switch filter.method { - case .equals: - // this is horrible... 😱 - return String(describing: field) == filter.value - } - } - } -} - struct ContextStore { let sourceConfig: SourceConfig @@ -202,7 +123,7 @@ struct ContextStore { .filter { item in refIds.contains(item.contextAwareIdentifier) } - .sorted(key: value.sort, order: value.order) + .sorted(frontMatterKey: value.sort, order: value.order) .limited(value.limit) result[key] = refs @@ -272,7 +193,7 @@ struct ContextStore { let refs = pageBundles .filter { $0.contentType.id == value.references } - .sorted(key: value.sort, order: value.order) + .sorted(frontMatterKey: value.sort, order: value.order) guard let idx = refs.firstIndex(where: { @@ -321,7 +242,7 @@ struct ContextStore { ) .contains(id) } - .sorted(key: value.sort, order: value.order) + .sorted(frontMatterKey: value.sort, order: value.order) .limited(value.limit) } } @@ -354,18 +275,19 @@ struct ContextStore { func getPageBundlesForSiteContext() -> [String: [PageBundle]] { var result: [String: [PageBundle]] = [:] + let dateFormatter = DateFormatters.baseFormatter + for contentType in contentTypes { for (key, value) in contentType.context?.site ?? [:] { result[key] = pageBundles .filter { $0.contentType.id == contentType.id } - .sorted(key: value.sort, order: value.order) - .filtered(value.filter) - // TODO: proper pagination + .sorted(frontMatterKey: value.sort, order: value.order) + .filtered(value.filter, dateFormatter: dateFormatter) .limited(value.limit) } } + return result } - } diff --git a/Sources/ToucanSDK/Extensions/PageBundle+Array.swift b/Sources/ToucanSDK/Extensions/PageBundle+Array.swift new file mode 100644 index 00000000..6aba44ea --- /dev/null +++ b/Sources/ToucanSDK/Extensions/PageBundle+Array.swift @@ -0,0 +1,73 @@ +// +// File.swift +// toucan +// +// Created by Viasz-Kádi Ferenc on 2024. 10. 17.. +// + +import Foundation +import Yams + +extension [PageBundle] { + + /// Sorts an array of PageBundle objects based on the given key and order. + /// + /// - Parameters: + /// - key: The key used for sorting the PageBundle objects. + /// - order: The order in which the sorting should be done (e.g., ascending or descending). + /// + /// - Returns: A sorted array of PageBundle objects, or the original array if key or order is nil. + func sorted( + frontMatterKey: String?, + order: ContentType.Order? + ) -> [PageBundle] { + guard + let frontMatterKey, + let order + else { + return self + } + return sorted { lhs, rhs in + lhs.compareForSorting( + for: rhs, + frontMatterKey: frontMatterKey, + order: order + ) + } + } + + /// Limits the number of elements in the collection to the specified value if provided. + /// If no value is provided, returns the entire collection. + /// + /// - Parameters: + /// - value: An optional integer specifying the maximum number of elements to return. + /// + /// - Returns: An array containing up to the specified number of elements, or the entire collection if no value is provided. + func limited(_ value: Int?) -> [PageBundle] { + guard let value else { + return self + } + return Array(prefix(value)) + } + + /// Filters an array of `PageBundle` objects based on the provided filter and date formatter. + /// + /// - Parameters: + /// - filter: An optional `ContentType.Filter` to apply. If `nil`, the original array is returned. + /// - dateFormatter: A `DateFormatter` used for date-based filtering. + /// + /// - Returns: A filtered array of `PageBundle` objects. If no filter is provided, the original array is returned. + func filtered( + _ filter: ContentType.Filter?, + dateFormatter: DateFormatter + ) -> [PageBundle] { + guard let filter else { + return self + } + + let result = self.filter { + $0.checkFilter(filter, dateFormatter: dateFormatter) + } + return result + } +} diff --git a/Sources/ToucanSDK/Extensions/PageBundle+Equal.swift b/Sources/ToucanSDK/Extensions/PageBundle+Equal.swift new file mode 100644 index 00000000..4a2e6670 --- /dev/null +++ b/Sources/ToucanSDK/Extensions/PageBundle+Equal.swift @@ -0,0 +1,111 @@ +// +// File.swift +// toucan +// +// Created by Viasz-Kádi Ferenc on 2024. 10. 17.. +// + +import Foundation + +extension String { + + /// Converts the string into a value of the specified data type. + /// + /// - Parameters: + /// - dataType: The target data type to which the string should be converted. + /// - dateFormatter: The date formatter used to convert the string to a date if the data type is `date`. + /// - Returns: The converted value of the specified type, or nil if the conversion fails. + func value( + for dataType: ContentType.Property.DataType, + dateFormatter: DateFormatter + ) -> T? { + switch dataType { + case .bool: + return Bool(self) as? T + case .int: + return Int(self) as? T + case .double: + return Double(self) as? T + case .string: + return self as? T + case .date: + return dateFormatter.date(from: self) as? T + } + } +} + +extension PageBundle { + + /// Checks if a filter is satisfied based on the current page's front matter. + /// + /// - Parameters: + /// - filter: The filter to check. + /// - dateFormatter: The date formatter used to parse date values. + /// - Returns: A boolean indicating whether the filter is satisfied. + func checkFilter( + _ filter: ContentType.Filter, + dateFormatter: DateFormatter + ) -> Bool { + guard + let field = frontMatter[filter.field], + let dataType = contentType.properties?[filter.field]?.type, + let filterValue: Any = filter.value.value( + for: dataType, + dateFormatter: dateFormatter + ) + else { + return false + } + + switch filter.method { + case .equals: + return areValuesEqual(field, filterValue) + } + } +} + +/// Compares two values of any type for equality. +/// +/// - Parameters: +/// - lhs: The first value to compare. +/// - rhs: The second value to compare. +/// - Returns: A boolean indicating whether the two values are equal. +func areValuesEqual(_ lhs: Any, _ rhs: Any) -> Bool { + switch (lhs, rhs) { + case (let lhs as Bool, let rhs as Bool): + return lhs == rhs + case (let lhs as Int, let rhs as Int): + return lhs == rhs + case (let lhs as Double, let rhs as Double): + return lhs == rhs + case (let lhs as String, let rhs as String): + return lhs == rhs + case (let lhs as Date, let rhs as Date): + return lhs == rhs + case (let lhs as [Any], let rhs as [Any]): + return lhs.elementsEqual(rhs, by: { areValuesEqual($0, $1) }) + case (let lhs as [String: Any], let rhs as [String: Any]): + return lhs.isEqualTo(rhs) + default: + return false + } +} + +extension Dictionary where Key == String, Value == Any { + + /// Compares two dictionaries for equality by checking their keys and values. + /// + /// - Parameters: + /// - rhs: The dictionary to compare against. + /// - Returns: A boolean indicating whether the two dictionaries are equal. + func isEqualTo(_ rhs: [Key: Value]) -> Bool { + guard self.count == rhs.count else { return false } + for (key, value) in self { + guard let rhsValue = rhs[key] else { + return false + } + return areValuesEqual(value, rhsValue) + } + return true + } +} diff --git a/Sources/ToucanSDK/Extensions/PageBundle+Sort.swift b/Sources/ToucanSDK/Extensions/PageBundle+Sort.swift new file mode 100644 index 00000000..c02a31fc --- /dev/null +++ b/Sources/ToucanSDK/Extensions/PageBundle+Sort.swift @@ -0,0 +1,61 @@ +// +// File.swift +// toucan +// +// Created by Viasz-Kádi Ferenc on 2024. 10. 17.. +// + +import Foundation + +extension PageBundle { + + /// Compares two `PageBundle` instances for sorting based on a specified front matter key and order. + /// + /// - Parameters: + /// - rhs: The `PageBundle` instance to compare with. + /// - frontMatterKey: The key in the front matter to use for comparison. + /// - order: The order (`asc` or `desc`) in which to perform the comparison. + /// - Returns: A Boolean value indicating whether the current `PageBundle` instance should be ordered before (`true`) or after (`false`) the rhs instance. + func compareForSorting( + for rhs: PageBundle, + frontMatterKey: String, + order: ContentType.Order + ) -> Bool { + guard + let lhsField = frontMatter[frontMatterKey], + let rhsField = rhs.frontMatter[frontMatterKey] + else { + return false + } + + switch order { + case .asc: + return compareValuesAscending(lhsField, rhsField) + case .desc: + return !compareValuesAscending(lhsField, rhsField) + } + } +} + +/// Compares two values of any type in ascending order. +/// +/// - Parameters: +/// - lhs: The first value to compare. +/// - rhs: The second value to compare. +/// - Returns: A Boolean value indicating whether the first value is less than the second value. +func compareValuesAscending(_ lhs: Any, _ rhs: Any) -> Bool { + switch (lhs, rhs) { + case let (lhs as Bool, rhs as Bool): + return !lhs && rhs + case let (lhs as Int, rhs as Int): + return lhs < rhs + case let (lhs as Double, rhs as Double): + return lhs < rhs + case let (lhs as String, rhs as String): + return lhs.caseInsensitiveCompare(rhs) == .orderedAscending + case let (lhs as Date, rhs as Date): + return lhs < rhs + default: + return String(describing: lhs) < String(describing: rhs) + } +} diff --git a/Sources/ToucanSDK/Renderers/HTMLRenderer.swift b/Sources/ToucanSDK/Renderers/HTMLRenderer.swift index b4bcf5de..3133822c 100644 --- a/Sources/ToucanSDK/Renderers/HTMLRenderer.swift +++ b/Sources/ToucanSDK/Renderers/HTMLRenderer.swift @@ -72,7 +72,10 @@ struct HTMLRenderer { } let pageBundles = source.pageBundles(by: contentType.id) - .sorted(key: pagination.sort, order: pagination.order) + .sorted( + frontMatterKey: pagination.sort, + order: pagination.order + ) let limit = pagination.limit let pages = pageBundles.chunks(ofCount: limit) @@ -216,7 +219,10 @@ struct HTMLRenderer { } let pageBundles = source.pageBundles(by: contentType.id) - .sorted(key: pagination.sort, order: pagination.order) + .sorted( + frontMatterKey: pagination.sort, + order: pagination.order + ) let limit = pagination.limit let pages = pageBundles.chunks(ofCount: limit) diff --git a/Sources/ToucanSDK/Source/SourceLoader.swift b/Sources/ToucanSDK/Source/SourceLoader.swift index 5d0dbe25..2c19d44c 100644 --- a/Sources/ToucanSDK/Source/SourceLoader.swift +++ b/Sources/ToucanSDK/Source/SourceLoader.swift @@ -77,6 +77,7 @@ struct SourceLoader { yamlParser: .init(), logger: logger ) + let contentTypes = try contentTypeLoader.load() let pageBundleLoader = PageBundleLoader( diff --git a/Sources/toucan-cli/Extensions/Toucan+UserErrors.swift b/Sources/toucan-cli/Extensions/Toucan+UserErrors.swift index e17fbac4..da9044d2 100644 --- a/Sources/toucan-cli/Extensions/Toucan+UserErrors.swift +++ b/Sources/toucan-cli/Extensions/Toucan+UserErrors.swift @@ -51,6 +51,11 @@ extension Toucan { let description = String(describing: underlyingError) let message = "YAML corrupted: `\(description)`" logger.error(.init(stringLiteral: message)) + case .typeMismatch(_, let context): + let underlyingError = context.underlyingError ?? error + let description = String(describing: underlyingError) + let message = "YAML type mismatch: `\(description)`" + logger.error(.init(stringLiteral: message)) default: logger.error("\(String(describing: error))") }