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

View File

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

View File

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

View File

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

View File

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

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