Improve logging during element scanning
This commit is contained in:
6
Sources/Generator/Content/DefaultValueProvider.swift
Normal file
6
Sources/Generator/Content/DefaultValueProvider.swift
Normal file
@@ -0,0 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
protocol DefaultValueProvider {
|
||||
|
||||
static var defaultValue: Self { get }
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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?
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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 }
|
||||
}
|
||||
|
@@ -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 }
|
||||
}
|
||||
|
8
Sources/Generator/Content/StringProperty.swift
Normal file
8
Sources/Generator/Content/StringProperty.swift
Normal file
@@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
protocol StringProperty {
|
||||
|
||||
init?(_ value: String)
|
||||
|
||||
static var castFailureReason: String { get }
|
||||
}
|
@@ -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 }
|
||||
}
|
||||
|
Reference in New Issue
Block a user