Improve logging during element scanning
This commit is contained in:
parent
4c2c4b7dd3
commit
1ceba25d4f
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
|
- 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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?
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 }
|
||||||
|
}
|
||||||
|
@ -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: 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 }
|
||||||
|
}
|
||||||
|
6
Sources/Generator/Extensions/Array+Extensions.swift
Normal file
6
Sources/Generator/Extensions/Array+Extensions.swift
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Array: DefaultValueProvider {
|
||||||
|
|
||||||
|
static var defaultValue: Array<Element> { [] }
|
||||||
|
}
|
6
Sources/Generator/Extensions/Bool+Extensions.swift
Normal file
6
Sources/Generator/Extensions/Bool+Extensions.swift
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Bool: DefaultValueProvider {
|
||||||
|
|
||||||
|
static var defaultValue: Bool { true }
|
||||||
|
}
|
26
Sources/Generator/Extensions/Date+Extensions.swift
Normal file
26
Sources/Generator/Extensions/Date+Extensions.swift
Normal 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() }
|
||||||
|
}
|
13
Sources/Generator/Extensions/Int+Extensions.swift
Normal file
13
Sources/Generator/Extensions/Int+Extensions.swift
Normal 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 }
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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 { "" }
|
||||||
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
150
Sources/Generator/Processing/MetadataInfoLogger.swift
Normal file
150
Sources/Generator/Processing/MetadataInfoLogger.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user