Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Sources/LeafKit/Exports.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import NIOConcurrencyHelpers
/// Various helper identities for convenience
extension Character {

// MARK: - LeafToken specific identities (Internal)
static var tagIndicator: Character = .octothorpe
static let tagIndicator = NIOLockedValueBox(Character.octothorpe)

var isValidInTagName: Bool {
return self.isLowercaseLetter
|| self.isUppercaseLetter
Expand Down
2 changes: 1 addition & 1 deletion Sources/LeafKit/LeafAST.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import NIO

/// `LeafAST` represents a "compiled," grammatically valid Leaf template (which may or may not be fully resolvable or erroring)
public struct LeafAST: Hashable {
public struct LeafAST: Hashable, Sendable {
// MARK: - Public

public func hash(into hasher: inout Hasher) { hasher.combine(name) }
Expand Down
67 changes: 43 additions & 24 deletions Sources/LeafKit/LeafCache/DefaultLeafCache.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
import NIOConcurrencyHelpers
import NIO

public final class DefaultLeafCache: SynchronousLeafCache {
/// `@unchecked Sendable` because uses locks to guarantee Sendability.
public final class DefaultLeafCache: SynchronousLeafCache, @unchecked Sendable {
// MARK: - Public - `LeafCache` Protocol Conformance


var __isEnabled = true
/// Global setting for enabling or disabling the cache
public var _isEnabled: Bool {
get {
self.lock.withLock {
self.__isEnabled
}
}
set(newValue) {
self.lock.withLock {
self.__isEnabled = newValue
}
}
}
/// Global setting for enabling or disabling the cache
public var isEnabled: Bool = true
public var isEnabled: Bool {
get {
self._isEnabled
}
set(newValue) {
self._isEnabled = newValue
}
}
/// Current count of cached documents
public var count: Int { self.lock.withLock { cache.count } }

Expand All @@ -25,17 +47,16 @@ public final class DefaultLeafCache: SynchronousLeafCache {
on loop: any EventLoop,
replace: Bool = false
) -> EventLoopFuture<LeafAST> {
// future fails if caching is enabled
guard isEnabled else { return loop.makeSucceededFuture(document) }

self.lock.lock()
defer { self.lock.unlock() }
// return an error if replace is false and the document name is already in cache
switch (self.cache.keys.contains(document.name),replace) {
self.lock.withLock {
// future fails if caching is enabled
guard __isEnabled else { return loop.makeSucceededFuture(document) }
// return an error if replace is false and the document name is already in cache
switch (self.cache.keys.contains(document.name),replace) {
case (true, false): return loop.makeFailedFuture(LeafError(.keyExists(document.name)))
default: self.cache[document.name] = document
}
return loop.makeSucceededFuture(document)
}
return loop.makeSucceededFuture(document)
}

/// - Parameters:
Expand All @@ -46,10 +67,10 @@ public final class DefaultLeafCache: SynchronousLeafCache {
documentName: String,
on loop: any EventLoop
) -> EventLoopFuture<LeafAST?> {
guard isEnabled == true else { return loop.makeSucceededFuture(nil) }
self.lock.lock()
defer { self.lock.unlock() }
return loop.makeSucceededFuture(self.cache[documentName])
self.lock.withLock {
guard __isEnabled == true else { return loop.makeSucceededFuture(nil) }
return loop.makeSucceededFuture(self.cache[documentName])
}
}

/// - Parameters:
Expand All @@ -61,14 +82,12 @@ public final class DefaultLeafCache: SynchronousLeafCache {
_ documentName: String,
on loop: any EventLoop
) -> EventLoopFuture<Bool?> {
guard isEnabled == true else { return loop.makeFailedFuture(LeafError(.cachingDisabled)) }

self.lock.lock()
defer { self.lock.unlock() }

guard self.cache[documentName] != nil else { return loop.makeSucceededFuture(nil) }
self.cache[documentName] = nil
return loop.makeSucceededFuture(true)
self.lock.withLock {
guard __isEnabled == true else { return loop.makeFailedFuture(LeafError(.cachingDisabled)) }
guard self.cache[documentName] != nil else { return loop.makeSucceededFuture(nil) }
self.cache[documentName] = nil
return loop.makeSucceededFuture(true)
}
}

// MARK: - Internal Only
Expand All @@ -78,9 +97,9 @@ public final class DefaultLeafCache: SynchronousLeafCache {

/// Blocking file load behavior
internal func retrieve(documentName: String) throws -> LeafAST? {
guard isEnabled == true else { throw LeafError(.cachingDisabled) }
self.lock.lock()
defer { self.lock.unlock() }
guard __isEnabled == true else { throw LeafError(.cachingDisabled) }
let result = self.cache[documentName]
guard result != nil else { throw LeafError(.noValueForKey(documentName)) }
return result
Expand Down
3 changes: 2 additions & 1 deletion Sources/LeafKit/LeafCache/LeafCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import NIO
/// adherents where the cache store itself is not a bottleneck.
///
/// `LeafAST.name` is to be used in all cases as the key for retrieving cached documents.
public protocol LeafCache {
@preconcurrency
public protocol LeafCache: Sendable {
/// Global setting for enabling or disabling the cache
var isEnabled : Bool { get set }
/// Current count of cached documents
Expand Down
83 changes: 43 additions & 40 deletions Sources/LeafKit/LeafConfiguration.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import Foundation
import NIOConcurrencyHelpers

/// General configuration of Leaf
/// - Sets the default View directory where templates will be looked for
/// - Guards setting the global tagIndicator (default `#`).
public struct LeafConfiguration {
public struct LeafConfiguration: Sendable {

/// Initialize Leaf with the default tagIndicator `#` and unfound imports throwing an exception
/// - Parameter rootDirectory: Default directory where templates will be found
Expand All @@ -23,9 +24,9 @@ public struct LeafConfiguration {
/// - Parameter tagIndicator: Unique tagIndicator - may only be set once.
/// - Parameter ignoreUnfoundImports: Ignore unfound imports - may only be set once.
public init(rootDirectory: String, tagIndicator: Character, ignoreUnfoundImports: Bool) {
if !Self.started {
Character.tagIndicator = tagIndicator
Self.started = true
if !Self.started.withLockedValue({ $0 }) {
Character.tagIndicator.withLockedValue { $0 = tagIndicator }
Self.started.withLockedValue { $0 = true }
}
self._rootDirectory = rootDirectory
self._ignoreUnfoundImports = ignoreUnfoundImports
Expand All @@ -42,53 +43,53 @@ public struct LeafConfiguration {
}

public static var encoding: String.Encoding {
get { _encoding }
set { if !Self.running { _encoding = newValue } }
get { _encoding.withLockedValue { $0 } }
set { if !Self.running { _encoding.withLockedValue { $0 = newValue } } }
}

public static var boolFormatter: (Bool) -> String {
get { _boolFormatter }
set { if !Self.running { _boolFormatter = newValue } }
get { _boolFormatter.withLockedValue { $0 } }
set { if !Self.running { _boolFormatter.withLockedValue { $0 = newValue } } }
}

public static var intFormatter: (Int) -> String {
get { _intFormatter }
set { if !Self.running { _intFormatter = newValue } }
get { _intFormatter.withLockedValue { $0 } }
set { if !Self.running { _intFormatter.withLockedValue { $0 = newValue } } }
}

public static var doubleFormatter: (Double) -> String {
get { _doubleFormatter }
set { if !Self.running { _doubleFormatter = newValue } }
get { _doubleFormatter.withLockedValue { $0 } }
set { if !Self.running { _doubleFormatter.withLockedValue { $0 = newValue } } }
}

public static var nilFormatter: () -> String {
get { _nilFormatter }
set { if !Self.running { _nilFormatter = newValue } }
get { _nilFormatter.withLockedValue { $0 } }
set { if !Self.running { _nilFormatter.withLockedValue { $0 = newValue } } }
}

public static var voidFormatter: () -> String {
get { _voidFormatter }
set { if !Self.running { _voidFormatter = newValue } }
get { _voidFormatter.withLockedValue { $0 } }
set { if !Self.running { _voidFormatter.withLockedValue { $0 = newValue } } }
}

public static var stringFormatter: (String) -> String {
get { _stringFormatter }
set { if !Self.running { _stringFormatter = newValue } }
get { _stringFormatter.withLockedValue { $0 } }
set { if !Self.running { _stringFormatter.withLockedValue { $0 = newValue } } }
}

public static var arrayFormatter: ([String]) -> String {
get { _arrayFormatter }
set { if !Self.running { _arrayFormatter = newValue } }
get { _arrayFormatter.withLockedValue { $0 } }
set { if !Self.running { _arrayFormatter.withLockedValue { $0 = newValue } } }
}

public static var dictFormatter: ([String: String]) -> String {
get { _dictFormatter }
set { if !Self.running { _dictFormatter = newValue } }
get { _dictFormatter.withLockedValue { $0 } }
set { if !Self.running { _dictFormatter.withLockedValue { $0 = newValue } } }
}

public static var dataFormatter: (Data) -> String? {
get { _dataFormatter }
set { if !Self.running { _dataFormatter = newValue } }
get { _dataFormatter.withLockedValue { $0 } }
set { if !Self.running { _dataFormatter.withLockedValue { $0 = newValue } } }
}

// MARK: - Internal/Private Only
Expand All @@ -99,27 +100,29 @@ public struct LeafConfiguration {
internal var _ignoreUnfoundImports: Bool {
willSet { assert(!accessed, "Changing property after LeafConfiguration has been read has no effect") }
}
internal static var _encoding: String.Encoding = .utf8
internal static var _boolFormatter: (Bool) -> String = { $0.description }
internal static var _intFormatter: (Int) -> String = { $0.description }
internal static var _doubleFormatter: (Double) -> String = { $0.description }
internal static var _nilFormatter: () -> String = { "" }
internal static var _voidFormatter: () -> String = { "" }
internal static var _stringFormatter: (String) -> String = { $0 }
internal static var _arrayFormatter: ([String]) -> String =

internal static let _encoding = NIOLockedValueBox<String.Encoding>(.utf8)
internal static let _boolFormatter = NIOLockedValueBox<(Bool) -> String>({ $0.description })
internal static let _intFormatter = NIOLockedValueBox<(Int) -> String>({ $0.description })
internal static let _doubleFormatter = NIOLockedValueBox<(Double) -> String>({ $0.description })
internal static let _nilFormatter = NIOLockedValueBox<(() -> String)>({ "" })
internal static let _voidFormatter = NIOLockedValueBox<(() -> String)>({ "" })
internal static let _stringFormatter = NIOLockedValueBox<((String) -> String)>({ $0 })
internal static let _arrayFormatter = NIOLockedValueBox<(([String]) -> String)>(
{ "[\($0.map {"\"\($0)\""}.joined(separator: ", "))]" }
internal static var _dictFormatter: ([String: String]) -> String =
)
internal static let _dictFormatter = NIOLockedValueBox<(([String: String]) -> String)>(
{ "[\($0.map { "\($0): \"\($1)\"" }.joined(separator: ", "))]" }
internal static var _dataFormatter: (Data) -> String? =
{ String(data: $0, encoding: Self._encoding) }

)
internal static let _dataFormatter = NIOLockedValueBox<((Data) -> String?)>(
{ String(data: $0, encoding: Self._encoding.withLockedValue { $0 }) }
)

/// Convenience flag for global write-once
private static var started = false
private static let started = NIOLockedValueBox(false)
private static var running: Bool {
assert(!Self.started, "LeafKit can only be configured prior to instantiating any LeafRenderer")
return Self.started
assert(!Self.started.withLockedValue { $0 }, "LeafKit can only be configured prior to instantiating any LeafRenderer")
return Self.started.withLockedValue { $0 }
}

/// Convenience flag for local lock-after-access
Expand Down
9 changes: 5 additions & 4 deletions Sources/LeafKit/LeafData/LeafData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ public struct LeafData: CustomStringConvertible,
ExpressibleByBooleanLiteral,
ExpressibleByArrayLiteral,
ExpressibleByFloatLiteral,
ExpressibleByNilLiteral {

ExpressibleByNilLiteral,
Sendable {

/// The concrete instantiable object types for a `LeafData`
public enum NaturalType: String, CaseIterable, Hashable {
public enum NaturalType: String, CaseIterable, Hashable, Sendable {
case bool
case string
case int
Expand Down Expand Up @@ -250,7 +251,7 @@ public struct LeafData: CustomStringConvertible,

/// Creates a new `LeafData` from `() -> LeafData` if possible or `nil` if not possible.
/// `returns` must specify a `NaturalType` that the function will return
internal static func lazy(_ lambda: @escaping () -> LeafData,
internal static func lazy(_ lambda: @Sendable @escaping () -> LeafData,
returns type: LeafData.NaturalType,
invariant sideEffects: Bool) throws -> LeafData {
LeafData(.lazy(f: lambda, returns: type, invariant: sideEffects))
Expand Down
5 changes: 3 additions & 2 deletions Sources/LeafKit/LeafData/LeafDataStorage.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import NIO
import Foundation

internal indirect enum LeafDataStorage: Equatable, CustomStringConvertible {
internal indirect enum LeafDataStorage: Equatable, CustomStringConvertible, Sendable {
// MARK: - Cases

// Static values
Expand All @@ -20,7 +20,8 @@ internal indirect enum LeafDataStorage: Equatable, CustomStringConvertible {

// Lazy resolvable function
// Must specify return tuple giving (returnType, invariance)
case lazy(f: () -> (LeafData),
@preconcurrency
case lazy(f: @Sendable () -> (LeafData),
returns: LeafData.NaturalType,
invariant: Bool)

Expand Down
4 changes: 2 additions & 2 deletions Sources/LeafKit/LeafError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
///
public struct LeafError: Error {
/// Possible cases of a LeafError.Reason, with applicable stored values where useful for the type
public enum Reason {
public enum Reason: Sendable {
// MARK: Errors related to loading raw templates
/// Attempted to access a template blocked for security reasons
case illegalAccess(String)
Expand Down Expand Up @@ -109,7 +109,7 @@ public struct LeafError: Error {
public struct LexerError: Error {
// MARK: - Public

public enum Reason {
public enum Reason: Sendable {
// MARK: Errors occuring during Lexing
/// A character not usable in parameters is present when Lexer is not expecting it
case invalidParameterToken(Character)
Expand Down
11 changes: 6 additions & 5 deletions Sources/LeafKit/LeafLexer/LeafLexer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ internal struct LeafLexer {
private mutating func nextToken() throws -> LeafToken? {
// if EOF, return nil - no more to read
guard let current = src.peek() else { return nil }
let isTagID = current == .tagIndicator
let isTagID = current == .tagIndicator.withLockedValue { $0 }
let isTagVal = current.isValidInTagName
let isCol = current == .colon
let next = src.peek(aheadBy: 1)
Expand Down Expand Up @@ -107,10 +107,11 @@ internal struct LeafLexer {
/// Consume all data until hitting an unescaped `tagIndicator` and return a `.raw` token
private mutating func lexRaw() -> LeafToken {
var slice = ""
while let current = src.peek(), current != .tagIndicator {
slice += src.readWhile { $0 != .tagIndicator && $0 != .backSlash }
let tagIndicator = Character.tagIndicator.withLockedValue({ $0 })
while let current = src.peek(), current != tagIndicator {
slice += src.readWhile { $0 != tagIndicator && $0 != .backSlash }
guard let newCurrent = src.peek(), newCurrent == .backSlash else { break }
if let next = src.peek(aheadBy: 1), next == .tagIndicator {
if let next = src.peek(aheadBy: 1), next == tagIndicator {
src.pop()
}
slice += src.pop()!.description
Expand All @@ -129,7 +130,7 @@ internal struct LeafLexer {
return .tagIndicator
} else {
state = .raw
return .raw(Character.tagIndicator.description)
return .raw((Character.tagIndicator.withLockedValue { $0 }).description)
}
}

Expand Down
Loading
Loading