diff --git a/Sources/Generator/Content/DefaultValueProvider.swift b/Sources/Generator/Content/DefaultValueProvider.swift new file mode 100644 index 0000000..d51877b --- /dev/null +++ b/Sources/Generator/Content/DefaultValueProvider.swift @@ -0,0 +1,6 @@ +import Foundation + +protocol DefaultValueProvider { + + static var defaultValue: Self { get } +} diff --git a/Sources/Generator/Content/Element+LocalizedMetadata.swift b/Sources/Generator/Content/Element+LocalizedMetadata.swift index 40c0f79..2de7a18 100644 --- a/Sources/Generator/Content/Element+LocalizedMetadata.swift +++ b/Sources/Generator/Content/Element+LocalizedMetadata.swift @@ -57,7 +57,7 @@ extension Element { - Note: If this value is inherited from the parent, if it is not defined. There must be at least one element in the path that defines this property. */ - let moreLinkText: String + let moreLinkText: String? /** The text on the back navigation link of **contained** elements. @@ -145,77 +145,52 @@ extension Element { extension Element.LocalizedMetadata { - init?(atRoot folder: URL, data: GenericMetadata.LocalizedMetadata) { + init?(atRoot folder: URL, data: GenericMetadata.LocalizedMetadata, log: MetadataInfoLogger) { // Go through all elements and check them for completeness // In the end, check that all required elements are present - var isComplete = true - func markAsIncomplete() { - isComplete = false - } + var isValid = true + let source = "root" - self.language = log - .required(data.language, name: "language", source: source) - .ifNil(markAsIncomplete) ?? "" - self.title = log - .required(data.title, name: "title", source: source) - .ifNil(markAsIncomplete) ?? "" + self.language = log.required(data.language, name: "language", source: source, &isValid) + self.title = log.required(data.title, name: "title", source: source, &isValid) self.subtitle = data.subtitle self.description = data.description self.linkPreviewTitle = data.linkPreviewTitle ?? data.title ?? "" - self.linkPreviewImage = log - .linkPreviewThumbnail(customFile: data.linkPreviewImage, for: language, in: folder, source: source) + self.linkPreviewImage = data.linkPreviewImage let linkPreviewDescription = data.linkPreviewDescription ?? data.description ?? data.subtitle - self.linkPreviewDescription = log - .required(linkPreviewDescription, name: "linkPreviewDescription", source: source) - .ifNil(markAsIncomplete) ?? "" - self.moreLinkText = data.moreLinkText ?? Element.LocalizedMetadata.moreLinkDefaultText - self.backLinkText = log - .required(data.backLinkText, name: "backLinkText", source: source) - .ifNil(markAsIncomplete) ?? "" + self.linkPreviewDescription = log.required(linkPreviewDescription, name: "linkPreviewDescription", source: source, &isValid) + self.moreLinkText = data.moreLinkText + self.backLinkText = log.required(data.backLinkText, name: "backLinkText", source: source, &isValid) self.parentBackLinkText = "" // Root has no parent - self.placeholderTitle = log - .required(data.placeholderTitle, name: "placeholderTitle", source: source) - .ifNil(markAsIncomplete) ?? "" - self.placeholderText = log - .required(data.placeholderText, name: "placeholderText", source: source) - .ifNil(markAsIncomplete) ?? "" + self.placeholderTitle = log.required(data.placeholderTitle, name: "placeholderTitle", source: source, &isValid) + self.placeholderText = log.required(data.placeholderText, name: "placeholderText", source: source, &isValid) self.titleSuffix = data.titleSuffix self.thumbnailSuffix = log.unused(data.thumbnailSuffix, "thumbnailSuffix", source: source) self.cornerText = log.unused(data.cornerText, "cornerText", source: source) - self.externalUrl = log.unexpected(data.externalUrl, name: "externalUrl", source: source) - self.relatedContentText = log - .required(data.relatedContentText, name: "relatedContentText", source: source) ?? "" - self.navigationTextAsNextPage = log - .required(data.navigationTextAsNextPage, name: "navigationTextAsNextPage", source: source) ?? "" - self.navigationTextAsPreviousPage = log - .required(data.navigationTextAsPreviousPage, name: "navigationTextAsPreviousPage", source: source) ?? "" + self.externalUrl = log.unused(data.externalUrl, "externalUrl", source: source) + self.relatedContentText = log.required(data.relatedContentText, name: "relatedContentText", source: source, &isValid) + self.navigationTextAsNextPage = log.required(data.navigationTextAsNextPage, name: "navigationTextAsNextPage", source: source, &isValid) + self.navigationTextAsPreviousPage = log.required(data.navigationTextAsPreviousPage, name: "navigationTextAsPreviousPage", source: source, &isValid) - guard isComplete else { + guard isValid else { return nil } } - init?(folder: URL, data: GenericMetadata.LocalizedMetadata, source: String, parent: Element.LocalizedMetadata) { + init?(folder: URL, data: GenericMetadata.LocalizedMetadata, source: String, parent: Element.LocalizedMetadata, log: MetadataInfoLogger) { // Go through all elements and check them for completeness // In the end, check that all required elements are present - var isComplete = true - func markAsIncomplete() { - isComplete = false - } + var isValid = true + self.language = parent.language - self.title = log - .required(data.title, name: "title", source: source) - .ifNil(markAsIncomplete) ?? "" + self.title = log.required(data.title, name: "title", source: source, &isValid) self.subtitle = data.subtitle self.description = data.description self.linkPreviewTitle = data.linkPreviewTitle ?? data.title ?? "" - self.linkPreviewImage = log - .linkPreviewThumbnail(customFile: data.linkPreviewImage, for: language, in: folder, source: source) + self.linkPreviewImage = data.linkPreviewImage let linkPreviewDescription = data.linkPreviewDescription ?? data.description ?? data.subtitle - self.linkPreviewDescription = log - .required(linkPreviewDescription, name: "linkPreviewDescription", source: source) - .ifNil(markAsIncomplete) ?? "" - self.moreLinkText = log.moreLinkText(data.moreLinkText, parent: parent.moreLinkText, source: source) + self.linkPreviewDescription = log.required(linkPreviewDescription, name: "linkPreviewDescription", source: source, &isValid) + self.moreLinkText = log.required(data.moreLinkText ?? parent.moreLinkText, name: "moreLinkText", source: source, &isValid) self.backLinkText = data.backLinkText ?? data.title ?? "" self.parentBackLinkText = parent.backLinkText self.placeholderTitle = data.placeholderTitle ?? parent.placeholderTitle @@ -228,7 +203,7 @@ extension Element.LocalizedMetadata { self.navigationTextAsPreviousPage = data.navigationTextAsPreviousPage ?? parent.navigationTextAsPreviousPage self.navigationTextAsNextPage = data.navigationTextAsNextPage ?? parent.navigationTextAsNextPage - guard isComplete else { + guard isValid else { return nil } } diff --git a/Sources/Generator/Content/Element.swift b/Sources/Generator/Content/Element.swift index 6b7d1fa..b6a1e28 100644 --- a/Sources/Generator/Content/Element.swift +++ b/Sources/Generator/Content/Element.swift @@ -157,122 +157,130 @@ struct Element { - Parameter folder: The root folder of the site content. - Note: Uses global objects. */ - init?(atRoot folder: URL) { + init?(atRoot folder: URL, log: MetadataInfoLogger) { self.inputFolder = folder self.path = "" let source = GenericMetadata.metadataFileName - guard let metadata = GenericMetadata(source: source) else { + guard let metadata = GenericMetadata(source: source, log: log) else { return nil } + var isValid = true + self.id = metadata.customId ?? Element.defaultRootId - self.author = log.required(metadata.author, name: "author", source: source) ?? "author" - self.topBarTitle = log - .required(metadata.topBarTitle, name: "topBarTitle", source: source) ?? "My Website" - self.date = log.unused(metadata.date, "date", source: source) - self.endDate = log.unused(metadata.endDate, "endDate", source: source) - self.state = log.state(metadata.state, source: source) + self.author = log.required(metadata.author, name: "author", source: source, &isValid) + self.topBarTitle = log.required(metadata.topBarTitle, name: "topBarTitle", source: source, &isValid) + self.date = log.castUnused(metadata.date, "date", source: source) + self.endDate = log.castUnused(metadata.endDate, "endDate", source: source) + self.state = log.cast(metadata.state, "state", source: source) self.sortIndex = log.unused(metadata.sortIndex, "sortIndex", source: source) self.externalFiles = metadata.externalFiles ?? [] self.requiredFiles = metadata.requiredFiles ?? [] // Paths are already relative to root self.images = metadata.images?.compactMap { ManualImage(input: $0, path: "") } ?? [] self.thumbnailPath = metadata.thumbnailPath ?? Element.defaultThumbnailName - self.thumbnailStyle = log.unused(metadata.thumbnailStyle, "thumbnailStyle", source: source) ?? .large - self.useManualSorting = log.unused(metadata.useManualSorting, "useManualSorting", source: source) ?? true + self.thumbnailStyle = log.castUnused(metadata.thumbnailStyle, "thumbnailStyle", source: source) + self.useManualSorting = log.unused(metadata.useManualSorting, "useManualSorting", source: source) self.overviewItemCount = metadata.overviewItemCount ?? Element.overviewItemCountDefault - self.headerType = log.headerType(metadata.headerType, source: source) - self.languages = log.required(metadata.languages, name: "languages", source: source)? + self.headerType = log.cast(metadata.headerType, "headerType", source: source) + self.showMostRecentSection = metadata.showMostRecentSection ?? false + self.languages = log.required(metadata.languages, name: "languages", source: source, &isValid) .compactMap { language in - .init(atRoot: folder, data: language) - } ?? [] + .init(atRoot: folder, data: language, log: log) + } // All properties initialized guard !languages.isEmpty else { - log.add(error: "No languages found", source: source) + log.error("No languages found", source: source) + return nil + } + + guard isValid else { return nil } files.add(page: path, id: id) - self.readElements(in: folder, source: nil) + self.readElements(in: folder, source: nil, log: log) } - mutating func readElements(in folder: URL, source: String?) { + mutating func readElements(in folder: URL, source: String?, log: MetadataInfoLogger) { let subFolders: [URL] do { subFolders = try FileManager.default .contentsOfDirectory(at: folder, includingPropertiesForKeys: [.isDirectoryKey]) .filter { $0.isDirectory } } catch { - log.add(error: "Failed to read subfolders", source: source ?? "root", error: error) + log.error("Failed to read subfolders: \(error)", source: source ?? "root") return } self.elements = subFolders.compactMap { subFolder in let s = (source.unwrapped { $0 + "/" } ?? "") + subFolder.lastPathComponent - return Element(parent: self, folder: subFolder, path: s) + return Element(parent: self, folder: subFolder, path: s, log: log) } } - init?(parent: Element, folder: URL, path: String) { + init?(parent: Element, folder: URL, path: String, log: MetadataInfoLogger) { self.inputFolder = folder self.path = path let source = path + "/" + GenericMetadata.metadataFileName - guard let metadata = GenericMetadata(source: source) else { + guard let metadata = GenericMetadata(source: source, log: log) else { return nil } + var isValid = true + self.id = metadata.customId ?? folder.lastPathComponent self.author = metadata.author ?? parent.author - self.topBarTitle = log - .unused(metadata.topBarTitle, "topBarTitle", source: source) ?? parent.topBarTitle - let date = log.date(from: metadata.date, property: "date", source: source).ifNil { - if !parent.useManualSorting { - log.add(error: "No 'date', but parent defines 'useManualSorting' = false", source: source) - } - } - self.date = date - self.endDate = log.date(from: metadata.endDate, property: "endDate", source: source).ifNotNil { - if date == nil { - log.add(warning: "Set 'endDate', but no 'date'", source: source) - } - } - let state = log.state(metadata.state, source: source) - self.state = state - self.sortIndex = metadata.sortIndex.ifNil { - if state != .hidden, parent.useManualSorting { - log.add(error: "No 'sortIndex', but parent defines 'useManualSorting' = true", source: source) - } - } + self.topBarTitle = log.unused(metadata.topBarTitle, "topBarTitle", source: source) + self.date = metadata.date.unwrapped { log.cast($0, "date", source: source) } + self.endDate = metadata.date.unwrapped { log.cast($0, "endDate", source: source) } + self.state = log.cast(metadata.state, "state", source: source) + self.sortIndex = metadata.sortIndex // TODO: Propagate external files from the parent if subpath matches? self.externalFiles = Element.rootPaths(for: metadata.externalFiles, path: path) self.requiredFiles = Element.rootPaths(for: metadata.requiredFiles, path: path) self.images = metadata.images?.compactMap { ManualImage(input: $0, path: path) } ?? [] self.thumbnailPath = metadata.thumbnailPath ?? Element.defaultThumbnailName - self.thumbnailStyle = log.thumbnailStyle(metadata.thumbnailStyle, source: source) + self.thumbnailStyle = log.cast(metadata.thumbnailStyle, "thumbnailStyle", source: source) self.useManualSorting = metadata.useManualSorting ?? false self.overviewItemCount = metadata.overviewItemCount ?? parent.overviewItemCount - self.headerType = log.headerType(metadata.headerType, source: source) + self.headerType = log.cast(metadata.headerType, "headerType", source: source) + self.showMostRecentSection = metadata.showMostRecentSection ?? false self.languages = parent.languages.compactMap { parentData in guard let data = metadata.languages?.first(where: { $0.language == parentData.language }) else { - log.add(info: "Language '\(parentData.language)' not found", source: source) + log.warning("Language '\(parentData.language)' not found", source: source) return nil } - return .init(folder: folder, data: data, source: source, parent: parentData) + return .init(folder: folder, data: data, source: source, parent: parentData, log: log) } // Check that each 'language' tag is present, and that all languages appear in the parent - log.required(metadata.languages, name: "languages", source: source)? - .compactMap { log.required($0.language, name: "language", source: source) } + log.required(metadata.languages, name: "languages", source: source, &isValid) + .compactMap { log.required($0.language, name: "language", source: source, &isValid) } .filter { language in !parent.languages.contains { $0.language == language } } .forEach { - log.add(warning: "Language '\($0)' not found in parent, so not generated", source: source) + log.warning("Language '\($0)' not found in parent, so not generated", source: source) } // All properties initialized + if self.date == nil, !parent.useManualSorting { + log.error("No 'date', but parent defines 'useManualSorting' = false", source: source) + } + if date == nil { + log.unused(self.endDate, "endDate", source: source) + } + if self.sortIndex == nil, state != .hidden, parent.useManualSorting { + log.error("No 'sortIndex', but parent defines 'useManualSorting' = true", source: source) + } + + guard isValid else { + return nil + } + files.add(page: path, id: id) - self.readElements(in: folder, source: path) + self.readElements(in: folder, source: path, log: log) } } diff --git a/Sources/Generator/Content/GenericMetadata+Localized.swift b/Sources/Generator/Content/GenericMetadata+Localized.swift index a50d1f0..010699d 100644 --- a/Sources/Generator/Content/GenericMetadata+Localized.swift +++ b/Sources/Generator/Content/GenericMetadata+Localized.swift @@ -14,8 +14,8 @@ extension GenericMetadata { let language: String? /** - - Note: This field is mandatory The title used in the page header. + - Note: This field is mandatory */ let title: String? diff --git a/Sources/Generator/Content/GenericMetadata.swift b/Sources/Generator/Content/GenericMetadata.swift index a1afd84..9954905 100644 --- a/Sources/Generator/Content/GenericMetadata.swift +++ b/Sources/Generator/Content/GenericMetadata.swift @@ -169,8 +169,8 @@ extension GenericMetadata { - Note: The decoding routine also checks for unknown properties, and writes them to the output. - Note: Uses global objects */ - init?(source: String) { - guard let data = files.dataOfOptionalFile(atPath: source, source: source) else { + init?(source: String, log: MetadataInfoLogger) { + guard let data = log.readPotentialMetadata(atPath: source, source: source) else { return nil } @@ -196,8 +196,7 @@ extension GenericMetadata { do { self = try decoder.decode(from: data) } catch { - print("Here \(data)") - log.failedToOpen(GenericMetadata.metadataFileName, requiredBy: source, error: error) + log.failedToDecodeMetadata(source: source, error: error) return nil } } diff --git a/Sources/Generator/Content/HeaderType.swift b/Sources/Generator/Content/HeaderType.swift index 4a14837..c344bde 100644 --- a/Sources/Generator/Content/HeaderType.swift +++ b/Sources/Generator/Content/HeaderType.swift @@ -17,3 +17,19 @@ enum HeaderType: String { */ case none } + +extension HeaderType: StringProperty { + + init?(_ value: String) { + self.init(rawValue: value) + } + + static var castFailureReason: String { + "Header type must be 'left', 'center' or 'none'" + } +} + +extension HeaderType: DefaultValueProvider { + + static var defaultValue: HeaderType { .left } +} diff --git a/Sources/Generator/Content/PageState.swift b/Sources/Generator/Content/PageState.swift index 2762285..b0a5faa 100644 --- a/Sources/Generator/Content/PageState.swift +++ b/Sources/Generator/Content/PageState.swift @@ -39,3 +39,19 @@ extension PageState { } } } + +extension PageState: StringProperty { + + init?(_ value: String) { + self.init(rawValue: value) + } + + static var castFailureReason: String { + "Page state must be 'standard', 'draft' or 'hidden'" + } +} + +extension PageState: DefaultValueProvider { + + static var defaultValue: PageState { .standard } +} diff --git a/Sources/Generator/Content/StringProperty.swift b/Sources/Generator/Content/StringProperty.swift new file mode 100644 index 0000000..0ec33c5 --- /dev/null +++ b/Sources/Generator/Content/StringProperty.swift @@ -0,0 +1,8 @@ +import Foundation + +protocol StringProperty { + + init?(_ value: String) + + static var castFailureReason: String { get } +} diff --git a/Sources/Generator/Content/ThumbnailStyle.swift b/Sources/Generator/Content/ThumbnailStyle.swift index 2f439a7..5836e25 100644 --- a/Sources/Generator/Content/ThumbnailStyle.swift +++ b/Sources/Generator/Content/ThumbnailStyle.swift @@ -33,3 +33,19 @@ enum ThumbnailStyle: String, CaseIterable { extension ThumbnailStyle: Codable { } + +extension ThumbnailStyle: StringProperty { + + init?(_ value: String) { + self.init(rawValue: value) + } + + static var castFailureReason: String { + "Thumbnail style must be 'large', 'square' or 'small'" + } +} + +extension ThumbnailStyle: DefaultValueProvider { + + static var defaultValue: ThumbnailStyle { .large } +} diff --git a/Sources/Generator/Extensions/Array+Extensions.swift b/Sources/Generator/Extensions/Array+Extensions.swift new file mode 100644 index 0000000..d52dca1 --- /dev/null +++ b/Sources/Generator/Extensions/Array+Extensions.swift @@ -0,0 +1,6 @@ +import Foundation + +extension Array: DefaultValueProvider { + + static var defaultValue: Array { [] } +} diff --git a/Sources/Generator/Extensions/Bool+Extensions.swift b/Sources/Generator/Extensions/Bool+Extensions.swift new file mode 100644 index 0000000..bd2966e --- /dev/null +++ b/Sources/Generator/Extensions/Bool+Extensions.swift @@ -0,0 +1,6 @@ +import Foundation + +extension Bool: DefaultValueProvider { + + static var defaultValue: Bool { true } +} diff --git a/Sources/Generator/Extensions/Date+Extensions.swift b/Sources/Generator/Extensions/Date+Extensions.swift new file mode 100644 index 0000000..6dcf757 --- /dev/null +++ b/Sources/Generator/Extensions/Date+Extensions.swift @@ -0,0 +1,26 @@ +import Foundation + +extension Date: StringProperty { + + private static let metadataDate: DateFormatter = { + let df = DateFormatter() + df.dateFormat = "dd.MM.yy" + return df + }() + + init?(_ value: String) { + guard let date = Date.metadataDate.date(from: value) else { + return nil + } + self = date + } + + static var castFailureReason: String { + "Date string format must be 'dd.MM.yy'" + } +} + +extension Date: DefaultValueProvider { + + static var defaultValue: Date { .init() } +} diff --git a/Sources/Generator/Extensions/Int+Extensions.swift b/Sources/Generator/Extensions/Int+Extensions.swift new file mode 100644 index 0000000..af732f0 --- /dev/null +++ b/Sources/Generator/Extensions/Int+Extensions.swift @@ -0,0 +1,13 @@ +import Foundation + +extension Int: StringProperty { + + static var castFailureReason: String { + "The string was not a valid integer" + } +} + +extension Int: DefaultValueProvider { + + static var defaultValue: Int { 0 } +} diff --git a/Sources/Generator/Extensions/Optional+Extensions.swift b/Sources/Generator/Extensions/Optional+Extensions.swift index c280067..1f94571 100644 --- a/Sources/Generator/Extensions/Optional+Extensions.swift +++ b/Sources/Generator/Extensions/Optional+Extensions.swift @@ -3,7 +3,7 @@ import Metal extension Optional { - func unwrapped(_ closure: (Wrapped) -> T) -> T? { + func unwrapped(_ closure: (Wrapped) -> T?) -> T? { if case let .some(value) = self { return closure(value) } diff --git a/Sources/Generator/Extensions/String+Extensions.swift b/Sources/Generator/Extensions/String+Extensions.swift index 5cb1335..9bcdde0 100644 --- a/Sources/Generator/Extensions/String+Extensions.swift +++ b/Sources/Generator/Extensions/String+Extensions.swift @@ -79,3 +79,8 @@ extension String { try data(using: .utf8)!.createFolderAndWrite(to: url) } } + +extension String: DefaultValueProvider { + + static var defaultValue: String { "" } +} diff --git a/Sources/Generator/Files/FileSystem.swift b/Sources/Generator/Files/FileSystem.swift index 021d8ea..2cfa5fb 100644 --- a/Sources/Generator/Files/FileSystem.swift +++ b/Sources/Generator/Files/FileSystem.swift @@ -18,6 +18,8 @@ final class FileSystem { input.appendingPathComponent(FileSystem.tempFileName) } + let generatorInfoFolder: URL + /** All files which should be copied to the output folder */ @@ -70,7 +72,7 @@ final class FileSystem { self.input = input self.output = output self.images = .init(input: input, output: output) - + self.generatorInfoFolder = input.appendingPathComponent("run") } func urlInOutputFolder(_ path: String) -> URL { @@ -100,20 +102,6 @@ final class FileSystem { } } - func dataOfOptionalFile(atPath path: String, source: String) -> Data? { - let url = input.appendingPathComponent(path) - guard exists(url) else { - return nil - } - - do { - return try Data(contentsOf: url) - } catch { - log.failedToOpen(path, requiredBy: source, error: error) - return nil - } - } - func contentOfOptionalFile(atPath path: String, source: String, createEmptyFileIfMissing: Bool = false) -> String? { let url = input.appendingPathComponent(path) guard exists(url) else { diff --git a/Sources/Generator/Files/ValidationLog.swift b/Sources/Generator/Files/ValidationLog.swift index ae44739..3e8e3ef 100644 --- a/Sources/Generator/Files/ValidationLog.swift +++ b/Sources/Generator/Files/ValidationLog.swift @@ -46,119 +46,7 @@ final class ValidationLog { add(info: .init(reason: reason, source: source, error: error)) } - @discardableResult - func unused(_ value: Optional, _ name: String, source: String) -> Optional { - if value != nil { - add(info: "Unused property '\(name)'", source: source) - } - return nil - } - - func unknown(property: String, source: String) { - add(info: "Unknown property '\(property)'", source: source) - } - - func required(_ value: Optional, name: String, source: String) -> Optional { - guard let value = value else { - add(error: "Missing property '\(name)'", source: source) - return nil - } - return value - } - - func unexpected(_ value: Optional, name: String, source: String) -> Optional { - if let value = value { - add(error: "Unexpected property '\(name)' = '\(value)'", source: source) - return nil - } - return nil - } - - func missing(_ file: String, requiredBy source: String) { - print("[ERROR] Missing file '\(file)' required by \(source)") - } - func failedToOpen(_ file: String, requiredBy source: String, error: Error?) { print("[ERROR] Failed to open file '\(file)' required by \(source): \(error?.localizedDescription ?? "No error provided")") } - - func state(_ raw: String?, source: String) -> PageState { - guard let raw = raw else { - return .standard - } - guard let state = PageState(rawValue: raw) else { - add(warning: "Invalid 'state' '\(raw)', using 'standard'", source: source) - return .standard - } - return state - } - - func headerType(_ raw: String?, source: String) -> HeaderType { - guard let raw = raw else { - return .left - } - guard let type = HeaderType(rawValue: raw) else { - add(warning: "Invalid 'headerType' '\(raw)', using 'left'", source: source) - return .left - } - return type - } - - func thumbnailStyle(_ raw: String?, source: String) -> ThumbnailStyle { - guard let raw = raw else { - return .large - } - guard let style = ThumbnailStyle(rawValue: raw) else { - add(warning: "Invalid 'thumbnailStyle' '\(raw)', using 'large'", source: source) - return .large - } - return style - } - - func linkPreviewThumbnail(customFile: String?, for language: String, in folder: URL, source: String) -> String? { - guard let customFile = customFile else { - return nil - } - let customFileUrl: URL - if customFile.starts(with: "/") { - customFileUrl = URL(fileURLWithPath: customFile) - } else { - customFileUrl = folder.appendingPathComponent(customFile) - } - guard customFileUrl.exists else { - missing(customFile, requiredBy: "property 'linkPreviewImage' in metadata of \(source)") - return nil - } - return customFile - } - - func moreLinkText(_ elementText: String?, parent parentText: String?, source: String) -> String { - if let elementText = elementText { - return elementText - } - let standard = Element.LocalizedMetadata.moreLinkDefaultText - guard let parentText = parentText, parentText != standard else { - add(error: "Missing property 'moreLinkText'", source: source) - return standard - } - - return parentText - } - - func date(from string: String?, property: String, source: String) -> Date? { - guard let string = string else { - return nil - } - guard let date = ValidationLog.metadataDate.date(from: string) else { - add(warning: "Invalid date string '\(string)' for property '\(property)'", source: source) - return nil - } - return date - } - - private static let metadataDate: DateFormatter = { - let df = DateFormatter() - df.dateFormat = "dd.MM.yy" - return df - }() } diff --git a/Sources/Generator/Processing/MetadataInfoLogger.swift b/Sources/Generator/Processing/MetadataInfoLogger.swift new file mode 100644 index 0000000..4baedc3 --- /dev/null +++ b/Sources/Generator/Processing/MetadataInfoLogger.swift @@ -0,0 +1,150 @@ +import Foundation + +final class MetadataInfoLogger { + + private let input: URL + + private var numberOfMetadataFiles = 0 + + private var unusedProperties: [(name: String, source: String)] = [] + + private var invalidProperties: [(name: String, source: String, reason: String)] = [] + + private var unknownProperties: [(name: String, source: String)] = [] + + private var missingProperties: [(name: String, source: String)] = [] + + private var unreadableMetadata: [(source: String, error: Error)] = [] + + private var warnings: [(source: String, message: String)] = [] + + private var errors: [(source: String, message: String)] = [] + + init(input: URL) { + self.input = input + } + + /** + Adds an info message if a value is set for an unused property. + - Note: Unused properties do not cause an element to be skipped. + */ + @discardableResult + func unused(_ value: Optional, _ name: String, source: String) -> T where T: DefaultValueProvider { + if let value { + unusedProperties.append((name, source)) + return value + } + return T.defaultValue + } + + /** + Cast a string value to another value, and using a default in case of errors. + - Note: Invalid string values do not cause an element to be skipped. + */ + func cast(_ value: String, _ name: String, source: String) -> T where T: DefaultValueProvider, T: StringProperty { + guard let result = T.init(value) else { + invalidProperties.append((name: name, source: source, reason: T.castFailureReason)) + return T.defaultValue + } + return result + } + + /** + Cast a string value to another value, and using a default in case of errors or missing values. + - Note: Invalid string values do not cause an element to be skipped. + */ + func cast(_ value: String?, _ name: String, source: String) -> T where T: DefaultValueProvider, T: StringProperty { + guard let value else { + return T.defaultValue + } + guard let result = T.init(value) else { + invalidProperties.append((name: name, source: source, reason: T.castFailureReason)) + return T.defaultValue + } + return result + } + + /** + Cast the string value of an unused property to another value, and using a default in case of errors. + - Note: Invalid string values do not cause an element to be skipped. + */ + func castUnused(_ value: String?, _ name: String, source: String) -> R where R: DefaultValueProvider, R: StringProperty { + unused(value.unwrapped { cast($0, name, source: source) }, name, source: source) + } + + /** + Note an unknown property. + - Note: Unknown properties do not cause an element to be skipped. + */ + func unknown(property: String, source: String) { + unknownProperties.append((name: property, source: source)) + } + + /** + Ensure that a property is set, and aborting metadata decoding. + - Note: Missing required properties cause an element to be skipped. + */ + func required(_ value: T?, name: String, source: String, _ valid: inout Bool) -> T where T: DefaultValueProvider { + guard let value = value else { + missingProperties.append((name, source)) + valid = false + return T.defaultValue + } + return value + } + + func warning(_ message: String, source: String) { + warnings.append((source, message)) + } + + func error(_ message: String, source: String) { + errors.append((source, message)) + } + + func failedToDecodeMetadata(source: String, error: Error) { + unreadableMetadata.append((source, error)) + } + + func readPotentialMetadata(atPath path: String, source: String) -> Data? { + let url = input.appendingPathComponent(path) + guard url.exists else { + return nil + } + + numberOfMetadataFiles += 1 + printMetadataScanUpdate() + do { + return try Data(contentsOf: url) + } catch { + unreadableMetadata.append((source, error)) + return nil + } + } + + // MARK: Printing + + private func printMetadataScanUpdate() { + print(String(format: "Scanning source files: %4d pages found \r", numberOfMetadataFiles), terminator: "") + } + + func printMetadataScanOverview() { + print(" Number of pages: \(numberOfMetadataFiles)") + print(" Unreadable files: \(unreadableMetadata.count)") + print(" Unused properties: \(unusedProperties.count)") + print(" Invalid properties: \(invalidProperties.count)") + print(" Unknown properties: \(unknownProperties.count)") + print(" Missing properties: \(missingProperties.count)") + } + + func writeResultsToFile(in folder: URL) throws { + let url = folder.appendingPathComponent("Metadata issues.txt") + var lines = ["Unreadable files:"] + unreadableMetadata.map { "\($0.source): \($0.error)" } + lines += ["Unused properties:"] + unusedProperties.map { "\($0.source): \($0.name)" } + lines += ["Invalid properties:"] + invalidProperties.map { "\($0.source): \($0.name) (\($0.reason))" } + lines += ["Unknown properties:"] + unknownProperties.map { "\($0.source): \($0.name)" } + lines += ["Missing properties:"] + missingProperties.map { "\($0.source): \($0.name)" } + + let data = lines.joined(separator: "\n").data(using: .utf8) + try data?.createFolderAndWrite(to: url) + } +} diff --git a/Sources/Generator/run.swift b/Sources/Generator/run.swift index 64d9503..062417e 100644 --- a/Sources/Generator/run.swift +++ b/Sources/Generator/run.swift @@ -17,6 +17,16 @@ struct CHGenerator: ParsableCommand { } } +private func loadSiteData(in folder: URL) throws -> Element? { + let log = MetadataInfoLogger(input: folder) + print("--- SOURCE FILES -----------------------------------") + let root = Element(atRoot: folder, log: log) + print(" ") + log.printMetadataScanOverview() + try log.writeResultsToFile(in: files.generatorInfoFolder) + return root +} + private func generate(configPath: String) throws { let configUrl = URL(fileURLWithPath: configPath) let data = try Data(contentsOf: configUrl) @@ -26,7 +36,8 @@ private func generate(configPath: String) throws { in: configuration.contentDirectory, to: configuration.outputDirectory) - siteRoot = Element(atRoot: configuration.contentDirectory) + // 2. Scan site elements + siteRoot = try loadSiteData(in: configuration.contentDirectory) guard siteRoot != nil else { return }