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
|
||||
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 }
|
||||
}
|
||||
|
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 {
|
||||
|
||||
func unwrapped<T>(_ closure: (Wrapped) -> T) -> T? {
|
||||
func unwrapped<T>(_ closure: (Wrapped) -> T?) -> T? {
|
||||
if case let .some(value) = self {
|
||||
return closure(value)
|
||||
}
|
||||
|
@ -79,3 +79,8 @@ extension String {
|
||||
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)
|
||||
}
|
||||
|
||||
let generatorInfoFolder: URL
|
||||
|
||||
/**
|
||||
All files which should be copied to the output folder
|
||||
*/
|
||||
@ -70,7 +72,7 @@ final class FileSystem {
|
||||
self.input = input
|
||||
self.output = output
|
||||
self.images = .init(input: input, output: output)
|
||||
|
||||
self.generatorInfoFolder = input.appendingPathComponent("run")
|
||||
}
|
||||
|
||||
func urlInOutputFolder(_ path: String) -> URL {
|
||||
@ -100,20 +102,6 @@ final class FileSystem {
|
||||
}
|
||||
}
|
||||
|
||||
func dataOfOptionalFile(atPath path: String, source: String) -> Data? {
|
||||
let url = input.appendingPathComponent(path)
|
||||
guard exists(url) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
do {
|
||||
return try Data(contentsOf: url)
|
||||
} catch {
|
||||
log.failedToOpen(path, requiredBy: source, error: error)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func contentOfOptionalFile(atPath path: String, source: String, createEmptyFileIfMissing: Bool = false) -> String? {
|
||||
let url = input.appendingPathComponent(path)
|
||||
guard exists(url) else {
|
||||
|
@ -46,119 +46,7 @@ final class ValidationLog {
|
||||
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?) {
|
||||
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 {
|
||||
let configUrl = URL(fileURLWithPath: configPath)
|
||||
let data = try Data(contentsOf: configUrl)
|
||||
@ -26,7 +36,8 @@ private func generate(configPath: String) throws {
|
||||
in: configuration.contentDirectory,
|
||||
to: configuration.outputDirectory)
|
||||
|
||||
siteRoot = Element(atRoot: configuration.contentDirectory)
|
||||
// 2. Scan site elements
|
||||
siteRoot = try loadSiteData(in: configuration.contentDirectory)
|
||||
guard siteRoot != nil else {
|
||||
return
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user