Improve logging during element scanning

This commit is contained in:
Christoph Hagen 2022-12-01 14:50:26 +01:00
parent 4c2c4b7dd3
commit 1ceba25d4f
19 changed files with 370 additions and 233 deletions

View File

@ -0,0 +1,6 @@
import Foundation
protocol DefaultValueProvider {
static var defaultValue: Self { get }
}

View File

@ -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 - 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. element in the path that defines this property.
*/ */
let moreLinkText: String let moreLinkText: String?
/** /**
The text on the back navigation link of **contained** elements. The text on the back navigation link of **contained** elements.
@ -145,77 +145,52 @@ extension Element {
extension Element.LocalizedMetadata { 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 // Go through all elements and check them for completeness
// In the end, check that all required elements are present // In the end, check that all required elements are present
var isComplete = true var isValid = true
func markAsIncomplete() {
isComplete = false
}
let source = "root" let source = "root"
self.language = log self.language = log.required(data.language, name: "language", source: source, &isValid)
.required(data.language, name: "language", source: source) self.title = log.required(data.title, name: "title", source: source, &isValid)
.ifNil(markAsIncomplete) ?? ""
self.title = log
.required(data.title, name: "title", source: source)
.ifNil(markAsIncomplete) ?? ""
self.subtitle = data.subtitle self.subtitle = data.subtitle
self.description = data.description self.description = data.description
self.linkPreviewTitle = data.linkPreviewTitle ?? data.title ?? "" self.linkPreviewTitle = data.linkPreviewTitle ?? data.title ?? ""
self.linkPreviewImage = log self.linkPreviewImage = data.linkPreviewImage
.linkPreviewThumbnail(customFile: data.linkPreviewImage, for: language, in: folder, source: source)
let linkPreviewDescription = data.linkPreviewDescription ?? data.description ?? data.subtitle let linkPreviewDescription = data.linkPreviewDescription ?? data.description ?? data.subtitle
self.linkPreviewDescription = log self.linkPreviewDescription = log.required(linkPreviewDescription, name: "linkPreviewDescription", source: source, &isValid)
.required(linkPreviewDescription, name: "linkPreviewDescription", source: source) self.moreLinkText = data.moreLinkText
.ifNil(markAsIncomplete) ?? "" self.backLinkText = log.required(data.backLinkText, name: "backLinkText", source: source, &isValid)
self.moreLinkText = data.moreLinkText ?? Element.LocalizedMetadata.moreLinkDefaultText
self.backLinkText = log
.required(data.backLinkText, name: "backLinkText", source: source)
.ifNil(markAsIncomplete) ?? ""
self.parentBackLinkText = "" // Root has no parent self.parentBackLinkText = "" // Root has no parent
self.placeholderTitle = log self.placeholderTitle = log.required(data.placeholderTitle, name: "placeholderTitle", source: source, &isValid)
.required(data.placeholderTitle, name: "placeholderTitle", source: source) self.placeholderText = log.required(data.placeholderText, name: "placeholderText", source: source, &isValid)
.ifNil(markAsIncomplete) ?? ""
self.placeholderText = log
.required(data.placeholderText, name: "placeholderText", source: source)
.ifNil(markAsIncomplete) ?? ""
self.titleSuffix = data.titleSuffix self.titleSuffix = data.titleSuffix
self.thumbnailSuffix = log.unused(data.thumbnailSuffix, "thumbnailSuffix", source: source) self.thumbnailSuffix = log.unused(data.thumbnailSuffix, "thumbnailSuffix", source: source)
self.cornerText = log.unused(data.cornerText, "cornerText", source: source) self.cornerText = log.unused(data.cornerText, "cornerText", source: source)
self.externalUrl = log.unexpected(data.externalUrl, name: "externalUrl", source: source) self.externalUrl = log.unused(data.externalUrl, "externalUrl", source: source)
self.relatedContentText = log self.relatedContentText = log.required(data.relatedContentText, name: "relatedContentText", source: source, &isValid)
.required(data.relatedContentText, name: "relatedContentText", source: source) ?? "" self.navigationTextAsNextPage = log.required(data.navigationTextAsNextPage, name: "navigationTextAsNextPage", source: source, &isValid)
self.navigationTextAsNextPage = log self.navigationTextAsPreviousPage = log.required(data.navigationTextAsPreviousPage, name: "navigationTextAsPreviousPage", source: source, &isValid)
.required(data.navigationTextAsNextPage, name: "navigationTextAsNextPage", source: source) ?? ""
self.navigationTextAsPreviousPage = log
.required(data.navigationTextAsPreviousPage, name: "navigationTextAsPreviousPage", source: source) ?? ""
guard isComplete else { guard isValid else {
return nil 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 // Go through all elements and check them for completeness
// In the end, check that all required elements are present // In the end, check that all required elements are present
var isComplete = true var isValid = true
func markAsIncomplete() {
isComplete = false
}
self.language = parent.language self.language = parent.language
self.title = log self.title = log.required(data.title, name: "title", source: source, &isValid)
.required(data.title, name: "title", source: source)
.ifNil(markAsIncomplete) ?? ""
self.subtitle = data.subtitle self.subtitle = data.subtitle
self.description = data.description self.description = data.description
self.linkPreviewTitle = data.linkPreviewTitle ?? data.title ?? "" self.linkPreviewTitle = data.linkPreviewTitle ?? data.title ?? ""
self.linkPreviewImage = log self.linkPreviewImage = data.linkPreviewImage
.linkPreviewThumbnail(customFile: data.linkPreviewImage, for: language, in: folder, source: source)
let linkPreviewDescription = data.linkPreviewDescription ?? data.description ?? data.subtitle let linkPreviewDescription = data.linkPreviewDescription ?? data.description ?? data.subtitle
self.linkPreviewDescription = log self.linkPreviewDescription = log.required(linkPreviewDescription, name: "linkPreviewDescription", source: source, &isValid)
.required(linkPreviewDescription, name: "linkPreviewDescription", source: source) self.moreLinkText = log.required(data.moreLinkText ?? parent.moreLinkText, name: "moreLinkText", source: source, &isValid)
.ifNil(markAsIncomplete) ?? ""
self.moreLinkText = log.moreLinkText(data.moreLinkText, parent: parent.moreLinkText, source: source)
self.backLinkText = data.backLinkText ?? data.title ?? "" self.backLinkText = data.backLinkText ?? data.title ?? ""
self.parentBackLinkText = parent.backLinkText self.parentBackLinkText = parent.backLinkText
self.placeholderTitle = data.placeholderTitle ?? parent.placeholderTitle self.placeholderTitle = data.placeholderTitle ?? parent.placeholderTitle
@ -228,7 +203,7 @@ extension Element.LocalizedMetadata {
self.navigationTextAsPreviousPage = data.navigationTextAsPreviousPage ?? parent.navigationTextAsPreviousPage self.navigationTextAsPreviousPage = data.navigationTextAsPreviousPage ?? parent.navigationTextAsPreviousPage
self.navigationTextAsNextPage = data.navigationTextAsNextPage ?? parent.navigationTextAsNextPage self.navigationTextAsNextPage = data.navigationTextAsNextPage ?? parent.navigationTextAsNextPage
guard isComplete else { guard isValid else {
return nil return nil
} }
} }

View File

@ -157,122 +157,130 @@ struct Element {
- Parameter folder: The root folder of the site content. - Parameter folder: The root folder of the site content.
- Note: Uses global objects. - Note: Uses global objects.
*/ */
init?(atRoot folder: URL) { init?(atRoot folder: URL, log: MetadataInfoLogger) {
self.inputFolder = folder self.inputFolder = folder
self.path = "" self.path = ""
let source = GenericMetadata.metadataFileName let source = GenericMetadata.metadataFileName
guard let metadata = GenericMetadata(source: source) else { guard let metadata = GenericMetadata(source: source, log: log) else {
return nil return nil
} }
var isValid = true
self.id = metadata.customId ?? Element.defaultRootId self.id = metadata.customId ?? Element.defaultRootId
self.author = log.required(metadata.author, name: "author", source: source) ?? "author" self.author = log.required(metadata.author, name: "author", source: source, &isValid)
self.topBarTitle = log self.topBarTitle = log.required(metadata.topBarTitle, name: "topBarTitle", source: source, &isValid)
.required(metadata.topBarTitle, name: "topBarTitle", source: source) ?? "My Website" self.date = log.castUnused(metadata.date, "date", source: source)
self.date = log.unused(metadata.date, "date", source: source) self.endDate = log.castUnused(metadata.endDate, "endDate", source: source)
self.endDate = log.unused(metadata.endDate, "endDate", source: source) self.state = log.cast(metadata.state, "state", source: source)
self.state = log.state(metadata.state, source: source)
self.sortIndex = log.unused(metadata.sortIndex, "sortIndex", source: source) self.sortIndex = log.unused(metadata.sortIndex, "sortIndex", source: source)
self.externalFiles = metadata.externalFiles ?? [] self.externalFiles = metadata.externalFiles ?? []
self.requiredFiles = metadata.requiredFiles ?? [] // Paths are already relative to root self.requiredFiles = metadata.requiredFiles ?? [] // Paths are already relative to root
self.images = metadata.images?.compactMap { ManualImage(input: $0, path: "") } ?? [] self.images = metadata.images?.compactMap { ManualImage(input: $0, path: "") } ?? []
self.thumbnailPath = metadata.thumbnailPath ?? Element.defaultThumbnailName self.thumbnailPath = metadata.thumbnailPath ?? Element.defaultThumbnailName
self.thumbnailStyle = log.unused(metadata.thumbnailStyle, "thumbnailStyle", source: source) ?? .large self.thumbnailStyle = log.castUnused(metadata.thumbnailStyle, "thumbnailStyle", source: source)
self.useManualSorting = log.unused(metadata.useManualSorting, "useManualSorting", source: source) ?? true self.useManualSorting = log.unused(metadata.useManualSorting, "useManualSorting", source: source)
self.overviewItemCount = metadata.overviewItemCount ?? Element.overviewItemCountDefault self.overviewItemCount = metadata.overviewItemCount ?? Element.overviewItemCountDefault
self.headerType = log.headerType(metadata.headerType, source: source) self.headerType = log.cast(metadata.headerType, "headerType", source: source)
self.languages = log.required(metadata.languages, name: "languages", source: source)? self.showMostRecentSection = metadata.showMostRecentSection ?? false
self.languages = log.required(metadata.languages, name: "languages", source: source, &isValid)
.compactMap { language in .compactMap { language in
.init(atRoot: folder, data: language) .init(atRoot: folder, data: language, log: log)
} ?? [] }
// All properties initialized // All properties initialized
guard !languages.isEmpty else { 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 return nil
} }
files.add(page: path, id: id) 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] let subFolders: [URL]
do { do {
subFolders = try FileManager.default subFolders = try FileManager.default
.contentsOfDirectory(at: folder, includingPropertiesForKeys: [.isDirectoryKey]) .contentsOfDirectory(at: folder, includingPropertiesForKeys: [.isDirectoryKey])
.filter { $0.isDirectory } .filter { $0.isDirectory }
} catch { } catch {
log.add(error: "Failed to read subfolders", source: source ?? "root", error: error) log.error("Failed to read subfolders: \(error)", source: source ?? "root")
return return
} }
self.elements = subFolders.compactMap { subFolder in self.elements = subFolders.compactMap { subFolder in
let s = (source.unwrapped { $0 + "/" } ?? "") + subFolder.lastPathComponent 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.inputFolder = folder
self.path = path self.path = path
let source = path + "/" + GenericMetadata.metadataFileName let source = path + "/" + GenericMetadata.metadataFileName
guard let metadata = GenericMetadata(source: source) else { guard let metadata = GenericMetadata(source: source, log: log) else {
return nil return nil
} }
var isValid = true
self.id = metadata.customId ?? folder.lastPathComponent self.id = metadata.customId ?? folder.lastPathComponent
self.author = metadata.author ?? parent.author self.author = metadata.author ?? parent.author
self.topBarTitle = log self.topBarTitle = log.unused(metadata.topBarTitle, "topBarTitle", source: source)
.unused(metadata.topBarTitle, "topBarTitle", source: source) ?? parent.topBarTitle self.date = metadata.date.unwrapped { log.cast($0, "date", source: source) }
let date = log.date(from: metadata.date, property: "date", source: source).ifNil { self.endDate = metadata.date.unwrapped { log.cast($0, "endDate", source: source) }
if !parent.useManualSorting { self.state = log.cast(metadata.state, "state", source: source)
log.add(error: "No 'date', but parent defines 'useManualSorting' = false", source: source) self.sortIndex = metadata.sortIndex
}
}
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)
}
}
// TODO: Propagate external files from the parent if subpath matches? // TODO: Propagate external files from the parent if subpath matches?
self.externalFiles = Element.rootPaths(for: metadata.externalFiles, path: path) self.externalFiles = Element.rootPaths(for: metadata.externalFiles, path: path)
self.requiredFiles = Element.rootPaths(for: metadata.requiredFiles, path: path) self.requiredFiles = Element.rootPaths(for: metadata.requiredFiles, path: path)
self.images = metadata.images?.compactMap { ManualImage(input: $0, path: path) } ?? [] self.images = metadata.images?.compactMap { ManualImage(input: $0, path: path) } ?? []
self.thumbnailPath = metadata.thumbnailPath ?? Element.defaultThumbnailName 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.useManualSorting = metadata.useManualSorting ?? false
self.overviewItemCount = metadata.overviewItemCount ?? parent.overviewItemCount 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 self.languages = parent.languages.compactMap { parentData in
guard let data = metadata.languages?.first(where: { $0.language == parentData.language }) else { 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 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 // Check that each 'language' tag is present, and that all languages appear in the parent
log.required(metadata.languages, name: "languages", source: source)? log.required(metadata.languages, name: "languages", source: source, &isValid)
.compactMap { log.required($0.language, name: "language", source: source) } .compactMap { log.required($0.language, name: "language", source: source, &isValid) }
.filter { language in .filter { language in
!parent.languages.contains { $0.language == language } !parent.languages.contains { $0.language == language }
} }
.forEach { .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 // 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) files.add(page: path, id: id)
self.readElements(in: folder, source: path) self.readElements(in: folder, source: path, log: log)
} }
} }

View File

@ -14,8 +14,8 @@ extension GenericMetadata {
let language: String? let language: String?
/** /**
- Note: This field is mandatory
The title used in the page header. The title used in the page header.
- Note: This field is mandatory
*/ */
let title: String? let title: String?

View File

@ -169,8 +169,8 @@ extension GenericMetadata {
- Note: The decoding routine also checks for unknown properties, and writes them to the output. - Note: The decoding routine also checks for unknown properties, and writes them to the output.
- Note: Uses global objects - Note: Uses global objects
*/ */
init?(source: String) { init?(source: String, log: MetadataInfoLogger) {
guard let data = files.dataOfOptionalFile(atPath: source, source: source) else { guard let data = log.readPotentialMetadata(atPath: source, source: source) else {
return nil return nil
} }
@ -196,8 +196,7 @@ extension GenericMetadata {
do { do {
self = try decoder.decode(from: data) self = try decoder.decode(from: data)
} catch { } catch {
print("Here \(data)") log.failedToDecodeMetadata(source: source, error: error)
log.failedToOpen(GenericMetadata.metadataFileName, requiredBy: source, error: error)
return nil return nil
} }
} }

View File

@ -17,3 +17,19 @@ enum HeaderType: String {
*/ */
case none 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 }
}

View File

@ -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 }
}

View File

@ -0,0 +1,8 @@
import Foundation
protocol StringProperty {
init?(_ value: String)
static var castFailureReason: String { get }
}

View File

@ -33,3 +33,19 @@ enum ThumbnailStyle: String, CaseIterable {
extension ThumbnailStyle: Codable { 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 }
}

View File

@ -0,0 +1,6 @@
import Foundation
extension Array: DefaultValueProvider {
static var defaultValue: Array<Element> { [] }
}

View File

@ -0,0 +1,6 @@
import Foundation
extension Bool: DefaultValueProvider {
static var defaultValue: Bool { true }
}

View File

@ -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() }
}

View File

@ -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 }
}

View File

@ -3,7 +3,7 @@ import Metal
extension Optional { extension Optional {
func unwrapped<T>(_ closure: (Wrapped) -> T) -> T? { func unwrapped<T>(_ closure: (Wrapped) -> T?) -> T? {
if case let .some(value) = self { if case let .some(value) = self {
return closure(value) return closure(value)
} }

View File

@ -79,3 +79,8 @@ extension String {
try data(using: .utf8)!.createFolderAndWrite(to: url) try data(using: .utf8)!.createFolderAndWrite(to: url)
} }
} }
extension String: DefaultValueProvider {
static var defaultValue: String { "" }
}

View File

@ -18,6 +18,8 @@ final class FileSystem {
input.appendingPathComponent(FileSystem.tempFileName) input.appendingPathComponent(FileSystem.tempFileName)
} }
let generatorInfoFolder: URL
/** /**
All files which should be copied to the output folder All files which should be copied to the output folder
*/ */
@ -70,7 +72,7 @@ final class FileSystem {
self.input = input self.input = input
self.output = output self.output = output
self.images = .init(input: input, output: output) self.images = .init(input: input, output: output)
self.generatorInfoFolder = input.appendingPathComponent("run")
} }
func urlInOutputFolder(_ path: String) -> URL { 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? { func contentOfOptionalFile(atPath path: String, source: String, createEmptyFileIfMissing: Bool = false) -> String? {
let url = input.appendingPathComponent(path) let url = input.appendingPathComponent(path)
guard exists(url) else { guard exists(url) else {

View File

@ -46,119 +46,7 @@ final class ValidationLog {
add(info: .init(reason: reason, source: source, error: error)) add(info: .init(reason: reason, source: source, error: error))
} }
@discardableResult
func unused<T, R>(_ value: Optional<T>, _ name: String, source: String) -> Optional<R> {
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<T>(_ value: Optional<T>, name: String, source: String) -> Optional<T> {
guard let value = value else {
add(error: "Missing property '\(name)'", source: source)
return nil
}
return value
}
func unexpected<T>(_ value: Optional<T>, name: String, source: String) -> Optional<T> {
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?) { func failedToOpen(_ file: String, requiredBy source: String, error: Error?) {
print("[ERROR] Failed to open file '\(file)' required by \(source): \(error?.localizedDescription ?? "No error provided")") 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
}()
} }

View File

@ -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<T>(_ value: Optional<T>, _ 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<T>(_ 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<T>(_ 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<R>(_ 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<T>(_ 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)
}
}

View File

@ -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 { private func generate(configPath: String) throws {
let configUrl = URL(fileURLWithPath: configPath) let configUrl = URL(fileURLWithPath: configPath)
let data = try Data(contentsOf: configUrl) let data = try Data(contentsOf: configUrl)
@ -26,7 +36,8 @@ private func generate(configPath: String) throws {
in: configuration.contentDirectory, in: configuration.contentDirectory,
to: configuration.outputDirectory) to: configuration.outputDirectory)
siteRoot = Element(atRoot: configuration.contentDirectory) // 2. Scan site elements
siteRoot = try loadSiteData(in: configuration.contentDirectory)
guard siteRoot != nil else { guard siteRoot != nil else {
return return
} }