Skip to content

Commit

Permalink
Reworked YAML file loader mechanics (#26)
Browse files Browse the repository at this point in the history
- Unified YAML file loader
- Better error handling
- FileLoader helper
  • Loading branch information
viaszkadi authored Oct 11, 2024
1 parent 53d1892 commit 2e4d2d5
Show file tree
Hide file tree
Showing 15 changed files with 313 additions and 164 deletions.
25 changes: 25 additions & 0 deletions Sources/ToucanSDK/Extensions/Array+Append.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// File.swift
// toucan
//
// Created by Viasz-Kádi Ferenc on 2024. 10. 10..
//

import Foundation

extension Array {

/// Appends the given item to the collection if no element in the collection satisfies the specified condition.
///
/// - Parameters:
/// - item: The element to be appended to the collection if the condition is not met.
/// - condition: A closure that takes an element of the collection as its argument and returns a Boolean value indicating whether the element satisfies the condition.
mutating func appendIfNot(
_ item: Element,
where condition: (Element) -> Bool
) {
if !contains(where: condition) {
append(item)
}
}
}
46 changes: 46 additions & 0 deletions Sources/ToucanSDK/Extensions/String+Yaml.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// File.swift
// toucan
//
// Created by Viasz-Kádi Ferenc on 2024. 10. 10..
//

import Foundation

extension String {

/// Decodes the current instance from a YAML string into a specified type.
///
/// - Parameter as: The type to decode the YAML string into.
/// - Returns: An optional instance of the specified type, decoded from the YAML string.
/// - Throws: An error if the YAML parsing fails.
func decodeYaml<T>(as: T.Type) throws -> T? {
try YamlParser().parse(self, as: T.self)
}

/// Decodes a YAML string into a dictionary representation.
///
/// - Returns: A dictionary with string keys and values of any type, representing the parsed YAML content.
/// Returns `nil` if the parsing fails.
/// - Throws: An error if the YAML parsing fails.
func decodeYaml() throws -> [String: Any]? {
try YamlParser().parse(self)
}
}

extension [String] {

/// Decodes an array of YAML-encoded data into a single merged dictionary.
///
/// - Returns: A dictionary of type `[String: Any]` representing the merged result of decoded YAML data.
/// - Throws: An error if any of the decoding operations fail.
func decodeYaml() throws -> [String: Any] {
try self
.compactMap {
try $0.decodeYaml()
}
.reduce([:]) { partialResult, item in
partialResult.recursivelyMerged(with: item)
}
}
}
11 changes: 7 additions & 4 deletions Sources/ToucanSDK/Parsers/FrontMatterParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@

struct FrontMatterParser {

func parse(
markdown: String
) throws -> [String: Any] {
/// Parses a given markdown string to extract metadata as a dictionary.
/// - Parameter markdown: The markdown content containing metadata enclosed within "---".
/// - Throws: An error if the YAML decoding fails.
/// - Returns: A dictionary containing the parsed metadata if available, otherwise an empty dictionary.
func parse(markdown: String) throws -> [String: Any] {
guard markdown.starts(with: "---") else {
return [:]
}
Expand All @@ -23,6 +25,7 @@ struct FrontMatterParser {
guard let rawMetadata = parts.first else {
return [:]
}
return try Yaml.parse(yaml: String(rawMetadata))

return try String(rawMetadata).decodeYaml() ?? [:]
}
}
63 changes: 63 additions & 0 deletions Sources/ToucanSDK/Parsers/YamlParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//
// File.swift
// toucan
//
// Created by Viasz-Kádi Ferenc on 2024. 10. 10..
//

import Foundation
import Yams

public struct YamlParser {

/// An enumeration representing possible errors that can occur while parsing the yaml.
public enum Error: Swift.Error {
/// Indicates an error related to parsing YAML.
case yaml(String)
}

/// A `Resolver` instance used during parsing.
let resolver: Resolver

/// Initializes a new instance with the specified resolver.
/// - Parameter resolver: The resolver to use for the instance. Defaults to a resolver with the `.timestamp` case removed.
init(resolver: Resolver = .default.removing(.timestamp)) {
self.resolver = resolver
}

/// Parses a YAML string and attempts to convert it to a specified type.
///
/// - Parameters:
/// - yaml: The YAML string to parse.
/// - as: The type to which the parsed YAML should be converted.
/// - Returns: An optional value of the specified type if the parsing is successful, or `nil` if the conversion fails.
/// - Throws: An `Error.yaml` if a `YamlError` occurs during parsing.
func parse<T>(_ yaml: String, as: T.Type) throws -> T? {
do {
return try Yams.load(yaml: yaml, resolver) as? T
}
catch let error as YamlError {
throw Error.yaml(error.description)
}
}

/// Parses a YAML string and converts it into a dictionary.
///
/// - Parameter yaml: A string containing YAML data.
/// - Returns: A dictionary with string keys and values of any type, or nil if parsing fails.
/// - Throws: An error if the YAML parsing fails.
func parse(_ yaml: String) throws -> [String: Any]? {
try parse(yaml, as: [String: Any].self)
}

/// Decodes a YAML string into a specified Decodable type.
///
/// - Parameters:
/// - yaml: A `String` containing the YAML-formatted data to decode.
/// - as: The type of the Decodable object that the YAML data should be decoded into.
/// - Returns: An instance of the specified type, decoded from the provided YAML string.
/// - Throws: An error if the decoding process fails.
func decode<T: Decodable>(_ yaml: String, as type: T.Type) throws -> T {
try YAMLDecoder().decode(T.self, from: yaml)
}
}
2 changes: 1 addition & 1 deletion Sources/ToucanSDK/Site/Types/Output+HTML.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ struct HTML: Output {
"site": site,
"page": page,
"pagination": pagination,
"year": year
"year": year,
]
)
.sanitized()
Expand Down
56 changes: 19 additions & 37 deletions Sources/ToucanSDK/Source/ConfigLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import Foundation
import FileManagerKit
import Yams
import Logging

private extension Config {
Expand Down Expand Up @@ -92,62 +91,45 @@ public struct ConfigLoader {

/// An enumeration representing possible errors that can occur while loading the configuration.
public enum Error: Swift.Error {
/// Indicates that a required configuration file is missing at the specified URL.
case missing(URL)
/// Indicates an error related to file operations.
case file(Swift.Error)
/// Indicates an error related to parsing YAML.
case yaml(YamlError)
}

/// The URL of the source files.
let sourceUrl: URL
/// The file manager used for file operations.
let fileManager: FileManager
/// A file loader used for loading files.
let fileLoader: FileLoader
/// The base URL to use for the configuration.
let baseUrl: String?
/// The logger instance
let logger: Logger

/// Loads the configuration.
///
/// - Returns: A `Config` object.
/// - Throws: An error if the configuration fails to load.
/// This function attempts to load a configuration file from a specified URL, parses the file contents,
/// and returns a `Config` object based on the file's data. If the file is missing or cannot be parsed,
/// an appropriate error is thrown.
///
/// - Returns: A `Config` object representing the loaded configuration.
/// - Throws: An error if the configuration file is missing or if its contents cannot be decoded.
func load() throws -> Config {
let configUrl = sourceUrl.appendingPathComponent("config")

let yamlConfigUrls = [
configUrl.appendingPathExtension("yaml"),
configUrl.appendingPathExtension("yml"),
]
for yamlConfigUrl in yamlConfigUrls {
guard fileManager.fileExists(at: yamlConfigUrl) else {
continue
}
do {
logger.debug(
"Loading config file: `\(yamlConfigUrl.absoluteString)`."
)
let rawYaml = try String(
contentsOf: yamlConfigUrl,
encoding: .utf8
)
let dict = try Yams.load(yaml: rawYaml) as? [String: Any] ?? [:]
let config = try dictToConfig(dict)
return config
}
catch let error as YamlError {
throw Error.yaml(error)
}
catch {
throw Error.file(error)
}
logger.debug("Loading config file: `\(configUrl.absoluteString)`.")

do {
let contents = try FileLoader.yaml.loadContents(at: configUrl)
let yaml = try contents.decodeYaml()
return dictToConfig(yaml)
}
catch FileLoader.Error.missing(let url) {
throw Error.missing(url)
}
throw Error.missing(sourceUrl)
}

func dictToConfig(
_ yaml: [String: Any]
) throws -> Config {
) -> Config {
// MARK: - site
let site = yaml.dict(Config.Keys.site)

Expand Down
46 changes: 16 additions & 30 deletions Sources/ToucanSDK/Source/ContentTypeLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,56 +6,42 @@
//

import Foundation
import FileManagerKit
import Yams
import Logging

/// A struct responsible for loading and managing content types.
struct ContentTypeLoader {

/// An enumeration representing possible errors that can occur while loading the configuration.
enum Error: Swift.Error {
case missing
/// Indicates an error related to file operations.
case file(Swift.Error)
/// Indicates an error related to parsing YAML.
case yaml(YamlError)
}

/// The URL of the source files.
let sourceUrl: URL

/// The configuration object that holds settings for the site.
let config: Config

/// The file manager used for file operations.
let fileManager: FileManager
let fileLoader: FileLoader
let yamlParser: YamlParser

/// The logger instance
let logger: Logger

/// Loads and returns an array of content types.
///
/// - Throws: An error if the content types could not be loaded.
/// - Returns: An array of `ContentType` objects.
func load() throws -> [ContentType] {
// TODO: use yaml loader
let typesUrl = sourceUrl.appendingPathComponent(config.types.folder)
let list = fileManager.listDirectory(at: typesUrl)
.filter { $0.hasSuffix(".yml") || $0.hasSuffix(".yaml") }
let contents = try fileLoader.findContents(at: typesUrl)

var types: [ContentType] = []
var useDefaultContentType = true
for file in list {
let decoder = YAMLDecoder()
let data = try Data(
contentsOf: typesUrl.appendingPathComponent(file)
)
let type = try decoder.decode(ContentType.self, from: data)
types.append(type)
if type.id == ContentType.default.id {
useDefaultContentType = false
}
logger.debug("Loading content type: `\(sourceUrl.absoluteString)`.")

var types = try contents.map {
try yamlParser.decode($0, as: ContentType.self)
}
if useDefaultContentType {
types.append(.default)

// Adding the default content type if not present
types.appendIfNot(.default) {
$0.id == ContentType.default.id
}

// TODO: pagination type is not allowed
types = types.filter { $0.id != ContentType.pagination.id }
types.append(.pagination)
Expand Down
Loading

0 comments on commit 2e4d2d5

Please sign in to comment.