diff --git a/Library/Sources/Public/Services/FileService.swift b/Library/Sources/Public/Services/FileService.swift index d7e1fdb..d9f56c5 100644 --- a/Library/Sources/Public/Services/FileService.swift +++ b/Library/Sources/Public/Services/FileService.swift @@ -1,7 +1,7 @@ import Foundation -public struct FileService: FileServicing { - +public struct FileService { + // MARK: Properties private let fileManager: FileManager @@ -12,6 +12,12 @@ public struct FileService: FileServicing { self.fileManager = fileManager } +} + +// MARK: - FileServicing + +extension FileService: FileServicing { + // MARK: Computed public var currentFolder: URL { diff --git a/Library/Sources/Public/Services/TemplateService.swift b/Library/Sources/Public/Services/TemplateService.swift new file mode 100644 index 0000000..4afed81 --- /dev/null +++ b/Library/Sources/Public/Services/TemplateService.swift @@ -0,0 +1,50 @@ +import Foundation +import Mustache + +public struct TemplateService { + + // MARK: Properties + + private let mustacheRenderer: MustacheLibrary + + // MARK: Initialisers + + public init( + bundle: Bundleable? = nil, + templateFolder: String + ) async throws (TemplateServiceError) { + guard let pathResources = (bundle ?? Bundle.module).resourcePath else { + throw .resourcePathNotFound + } + + let pathTemplates = pathResources + "/" + templateFolder + + do { + self.mustacheRenderer = try await MustacheLibrary(directory: pathTemplates) + } catch { + throw .serviceNotInitialized + } + } + +} + +// MARK: - TemplateServicing + +extension TemplateService: TemplateServicing { + + // MARK: Functions + + public func render(_ object: Any, on template: String) async throws (TemplateServiceError) -> String { + guard mustacheRenderer.getTemplate(named: template) != nil else { + throw .templateNotFound + } + + guard let content = mustacheRenderer.render(object, withTemplate: template) else { + throw .contentNotRendered + } + + return content + } + +} + diff --git a/Test/Sources/Cases/Public/Services/TemplateServiceTests.swift b/Test/Sources/Cases/Public/Services/TemplateServiceTests.swift new file mode 100644 index 0000000..5da5fa3 --- /dev/null +++ b/Test/Sources/Cases/Public/Services/TemplateServiceTests.swift @@ -0,0 +1,47 @@ +import ColibriLibrary +import Foundation +import Testing + +struct TemplateServiceTests { + + // MARK: Properties + + private let spy = TemplateServiceSpy() + + // MARK: Functions tests + + @Test(arguments: [String.content]) + func render(_ content: String) async throws { + // GIVEN + let service = TemplateServiceMock(action: .render(content), spy: spy) + + // WHEN + let result = try await service.render([:], on: .template) + + // THEN + #expect(result == content) + + #expect(spy.actions.isEmpty == false) + } + + @Test(arguments: [TemplateServiceError.serviceNotInitialized, .resourcePathNotFound, .templateNotFound, .contentNotRendered]) + func render(throws error: TemplateServiceError) async throws { + let service = TemplateServiceMock(action: .error(error), spy: spy) + + // WHEN + // THEN + await #expect(throws: error) { + try await service.render([:], on: .template) + } + + #expect(spy.actions.isEmpty == true) + } + +} + +// MARK: - String+Constants + +private extension String { + static let content = "" + static let template = "" +} diff --git a/Test/Sources/Helpers/Mocks/TemplateServiceMock.swift b/Test/Sources/Helpers/Mocks/TemplateServiceMock.swift new file mode 100644 index 0000000..3b03345 --- /dev/null +++ b/Test/Sources/Helpers/Mocks/TemplateServiceMock.swift @@ -0,0 +1,75 @@ +import ColibriLibrary +import Foundation + +final class TemplateServiceMock { + + // MARK: Properties + + private var actions: [Action] = [] + + private weak var spy: TemplateServiceSpy? + + // MARK: Initialisers + + init( + action: Action, + spy: TemplateServiceSpy? = nil + ) { + self.actions.append(action) + self.spy = spy + } + +} + +// MARK: - TemplateServicing + +extension TemplateServiceMock: TemplateServicing { + + // MARK: Functions + + @discardableResult + func render(_ object: Any, on template: String) async throws(TemplateServiceError) -> String { + guard let nextAction else { return .empty } + + switch nextAction { + case .error(let error): + throw error + case .render(let content): + try await spy?.render(object, on: template) + return content + } + } + +} + +// MARK: - Helpers + +private extension TemplateServiceMock { + + // MARK: Computed + + var nextAction: Action? { + guard !actions.isEmpty else { + return nil + } + + return actions.removeFirst() + } + +} + + +// MARK: - Actions + +extension TemplateServiceMock { + enum Action { + case error(TemplateServiceError) + case render(String) + } +} + +// MARK: - String+Constants + +private extension String { + static let empty = "" +} diff --git a/Test/Sources/Helpers/Spies/FileServiceSpy.swift b/Test/Sources/Helpers/Spies/FileServiceSpy.swift index 34ccd63..971e0a9 100644 --- a/Test/Sources/Helpers/Spies/FileServiceSpy.swift +++ b/Test/Sources/Helpers/Spies/FileServiceSpy.swift @@ -1,7 +1,6 @@ +import ColibriLibrary import Foundation -@testable import ColibriLibrary - final class FileServiceSpy { // MARK: Properties diff --git a/Test/Sources/Helpers/Spies/TemplateServiceSpy.swift b/Test/Sources/Helpers/Spies/TemplateServiceSpy.swift new file mode 100644 index 0000000..5922300 --- /dev/null +++ b/Test/Sources/Helpers/Spies/TemplateServiceSpy.swift @@ -0,0 +1,38 @@ +import ColibriLibrary + +final class TemplateServiceSpy { + + // MARK: Properties + + private(set) var actions: [Action] = [] + +} + +// MARK: - TemplateServicing + +extension TemplateServiceSpy: TemplateServicing { + + // MARK: Functions + + @discardableResult + func render(_ object: Any, on template: String) async throws(TemplateServiceError) -> String { + actions.append(.rendered(object, template)) + + return .content + } + +} + +// MARK: - Actions + +extension TemplateServiceSpy { + enum Action { + case rendered(_ object: Any, _ template: String) + } +} + +// MARK: - String+Constants + +private extension String { + static let content = "" +}